diff --git a/src/components/Checkbox/Checkbox.scss b/src/components/Checkbox/Checkbox.scss index 6ddc046c85..221e0cdff8 100644 --- a/src/components/Checkbox/Checkbox.scss +++ b/src/components/Checkbox/Checkbox.scss @@ -33,11 +33,11 @@ $block: '.#{variables.$ns}checkbox'; justify-content: center; pointer-events: none; - visibility: hidden; - color: transparent; + opacity: 0; + color: var(--g-color-text-brand-contrast); transform: translateY(-5px); transition: - color 0.1s, + opacity 0.1s, transform 0.2s; } @@ -143,7 +143,7 @@ $block: '.#{variables.$ns}checkbox'; #{$block}__icon { visibility: visible; - color: var(--g-color-text-brand-contrast); + opacity: 1; transform: translateX(0); } } @@ -163,4 +163,40 @@ $block: '.#{variables.$ns}checkbox'; } } } + + &_invalid { + #{$block}__indicator { + &::before { + border: 1px solid var(--g-color-line-danger); + } + } + + #{$block}__icon { + color: var(--g-color-base-background); + } + + &:hover { + #{$block}__indicator { + &::before { + border: 1px solid var(--g-color-line-danger); + } + } + } + + &#{$block}_checked, + &#{$block}_indeterminate { + #{$block}__indicator { + &::before { + background-color: var(--g-color-line-danger); + border: transparent; + } + } + } + + &#{$block}_disabled { + #{$block}__icon { + color: var(--g-color-base-generic-accent-disabled-solid); + } + } + } } diff --git a/src/components/Checkbox/Checkbox.tsx b/src/components/Checkbox/Checkbox.tsx index c79497cff7..fec8bcec7e 100644 --- a/src/components/Checkbox/Checkbox.tsx +++ b/src/components/Checkbox/Checkbox.tsx @@ -4,7 +4,7 @@ import * as React from 'react'; import {useCheckbox} from '../../hooks/private'; import {ControlLabel} from '../ControlLabel'; -import type {ControlProps, DOMProps, QAProps} from '../types'; +import type {ControlProps, ControlValidationProps, DOMProps, QAProps} from '../types'; import {block} from '../utils/cn'; import {CheckboxDashIcon} from './CheckboxDashIcon'; @@ -14,7 +14,7 @@ import './Checkbox.scss'; export type CheckboxSize = 'm' | 'l' | 'xl'; -export interface CheckboxProps extends ControlProps, DOMProps, QAProps { +export interface CheckboxProps extends ControlProps, ControlValidationProps, DOMProps, QAProps { size?: CheckboxSize; content?: React.ReactNode; children?: React.ReactNode; @@ -29,6 +29,7 @@ export const Checkbox = React.forwardRef( size = 'm', indeterminate, disabled = false, + validationState, content, children, title, @@ -66,6 +67,7 @@ export const Checkbox = React.forwardRef( disabled, indeterminate, checked, + invalid: validationState === 'invalid', }, className, )} diff --git a/src/components/Checkbox/README-ru.md b/src/components/Checkbox/README-ru.md index 7115167b93..82531bb9f0 100644 --- a/src/components/Checkbox/README-ru.md +++ b/src/components/Checkbox/README-ru.md @@ -136,25 +136,26 @@ LANDING_BLOCK--> ## Свойства -| Имя | Описание | Тип | Значение по умолчанию | -| :------------- | :---------------------------------------------------------------------------------------------------------------------- | :-------------------------------------------: | :-------------------: | -| children | Содержимое чекбокса (как правило, лейбл). | `ReactNode` | | -| content | Содержимое чекбокса (альтернатива `children`). | `ReactNode` | | -| disabled | Включает или отключает состояние `disabled` у чекбокса. | `boolean` | `false` | -| checked | Включает или отключает состояние `checked` у чекбокса. | `boolean` | `false` | -| defaultChecked | Задает начальное состояние `checked` при монтировании компонента. | `boolean` | `false` | -| onUpdate | Срабатывает при изменении состояния чекбокса пользователем и передает значение `checked` как аргумент обратного вызова. | `(checked: boolean) => void` | | -| onChange | Срабатывает при изменении состояния чекбокса пользователем и передает событие изменения как аргумент обратного вызова. | `Function` | | -| onFocus | Обработчик события, вызываемый, когда элемент ввода чекбокса получает фокус. | `Function` | | -| onBlur | Обработчик события, вызываемый, когда элемент ввода чекбокса теряет фокус. | `Function` | | -| size | Определяет размер чекбокса. | `m` `l` | `m` | -| id | HTML-атрибут `id`. | `string` | | -| qa | HTML-атрибут `data-qa`, используется для тестирования. | `string` | | -| style | HTML-атрибут `style`. | `React.CSSProperties` | | -| className | HTML-атрибут `class`. | `string` | | -| title | HTML-атрибут `title`. | `string` | | -| name | HTML-атрибут `name` для элемента ввода. | `string` | | -| value | HTML-атрибут `value` для элемента ввода. | `string` | | -| indeterminate | Включает или отключает состояние `indeterminate` у чекбокса. | `boolean` | `false` | -| controlProps | Дополнительные свойства базового элемента ввода. | `React.InputHTMLAttributes` | | -| controlRef | Ссылка на базовый элемент ввода. | `React.Ref` | | +| Имя | Описание | Тип | Значение по умолчанию | +| :-------------- | :---------------------------------------------------------------------------------------------------------------------- | :-------------------------------------------: | :-------------------: | +| children | Содержимое чекбокса (как правило, лейбл). | `ReactNode` | | +| content | Содержимое чекбокса (альтернатива `children`). | `ReactNode` | | +| disabled | Включает или отключает состояние `disabled` у чекбокса. | `boolean` | `false` | +| validationState | Состояние валидации. | `"invalid"` | | +| checked | Включает или отключает состояние `checked` у чекбокса. | `boolean` | `false` | +| defaultChecked | Задает начальное состояние `checked` при монтировании компонента. | `boolean` | `false` | +| onUpdate | Срабатывает при изменении состояния чекбокса пользователем и передает значение `checked` как аргумент обратного вызова. | `(checked: boolean) => void` | | +| onChange | Срабатывает при изменении состояния чекбокса пользователем и передает событие изменения как аргумент обратного вызова. | `Function` | | +| onFocus | Обработчик события, вызываемый, когда элемент ввода чекбокса получает фокус. | `Function` | | +| onBlur | Обработчик события, вызываемый, когда элемент ввода чекбокса теряет фокус. | `Function` | | +| size | Определяет размер чекбокса. | `m` `l` | `m` | +| id | HTML-атрибут `id`. | `string` | | +| qa | HTML-атрибут `data-qa`, используется для тестирования. | `string` | | +| style | HTML-атрибут `style`. | `React.CSSProperties` | | +| className | HTML-атрибут `class`. | `string` | | +| title | HTML-атрибут `title`. | `string` | | +| name | HTML-атрибут `name` для элемента ввода. | `string` | | +| value | HTML-атрибут `value` для элемента ввода. | `string` | | +| indeterminate | Включает или отключает состояние `indeterminate` у чекбокса. | `boolean` | `false` | +| controlProps | Дополнительные свойства базового элемента ввода. | `React.InputHTMLAttributes` | | +| controlRef | Ссылка на базовый элемент ввода. | `React.Ref` | | diff --git a/src/components/Checkbox/README.md b/src/components/Checkbox/README.md index db5a5b1d96..d810bd2ce7 100644 --- a/src/components/Checkbox/README.md +++ b/src/components/Checkbox/README.md @@ -136,25 +136,26 @@ LANDING_BLOCK--> ## Properties -| Name | Description | Type | Default | -| :------------- | :---------------------------------------------------------------------------------------------------- | :-------------------------------------------: | :-----: | -| children | Checkbox content (usually, a label). | `ReactNode` | | -| content | Checkbox content (alternative to children). | `ReactNode` | | -| disabled | Toggles the `disabled` state of the checkbox. | `boolean` | `false` | -| checked | Toggles the `checked` state of the checkbox. | `boolean` | `false` | -| defaultChecked | Sets the initial checked state when the component is mounted. | `boolean` | `false` | -| onUpdate | Fires when the user changes the checkbox state and provides the checked value as a callback argument. | `(checked: boolean) => void` | | -| onChange | Fires when the user changes the checkbox state and provides the change event as a callback argument. | `Function` | | -| onFocus | Event handler to use when the checkbox input element receives focus. | `Function` | | -| onBlur | Event handler to use when the checkbox input element loses focus. | `Function` | | -| size | Determines the checkbox size. | `m` `l` | `m` | -| id | `id` HTML attribute | `string` | | -| qa | `data-qa` HTML attribute, used for testing | `string` | | -| style | `style` HTML attribute | `React.CSSProperties` | | -| className | `class` HTML attribute | `string` | | -| title | `title` HTML attribute | `string` | | -| name | `name` HTML attribute for the input element. | `string` | | -| value | `value` HTML attribute for the input element. | `string` | | -| indeterminate | Toggles the `indeterminate` state of the checkbox. | `boolean` | `false` | -| controlProps | Additional propeties for the underlying input element. | `React.InputHTMLAttributes` | | -| controlRef | Ref to the underlying input element. | `React.Ref` | | +| Name | Description | Type | Default | +| :-------------- | :---------------------------------------------------------------------------------------------------- | :-------------------------------------------: | :-----: | +| children | Checkbox content (usually, a label). | `ReactNode` | | +| content | Checkbox content (alternative to children). | `ReactNode` | | +| disabled | Toggles the `disabled` state of the checkbox. | `boolean` | `false` | +| validationState | Validation state. | `"invalid"` | | +| checked | Toggles the `checked` state of the checkbox. | `boolean` | `false` | +| defaultChecked | Sets the initial checked state when the component is mounted. | `boolean` | `false` | +| onUpdate | Fires when the user changes the checkbox state and provides the checked value as a callback argument. | `(checked: boolean) => void` | | +| onChange | Fires when the user changes the checkbox state and provides the change event as a callback argument. | `Function` | | +| onFocus | Event handler to use when the checkbox input element receives focus. | `Function` | | +| onBlur | Event handler to use when the checkbox input element loses focus. | `Function` | | +| size | Determines the checkbox size. | `m` `l` | `m` | +| id | `id` HTML attribute | `string` | | +| qa | `data-qa` HTML attribute, used for testing | `string` | | +| style | `style` HTML attribute | `React.CSSProperties` | | +| className | `class` HTML attribute | `string` | | +| title | `title` HTML attribute | `string` | | +| name | `name` HTML attribute for the input element. | `string` | | +| value | `value` HTML attribute for the input element. | `string` | | +| indeterminate | Toggles the `indeterminate` state of the checkbox. | `boolean` | `false` | +| controlProps | Additional propeties for the underlying input element. | `React.InputHTMLAttributes` | | +| controlRef | Ref to the underlying input element. | `React.Ref` | | diff --git a/src/components/Checkbox/__snapshots__/Checkbox.visual.test.tsx-snapshots/Checkbox-smoke-checked-dark-chromium-linux.png b/src/components/Checkbox/__snapshots__/Checkbox.visual.test.tsx-snapshots/Checkbox-smoke-checked-dark-chromium-linux.png new file mode 100644 index 0000000000..11585ae890 Binary files /dev/null and b/src/components/Checkbox/__snapshots__/Checkbox.visual.test.tsx-snapshots/Checkbox-smoke-checked-dark-chromium-linux.png differ diff --git a/src/components/Checkbox/__snapshots__/Checkbox.visual.test.tsx-snapshots/Checkbox-smoke-checked-light-chromium-linux.png b/src/components/Checkbox/__snapshots__/Checkbox.visual.test.tsx-snapshots/Checkbox-smoke-checked-light-chromium-linux.png new file mode 100644 index 0000000000..6b469a86a2 Binary files /dev/null and b/src/components/Checkbox/__snapshots__/Checkbox.visual.test.tsx-snapshots/Checkbox-smoke-checked-light-chromium-linux.png differ diff --git a/src/components/Checkbox/__snapshots__/Checkbox.visual.test.tsx-snapshots/Checkbox-smoke-dark-chromium-linux.png b/src/components/Checkbox/__snapshots__/Checkbox.visual.test.tsx-snapshots/Checkbox-smoke-dark-chromium-linux.png index e84e7ccb4b..562177811e 100644 Binary files a/src/components/Checkbox/__snapshots__/Checkbox.visual.test.tsx-snapshots/Checkbox-smoke-dark-chromium-linux.png and b/src/components/Checkbox/__snapshots__/Checkbox.visual.test.tsx-snapshots/Checkbox-smoke-dark-chromium-linux.png differ diff --git a/src/components/Checkbox/__snapshots__/Checkbox.visual.test.tsx-snapshots/Checkbox-smoke-indeterminate-dark-chromium-linux.png b/src/components/Checkbox/__snapshots__/Checkbox.visual.test.tsx-snapshots/Checkbox-smoke-indeterminate-dark-chromium-linux.png new file mode 100644 index 0000000000..420bd5dd15 Binary files /dev/null and b/src/components/Checkbox/__snapshots__/Checkbox.visual.test.tsx-snapshots/Checkbox-smoke-indeterminate-dark-chromium-linux.png differ diff --git a/src/components/Checkbox/__snapshots__/Checkbox.visual.test.tsx-snapshots/Checkbox-smoke-indeterminate-light-chromium-linux.png b/src/components/Checkbox/__snapshots__/Checkbox.visual.test.tsx-snapshots/Checkbox-smoke-indeterminate-light-chromium-linux.png new file mode 100644 index 0000000000..ccfe22c063 Binary files /dev/null and b/src/components/Checkbox/__snapshots__/Checkbox.visual.test.tsx-snapshots/Checkbox-smoke-indeterminate-light-chromium-linux.png differ diff --git a/src/components/Checkbox/__snapshots__/Checkbox.visual.test.tsx-snapshots/Checkbox-smoke-light-chromium-linux.png b/src/components/Checkbox/__snapshots__/Checkbox.visual.test.tsx-snapshots/Checkbox-smoke-light-chromium-linux.png index e6af9c672a..8970951108 100644 Binary files a/src/components/Checkbox/__snapshots__/Checkbox.visual.test.tsx-snapshots/Checkbox-smoke-light-chromium-linux.png and b/src/components/Checkbox/__snapshots__/Checkbox.visual.test.tsx-snapshots/Checkbox-smoke-light-chromium-linux.png differ diff --git a/src/components/Checkbox/__stories__/Checkbox.stories.tsx b/src/components/Checkbox/__stories__/Checkbox.stories.tsx index 7daa2bbad5..7cb452e6e9 100644 --- a/src/components/Checkbox/__stories__/Checkbox.stories.tsx +++ b/src/components/Checkbox/__stories__/Checkbox.stories.tsx @@ -66,6 +66,22 @@ export const Disabled: Story = { ), }; +export const Invalid: Story = { + render: (args) => ( + + + Unchecked + + + Checked + + + Indeterminate + + + ), +}; + export const ShowcaseStory: Story = { render: () => , name: 'Showcase', diff --git a/src/components/Checkbox/__stories__/CheckboxShowcase.tsx b/src/components/Checkbox/__stories__/CheckboxShowcase.tsx index b1e041e57e..46e344f61e 100644 --- a/src/components/Checkbox/__stories__/CheckboxShowcase.tsx +++ b/src/components/Checkbox/__stories__/CheckboxShowcase.tsx @@ -39,6 +39,42 @@ export function CheckboxShowcase() { + +
+ +
+
+ +
+
+ +
+
+ +
+ +
+
+ +
+
+ +
+
diff --git a/src/components/Checkbox/__tests__/Checkbox.test.tsx b/src/components/Checkbox/__tests__/Checkbox.test.tsx index dcf57edfdc..7472ba9640 100644 --- a/src/components/Checkbox/__tests__/Checkbox.test.tsx +++ b/src/components/Checkbox/__tests__/Checkbox.test.tsx @@ -39,6 +39,20 @@ describe('Checkbox', () => { expect(checkbox).not.toBeDisabled(); }); + test('invalid when validationState="invalid" prop is given', () => { + render(); + const checkbox = screen.getByRole('checkbox'); + + expect(checkbox).toBeInvalid(); + }); + + test('valid when validationState prop is not given', () => { + render(); + const checkbox = screen.getByRole('checkbox'); + + expect(checkbox).toBeValid(); + }); + test('render with indeterminate', () => { render(); const checkbox = screen.getByRole('checkbox'); diff --git a/src/components/Checkbox/__tests__/Checkbox.visual.test.tsx b/src/components/Checkbox/__tests__/Checkbox.visual.test.tsx index a30f9ba1e3..bbbb99440d 100644 --- a/src/components/Checkbox/__tests__/Checkbox.visual.test.tsx +++ b/src/components/Checkbox/__tests__/Checkbox.visual.test.tsx @@ -4,23 +4,76 @@ import {createSmokeScenarios} from '../../../stories/tests-factory/create-smoke- import type {CheckboxProps} from '../Checkbox'; import {Checkbox} from '../Checkbox'; -import {checkedCases, disabledCases, indeterminateCases, sizeCases} from './cases'; +import {disabledCases, sizeCases, validationStateCases} from './cases'; test.describe('Checkbox', {tag: '@Checkbox'}, () => { + const defaultProps: CheckboxProps = { + name: '', + value: '', + content: 'Checkbox label', + }; + + const commonPropsCases = { + size: sizeCases, + disabled: disabledCases, + validationState: validationStateCases, + } as const; + smokeTest('', async ({mount, expectScreenshot}) => { + const smokeScenarios = createSmokeScenarios( + defaultProps, + commonPropsCases, + {}, + ); + + await mount( +
+ {smokeScenarios.map(([title, props]) => ( +
+

{title}

+
+ +
+
+ ))} +
, + ); + + await expectScreenshot({}); + }); + + smokeTest('checked', async ({mount, expectScreenshot}) => { const smokeScenarios = createSmokeScenarios( { - name: '', - value: '', - content: 'Checkbox label', + ...defaultProps, + checked: true, }, + commonPropsCases, + ); + + await mount( +
+ {smokeScenarios.map(([title, props]) => ( +
+

{title}

+
+ +
+
+ ))} +
, + ); + + await expectScreenshot({}); + }); + + smokeTest('indeterminate', async ({mount, expectScreenshot}) => { + const smokeScenarios = createSmokeScenarios( { - size: sizeCases, - disabled: disabledCases, - checked: checkedCases, - indeterminate: indeterminateCases, + ...defaultProps, + indeterminate: true, }, - {}, + commonPropsCases, ); await mount( diff --git a/src/components/Checkbox/__tests__/cases.tsx b/src/components/Checkbox/__tests__/cases.tsx index 1ee6b3cc97..296978eb30 100644 --- a/src/components/Checkbox/__tests__/cases.tsx +++ b/src/components/Checkbox/__tests__/cases.tsx @@ -5,6 +5,4 @@ export const sizeCases: Array = ['m', 'l']; export const disabledCases: Cases = [true]; -export const checkedCases: Cases = [true]; - -export const indeterminateCases: Cases = [true]; +export const validationStateCases: Cases = ['invalid']; diff --git a/src/components/Radio/README-ru.md b/src/components/Radio/README-ru.md index cd55fbcdce..a44959f6be 100644 --- a/src/components/Radio/README-ru.md +++ b/src/components/Radio/README-ru.md @@ -132,25 +132,26 @@ LANDING_BLOCK--> ## Свойства -| Имя | Описание | Тип | Значение по умолчанию | -| :------------- | :------------------------------------------------------------------------------------------------------------------- | :-------------------------------------------: | :-------------------: | -| children | Содержимое радио (как правило, лейбл). | `ReactNode` | | -| content | Содержимое радио (альтернатива `children`). | `ReactNode` | | -| disabled | Включает или отключает состояние `disabled` у радио. | `boolean` | `false` | -| checked | Включает или отключает состояние `checked` у радио. | `boolean` | `false` | -| defaultChecked | Задает начальное состояние `checked` при монтировании компонента. | `boolean` | `false` | -| onUpdate | Срабатывает при изменении состояния радио пользователем и передает значение `checked` как аргумент обратного вызова. | `(checked: boolean) => void` | | -| onChange | Срабатывает при изменении состояния радио пользователем и передает событие изменения как аргумент обратного вызова. | `Function` | | -| onFocus | Обработчик события, вызываемый, когда элемент ввода радио получает фокус. | `Function` | | -| onBlur | Обработчик события, вызываемый, когда элемент ввода радио теряет фокус. | `Function` | | -| size | Определяет размер радио. | `m` `l` | `m` | -| id | HTML-атрибут `id`. | `string` | | -| qa | HTML-атрибут `data-qa`, используется для тестирования. | `string` | | -| style | HTML-атрибут `style`. | `React.CSSProperties` | | -| className | HTML-атрибут `class`. | `string` | | -| title | HTML-атрибут `title`. | `string` | | -| name | HTML-атрибут `name` для элемента ввода. | `string` | | -| value | Значение контрола. | `string` | | -| indeterminate | Включает или отключает состояние неопределенности радио. | `boolean` | `false` | -| controlProps | Дополнительные свойства базового элемента ввода. | `React.InputHTMLAttributes` | | -| controlRef | Ссылка на базовый элемент ввода. | `React.Ref` | | +| Имя | Описание | Тип | Значение по умолчанию | +| :-------------- | :------------------------------------------------------------------------------------------------------------------- | :-------------------------------------------: | :-------------------: | +| children | Содержимое радио (как правило, лейбл). | `ReactNode` | | +| content | Содержимое радио (альтернатива `children`). | `ReactNode` | | +| disabled | Включает или отключает состояние `disabled` у радио. | `boolean` | `false` | +| validationState | Состояние валидации. | `"invalid"` | | +| checked | Включает или отключает состояние `checked` у радио. | `boolean` | `false` | +| defaultChecked | Задает начальное состояние `checked` при монтировании компонента. | `boolean` | `false` | +| onUpdate | Срабатывает при изменении состояния радио пользователем и передает значение `checked` как аргумент обратного вызова. | `(checked: boolean) => void` | | +| onChange | Срабатывает при изменении состояния радио пользователем и передает событие изменения как аргумент обратного вызова. | `Function` | | +| onFocus | Обработчик события, вызываемый, когда элемент ввода радио получает фокус. | `Function` | | +| onBlur | Обработчик события, вызываемый, когда элемент ввода радио теряет фокус. | `Function` | | +| size | Определяет размер радио. | `m` `l` | `m` | +| id | HTML-атрибут `id`. | `string` | | +| qa | HTML-атрибут `data-qa`, используется для тестирования. | `string` | | +| style | HTML-атрибут `style`. | `React.CSSProperties` | | +| className | HTML-атрибут `class`. | `string` | | +| title | HTML-атрибут `title`. | `string` | | +| name | HTML-атрибут `name` для элемента ввода. | `string` | | +| value | Значение контрола. | `string` | | +| indeterminate | Включает или отключает состояние неопределенности радио. | `boolean` | `false` | +| controlProps | Дополнительные свойства базового элемента ввода. | `React.InputHTMLAttributes` | | +| controlRef | Ссылка на базовый элемент ввода. | `React.Ref` | | diff --git a/src/components/Radio/README.md b/src/components/Radio/README.md index 15a058aba8..d6f9dcdadf 100644 --- a/src/components/Radio/README.md +++ b/src/components/Radio/README.md @@ -129,25 +129,26 @@ LANDING_BLOCK--> ## Properties -| Name | Description | Type | Default | -| :------------- | :------------------------------------------------------------------------------------------------------- | :-------------------------------------------: | :-----: | -| children | The content of the radio (usually, a label). | `ReactNode` | | -| content | The content of the radio (alternative to children). | `ReactNode` | | -| disabled | Toggles the `disabled` state of the radio. | `boolean` | `false` | -| checked | Toggles the `checked` state of the radio. | `boolean` | `false` | -| defaultChecked | Sets the initial checked state when the component is mounted | `boolean` | `false` | -| onUpdate | Fires when the radio state is changed by the user and provides the checked value as a callback argument. | `(checked: boolean) => void` | | -| onChange | Fires when the radio state is changed by the user and provides the change event as a callback argument. | `Function` | | -| onFocus | Event handler to use when the radio input element receives focus. | `Function` | | -| onBlur | Event handler to use when the radio input element loses focus. | `Function` | | -| size | Sets the size of the radio. | `m` `l` | `m` | -| id | `id` HTML attribute | `string` | | -| qa | `data-qa` HTML attribute, used for testing. | `string` | | -| style | `style` HTML attribute | `React.CSSProperties` | | -| className | `class` HTML attribute | `string` | | -| title | `title` HTML attribute | `string` | | -| name | `name` HTML attribute for the input element | `string` | | -| value | Control value | `string` | | -| indeterminate | Toggles the indeterminate state of the radio. | `boolean` | `false` | -| controlProps | Additional propeties for the underlying input element | `React.InputHTMLAttributes` | | -| controlRef | Ref to the underlying input element | `React.Ref` | | +| Name | Description | Type | Default | +| :-------------- | :------------------------------------------------------------------------------------------------------- | :-------------------------------------------: | :-----: | +| children | The content of the radio (usually, a label). | `ReactNode` | | +| content | The content of the radio (alternative to children). | `ReactNode` | | +| disabled | Toggles the `disabled` state of the radio. | `boolean` | `false` | +| validationState | Validation state. | `"invalid"` | | +| checked | Toggles the `checked` state of the radio. | `boolean` | `false` | +| defaultChecked | Sets the initial checked state when the component is mounted | `boolean` | `false` | +| onUpdate | Fires when the radio state is changed by the user and provides the checked value as a callback argument. | `(checked: boolean) => void` | | +| onChange | Fires when the radio state is changed by the user and provides the change event as a callback argument. | `Function` | | +| onFocus | Event handler to use when the radio input element receives focus. | `Function` | | +| onBlur | Event handler to use when the radio input element loses focus. | `Function` | | +| size | Sets the size of the radio. | `m` `l` | `m` | +| id | `id` HTML attribute | `string` | | +| qa | `data-qa` HTML attribute, used for testing. | `string` | | +| style | `style` HTML attribute | `React.CSSProperties` | | +| className | `class` HTML attribute | `string` | | +| title | `title` HTML attribute | `string` | | +| name | `name` HTML attribute for the input element | `string` | | +| value | Control value | `string` | | +| indeterminate | Toggles the indeterminate state of the radio. | `boolean` | `false` | +| controlProps | Additional propeties for the underlying input element | `React.InputHTMLAttributes` | | +| controlRef | Ref to the underlying input element | `React.Ref` | | diff --git a/src/components/Radio/Radio.scss b/src/components/Radio/Radio.scss index a05b774278..bf0de2200d 100644 --- a/src/components/Radio/Radio.scss +++ b/src/components/Radio/Radio.scss @@ -28,10 +28,10 @@ $discMarginXLSize: 8px; } } - &__disc::before { - content: ''; + &__disc { position: absolute; + overflow: hidden; border: none; background-color: var(--g-color-text-brand-contrast); border-radius: 50%; @@ -39,6 +39,7 @@ $discMarginXLSize: 8px; transform: scale(0.1); transition: opacity 0.1s, + background-color 0.1s, transform 0.2s; } @@ -79,7 +80,7 @@ $discMarginXLSize: 8px; height: 14px; } - #{$block}__disc::before { + #{$block}__disc { inset: $discMarginMSize; } } @@ -90,7 +91,7 @@ $discMarginXLSize: 8px; height: 17px; } - #{$block}__disc::before { + #{$block}__disc { inset: $discMarginLSize; } } @@ -101,7 +102,7 @@ $discMarginXLSize: 8px; height: 24px; } - #{$block}__disc::before { + #{$block}__disc { inset: $discMarginXLSize; } } @@ -122,7 +123,7 @@ $discMarginXLSize: 8px; border: transparent; } - #{$block}__disc::before { + #{$block}__disc { opacity: 1; transform: scale(1); } @@ -138,9 +139,44 @@ $discMarginXLSize: 8px; } &#{$block}_checked { - #{$block}__disc::before { + #{$block}__disc { background-color: var(--g-color-text-hint); } } } + + &_invalid { + #{$block}__indicator { + &::before { + border: 1px solid var(--g-color-line-danger); + } + + #{$block}__disc { + background-color: var(--g-color-base-background); + } + } + + &:hover { + #{$block}__indicator { + &::before { + border: 1px solid var(--g-color-line-danger); + } + } + } + + &#{$block}_checked { + #{$block}__indicator { + &::before { + background-color: var(--g-color-line-danger); + border: transparent; + } + } + } + + &#{$block}_disabled { + #{$block}__disc { + background-color: var(--g-color-base-generic-accent-disabled-solid); + } + } + } } diff --git a/src/components/Radio/Radio.tsx b/src/components/Radio/Radio.tsx index b4683b9319..9dc05b096e 100644 --- a/src/components/Radio/Radio.tsx +++ b/src/components/Radio/Radio.tsx @@ -4,7 +4,7 @@ import * as React from 'react'; import {useRadio} from '../../hooks/private'; import {ControlLabel} from '../ControlLabel'; -import type {ControlProps, DOMProps, QAProps} from '../types'; +import type {ControlProps, ControlValidationProps, DOMProps, QAProps} from '../types'; import {block} from '../utils/cn'; import './Radio.scss'; @@ -13,7 +13,7 @@ const b = block('radio'); export type RadioSize = 'm' | 'l' | 'xl'; -export interface RadioProps extends ControlProps, DOMProps, QAProps { +export interface RadioProps extends ControlProps, ControlValidationProps, DOMProps, QAProps { value: string; size?: RadioSize; content?: React.ReactNode; @@ -22,7 +22,17 @@ export interface RadioProps extends ControlProps, DOMProps, QAProps { } export const Radio = React.forwardRef(function Radio(props, ref) { - const {size = 'm', disabled = false, content, children, title, style, className, qa} = props; + const { + size = 'm', + disabled = false, + validationState, + content, + children, + title, + style, + className, + qa, + } = props; const {checked, inputProps} = useRadio(props); const text = content || children; @@ -46,6 +56,7 @@ export const Radio = React.forwardRef(function Rad size, disabled, checked, + invalid: validationState === 'invalid', }, className, )} diff --git a/src/components/Radio/__snapshots__/Radio.visual.test.tsx-snapshots/Radio-smoke-dark-chromium-linux.png b/src/components/Radio/__snapshots__/Radio.visual.test.tsx-snapshots/Radio-smoke-dark-chromium-linux.png index 9d7eb5ef21..a86fe07220 100644 Binary files a/src/components/Radio/__snapshots__/Radio.visual.test.tsx-snapshots/Radio-smoke-dark-chromium-linux.png and b/src/components/Radio/__snapshots__/Radio.visual.test.tsx-snapshots/Radio-smoke-dark-chromium-linux.png differ diff --git a/src/components/Radio/__snapshots__/Radio.visual.test.tsx-snapshots/Radio-smoke-default-checked-dark-chromium-linux.png b/src/components/Radio/__snapshots__/Radio.visual.test.tsx-snapshots/Radio-smoke-default-checked-dark-chromium-linux.png index 741cb5bf85..18f6551ecb 100644 Binary files a/src/components/Radio/__snapshots__/Radio.visual.test.tsx-snapshots/Radio-smoke-default-checked-dark-chromium-linux.png and b/src/components/Radio/__snapshots__/Radio.visual.test.tsx-snapshots/Radio-smoke-default-checked-dark-chromium-linux.png differ diff --git a/src/components/Radio/__snapshots__/Radio.visual.test.tsx-snapshots/Radio-smoke-default-checked-light-chromium-linux.png b/src/components/Radio/__snapshots__/Radio.visual.test.tsx-snapshots/Radio-smoke-default-checked-light-chromium-linux.png index 5670bdfe44..295210ed3a 100644 Binary files a/src/components/Radio/__snapshots__/Radio.visual.test.tsx-snapshots/Radio-smoke-default-checked-light-chromium-linux.png and b/src/components/Radio/__snapshots__/Radio.visual.test.tsx-snapshots/Radio-smoke-default-checked-light-chromium-linux.png differ diff --git a/src/components/Radio/__snapshots__/Radio.visual.test.tsx-snapshots/Radio-smoke-disabled-dark-chromium-linux.png b/src/components/Radio/__snapshots__/Radio.visual.test.tsx-snapshots/Radio-smoke-disabled-dark-chromium-linux.png index 0653ce9107..2e7615b1e8 100644 Binary files a/src/components/Radio/__snapshots__/Radio.visual.test.tsx-snapshots/Radio-smoke-disabled-dark-chromium-linux.png and b/src/components/Radio/__snapshots__/Radio.visual.test.tsx-snapshots/Radio-smoke-disabled-dark-chromium-linux.png differ diff --git a/src/components/Radio/__snapshots__/Radio.visual.test.tsx-snapshots/Radio-smoke-disabled-light-chromium-linux.png b/src/components/Radio/__snapshots__/Radio.visual.test.tsx-snapshots/Radio-smoke-disabled-light-chromium-linux.png index 60d23f9b56..6a46877c28 100644 Binary files a/src/components/Radio/__snapshots__/Radio.visual.test.tsx-snapshots/Radio-smoke-disabled-light-chromium-linux.png and b/src/components/Radio/__snapshots__/Radio.visual.test.tsx-snapshots/Radio-smoke-disabled-light-chromium-linux.png differ diff --git a/src/components/Radio/__snapshots__/Radio.visual.test.tsx-snapshots/Radio-smoke-light-chromium-linux.png b/src/components/Radio/__snapshots__/Radio.visual.test.tsx-snapshots/Radio-smoke-light-chromium-linux.png index 7b2c9c7287..b2b9fbc714 100644 Binary files a/src/components/Radio/__snapshots__/Radio.visual.test.tsx-snapshots/Radio-smoke-light-chromium-linux.png and b/src/components/Radio/__snapshots__/Radio.visual.test.tsx-snapshots/Radio-smoke-light-chromium-linux.png differ diff --git a/src/components/Radio/__stories__/Radio.stories.tsx b/src/components/Radio/__stories__/Radio.stories.tsx index 6905508e82..f495a11bac 100644 --- a/src/components/Radio/__stories__/Radio.stories.tsx +++ b/src/components/Radio/__stories__/Radio.stories.tsx @@ -56,6 +56,19 @@ export const Disabled: Story = { ), }; +export const Invalid: Story = { + render: (args) => ( + + + Unchecked + + + Checked + + + ), +}; + export const ShowcaseStory: Story = { render: () => , name: 'Showcase', diff --git a/src/components/Radio/__stories__/RadioShowcase.tsx b/src/components/Radio/__stories__/RadioShowcase.tsx index 60fe2fc4fb..a92cd6869d 100644 --- a/src/components/Radio/__stories__/RadioShowcase.tsx +++ b/src/components/Radio/__stories__/RadioShowcase.tsx @@ -42,6 +42,48 @@ export function RadioShowcase() { />
+ +
+ +
+
+ +
+
+ +
+ +
+
+ +
+
diff --git a/src/components/Radio/__tests__/Radio.test.tsx b/src/components/Radio/__tests__/Radio.test.tsx index 3ab4bde0aa..614afbb788 100644 --- a/src/components/Radio/__tests__/Radio.test.tsx +++ b/src/components/Radio/__tests__/Radio.test.tsx @@ -42,6 +42,20 @@ describe('Radio', () => { expect(radio).not.toBeDisabled(); }); + test('invalid when validationState="invalid" prop is given', () => { + render(); + const radio = screen.getByRole('radio'); + + expect(radio).toBeInvalid(); + }); + + test('valid when validationState prop is not given', () => { + render(); + const radio = screen.getByRole('radio'); + + expect(radio).toBeValid(); + }); + test('set given title to label', () => { const title = 'Some title'; diff --git a/src/components/Radio/__tests__/Radio.visual.test.tsx b/src/components/Radio/__tests__/Radio.visual.test.tsx index 204ac56e20..a2c2739ed9 100644 --- a/src/components/Radio/__tests__/Radio.visual.test.tsx +++ b/src/components/Radio/__tests__/Radio.visual.test.tsx @@ -4,7 +4,7 @@ import {createSmokeScenarios} from '../../../stories/tests-factory/create-smoke- import type {RadioProps} from '../Radio'; import {Radio} from '../Radio'; -import {sizeCases} from './cases'; +import {sizeCases, validationStateCases} from './cases'; test.describe('Radio', {tag: '@Radio'}, () => { const defaultProps: RadioProps = { @@ -14,6 +14,7 @@ test.describe('Radio', {tag: '@Radio'}, () => { const commonPropsCases = { size: sizeCases, + validationState: validationStateCases, } as const; smokeTest('', async ({mount, expectScreenshot}) => { diff --git a/src/components/Radio/__tests__/cases.tsx b/src/components/Radio/__tests__/cases.tsx index fcf67f2ec9..b5a8eb8b81 100644 --- a/src/components/Radio/__tests__/cases.tsx +++ b/src/components/Radio/__tests__/cases.tsx @@ -2,3 +2,5 @@ import type {Cases} from '../../../stories/tests-factory/models'; import type {RadioProps} from '../Radio'; export const sizeCases: Cases = ['m', 'l']; + +export const validationStateCases: Cases = ['invalid']; diff --git a/src/components/RadioGroup/README-ru.md b/src/components/RadioGroup/README-ru.md index 6105ad2982..0d0ced40cb 100644 --- a/src/components/RadioGroup/README-ru.md +++ b/src/components/RadioGroup/README-ru.md @@ -48,6 +48,50 @@ const options: RadioGroupOption[] = [ +### Состояние ошибки + + + + + +```tsx +const options: RadioGroupOption[] = [ + {value: 'Value 1', content: 'Value 1'}, + {value: 'Value 2', content: 'Value 2'}, + {value: 'Value 3', content: 'Value 3'}, +]; + +; +``` + + + ### Размер Размер `RadioGroup` можно настроить с помощью свойства `size`. Размер по умолчанию — `m`. @@ -152,6 +196,7 @@ LANDING_BLOCK--> | :-------------- | :------------------------------------------------------------------------------------------------------------------ | :-----------------------: | :-------------------: | | children | Содержимое радиогруппы. | `ReactNode` | | | disabled | Включает или отключает состояние `disabled` у радиогруппы. | `boolean` | `false` | +| validationState | Состояние валидации. | `"invalid"` | | | options | Варианты для радиогруппы. | `RadioGroupOption[]` | | | optionClassName | HTML-атрибут `class` для дочерних элементов радиогруппы. | `string` | | | direction | Определяет направление расположения радиогруппы. | `horizontal - vertical` | `"horizontal"` | diff --git a/src/components/RadioGroup/README.md b/src/components/RadioGroup/README.md index 51d1e015b2..1592829936 100644 --- a/src/components/RadioGroup/README.md +++ b/src/components/RadioGroup/README.md @@ -48,6 +48,50 @@ const options: RadioGroupOption[] = [ +### Error state + + + + + +```tsx +const options: RadioGroupOption[] = [ + {value: 'Value 1', content: 'Value 1'}, + {value: 'Value 2', content: 'Value 2'}, + {value: 'Value 3', content: 'Value 3'}, +]; + +; +``` + + + ### Size Use the `size` property to manage the `RadioGroup` size. The default size is `m`. @@ -161,6 +205,7 @@ LANDING_BLOCK--> | :-------------- | :------------------------------------------------------------------------------------------------ | :-----------------------: | :------------: | | children | The content of the radio group. | `ReactNode` | | | disabled | Toggles the `disabled` state of the radio group. | `boolean` | `false` | +| validationState | Validation state. | `"invalid"` | | | options | Options for radio group. | `RadioGroupOption[]` | | | optionClassName | `class` HTML attribute for the radio children. | `string` | | | direction | Determines the direction of the radio group. | `horizontal - vertical` | `"horizontal"` | diff --git a/src/components/RadioGroup/RadioGroup.tsx b/src/components/RadioGroup/RadioGroup.tsx index 36c1ab4489..80d0c1a79e 100644 --- a/src/components/RadioGroup/RadioGroup.tsx +++ b/src/components/RadioGroup/RadioGroup.tsx @@ -5,7 +5,13 @@ import * as React from 'react'; import {useRadioGroup} from '../../hooks/private'; import {Radio} from '../Radio'; import type {RadioProps, RadioSize} from '../Radio'; -import type {ControlGroupOption, ControlGroupProps, DOMProps, QAProps} from '../types'; +import type { + ControlGroupOption, + ControlGroupProps, + ControlGroupValidationProps, + DOMProps, + QAProps, +} from '../types'; import {block} from '../utils/cn'; import './RadioGroup.scss'; @@ -16,7 +22,11 @@ export type RadioGroupOption = ControlGroupOption; export type RadioGroupSize = RadioSize; export type RadioGroupDirection = 'vertical' | 'horizontal'; -export interface RadioGroupProps extends ControlGroupProps, DOMProps, QAProps { +export interface RadioGroupProps + extends ControlGroupProps, + ControlGroupValidationProps, + DOMProps, + QAProps { size?: RadioGroupSize; direction?: RadioGroupDirection; children?: @@ -46,11 +56,11 @@ export const RadioGroup = React.forwardRef( if (!options) { options = ( React.Children.toArray(children) as React.ReactElement[] - ).map(({props}) => ({ - value: props.value, - content: props.content || props.children, - disabled: props.disabled, - qa: props.qa, + ).map(({props: optionProps}) => ({ + value: optionProps.value, + content: optionProps.content || optionProps.children, + disabled: optionProps.disabled, + qa: optionProps.qa, })); } diff --git a/src/components/RadioGroup/__snapshots__/RadioGroup.visual.test.tsx-snapshots/RadioGroup-smoke-dark-chromium-linux.png b/src/components/RadioGroup/__snapshots__/RadioGroup.visual.test.tsx-snapshots/RadioGroup-smoke-dark-chromium-linux.png index 7b7d42067a..cbad8a378b 100644 Binary files a/src/components/RadioGroup/__snapshots__/RadioGroup.visual.test.tsx-snapshots/RadioGroup-smoke-dark-chromium-linux.png and b/src/components/RadioGroup/__snapshots__/RadioGroup.visual.test.tsx-snapshots/RadioGroup-smoke-dark-chromium-linux.png differ diff --git a/src/components/RadioGroup/__snapshots__/RadioGroup.visual.test.tsx-snapshots/RadioGroup-smoke-light-chromium-linux.png b/src/components/RadioGroup/__snapshots__/RadioGroup.visual.test.tsx-snapshots/RadioGroup-smoke-light-chromium-linux.png index 050433663b..d31d5dc3a4 100644 Binary files a/src/components/RadioGroup/__snapshots__/RadioGroup.visual.test.tsx-snapshots/RadioGroup-smoke-light-chromium-linux.png and b/src/components/RadioGroup/__snapshots__/RadioGroup.visual.test.tsx-snapshots/RadioGroup-smoke-light-chromium-linux.png differ diff --git a/src/components/RadioGroup/__stories__/RadioGroup.stories.tsx b/src/components/RadioGroup/__stories__/RadioGroup.stories.tsx index 5904e2dd63..f3a63aa84c 100644 --- a/src/components/RadioGroup/__stories__/RadioGroup.stories.tsx +++ b/src/components/RadioGroup/__stories__/RadioGroup.stories.tsx @@ -54,6 +54,13 @@ export const Disabled: Story = { }, }; +export const Invalid: Story = { + args: { + ...Default.args, + validationState: 'invalid', + }, +}; + export const Direction: Story = { args: { ...Default.args, diff --git a/src/components/RadioGroup/__stories__/RadioGroupShowcase.tsx b/src/components/RadioGroup/__stories__/RadioGroupShowcase.tsx index feb14f9136..5462d2ac44 100644 --- a/src/components/RadioGroup/__stories__/RadioGroupShowcase.tsx +++ b/src/components/RadioGroup/__stories__/RadioGroupShowcase.tsx @@ -36,6 +36,15 @@ export function RadioGroupShowcase() { /> + + + + diff --git a/src/components/RadioGroup/__tests__/RadioGroup.test.tsx b/src/components/RadioGroup/__tests__/RadioGroup.test.tsx index 43ef320796..de52556d3f 100644 --- a/src/components/RadioGroup/__tests__/RadioGroup.test.tsx +++ b/src/components/RadioGroup/__tests__/RadioGroup.test.tsx @@ -65,6 +65,33 @@ describe('RadioGroup', () => { }); }); + test('all children are invalid when validationState="invalid" prop is given', () => { + render( + , + ); + const component = screen.getByTestId(qaId); + const radios = within(component).getAllByRole('radio'); + + radios.forEach((radio: HTMLElement) => { + expect(radio).toBeInvalid(); + }); + }); + + test('all children are valid when validationState prop is not given', () => { + render(); + const component = screen.getByTestId(qaId); + const radios = within(component).getAllByRole('radio'); + + radios.forEach((radio: HTMLElement) => { + expect(radio).toBeValid(); + }); + }); + test('a proper radio is disabled when disabled=false prop is given to one of the option', () => { const customOptions: RadioGroupOption[] = [ {value: 'Disabled', content: 'Disabled', disabled: true}, diff --git a/src/components/RadioGroup/__tests__/RadioGroup.visual.test.tsx b/src/components/RadioGroup/__tests__/RadioGroup.visual.test.tsx index d61bf39ace..9bfa1efaf5 100644 --- a/src/components/RadioGroup/__tests__/RadioGroup.visual.test.tsx +++ b/src/components/RadioGroup/__tests__/RadioGroup.visual.test.tsx @@ -4,7 +4,7 @@ import {createSmokeScenarios} from '../../../stories/tests-factory/create-smoke- import type {RadioGroupOption, RadioGroupProps} from '../RadioGroup'; import {RadioGroup} from '../RadioGroup'; -import {directionCases, sizeCases} from './cases'; +import {directionCases, sizeCases, validationStateCases} from './cases'; test.describe('RadioGroup', {tag: '@RadioGroup'}, () => { const options: RadioGroupOption[] = [ @@ -21,6 +21,7 @@ test.describe('RadioGroup', {tag: '@RadioGroup'}, () => { smokeTest('', async ({mount, expectScreenshot}) => { const smokeScenarios = createSmokeScenarios(defaultProps, { size: sizeCases, + validationState: validationStateCases, direction: directionCases, }); diff --git a/src/components/RadioGroup/__tests__/cases.tsx b/src/components/RadioGroup/__tests__/cases.tsx index dd774cde98..83c4b1b6b2 100644 --- a/src/components/RadioGroup/__tests__/cases.tsx +++ b/src/components/RadioGroup/__tests__/cases.tsx @@ -2,4 +2,7 @@ import type {Cases} from '../../../stories/tests-factory/models'; import type {RadioGroupProps} from '../RadioGroup'; export const sizeCases: Cases = ['m', 'l']; + +export const validationStateCases: Cases = ['invalid']; + export const directionCases: Cases = ['vertical', 'horizontal']; diff --git a/src/components/types.ts b/src/components/types.ts index 9307435ff9..03517d3125 100644 --- a/src/components/types.ts +++ b/src/components/types.ts @@ -42,6 +42,10 @@ export interface ControlProps controlRef?: React.Ref; } +export interface ControlValidationProps { + validationState?: 'invalid'; +} + export interface ControlGroupOption { value: ValueType; content?: React.ReactNode; @@ -50,6 +54,10 @@ export interface ControlGroupOption { title?: string; } +export interface ControlGroupValidationProps { + validationState?: 'invalid'; +} + export interface ControlGroupProps extends AriaLabelingProps { name?: string; value?: ValueType | null; diff --git a/src/hooks/private/useCheckbox/README.md b/src/hooks/private/useCheckbox/README.md index cfb7f4671f..21e218d863 100644 --- a/src/hooks/private/useCheckbox/README.md +++ b/src/hooks/private/useCheckbox/README.md @@ -4,21 +4,22 @@ The `useCheckbox` hook need to generate props for checkbox control ## Properties -| Name | Description | Type | Default | -| :------------- | :-------------------------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------: | :-----: | -| name | Name | `string` | | -| value | Value | `string` | | -| checked | Checked flag | `boolean` | | -| defaultChecked | Default checked value | `boolean` | | -| indeterminate | Indeterminate flag | `boolean` | | -| controlRef | Ref-link on control element | `React.Ref` | | -| controlProps | Another control props | `Omit` | | -| disabled | Disabled flag | `boolean` | | -| onUpdate | OnUpdate callback | `(checked: boolean) => void` | | -| onChange | OnChange callback | `(event: React.ChangeEvent) => void` | | -| onFocus | OnFocus callback | `(event: React.FocusEvent) => void` | | -| onBlur | OnBlur callback | `(event: React.FocusEvent) => void` | | -| id | ID attribute | `(event: React.FocusEvent) => void` | | +| Name | Description | Type | Default | +| :-------------- | :-------------------------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------: | :-----: | +| name | Name | `string` | | +| value | Value | `string` | | +| checked | Checked flag | `boolean` | | +| defaultChecked | Default checked value | `boolean` | | +| indeterminate | Indeterminate flag | `boolean` | | +| controlRef | Ref-link on control element | `React.Ref` | | +| controlProps | Another control props | `Omit` | | +| disabled | Disabled flag | `boolean` | | +| validationState | Validation state | `"invalid"` | | +| onUpdate | OnUpdate callback | `(checked: boolean) => void` | | +| onChange | OnChange callback | `(event: React.ChangeEvent) => void` | | +| onFocus | OnFocus callback | `(event: React.FocusEvent) => void` | | +| onBlur | OnBlur callback | `(event: React.FocusEvent) => void` | | +| id | ID attribute | `(event: React.FocusEvent) => void` | | ## Result diff --git a/src/hooks/private/useCheckbox/useCheckbox.ts b/src/hooks/private/useCheckbox/useCheckbox.ts index c40045fffd..6fd4581692 100644 --- a/src/hooks/private/useCheckbox/useCheckbox.ts +++ b/src/hooks/private/useCheckbox/useCheckbox.ts @@ -1,11 +1,11 @@ import * as React from 'react'; -import {useControlledState, useForkRef} from '../..'; -import type {ControlProps} from '../../../components/types'; +import type {ControlProps, ControlValidationProps} from '../../../components/types'; import {eventBroker} from '../../../components/utils/event-broker'; +import {useControlledState, useForkRef} from '../../../hooks'; import {useFormResetHandler} from '../useFormResetHandler'; -export type UseCheckboxProps = ControlProps; +export type UseCheckboxProps = ControlProps & ControlValidationProps; export type UseCheckboxResult = { checked: boolean; @@ -19,6 +19,7 @@ export function useCheckbox({ defaultChecked, checked, indeterminate, + validationState, onUpdate, onChange, controlRef, @@ -84,6 +85,7 @@ export function useCheckbox({ defaultChecked: defaultChecked, checked: inputChecked, 'aria-checked': inputAriaChecked, + 'aria-invalid': validationState === 'invalid' || undefined, ref: handleRef, }; diff --git a/src/hooks/private/useRadio/README.md b/src/hooks/private/useRadio/README.md index 8b3aab1e04..93ed32635a 100644 --- a/src/hooks/private/useRadio/README.md +++ b/src/hooks/private/useRadio/README.md @@ -4,20 +4,21 @@ The `useRadio` hook need to generate props for radio group control ## Properties -| Name | Description | Type | Default | -| :------------- | :-------------------------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------: | :-----: | -| name | Name | `string` | | -| value | Value | `string` | | -| checked | Checked flag | `boolean` | | -| defaultChecked | Default checked value | `boolean` | | -| controlRef | Ref-link on control element | `React.Ref` | | -| controlProps | Another control props | `Omit` | | -| disabled | Disabled flag | `boolean` | | -| onUpdate | OnUpdate callback | `(checked: boolean) => void` | | -| onChange | OnChange callback | `(event: React.ChangeEvent) => void` | | -| onFocus | OnFocus callback | `(event: React.FocusEvent) => void` | | -| onBlur | OnBlur callback | `(event: React.FocusEvent) => void` | | -| id | ID attribute | `(event: React.FocusEvent) => void` | | +| Name | Description | Type | Default | +| :-------------- | :-------------------------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------: | :-----: | +| name | Name | `string` | | +| value | Value | `string` | | +| checked | Checked flag | `boolean` | | +| defaultChecked | Default checked value | `boolean` | | +| controlRef | Ref-link on control element | `React.Ref` | | +| controlProps | Another control props | `Omit` | | +| disabled | Disabled flag | `boolean` | | +| validationState | Validation state | `"invalid"` | | +| onUpdate | OnUpdate callback | `(checked: boolean) => void` | | +| onChange | OnChange callback | `(event: React.ChangeEvent) => void` | | +| onFocus | OnFocus callback | `(event: React.FocusEvent) => void` | | +| onBlur | OnBlur callback | `(event: React.FocusEvent) => void` | | +| id | ID attribute | `(event: React.FocusEvent) => void` | | ## Result diff --git a/src/hooks/private/useRadio/useRadio.ts b/src/hooks/private/useRadio/useRadio.ts index 4637d95c1b..69d6748bf2 100644 --- a/src/hooks/private/useRadio/useRadio.ts +++ b/src/hooks/private/useRadio/useRadio.ts @@ -1,11 +1,11 @@ import * as React from 'react'; import {useControlledState, useForkRef, useUniqId} from '../..'; -import type {ControlProps} from '../../../components/types'; +import type {ControlProps, ControlValidationProps} from '../../../components/types'; import {eventBroker} from '../../../components/utils/event-broker'; import {useFormResetHandler} from '../useFormResetHandler'; -export type UseRadioProps = ControlProps; +export type UseRadioProps = ControlProps & ControlValidationProps; export type UseRadioResult = { checked: boolean; @@ -18,6 +18,7 @@ export function useRadio({ checked, defaultChecked, disabled, + validationState, controlRef, controlProps, onUpdate, @@ -69,6 +70,7 @@ export function useRadio({ checked, defaultChecked: defaultChecked, 'aria-checked': isChecked, + 'aria-invalid': validationState === 'invalid' || undefined, ref: handleRef, }; diff --git a/src/hooks/private/useRadioGroup/README.md b/src/hooks/private/useRadioGroup/README.md index f3170c01ca..b59b9a3fc2 100644 --- a/src/hooks/private/useRadioGroup/README.md +++ b/src/hooks/private/useRadioGroup/README.md @@ -4,17 +4,18 @@ The `useRadioGroup` hook need to generate props for radio group control ## Properties -| Name | Description | Type | Default | -| :----------- | :---------------- | :--------------------------------------------------------------------------------------------: | :-----: | -| name | Radio group name | `string` | | -| value | Value | `string` | | -| defaultValue | Default value | `string` | | -| options | Options | `{value: string; content?: React.ReactNode; children?: React.ReactNode; disabled?: boolean}[]` | [] | -| disabled | Disabled flag | `boolean` | | -| onUpdate | OnUpdate callback | `(value: string) => void` | | -| onChange | OnChange callback | `(event: React.ChangeEvent) => void` | | -| onFocus | OnFocus callback | `(event: React.FocusEvent) => void` | | -| onBlur | OnBlur callback | `(event: React.FocusEvent) => void` | | +| Name | Description | Type | Default | +| :-------------- | :---------------- | :--------------------------------------------------------------------------------------------: | :-----: | +| name | Radio group name | `string` | | +| value | Value | `string` | | +| defaultValue | Default value | `string` | | +| options | Options | `{value: string; content?: React.ReactNode; children?: React.ReactNode; disabled?: boolean}[]` | [] | +| disabled | Disabled flag | `boolean` | | +| validationState | Validation state | `"invalid"` | | +| onUpdate | OnUpdate callback | `(value: string) => void` | | +| onChange | OnChange callback | `(event: React.ChangeEvent) => void` | | +| onFocus | OnFocus callback | `(event: React.FocusEvent) => void` | | +| onBlur | OnBlur callback | `(event: React.FocusEvent) => void` | | ## Result diff --git a/src/hooks/private/useRadioGroup/useRadioGroup.ts b/src/hooks/private/useRadioGroup/useRadioGroup.ts index c43f7fa5af..9bebc30b3a 100644 --- a/src/hooks/private/useRadioGroup/useRadioGroup.ts +++ b/src/hooks/private/useRadioGroup/useRadioGroup.ts @@ -1,7 +1,12 @@ import * as React from 'react'; import {useControlledState, useFocusWithin, useUniqId} from '../..'; -import type {ControlGroupOption, ControlGroupProps} from '../../../components/types'; +import type { + ControlGroupOption, + ControlGroupProps, + ControlGroupValidationProps, + ControlValidationProps, +} from '../../../components/types'; import {filterDOMProps} from '../../../components/utils/filterDOMProps'; import {useFormResetHandler} from '../useFormResetHandler'; @@ -9,15 +14,17 @@ import type {RadioGroupContextProps} from './types'; interface OptionsProps extends Omit< - ControlGroupProps, - 'options' | 'defaultValue' | 'aria-label' | 'aria-labelledby' | 'onUpdate' | 'value' - > { + ControlGroupProps, + 'options' | 'defaultValue' | 'aria-label' | 'aria-labelledby' | 'onUpdate' | 'value' + >, + ControlValidationProps { value: ValueType; checked: boolean; content: ControlGroupOption['content']; } -export type UseRadioGroupProps = ControlGroupProps; +export type UseRadioGroupProps = ControlGroupProps & + ControlGroupValidationProps; export type UseRadioGroupResult = { containerProps: Pick & { @@ -37,6 +44,7 @@ export function useRadioGroup( defaultValue, options = [], disabled, + validationState, onUpdate, onChange, onFocus, @@ -44,6 +52,7 @@ export function useRadioGroup( } = props; const controlId = useUniqId(); + const [currentValue, setValueState] = useControlledState( value, defaultValue ?? null, @@ -93,6 +102,7 @@ export function useRadioGroup( title: option.title, checked: currentValue === String(option.value), disabled: disabled || option.disabled, + validationState, onChange: handleChange, ref: fieldRef, })); diff --git a/src/hooks/useFocusWithin/useFocusWithin.ts b/src/hooks/useFocusWithin/useFocusWithin.ts index 0ebede3804..f6a945b386 100644 --- a/src/hooks/useFocusWithin/useFocusWithin.ts +++ b/src/hooks/useFocusWithin/useFocusWithin.ts @@ -38,11 +38,11 @@ export interface UseFocusWithinResult { /** * Handles focus events for the target and its descendants. * - * @param {Object} props - * @param {boolean} [props.isDisabled=false] - whether the focus within events should be disabled. - * @param {onFocusEventCallback} props.onFocusWithin - handler that is called when the target element or a descendant receives focus. - * @param {onFocusEventCallback} props.onBlurWithin - handler that is called when the target element and all descendants lose focus. - * @param {onFocusChangeCallback} props.onFocusChange - handler that is called when the the focus within state changes. + * @param {Object} optionProps + * @param {boolean} [optionProps.isDisabled=false] - whether the focus within events should be disabled. + * @param {onFocusEventCallback} optionProps.onFocusWithin - handler that is called when the target element or a descendant receives focus. + * @param {onFocusEventCallback} optionProps.onBlurWithin - handler that is called when the target element and all descendants lose focus. + * @param {onFocusChangeCallback} optionProps.onFocusChange - handler that is called when the the focus within state changes. * * @returns container props * diff --git a/styles/themes/dark-hc/base.scss b/styles/themes/dark-hc/base.scss index f868975406..d9e6c92635 100644 --- a/styles/themes/dark-hc/base.scss +++ b/styles/themes/dark-hc/base.scss @@ -6,6 +6,7 @@ --g-color-base-generic-medium-hover: var(--g-color-private-white-400); --g-color-base-generic-accent: var(--g-color-private-white-200); --g-color-base-generic-accent-disabled: var(--g-color-private-white-150); + --g-color-base-generic-accent-disabled-solid: var(--g-color-private-white-150-solid); --g-color-base-generic-ultralight: var(--g-color-private-white-50); --g-color-base-simple-hover: var(--g-color-private-white-250); --g-color-base-simple-hover-solid: var(--g-color-private-white-250-solid); diff --git a/styles/themes/dark/base.scss b/styles/themes/dark/base.scss index 83df85f828..37e3fd6aa2 100644 --- a/styles/themes/dark/base.scss +++ b/styles/themes/dark/base.scss @@ -6,6 +6,7 @@ --g-color-base-generic-medium-hover: var(--g-color-private-white-300); --g-color-base-generic-accent: var(--g-color-private-white-150); --g-color-base-generic-accent-disabled: var(--g-color-private-white-70); + --g-color-base-generic-accent-disabled-solid: var(--g-color-private-white-70-solid); --g-color-base-generic-ultralight: var(--g-color-private-white-20-solid); --g-color-base-simple-hover: var(--g-color-private-white-100); --g-color-base-simple-hover-solid: var(--g-color-private-white-100-solid); diff --git a/styles/themes/light-hc/base.scss b/styles/themes/light-hc/base.scss index 5b5e2b4609..a36348fc94 100644 --- a/styles/themes/light-hc/base.scss +++ b/styles/themes/light-hc/base.scss @@ -6,6 +6,7 @@ --g-color-base-generic-medium-hover: var(--g-color-private-black-350); --g-color-base-generic-accent: var(--g-color-private-black-250); --g-color-base-generic-accent-disabled: var(--g-color-private-black-150); + --g-color-base-generic-accent-disabled-solid: var(--g-color-private-black-150-solid); --g-color-base-generic-ultralight: var(--g-color-private-black-50-solid); --g-color-base-simple-hover: var(--g-color-private-black-150); --g-color-base-simple-hover-solid: var(--g-color-private-black-150-solid); diff --git a/styles/themes/light/base.scss b/styles/themes/light/base.scss index 57a09d43a3..e213b8e527 100644 --- a/styles/themes/light/base.scss +++ b/styles/themes/light/base.scss @@ -6,6 +6,7 @@ --g-color-base-generic-medium-hover: var(--g-color-private-black-250); --g-color-base-generic-accent: var(--g-color-private-black-150); --g-color-base-generic-accent-disabled: var(--g-color-private-black-70); + --g-color-base-generic-accent-disabled-solid: var(--g-color-private-black-70-solid); --g-color-base-generic-ultralight: var(--g-color-private-black-20-solid); --g-color-base-simple-hover: var(--g-color-private-black-50); --g-color-base-simple-hover-solid: var(--g-color-private-black-50-solid); diff --git a/styles/themes/light/private.scss b/styles/themes/light/private.scss index 99aae395ed..6627f30cb1 100644 --- a/styles/themes/light/private.scss +++ b/styles/themes/light/private.scss @@ -45,6 +45,7 @@ --g-color-private-black-20-solid: rgb(250, 250, 250); --g-color-private-black-50-solid: rgb(242, 242, 242); + --g-color-private-black-70-solid: rgb(237, 237, 237); --g-color-private-black-100-solid: rgb(229, 229, 229); --g-color-private-black-150-solid: rgb(217, 217, 217); --g-color-private-black-200-solid: rgb(204, 204, 204);