From 8b9879145616fdf07a667d358c0ec4712ebb23aa Mon Sep 17 00:00:00 2001 From: Joan Perals Tresserra Date: Tue, 18 Nov 2025 19:16:09 +0100 Subject: [PATCH 01/22] Move style utils to internal radio button component --- src/internal/components/radio-button/index.tsx | 2 +- .../components/radio-button}/style.tsx | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) rename src/{radio-group => internal/components/radio-button}/style.tsx (92%) diff --git a/src/internal/components/radio-button/index.tsx b/src/internal/components/radio-button/index.tsx index 31658cbd26..1c18bdd630 100644 --- a/src/internal/components/radio-button/index.tsx +++ b/src/internal/components/radio-button/index.tsx @@ -7,12 +7,12 @@ import { useMergeRefs } from '@cloudscape-design/component-toolkit/internal'; import { useSingleTabStopNavigation } from '@cloudscape-design/component-toolkit/internal'; import { copyAnalyticsMetadataAttribute } from '@cloudscape-design/component-toolkit/internal/analytics-metadata'; -import { getAbstractSwitchStyles, getInnerCircleStyle, getOuterCircleStyle } from '../../../radio-group/style'; import { getBaseProps } from '../../base-component'; import AbstractSwitch from '../../components/abstract-switch'; import { fireNonCancelableEvent } from '../../events'; import { InternalBaseComponentProps } from '../../hooks/use-base-component'; import { RadioButtonProps } from './interfaces'; +import { getAbstractSwitchStyles, getInnerCircleStyle, getOuterCircleStyle } from './style'; import styles from './styles.css.js'; import testUtilStyles from './test-classes/styles.css.js'; diff --git a/src/radio-group/style.tsx b/src/internal/components/radio-button/style.tsx similarity index 92% rename from src/radio-group/style.tsx rename to src/internal/components/radio-button/style.tsx index 84273a2b8a..04de395703 100644 --- a/src/radio-group/style.tsx +++ b/src/internal/components/radio-button/style.tsx @@ -1,8 +1,8 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import { SYSTEM } from '../internal/environment'; -import { getComputedAbstractSwitchState } from '../internal/utils/style'; -import { RadioGroupProps } from './interfaces'; +import { RadioGroupProps } from '../../../radio-group/interfaces'; +import { SYSTEM } from '../../environment'; +import { getComputedAbstractSwitchState } from '../../utils/style'; export function getOuterCircleStyle( style: RadioGroupProps.Style | undefined, From 712018ef6f212891ad08edeac141ac4c0c9960df Mon Sep 17 00:00:00 2001 From: Joan Perals Tresserra Date: Tue, 18 Nov 2025 12:33:11 +0100 Subject: [PATCH 02/22] feat: Add Radio Button component to Core --- build-tools/utils/pluralize.js | 1 + pages/radio-button/cards.page.tsx | 112 ++++++ pages/radio-button/cards.scss | 48 +++ pages/radio-button/common.tsx | 39 ++ pages/radio-button/custom-style.tsx | 47 +++ pages/radio-button/focus-test.page.tsx | 27 ++ pages/radio-button/permutations.page.tsx | 31 ++ pages/radio-button/style-custom.page.tsx | 33 ++ pages/radio-button/styles.scss | 19 + pages/radio-button/table.page.tsx | 123 ++++++ pages/radio-button/table.scss | 43 +++ pages/radio-button/test.page.tsx | 233 +++++++++++ pages/radio-group/common-permutations.tsx | 10 +- pages/radio-group/style-custom.page.tsx | 52 +-- .../__snapshots__/documenter.test.ts.snap | 361 ++++++++++++++++++ .../test-utils-wrappers.test.tsx.snap | 66 ++++ .../components/radio-button/interfaces.ts | 8 - .../__tests__/radio-button.test.tsx | 35 ++ src/radio-button/index.tsx | 35 ++ src/radio-button/interfaces.ts | 5 + src/radio-button/styles.scss | 10 + .../__tests__/radio-group.test.tsx | 2 +- .../page-size-preference.ts | 2 +- .../radio-button.ts => radio-button/index.ts} | 0 src/test-utils/dom/radio-group/index.ts | 2 +- src/test-utils/dom/tiles/tile.ts | 2 +- 26 files changed, 1280 insertions(+), 66 deletions(-) create mode 100644 pages/radio-button/cards.page.tsx create mode 100644 pages/radio-button/cards.scss create mode 100644 pages/radio-button/common.tsx create mode 100644 pages/radio-button/custom-style.tsx create mode 100644 pages/radio-button/focus-test.page.tsx create mode 100644 pages/radio-button/permutations.page.tsx create mode 100644 pages/radio-button/style-custom.page.tsx create mode 100644 pages/radio-button/styles.scss create mode 100644 pages/radio-button/table.page.tsx create mode 100644 pages/radio-button/table.scss create mode 100644 pages/radio-button/test.page.tsx create mode 100644 src/radio-button/__tests__/radio-button.test.tsx create mode 100644 src/radio-button/index.tsx create mode 100644 src/radio-button/interfaces.ts create mode 100644 src/radio-button/styles.scss rename src/test-utils/dom/{radio-group/radio-button.ts => radio-button/index.ts} (100%) diff --git a/build-tools/utils/pluralize.js b/build-tools/utils/pluralize.js index 76078281f6..c3f11c3ed9 100644 --- a/build-tools/utils/pluralize.js +++ b/build-tools/utils/pluralize.js @@ -59,6 +59,7 @@ const pluralizationMap = { ProgressBar: 'ProgressBars', PromptInput: 'PromptInputs', PropertyFilter: 'PropertyFilters', + RadioButton: 'RadioButtons', RadioGroup: 'RadioGroups', S3ResourceSelector: 'S3ResourceSelectors', SegmentedControl: 'SegmentedControls', diff --git a/pages/radio-button/cards.page.tsx b/pages/radio-button/cards.page.tsx new file mode 100644 index 0000000000..ae5691025e --- /dev/null +++ b/pages/radio-button/cards.page.tsx @@ -0,0 +1,112 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +import React, { useContext, useRef, useState } from 'react'; +import clsx from 'clsx'; + +import Box from '~components/box'; +import Checkbox from '~components/checkbox'; +import FormField from '~components/form-field'; +import RadioButton, { RadioButtonProps } from '~components/radio-button'; +import SpaceBetween from '~components/space-between'; + +import AppContext, { AppContextType } from '../app/app-context'; +import ScreenshotArea from '../utils/screenshot-area'; +import { ExtraOptions, options } from './common'; + +import styles from './cards.scss'; + +type RadioButtonDemoContext = React.Context< + AppContextType<{ + disabled?: boolean; + readOnly?: boolean; + }> +>; + +const Card = ({ + label, + checked, + onChange, + description, + disabled, + readOnly, +}: Omit & { label: string }) => { + const ref = useRef(null); + return ( +
  • +
    +

    + {label} +

    +

    {description}

    +
    + +
  • + ); +}; + +export default function RadioButtonsCardsPage() { + const { urlParams, setUrlParams } = useContext(AppContext as RadioButtonDemoContext); + + const [value, setValue] = useState(''); + + return ( +
    +

    Radio button — custom card selection

    + + + + + setUrlParams({ ...urlParams, disabled: detail.checked })} + description="Make one of the radio buttons disabled" + > + Disabled + + + setUrlParams({ ...urlParams, readOnly: detail.checked })} + description="Make one of the radio buttons read-only" + > + Read-only + + + + + +
    + +
      + {options.map((option, index) => ( + { + if (detail.checked) { + setValue(option.value); + } + }} + disabled={option.allowDisabled && urlParams.disabled} + readOnly={option.allowReadOnly && urlParams.readOnly} + /> + ))} +
    + + + +
    +
    + ); +} diff --git a/pages/radio-button/cards.scss b/pages/radio-button/cards.scss new file mode 100644 index 0000000000..e68caab42b --- /dev/null +++ b/pages/radio-button/cards.scss @@ -0,0 +1,48 @@ +/* + Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + SPDX-License-Identifier: Apache-2.0 +*/ + +@use '~design-tokens' as awsui; + +.cards { + display: flex; + flex-wrap: wrap; + gap: awsui.$space-scaled-xs; + list-style-type: none; + margin-block: 0; + margin-inline: 0; + padding-block: 0; + padding-inline: 0; +} + +.card { + $border-radius: awsui.$space-scaled-s; + align-items: start; + border-color: awsui.$color-border-control-default; + border-start-start-radius: $border-radius; + border-start-end-radius: $border-radius; + border-end-start-radius: $border-radius; + border-end-end-radius: $border-radius; + border-block-style: solid; + border-inline-style: solid; + border-block-width: 1px; + border-inline-width: 1px; + display: flex; + gap: awsui.$space-scaled-s; + inline-size: 300px; + justify-content: space-between; + padding-block: awsui.$space-scaled-l; + padding-inline: awsui.$space-scaled-l; + + &--selected { + border-color: awsui.$color-border-item-focused; + } +} + +.heading { + margin-block: 0; +} +.description { + margin-block: awsui.$space-scaled-xs 0; +} diff --git a/pages/radio-button/common.tsx b/pages/radio-button/common.tsx new file mode 100644 index 0000000000..2bf5d4fa80 --- /dev/null +++ b/pages/radio-button/common.tsx @@ -0,0 +1,39 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +import React, { useState } from 'react'; + +import FormField from '~components/form-field'; +import Input from '~components/input'; +import { RadioButtonProps } from '~components/radio-button'; + +import createPermutations from '../utils/permutations'; + +export const options = [ + { value: 'email', label: 'E-Mail', description: 'First option' }, + { value: 'phone', label: 'Telephone', description: 'Second option', allowDisabled: true, allowReadOnly: true }, + { value: 'mail', label: 'Postal mail', description: 'Third option' }, +]; + +export const ExtraOptions = () => { + const [value, setValue] = useState(''); + return ( + + setValue(detail.value)} placeholder="Enter your address" /> + + ); +}; + +export const shortText = 'Short text'; + +export const longText = + 'Long text, long enough to wrap. Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Whatever.'; + +export const permutations = createPermutations>([ + { + description: [undefined, shortText, longText], + children: [undefined, shortText, longText], + readOnly: [false, true], + disabled: [false, true], + checked: [true, false], + }, +]); diff --git a/pages/radio-button/custom-style.tsx b/pages/radio-button/custom-style.tsx new file mode 100644 index 0000000000..d50e07afa4 --- /dev/null +++ b/pages/radio-button/custom-style.tsx @@ -0,0 +1,47 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +import { palette } from '../app/themes/style-api'; + +export default { + input: { + stroke: { + default: palette.neutral80, + disabled: palette.neutral100, + readOnly: palette.neutral90, + }, + fill: { + checked: palette.teal80, + default: palette.neutral10, + disabled: palette.neutral60, + readOnly: palette.neutral40, + }, + circle: { + fill: { + checked: palette.neutral10, + disabled: palette.neutral10, + readOnly: palette.neutral80, + }, + }, + focusRing: { + borderColor: palette.teal80, + borderRadius: '2px', + borderWidth: '1px', + }, + }, + label: { + color: { + checked: `light-dark(${palette.neutral100}, ${palette.neutral10})`, + default: `light-dark(${palette.neutral100}, ${palette.neutral10})`, + disabled: `light-dark(${palette.neutral80}, ${palette.neutral40})`, + readOnly: `light-dark(${palette.neutral80}, ${palette.neutral40})`, + }, + }, + description: { + color: { + checked: `light-dark(${palette.neutral100}, ${palette.neutral10})`, + default: `light-dark(${palette.neutral100}, ${palette.neutral10})`, + disabled: `light-dark(${palette.neutral80}, ${palette.neutral40})`, + readOnly: `light-dark(${palette.neutral80}, ${palette.neutral40})`, + }, + }, +}; diff --git a/pages/radio-button/focus-test.page.tsx b/pages/radio-button/focus-test.page.tsx new file mode 100644 index 0000000000..92f1604f17 --- /dev/null +++ b/pages/radio-button/focus-test.page.tsx @@ -0,0 +1,27 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +import React, { useState } from 'react'; + +import RadioButton from '~components/radio-button'; + +import ScreenshotArea from '../utils/screenshot-area'; + +export default function RadioButtonScenario() { + const [checked, setChecked] = useState(false); + return ( +
    +

    Radio buttons should be focused using the correct highlight

    +

    + Click here to focus so we can tab to the content below{' '} + +

    + + setChecked(detail.checked)}> + Radio button label + + +
    + ); +} diff --git a/pages/radio-button/permutations.page.tsx b/pages/radio-button/permutations.page.tsx new file mode 100644 index 0000000000..b68730e96d --- /dev/null +++ b/pages/radio-button/permutations.page.tsx @@ -0,0 +1,31 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +import React from 'react'; + +import RadioButton from '~components/radio-button'; + +import PermutationsView from '../utils/permutations-view'; +import ScreenshotArea from '../utils/screenshot-area'; +import { permutations } from './common'; + +export default function RadioButtonPermutations() { + return ( + <> +

    RadioButton permutations

    + + ( + { + /*empty handler to suppress react controlled property warning*/ + }} + {...permutation} + name={`radio-group-${index}`} + /> + )} + /> + + + ); +} diff --git a/pages/radio-button/style-custom.page.tsx b/pages/radio-button/style-custom.page.tsx new file mode 100644 index 0000000000..43c2b0d1eb --- /dev/null +++ b/pages/radio-button/style-custom.page.tsx @@ -0,0 +1,33 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +import React from 'react'; + +import RadioButton from '~components/radio-button'; + +import PermutationsView from '../utils/permutations-view'; +import ScreenshotArea from '../utils/screenshot-area'; +import { permutations } from './common'; +import customStyle from './custom-style'; + +export default function RadioButtonPermutations() { + return ( + <> +

    RadioButton permutations with custom styles

    + + ( + { + /*empty handler to suppress react controlled property warning*/ + }} + {...permutation} + name={`radio-group-${index}`} + style={customStyle} + /> + )} + /> + + + ); +} diff --git a/pages/radio-button/styles.scss b/pages/radio-button/styles.scss new file mode 100644 index 0000000000..a9ea24efb3 --- /dev/null +++ b/pages/radio-button/styles.scss @@ -0,0 +1,19 @@ +/* + Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + SPDX-License-Identifier: Apache-2.0 +*/ + +@use '~design-tokens' as awsui; + +.radio-group { + display: flex; + column-gap: awsui.$space-scaled-m; + row-gap: awsui.$space-scaled-xs; + &--horizontal { + flex-direction: row; + flex-wrap: wrap; + } + &--vertical { + flex-direction: column; + } +} diff --git a/pages/radio-button/table.page.tsx b/pages/radio-button/table.page.tsx new file mode 100644 index 0000000000..b23f8b0aca --- /dev/null +++ b/pages/radio-button/table.page.tsx @@ -0,0 +1,123 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +import React, { useContext, useRef, useState } from 'react'; +import clsx from 'clsx'; + +import Box from '~components/box'; +import Checkbox from '~components/checkbox'; +import FormField from '~components/form-field'; +import RadioButton, { RadioButtonProps } from '~components/radio-button'; +import SpaceBetween from '~components/space-between'; + +import AppContext, { AppContextType } from '../app/app-context'; +import ScreenshotArea from '../utils/screenshot-area'; +import { ExtraOptions, options } from './common'; + +import styles from './table.scss'; + +type RadioButtonDemoContext = React.Context< + AppContextType<{ + disabled?: boolean; + readOnly?: boolean; + }> +>; + +const Row = ({ + label, + checked, + description, + disabled, + readOnly, + onChange, +}: Omit & { label: string }) => { + const ref = useRef(null); + return ( + + + + + + {label} + + {description} + + ); +}; + +export default function RadioButtonsTablePage() { + const { urlParams, setUrlParams } = useContext(AppContext as RadioButtonDemoContext); + + const [value, setValue] = useState(''); + + return ( +
    +

    Radio button — custom table selection

    + + + + + setUrlParams({ ...urlParams, disabled: detail.checked })} + description="Make one of the radio buttons disabled" + > + Disabled + + + setUrlParams({ ...urlParams, readOnly: detail.checked })} + description="Make one of the radio buttons read-only" + > + Read-only + + + + + +
    + + + + + + + + + + + {options.map((option, index) => ( + { + if (detail.checked) { + setValue(option.value); + } + }} + disabled={option.allowDisabled && urlParams.disabled} + readOnly={option.allowReadOnly && urlParams.readOnly} + > + {option.label} + + ))} + +
    NameDescription
    + + + +
    +
    + ); +} diff --git a/pages/radio-button/table.scss b/pages/radio-button/table.scss new file mode 100644 index 0000000000..21096dc117 --- /dev/null +++ b/pages/radio-button/table.scss @@ -0,0 +1,43 @@ +/* + Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + SPDX-License-Identifier: Apache-2.0 +*/ + +@use '~design-tokens' as awsui; + +.table { + border-collapse: collapse; +} + +.head { + background-color: awsui.$color-background-cell-shaded; +} + +.cell, +.header-cell { + padding-inline: awsui.$space-scaled-s; +} + +.cell { + padding-block: awsui.$space-scaled-xs; +} + +.header-cell, +.row { + border-block-style: solid; + border-inline-style: solid; + border-block-width: 1px; + border-inline-width: 1px; + border-color: awsui.$color-border-divider-default; +} + +.header-cell { + padding-block: awsui.$space-scaled-xxs; + text-align: start; +} + +.row { + &--selected { + outline: 1px solid awsui.$color-border-item-focused; + } +} diff --git a/pages/radio-button/test.page.tsx b/pages/radio-button/test.page.tsx new file mode 100644 index 0000000000..ab5f1ad637 --- /dev/null +++ b/pages/radio-button/test.page.tsx @@ -0,0 +1,233 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +import React, { useContext, useState } from 'react'; +import clsx from 'clsx'; + +import Box from '~components/box'; +import Checkbox from '~components/checkbox'; +import ColumnLayout from '~components/column-layout'; +import FormField from '~components/form-field'; +import RadioButton, { RadioButtonProps } from '~components/radio-button'; +import RadioGroup from '~components/radio-group'; +import SpaceBetween from '~components/space-between'; + +import AppContext, { AppContextType } from '../app/app-context'; +import ScreenshotArea from '../utils/screenshot-area'; +import { ExtraOptions, options } from './common'; +import customStyle from './custom-style'; + +import styles from './styles.scss'; + +type Orientation = 'horizontal' | 'vertical'; +type Wrapper = 'form-field' | 'fieldset' | 'div'; + +type RadioButtonDemoContext = React.Context< + AppContextType<{ + customStyle?: boolean; + descriptions?: boolean; + disabled?: boolean; + interactiveElementsInDescriptions?: boolean; + interactiveElementsInLabels?: boolean; + orientation?: Orientation; + radioGroupRole?: boolean; + readOnly?: boolean; + wrapper: Wrapper; + }> +>; + +const label = 'Choose a quantity'; + +const CustomRadioButton = ({ + checked, + description, + children, + disabled, + onChange, + readOnly, +}: Omit) => { + const { urlParams } = useContext(AppContext as RadioButtonDemoContext); + + const fullDescription = urlParams.descriptions ? ( + urlParams.interactiveElementsInDescriptions ? ( + <> + {description}. Learn more + + ) : ( + description + ) + ) : undefined; + + return ( + + {children} + {urlParams.interactiveElementsInLabels && ( + <> + {' '} + + + )} + + ); +}; + +export default function RadioButtonsPage() { + const { urlParams, setUrlParams } = useContext(AppContext as RadioButtonDemoContext); + + const [value, setValue] = useState(''); + + const className = clsx(styles['radio-group'], styles[`radio-group--${urlParams.orientation || 'vertical'}`]); + + const radioButtons = ( +
    + {options.map((option, index) => ( + { + if (detail.checked) { + setValue(option.value); + } + }} + disabled={option.allowDisabled && urlParams.disabled} + readOnly={option.allowReadOnly && urlParams.readOnly} + > + {option.label} + + ))} +
    + ); + return ( +
    +

    Radio button

    + + + + + setUrlParams({ ...urlParams, orientation: detail.value as Orientation })} + items={[ + { + label: 'Vertical', + value: 'vertical', + }, + { + label: 'Horizontal', + value: 'horizontal', + }, + ]} + /> + + + setUrlParams({ ...urlParams, wrapper: detail.value as Wrapper })} + items={[ + { + label: 'Cloudscape Form Field around div', + value: 'form-field', + }, + { + label: 'Native HTML fieldset around div', + value: 'fieldset', + }, + { + label: 'Div only', + value: 'div', + }, + ]} + /> + + + setUrlParams({ ...urlParams, radioGroupRole: detail.checked })} + description="Add `radiogroup` role to the wrapping div" + > + Use radiogroup role + + + + + + setUrlParams({ ...urlParams, disabled: detail.checked })} + description="Make one of the radio buttons disabled" + > + Disabled + + + setUrlParams({ ...urlParams, readOnly: detail.checked })} + description="Make one of the radio buttons read-only" + > + Read-only + + + setUrlParams({ ...urlParams, descriptions: detail.checked })} + description="Show descriptions" + > + Descriptions + + + {urlParams.descriptions && ( + + setUrlParams({ ...urlParams, interactiveElementsInDescriptions: detail.checked }) + } + > + Interactive elements in descriptions + + )} + + setUrlParams({ ...urlParams, interactiveElementsInLabels: detail.checked })} + > + Interactive elements in labels + + + setUrlParams({ ...urlParams, customStyle: detail.checked })} + description="Use the style API to customize the component styles" + > + Use custom style + + + + + +
    + + {urlParams.wrapper === 'form-field' ? ( + {radioButtons} + ) : urlParams.wrapper === 'fieldset' ? ( +
    + {label} + {radioButtons} +
    + ) : ( + radioButtons + )} + + + +
    +
    + ); +} diff --git a/pages/radio-group/common-permutations.tsx b/pages/radio-group/common-permutations.tsx index 3e9cc0994f..a5583b74aa 100644 --- a/pages/radio-group/common-permutations.tsx +++ b/pages/radio-group/common-permutations.tsx @@ -5,6 +5,7 @@ import React from 'react'; import Link from '~components/link'; import { RadioGroupProps } from '~components/radio-group'; +import { longText } from '../radio-button/common'; import createPermutations from '../utils/permutations'; const permutations = createPermutations([ @@ -19,15 +20,12 @@ const permutations = createPermutations([ [ { value: 'first', - label: - 'Long text, long enough to wrap. Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Whatever.', + label: longText, }, { value: 'second', - label: - 'Long text, long enough to wrap. Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Whatever.', - description: - 'Long text, long enough to wrap. Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Whatever.', + label: longText, + description: longText, }, ], ], diff --git a/pages/radio-group/style-custom.page.tsx b/pages/radio-group/style-custom.page.tsx index 29ff870930..15ca77483b 100644 --- a/pages/radio-group/style-custom.page.tsx +++ b/pages/radio-group/style-custom.page.tsx @@ -4,7 +4,7 @@ import React from 'react'; import { RadioGroup, SpaceBetween } from '~components'; -import { palette } from '../app/themes/style-api'; +import customStyle from '../radio-button/custom-style'; import ScreenshotArea from '../utils/screenshot-area'; export default function CustomRadio() { @@ -27,59 +27,15 @@ export default function CustomRadio() { }, ]; - const style = { - input: { - stroke: { - default: palette.neutral80, - disabled: palette.neutral100, - readOnly: palette.neutral90, - }, - fill: { - checked: palette.teal80, - default: palette.neutral10, - disabled: palette.neutral60, - readOnly: palette.neutral40, - }, - circle: { - fill: { - checked: palette.neutral10, - disabled: palette.neutral10, - readOnly: palette.neutral80, - }, - }, - focusRing: { - borderColor: palette.teal80, - borderRadius: '2px', - borderWidth: '1px', - }, - }, - label: { - color: { - checked: `light-dark(${palette.neutral100}, ${palette.neutral10})`, - default: `light-dark(${palette.neutral100}, ${palette.neutral10})`, - disabled: `light-dark(${palette.neutral80}, ${palette.neutral40})`, - readOnly: `light-dark(${palette.neutral80}, ${palette.neutral40})`, - }, - }, - description: { - color: { - checked: `light-dark(${palette.neutral100}, ${palette.neutral10})`, - default: `light-dark(${palette.neutral100}, ${palette.neutral10})`, - disabled: `light-dark(${palette.neutral80}, ${palette.neutral40})`, - readOnly: `light-dark(${palette.neutral80}, ${palette.neutral40})`, - }, - }, - }; - return (

    Custom Radio

    - - - + + +
    diff --git a/src/__tests__/snapshot-tests/__snapshots__/documenter.test.ts.snap b/src/__tests__/snapshot-tests/__snapshots__/documenter.test.ts.snap index ba20b1e7ae..38692629dc 100644 --- a/src/__tests__/snapshot-tests/__snapshots__/documenter.test.ts.snap +++ b/src/__tests__/snapshot-tests/__snapshots__/documenter.test.ts.snap @@ -20222,6 +20222,367 @@ This is only shown when \`statusType\` is set to \`finished\` or not set at all. } `; +exports[`Components definition for radio-button matches the snapshot: radio-button 1`] = ` +{ + "dashCaseName": "radio-button", + "events": [ + { + "cancelable": false, + "description": "Called when the user changes the component state. The event \`detail\` contains the current value for the \`checked\` property.", + "detailInlineType": { + "name": "RadioButtonProps.ChangeDetail", + "properties": [ + { + "name": "checked", + "optional": false, + "type": "boolean", + }, + ], + "type": "object", + }, + "detailType": "RadioButtonProps.ChangeDetail", + "name": "onChange", + }, + ], + "functions": [ + { + "description": "Sets input focus onto the UI control.", + "name": "focus", + "parameters": [], + "returnType": "void", + }, + ], + "name": "RadioButton", + "properties": [ + { + "description": "Specifies if the component is selected.", + "name": "checked", + "optional": false, + "type": "boolean", + }, + { + "deprecatedTag": "Custom CSS is not supported. For testing and other use cases, use [data attributes](https://developer.mozilla.org/en-US/docs/Learn/HTML/Howto/Use_data_attributes).", + "description": "Adds the specified classes to the root element of the component.", + "name": "className", + "optional": true, + "type": "string", + }, + { + "description": "Specifies the ID of the native form element. You can use it to relate +a label element's \`for\` attribute to this control.", + "name": "controlId", + "optional": true, + "type": "string", + }, + { + "description": "Specifies if the control is disabled, which prevents the +user from modifying the value and prevents the value from +being included in a form submission. A disabled control can't +receive focus.", + "name": "disabled", + "optional": true, + "type": "boolean", + }, + { + "deprecatedTag": "The usage of the \`id\` attribute is reserved for internal use cases. For testing and other use cases, +use [data attributes](https://developer.mozilla.org/en-US/docs/Learn/HTML/Howto/Use_data_attributes). If you must +use the \`id\` attribute, consider setting it on a parent element instead.", + "description": "Adds the specified ID to the root element of the component.", + "name": "id", + "optional": true, + "type": "string", + }, + { + "description": "Name of the group that the radio button belongs to.", + "name": "name", + "optional": false, + "type": "string", + }, + { + "description": "Attributes to add to the native \`input\` element. +Some attributes will be automatically combined with internal attribute values: +- \`className\` will be appended. +- Event handlers will be chained, unless the default is prevented. + +We do not support using this attribute to apply custom styling.", + "inlineType": { + "name": "Omit, "children"> & Record<\`data-\${string}\`, string>", + "type": "union", + "values": [ + "Omit, "children">", + "Record<\`data-\${string}\`, string>", + ], + }, + "name": "nativeInputAttributes", + "optional": true, + "type": "Omit, "children"> & Record<\`data-\${string}\`, string>", + }, + { + "description": "Specifies if the radio button is read-only, which prevents the +user from modifying the value, but does not prevent the value from +being included in a form submission. A read-only control is still focusable. + +This property should be set for either all or none of the radio buttons in a group.", + "name": "readOnly", + "optional": true, + "type": "boolean", + }, + { + "inlineType": { + "name": "RadioButtonProps.Style", + "properties": [ + { + "inlineType": { + "name": "object", + "properties": [ + { + "inlineType": { + "name": "object", + "properties": [ + { + "name": "checked", + "optional": true, + "type": "string", + }, + { + "name": "default", + "optional": true, + "type": "string", + }, + { + "name": "disabled", + "optional": true, + "type": "string", + }, + { + "name": "readOnly", + "optional": true, + "type": "string", + }, + ], + "type": "object", + }, + "name": "color", + "optional": true, + "type": "{ checked?: string | undefined; default?: string | undefined; disabled?: string | undefined; readOnly?: string | undefined; }", + }, + ], + "type": "object", + }, + "name": "description", + "optional": true, + "type": "{ color?: { checked?: string | undefined; default?: string | undefined; disabled?: string | undefined; readOnly?: string | undefined; } | undefined; }", + }, + { + "inlineType": { + "name": "object", + "properties": [ + { + "inlineType": { + "name": "object", + "properties": [ + { + "inlineType": { + "name": "{ checked?: string | undefined; disabled?: string | undefined; readOnly?: string | undefined; }", + "properties": [ + { + "name": "checked", + "optional": true, + "type": "string", + }, + { + "name": "disabled", + "optional": true, + "type": "string", + }, + { + "name": "readOnly", + "optional": true, + "type": "string", + }, + ], + "type": "object", + }, + "name": "fill", + "optional": true, + "type": "{ checked?: string | undefined; disabled?: string | undefined; readOnly?: string | undefined; }", + }, + ], + "type": "object", + }, + "name": "circle", + "optional": true, + "type": "{ fill?: { checked?: string | undefined; disabled?: string | undefined; readOnly?: string | undefined; } | undefined; }", + }, + { + "inlineType": { + "name": "object", + "properties": [ + { + "name": "checked", + "optional": true, + "type": "string", + }, + { + "name": "default", + "optional": true, + "type": "string", + }, + { + "name": "disabled", + "optional": true, + "type": "string", + }, + { + "name": "readOnly", + "optional": true, + "type": "string", + }, + ], + "type": "object", + }, + "name": "fill", + "optional": true, + "type": "{ checked?: string | undefined; default?: string | undefined; disabled?: string | undefined; readOnly?: string | undefined; }", + }, + { + "inlineType": { + "name": "object", + "properties": [ + { + "name": "borderColor", + "optional": true, + "type": "string", + }, + { + "name": "borderRadius", + "optional": true, + "type": "string", + }, + { + "name": "borderWidth", + "optional": true, + "type": "string", + }, + ], + "type": "object", + }, + "name": "focusRing", + "optional": true, + "type": "{ borderColor?: string | undefined; borderRadius?: string | undefined; borderWidth?: string | undefined; }", + }, + { + "inlineType": { + "name": "{ default?: string | undefined; disabled?: string | undefined; readOnly?: string | undefined; }", + "properties": [ + { + "name": "default", + "optional": true, + "type": "string", + }, + { + "name": "disabled", + "optional": true, + "type": "string", + }, + { + "name": "readOnly", + "optional": true, + "type": "string", + }, + ], + "type": "object", + }, + "name": "stroke", + "optional": true, + "type": "{ default?: string | undefined; disabled?: string | undefined; readOnly?: string | undefined; }", + }, + ], + "type": "object", + }, + "name": "input", + "optional": true, + "type": "{ fill?: { checked?: string | undefined; default?: string | undefined; disabled?: string | undefined; readOnly?: string | undefined; } | undefined; stroke?: { default?: string | undefined; disabled?: string | undefined; readOnly?: string | undefined; } | undefined; circle?: { ...; } | undefined; focusRing?: { ...; }...", + }, + { + "inlineType": { + "name": "object", + "properties": [ + { + "inlineType": { + "name": "object", + "properties": [ + { + "name": "checked", + "optional": true, + "type": "string", + }, + { + "name": "default", + "optional": true, + "type": "string", + }, + { + "name": "disabled", + "optional": true, + "type": "string", + }, + { + "name": "readOnly", + "optional": true, + "type": "string", + }, + ], + "type": "object", + }, + "name": "color", + "optional": true, + "type": "{ checked?: string | undefined; default?: string | undefined; disabled?: string | undefined; readOnly?: string | undefined; }", + }, + ], + "type": "object", + }, + "name": "label", + "optional": true, + "type": "{ color?: { checked?: string | undefined; default?: string | undefined; disabled?: string | undefined; readOnly?: string | undefined; } | undefined; }", + }, + ], + "type": "object", + }, + "name": "style", + "optional": true, + "type": "RadioButtonProps.Style", + }, + { + "description": "Sets the value attribute to the native control. +If using native form submission, this value is sent to the server if the radio button is checked. +It is never shown to the user by their user agent. +For more details, see the [MDN documentation](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/input/radio#value).", + "name": "value", + "optional": true, + "type": "string", + }, + ], + "regions": [ + { + "description": "The control's label that's displayed next to the radio button. A state change occurs when a user clicks on it.", + "displayName": "label", + "isDefault": true, + "name": "children", + }, + { + "description": "Description that appears below the label.", + "isDefault": false, + "name": "description", + }, + ], + "releaseStatus": "stable", + "systemTags": [ + "core", + ], +} +`; + exports[`Components definition for radio-group matches the snapshot: radio-group 1`] = ` { "dashCaseName": "radio-group", diff --git a/src/__tests__/snapshot-tests/__snapshots__/test-utils-wrappers.test.tsx.snap b/src/__tests__/snapshot-tests/__snapshots__/test-utils-wrappers.test.tsx.snap index cb85907a8b..9e433a47f5 100644 --- a/src/__tests__/snapshot-tests/__snapshots__/test-utils-wrappers.test.tsx.snap +++ b/src/__tests__/snapshot-tests/__snapshots__/test-utils-wrappers.test.tsx.snap @@ -66,6 +66,7 @@ import PopoverWrapper from './popover'; import ProgressBarWrapper from './progress-bar'; import PromptInputWrapper from './prompt-input'; import PropertyFilterWrapper from './property-filter'; +import RadioButtonWrapper from './radio-button'; import RadioGroupWrapper from './radio-group'; import S3ResourceSelectorWrapper from './s3-resource-selector'; import SegmentedControlWrapper from './segmented-control'; @@ -152,6 +153,7 @@ export { PopoverWrapper }; export { ProgressBarWrapper }; export { PromptInputWrapper }; export { PropertyFilterWrapper }; +export { RadioButtonWrapper }; export { RadioGroupWrapper }; export { S3ResourceSelectorWrapper }; export { SegmentedControlWrapper }; @@ -1266,6 +1268,25 @@ findPropertyFilter(selector?: string): PropertyFilterWrapper | null; * @returns {Array} */ findAllPropertyFilters(selector?: string): Array; +/** + * Returns the wrapper of the first RadioButton that matches the specified CSS selector. + * If no CSS selector is specified, returns the wrapper of the first RadioButton. + * If no matching RadioButton is found, returns \`null\`. + * + * @param {string} [selector] CSS Selector + * @returns {RadioButtonWrapper | null} + */ +findRadioButton(selector?: string): RadioButtonWrapper | null; + +/** + * Returns an array of RadioButton wrapper that matches the specified CSS selector. + * If no CSS selector is specified, returns all of the RadioButtons inside the current wrapper. + * If no matching RadioButton is found, returns an empty array. + * + * @param {string} [selector] CSS Selector + * @returns {Array} + */ +findAllRadioButtons(selector?: string): Array; /** * Returns the wrapper of the first RadioGroup that matches the specified CSS selector. * If no CSS selector is specified, returns the wrapper of the first RadioGroup. @@ -2524,6 +2545,19 @@ ElementWrapper.prototype.findPropertyFilter = function(selector) { ElementWrapper.prototype.findAllPropertyFilters = function(selector) { return this.findAllComponents(PropertyFilterWrapper, selector); }; +ElementWrapper.prototype.findRadioButton = function(selector) { + let rootSelector = \`.\${RadioButtonWrapper.rootSelector}\`; + if("legacyRootSelector" in RadioButtonWrapper && RadioButtonWrapper.legacyRootSelector){ + rootSelector = \`:is(.\${RadioButtonWrapper.rootSelector}, .\${RadioButtonWrapper.legacyRootSelector})\`; + } + // casting to 'any' is needed to avoid this issue with generics + // https://github.com/microsoft/TypeScript/issues/29132 + return (this as any).findComponent(selector ? appendSelector(selector, rootSelector) : rootSelector, RadioButtonWrapper); +}; + +ElementWrapper.prototype.findAllRadioButtons = function(selector) { + return this.findAllComponents(RadioButtonWrapper, selector); +}; ElementWrapper.prototype.findRadioGroup = function(selector) { let rootSelector = \`.\${RadioGroupWrapper.rootSelector}\`; if("legacyRootSelector" in RadioGroupWrapper && RadioGroupWrapper.legacyRootSelector){ @@ -2952,6 +2986,7 @@ import PopoverWrapper from './popover'; import ProgressBarWrapper from './progress-bar'; import PromptInputWrapper from './prompt-input'; import PropertyFilterWrapper from './property-filter'; +import RadioButtonWrapper from './radio-button'; import RadioGroupWrapper from './radio-group'; import S3ResourceSelectorWrapper from './s3-resource-selector'; import SegmentedControlWrapper from './segmented-control'; @@ -3038,6 +3073,7 @@ export { PopoverWrapper }; export { ProgressBarWrapper }; export { PromptInputWrapper }; export { PropertyFilterWrapper }; +export { RadioButtonWrapper }; export { RadioGroupWrapper }; export { S3ResourceSelectorWrapper }; export { SegmentedControlWrapper }; @@ -4038,6 +4074,23 @@ findPropertyFilter(selector?: string): PropertyFilterWrapper; * @returns {MultiElementWrapper} */ findAllPropertyFilters(selector?: string): MultiElementWrapper; +/** + * Returns a wrapper that matches the RadioButtons with the specified CSS selector. + * If no CSS selector is specified, returns a wrapper that matches RadioButtons. + * + * @param {string} [selector] CSS Selector + * @returns {RadioButtonWrapper} + */ +findRadioButton(selector?: string): RadioButtonWrapper; + +/** + * Returns a multi-element wrapper that matches RadioButtons with the specified CSS selector. + * If no CSS selector is specified, returns a multi-element wrapper that matches RadioButtons. + * + * @param {string} [selector] CSS Selector + * @returns {MultiElementWrapper} + */ +findAllRadioButtons(selector?: string): MultiElementWrapper; /** * Returns a wrapper that matches the RadioGroups with the specified CSS selector. * If no CSS selector is specified, returns a wrapper that matches RadioGroups. @@ -5242,6 +5295,19 @@ ElementWrapper.prototype.findPropertyFilter = function(selector) { ElementWrapper.prototype.findAllPropertyFilters = function(selector) { return this.findAllComponents(PropertyFilterWrapper, selector); }; +ElementWrapper.prototype.findRadioButton = function(selector) { + let rootSelector = \`.\${RadioButtonWrapper.rootSelector}\`; + if("legacyRootSelector" in RadioButtonWrapper && RadioButtonWrapper.legacyRootSelector){ + rootSelector = \`:is(.\${RadioButtonWrapper.rootSelector}, .\${RadioButtonWrapper.legacyRootSelector})\`; + } + // casting to 'any' is needed to avoid this issue with generics + // https://github.com/microsoft/TypeScript/issues/29132 + return (this as any).findComponent(selector ? appendSelector(selector, rootSelector) : rootSelector, RadioButtonWrapper); +}; + +ElementWrapper.prototype.findAllRadioButtons = function(selector) { + return this.findAllComponents(RadioButtonWrapper, selector); +}; ElementWrapper.prototype.findRadioGroup = function(selector) { let rootSelector = \`.\${RadioGroupWrapper.rootSelector}\`; if("legacyRootSelector" in RadioGroupWrapper && RadioGroupWrapper.legacyRootSelector){ diff --git a/src/internal/components/radio-button/interfaces.ts b/src/internal/components/radio-button/interfaces.ts index 3b29d72a2c..c2e7380b40 100644 --- a/src/internal/components/radio-button/interfaces.ts +++ b/src/internal/components/radio-button/interfaces.ts @@ -4,9 +4,6 @@ import React from 'react'; import { BaseComponentProps } from '../../base-component'; import { NonCancelableEventHandler } from '../../events'; -/** - * @awsuiSystem core - */ import { NativeAttributes } from '../../utils/with-native-attributes'; export interface RadioButtonProps extends BaseComponentProps { @@ -52,8 +49,6 @@ export interface RadioButtonProps extends BaseComponentProps { * - Event handlers will be chained, unless the default is prevented. * * We do not support using this attribute to apply custom styling. - * - * @awsuiSystem core */ nativeInputAttributes?: NativeAttributes>; @@ -71,9 +66,6 @@ export interface RadioButtonProps extends BaseComponentProps { */ readOnly?: boolean; - /** - * @awsuiSystem core - */ style?: RadioButtonProps.Style; /** diff --git a/src/radio-button/__tests__/radio-button.test.tsx b/src/radio-button/__tests__/radio-button.test.tsx new file mode 100644 index 0000000000..b66c891c3a --- /dev/null +++ b/src/radio-button/__tests__/radio-button.test.tsx @@ -0,0 +1,35 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +import React from 'react'; +import { render } from '@testing-library/react'; + +import RadioButton from '../../../lib/components/radio-button'; +import createWrapper from '../../../lib/components/test-utils/dom'; + +describe('test utils', () => { + test('findRadioButton', () => { + const wrapper = createWrapper( + render( + + a + + ).container.parentElement! + ); + expect(wrapper.findRadioButton()).toBeTruthy(); + }); + test('findAllRadioButtons', () => { + const wrapper = createWrapper( + render( +
    + + a + + + b + +
    + ).container + ); + expect(wrapper.findAllRadioButtons()).toHaveLength(2); + }); +}); diff --git a/src/radio-button/index.tsx b/src/radio-button/index.tsx new file mode 100644 index 0000000000..437fe4b5a0 --- /dev/null +++ b/src/radio-button/index.tsx @@ -0,0 +1,35 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +'use client'; +import React from 'react'; +import clsx from 'clsx'; + +import InternalRadioButton from '../internal/components/radio-button'; +import useBaseComponent from '../internal/hooks/use-base-component'; +import { applyDisplayName } from '../internal/utils/apply-display-name'; +import { RadioButtonProps } from './interfaces'; + +import styles from './styles.css.js'; + +export { RadioButtonProps }; + +const RadioButton = React.forwardRef((props: RadioButtonProps, ref: React.Ref) => { + const baseComponentProps = useBaseComponent('RadioButton', { + props: { readOnly: Boolean(props.readOnly), disabled: Boolean(props.disabled) }, + }); + return ( + + ); +}); + +applyDisplayName(RadioButton, 'RadioButton'); + +/** + * @awsuiSystem core + */ +export default RadioButton; diff --git a/src/radio-button/interfaces.ts b/src/radio-button/interfaces.ts new file mode 100644 index 0000000000..cc019d590b --- /dev/null +++ b/src/radio-button/interfaces.ts @@ -0,0 +1,5 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +import { RadioButtonProps } from '../internal/components/radio-button/interfaces'; + +export { RadioButtonProps }; diff --git a/src/radio-button/styles.scss b/src/radio-button/styles.scss new file mode 100644 index 0000000000..46cfea8d53 --- /dev/null +++ b/src/radio-button/styles.scss @@ -0,0 +1,10 @@ +/* + Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + SPDX-License-Identifier: Apache-2.0 +*/ + +@use '../internal/styles' as styles; + +.radio-button { + @include styles.styles-reset; +} diff --git a/src/radio-group/__tests__/radio-group.test.tsx b/src/radio-group/__tests__/radio-group.test.tsx index 5867dda105..5e524b9eaa 100644 --- a/src/radio-group/__tests__/radio-group.test.tsx +++ b/src/radio-group/__tests__/radio-group.test.tsx @@ -11,7 +11,7 @@ import { import '../../__a11y__/to-validate-a11y'; import RadioGroup, { RadioGroupProps } from '../../../lib/components/radio-group'; import createWrapper from '../../../lib/components/test-utils/dom'; -import RadioButtonWrapper from '../../../lib/components/test-utils/dom/radio-group/radio-button'; +import RadioButtonWrapper from '../../../lib/components/test-utils/dom/radio-button'; import customCssProps from '../../internal/generated/custom-css-properties'; import abstractSwitchStyles from '../../../lib/components/internal/components/abstract-switch/styles.css.js'; diff --git a/src/test-utils/dom/collection-preferences/page-size-preference.ts b/src/test-utils/dom/collection-preferences/page-size-preference.ts index 4b9c0750ab..583fc5f06f 100644 --- a/src/test-utils/dom/collection-preferences/page-size-preference.ts +++ b/src/test-utils/dom/collection-preferences/page-size-preference.ts @@ -3,8 +3,8 @@ import { ComponentWrapper, ElementWrapper } from '@cloudscape-design/test-utils-core/dom'; import FormFieldWrapper from '../form-field'; +import RadioButtonWrapper from '../radio-button'; import RadioGroupWrapper from '../radio-group'; -import RadioButtonWrapper from '../radio-group/radio-button'; import styles from '../../../collection-preferences/styles.selectors.js'; diff --git a/src/test-utils/dom/radio-group/radio-button.ts b/src/test-utils/dom/radio-button/index.ts similarity index 100% rename from src/test-utils/dom/radio-group/radio-button.ts rename to src/test-utils/dom/radio-button/index.ts diff --git a/src/test-utils/dom/radio-group/index.ts b/src/test-utils/dom/radio-group/index.ts index 9f43774326..c36c8beb99 100644 --- a/src/test-utils/dom/radio-group/index.ts +++ b/src/test-utils/dom/radio-group/index.ts @@ -3,7 +3,7 @@ import { ComponentWrapper, ElementWrapper } from '@cloudscape-design/test-utils-core/dom'; import { escapeSelector } from '@cloudscape-design/test-utils-core/utils'; -import RadioButtonWrapper from './radio-button'; +import RadioButtonWrapper from '../radio-button'; import radioButtonStyles from '../../../internal/components/radio-button/test-classes/styles.selectors.js'; import styles from '../../../radio-group/test-classes/styles.selectors.js'; diff --git a/src/test-utils/dom/tiles/tile.ts b/src/test-utils/dom/tiles/tile.ts index 7320da5262..2badb890e3 100644 --- a/src/test-utils/dom/tiles/tile.ts +++ b/src/test-utils/dom/tiles/tile.ts @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 import { ComponentWrapper, ElementWrapper } from '@cloudscape-design/test-utils-core/dom'; -import RadioButtonWrapper from '../radio-group/radio-button'; +import RadioButtonWrapper from '../radio-button'; import styles from '../../../tiles/styles.selectors.js'; From 8f18a77a41aa85f25545461201ee6c5e2b9e52cb Mon Sep 17 00:00:00 2001 From: Joan Perals Tresserra Date: Tue, 18 Nov 2025 17:03:36 +0100 Subject: [PATCH 03/22] Fix a11y in dev pages --- pages/radio-button/common.tsx | 37 ++++++++++++++++++++++-- pages/radio-button/permutations.page.tsx | 14 ++------- pages/radio-button/style-custom.page.tsx | 15 ++-------- 3 files changed, 39 insertions(+), 27 deletions(-) diff --git a/pages/radio-button/common.tsx b/pages/radio-button/common.tsx index 2bf5d4fa80..c512abbfb3 100644 --- a/pages/radio-button/common.tsx +++ b/pages/radio-button/common.tsx @@ -4,7 +4,7 @@ import React, { useState } from 'react'; import FormField from '~components/form-field'; import Input from '~components/input'; -import { RadioButtonProps } from '~components/radio-button'; +import RadioButton, { RadioButtonProps } from '~components/radio-button'; import createPermutations from '../utils/permutations'; @@ -28,7 +28,9 @@ export const shortText = 'Short text'; export const longText = 'Long text, long enough to wrap. Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Whatever.'; -export const permutations = createPermutations>([ +type Permutation = Omit; + +export const permutations = createPermutations([ { description: [undefined, shortText, longText], children: [undefined, shortText, longText], @@ -37,3 +39,34 @@ export const permutations = createPermutations>([ checked: [true, false], }, ]); + +export const RadioButtonPermutation = ({ + children, + description, + readOnly, + disabled, + checked, + index, +}: Permutation & { index?: number }) => { + const commonProps = { + children, + description, + readOnly, + disabled, + checked, + name: `radio-group-${index}`, + onChange: () => { + /*empty handler to suppress react controlled property warning*/ + }, + }; + if (children) { + return ; + } else { + // If no visual label provided, add a label for screen readers + return ( + + ); + } +}; diff --git a/pages/radio-button/permutations.page.tsx b/pages/radio-button/permutations.page.tsx index b68730e96d..18a4c00e53 100644 --- a/pages/radio-button/permutations.page.tsx +++ b/pages/radio-button/permutations.page.tsx @@ -2,11 +2,9 @@ // SPDX-License-Identifier: Apache-2.0 import React from 'react'; -import RadioButton from '~components/radio-button'; - import PermutationsView from '../utils/permutations-view'; import ScreenshotArea from '../utils/screenshot-area'; -import { permutations } from './common'; +import { permutations, RadioButtonPermutation } from './common'; export default function RadioButtonPermutations() { return ( @@ -15,15 +13,7 @@ export default function RadioButtonPermutations() { ( - { - /*empty handler to suppress react controlled property warning*/ - }} - {...permutation} - name={`radio-group-${index}`} - /> - )} + render={(permutation, index) => } /> diff --git a/pages/radio-button/style-custom.page.tsx b/pages/radio-button/style-custom.page.tsx index 43c2b0d1eb..8be4d61604 100644 --- a/pages/radio-button/style-custom.page.tsx +++ b/pages/radio-button/style-custom.page.tsx @@ -2,11 +2,9 @@ // SPDX-License-Identifier: Apache-2.0 import React from 'react'; -import RadioButton from '~components/radio-button'; - import PermutationsView from '../utils/permutations-view'; import ScreenshotArea from '../utils/screenshot-area'; -import { permutations } from './common'; +import { permutations, RadioButtonPermutation } from './common'; import customStyle from './custom-style'; export default function RadioButtonPermutations() { @@ -16,16 +14,7 @@ export default function RadioButtonPermutations() { ( - { - /*empty handler to suppress react controlled property warning*/ - }} - {...permutation} - name={`radio-group-${index}`} - style={customStyle} - /> - )} + render={(permutation, index) => } /> From eea515e672e6ebe85703b929ec0adca3e3d089b1 Mon Sep 17 00:00:00 2001 From: Joan Perals Tresserra Date: Tue, 18 Nov 2025 18:12:36 +0100 Subject: [PATCH 04/22] Add system tag --- src/internal/components/radio-button/interfaces.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/internal/components/radio-button/interfaces.ts b/src/internal/components/radio-button/interfaces.ts index c2e7380b40..2d0e3e8d41 100644 --- a/src/internal/components/radio-button/interfaces.ts +++ b/src/internal/components/radio-button/interfaces.ts @@ -4,6 +4,9 @@ import React from 'react'; import { BaseComponentProps } from '../../base-component'; import { NonCancelableEventHandler } from '../../events'; +/** + * @awsuiSystem core + */ import { NativeAttributes } from '../../utils/with-native-attributes'; export interface RadioButtonProps extends BaseComponentProps { @@ -49,6 +52,8 @@ export interface RadioButtonProps extends BaseComponentProps { * - Event handlers will be chained, unless the default is prevented. * * We do not support using this attribute to apply custom styling. + * + * @awsuiSystem core */ nativeInputAttributes?: NativeAttributes>; From 12e6bffeecf83b8799c9cb82cdff07f5060e79f5 Mon Sep 17 00:00:00 2001 From: Joan Perals Tresserra Date: Tue, 18 Nov 2025 19:20:13 +0100 Subject: [PATCH 05/22] Duplicate interface to workarond TS compile issue --- src/radio-group/interfaces.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/radio-group/interfaces.ts b/src/radio-group/interfaces.ts index 1db65fe3b3..6de71db76c 100644 --- a/src/radio-group/interfaces.ts +++ b/src/radio-group/interfaces.ts @@ -88,7 +88,12 @@ export namespace RadioGroupProps { value: string; } - export type Ref = RadioButtonProps.Ref; + export interface Ref { + /** + * Sets input focus onto the UI control. + */ + focus(): void; + } export type Style = RadioButtonProps.Style; } From d61fe0b4587d18ff40eab20798a6f8540bc60e09 Mon Sep 17 00:00:00 2001 From: Joan Perals Tresserra Date: Tue, 18 Nov 2025 21:03:16 +0100 Subject: [PATCH 06/22] Update API docs --- .../snapshot-tests/__snapshots__/documenter.test.ts.snap | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/__tests__/snapshot-tests/__snapshots__/documenter.test.ts.snap b/src/__tests__/snapshot-tests/__snapshots__/documenter.test.ts.snap index 38692629dc..e7b44f6efb 100644 --- a/src/__tests__/snapshot-tests/__snapshots__/documenter.test.ts.snap +++ b/src/__tests__/snapshot-tests/__snapshots__/documenter.test.ts.snap @@ -20315,6 +20315,9 @@ We do not support using this attribute to apply custom styling.", }, "name": "nativeInputAttributes", "optional": true, + "systemTags": [ + "core", + ], "type": "Omit, "children"> & Record<\`data-\${string}\`, string>", }, { From fa7bf45fa02c24359f61e43a1b29358be15a3b8e Mon Sep 17 00:00:00 2001 From: Joan Perals Tresserra Date: Tue, 18 Nov 2025 22:12:26 +0100 Subject: [PATCH 07/22] Revert "Move style utils to internal radio button component" This reverts commit b9ec77819dbcec20029eeb37691a0fec3bc5cfcc. --- src/internal/components/radio-button/index.tsx | 2 +- .../components/radio-button => radio-group}/style.tsx | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) rename src/{internal/components/radio-button => radio-group}/style.tsx (92%) diff --git a/src/internal/components/radio-button/index.tsx b/src/internal/components/radio-button/index.tsx index 1c18bdd630..31658cbd26 100644 --- a/src/internal/components/radio-button/index.tsx +++ b/src/internal/components/radio-button/index.tsx @@ -7,12 +7,12 @@ import { useMergeRefs } from '@cloudscape-design/component-toolkit/internal'; import { useSingleTabStopNavigation } from '@cloudscape-design/component-toolkit/internal'; import { copyAnalyticsMetadataAttribute } from '@cloudscape-design/component-toolkit/internal/analytics-metadata'; +import { getAbstractSwitchStyles, getInnerCircleStyle, getOuterCircleStyle } from '../../../radio-group/style'; import { getBaseProps } from '../../base-component'; import AbstractSwitch from '../../components/abstract-switch'; import { fireNonCancelableEvent } from '../../events'; import { InternalBaseComponentProps } from '../../hooks/use-base-component'; import { RadioButtonProps } from './interfaces'; -import { getAbstractSwitchStyles, getInnerCircleStyle, getOuterCircleStyle } from './style'; import styles from './styles.css.js'; import testUtilStyles from './test-classes/styles.css.js'; diff --git a/src/internal/components/radio-button/style.tsx b/src/radio-group/style.tsx similarity index 92% rename from src/internal/components/radio-button/style.tsx rename to src/radio-group/style.tsx index 04de395703..84273a2b8a 100644 --- a/src/internal/components/radio-button/style.tsx +++ b/src/radio-group/style.tsx @@ -1,8 +1,8 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import { RadioGroupProps } from '../../../radio-group/interfaces'; -import { SYSTEM } from '../../environment'; -import { getComputedAbstractSwitchState } from '../../utils/style'; +import { SYSTEM } from '../internal/environment'; +import { getComputedAbstractSwitchState } from '../internal/utils/style'; +import { RadioGroupProps } from './interfaces'; export function getOuterCircleStyle( style: RadioGroupProps.Style | undefined, From c7c6d07bab094402915b8f318b27704ac9f1ec59 Mon Sep 17 00:00:00 2001 From: Joan Perals Tresserra Date: Tue, 18 Nov 2025 22:14:06 +0100 Subject: [PATCH 08/22] Fix documenter snapshot --- .../__snapshots__/documenter.test.ts.snap | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/src/__tests__/snapshot-tests/__snapshots__/documenter.test.ts.snap b/src/__tests__/snapshot-tests/__snapshots__/documenter.test.ts.snap index e7b44f6efb..420237d14f 100644 --- a/src/__tests__/snapshot-tests/__snapshots__/documenter.test.ts.snap +++ b/src/__tests__/snapshot-tests/__snapshots__/documenter.test.ts.snap @@ -20681,20 +20681,6 @@ is provided by its parent form field component.", "optional": true, "type": "string", }, - { - "description": "Defines the direction in which the radio buttons are laid out.", - "inlineType": { - "name": ""horizontal" | "vertical"", - "type": "union", - "values": [ - "horizontal", - "vertical", - ], - }, - "name": "direction", - "optional": true, - "type": "string", - }, { "deprecatedTag": "The usage of the \`id\` attribute is reserved for internal use cases. For testing and other use cases, use [data attributes](https://developer.mozilla.org/en-US/docs/Learn/HTML/Howto/Use_data_attributes). If you must From 5cb252f85d17928bf1b38eb16d78f0f04838da05 Mon Sep 17 00:00:00 2001 From: Joan Perals Tresserra Date: Wed, 19 Nov 2025 00:03:45 +0100 Subject: [PATCH 09/22] Add coverage --- .../__tests__/radio-button.test.tsx | 137 ++++++++++++++++-- 1 file changed, 123 insertions(+), 14 deletions(-) diff --git a/src/radio-button/__tests__/radio-button.test.tsx b/src/radio-button/__tests__/radio-button.test.tsx index b66c891c3a..9753e0880e 100644 --- a/src/radio-button/__tests__/radio-button.test.tsx +++ b/src/radio-button/__tests__/radio-button.test.tsx @@ -3,33 +3,142 @@ import React from 'react'; import { render } from '@testing-library/react'; +import { RadioButtonProps } from '../../../lib/components/internal/components/radio-button/interfaces'; import RadioButton from '../../../lib/components/radio-button'; import createWrapper from '../../../lib/components/test-utils/dom'; +function renderRadioButton(node: React.ReactNode) { + const wrapper = createWrapper(render(<>{node}).container); + return wrapper.findRadioButton()!; +} + +describe('native attributes', () => { + test('propagates the value of the `name` prop to the `name` attribute of the native element', () => { + const radioButton = renderRadioButton(); + expect(radioButton.findNativeInput()!.getElement().getAttribute('name')).toBe('my-radio-group-name'); + }); + + test('propagates the value of the `value` prop to the `value` attribute of the native element', () => { + const radioButton = renderRadioButton(); + expect(radioButton.findNativeInput()!.getElement().getAttribute('value')).toBe('my-radio-button-value'); + }); +}); + +describe('events', () => { + test('fires a single onChange event on input click', () => { + const onChange = jest.fn(); + const radioButton = renderRadioButton(); + radioButton.findNativeInput()!.click(); + expect(onChange).toHaveBeenCalledTimes(1); + expect(onChange).toHaveBeenCalledWith(expect.objectContaining({ detail: { checked: true } })); + }); + + test('fires a single onChange event on label click', () => { + const onChange = jest.fn(); + const radioButton = renderRadioButton( + + My radio button label + + ); + radioButton.findLabel()!.click(); + expect(onChange).toHaveBeenCalledTimes(1); + expect(onChange).toHaveBeenCalledWith(expect.objectContaining({ detail: { checked: true } })); + }); + + test('fires a single onChange event on description click', () => { + const onChange = jest.fn(); + const radioButton = renderRadioButton( + + ); + radioButton.findDescription()!.click(); + expect(onChange).toHaveBeenCalledTimes(1); + expect(onChange).toHaveBeenCalledWith(expect.objectContaining({ detail: { checked: true } })); + }); + + test('does not trigger change handler if disabled', () => { + const onChange = jest.fn(); + const radioButton = renderRadioButton( + + ); + + radioButton.findLabel().click(); + + expect(radioButton.findNativeInput().getElement()).not.toBeChecked(); + expect(onChange).not.toHaveBeenCalled(); + }); + + test('does not trigger change handler if readOnly', () => { + const onChange = jest.fn(); + const radioButton = renderRadioButton( + + ); + + radioButton.findLabel().click(); + + expect(radioButton.findNativeInput().getElement()).not.toBeChecked(); + expect(onChange).not.toHaveBeenCalled(); + }); + + test('can be focused via API', () => { + let checkboxRef: RadioButtonProps.Ref | null = null; + + const radioButton = renderRadioButton( (checkboxRef = ref)} checked={false} />); + expect(checkboxRef).toBeDefined(); + + checkboxRef!.focus(); + expect(radioButton.findNativeInput().getElement()).toHaveFocus(); + }); + + test('does not trigger any change events when value is changed through api', () => { + const onChange = jest.fn(); + const { container, rerender } = render(); + const radioButton = createWrapper(container).findRadioButton()!; + expect(radioButton.findNativeInput().getElement()).not.toBeChecked(); + + rerender(); + expect(radioButton.findNativeInput().getElement()).toBeChecked(); + + rerender(); + expect(onChange).not.toHaveBeenCalled(); + }); +}); + describe('test utils', () => { test('findRadioButton', () => { - const wrapper = createWrapper( - render( - - a - - ).container.parentElement! - ); - expect(wrapper.findRadioButton()).toBeTruthy(); + const radioButton = renderRadioButton(); + expect(radioButton).toBeTruthy(); }); + test('findAllRadioButtons', () => { const wrapper = createWrapper( render(
    - - a - - - b - + +
    ).container ); expect(wrapper.findAllRadioButtons()).toHaveLength(2); }); + + test('findLabel', () => { + const radioButton = renderRadioButton( + + My radio button label + + ); + expect(radioButton.findLabel().getElement().textContent).toBe('My radio button label'); + }); + + test('findDescription', () => { + const radioButton = renderRadioButton( + + ); + expect(radioButton.findDescription()!.getElement().textContent).toBe('My radio button description'); + }); + + test('findNativeInput', () => { + const radioButton = renderRadioButton(); + expect(radioButton.findNativeInput()!.getElement().tagName).toBe('INPUT'); + }); }); From 2af378e51e5475f63032b21b78d5e22aa4d39cc9 Mon Sep 17 00:00:00 2001 From: Joan Perals Tresserra Date: Wed, 19 Nov 2025 08:57:05 +0100 Subject: [PATCH 10/22] Add coverage and support for native attributes --- .../components/radio-button/index.tsx | 7 +- .../__tests__/radio-button.test.tsx | 120 +++++++++++++++--- 2 files changed, 108 insertions(+), 19 deletions(-) diff --git a/src/internal/components/radio-button/index.tsx b/src/internal/components/radio-button/index.tsx index 31658cbd26..1208abd5fe 100644 --- a/src/internal/components/radio-button/index.tsx +++ b/src/internal/components/radio-button/index.tsx @@ -12,6 +12,7 @@ import { getBaseProps } from '../../base-component'; import AbstractSwitch from '../../components/abstract-switch'; import { fireNonCancelableEvent } from '../../events'; import { InternalBaseComponentProps } from '../../hooks/use-base-component'; +import WithNativeAttributes from '../../utils/with-native-attributes'; import { RadioButtonProps } from './interfaces'; import styles from './styles.css.js'; @@ -30,6 +31,7 @@ export default React.forwardRef(function RadioButton( readOnly, className, style, + nativeInputAttributes, ...rest }: RadioButtonProps & InternalBaseComponentProps, ref: React.Ref @@ -55,8 +57,11 @@ export default React.forwardRef(function RadioButton( __internalRootRef={rest.__internalRootRef} {...copyAnalyticsMetadataAttribute(rest)} nativeControl={nativeControlProps => ( - > {...nativeControlProps} + tag="input" + componentName="RadioButton" + nativeAttributes={nativeInputAttributes} tabIndex={tabIndex} type="radio" ref={mergedRefs} diff --git a/src/radio-button/__tests__/radio-button.test.tsx b/src/radio-button/__tests__/radio-button.test.tsx index 9753e0880e..4c3f66809e 100644 --- a/src/radio-button/__tests__/radio-button.test.tsx +++ b/src/radio-button/__tests__/radio-button.test.tsx @@ -6,28 +6,59 @@ import { render } from '@testing-library/react'; import { RadioButtonProps } from '../../../lib/components/internal/components/radio-button/interfaces'; import RadioButton from '../../../lib/components/radio-button'; import createWrapper from '../../../lib/components/test-utils/dom'; +import customCssProps from '../../internal/generated/custom-css-properties'; + +import abstractSwitchStyles from '../../../lib/components/internal/components/abstract-switch/styles.css.js'; +import styles from '../../../lib/components/internal/components/radio-button/styles.selectors.js'; function renderRadioButton(node: React.ReactNode) { const wrapper = createWrapper(render(<>{node}).container); return wrapper.findRadioButton()!; } -describe('native attributes', () => { +describe('native attributes from props', () => { + test('sets the `checked` attribute of the native element to true when `checked` is true', () => { + const radioButton = renderRadioButton(); + expect(radioButton.findNativeInput()!.getElement().checked).toBe(true); + }); + + test('sets the `checked` attribute of the native element to false when `checked` is false', () => { + const radioButton = renderRadioButton(); + expect(radioButton.findNativeInput()!.getElement().checked).toBe(false); + }); + test('propagates the value of the `name` prop to the `name` attribute of the native element', () => { const radioButton = renderRadioButton(); expect(radioButton.findNativeInput()!.getElement().getAttribute('name')).toBe('my-radio-group-name'); }); test('propagates the value of the `value` prop to the `value` attribute of the native element', () => { - const radioButton = renderRadioButton(); + const radioButton = renderRadioButton(); expect(radioButton.findNativeInput()!.getElement().getAttribute('value')).toBe('my-radio-button-value'); }); }); +describe('native attributes API', () => { + it('adds native attributes', () => { + const { container } = render( + + ); + expect(container.querySelector('[data-testid="my-test-id"]')).not.toBeNull(); + }); + it('concatenates class names', () => { + const { container } = render( + + ); + const input = container.querySelector('input'); + expect(input).toHaveClass(abstractSwitchStyles['native-input']); + expect(input).toHaveClass('additional-class'); + }); +}); + describe('events', () => { test('fires a single onChange event on input click', () => { const onChange = jest.fn(); - const radioButton = renderRadioButton(); + const radioButton = renderRadioButton(); radioButton.findNativeInput()!.click(); expect(onChange).toHaveBeenCalledTimes(1); expect(onChange).toHaveBeenCalledWith(expect.objectContaining({ detail: { checked: true } })); @@ -36,7 +67,7 @@ describe('events', () => { test('fires a single onChange event on label click', () => { const onChange = jest.fn(); const radioButton = renderRadioButton( - + My radio button label ); @@ -48,7 +79,7 @@ describe('events', () => { test('fires a single onChange event on description click', () => { const onChange = jest.fn(); const radioButton = renderRadioButton( - + ); radioButton.findDescription()!.click(); expect(onChange).toHaveBeenCalledTimes(1); @@ -58,7 +89,7 @@ describe('events', () => { test('does not trigger change handler if disabled', () => { const onChange = jest.fn(); const radioButton = renderRadioButton( - + ); radioButton.findLabel().click(); @@ -70,7 +101,7 @@ describe('events', () => { test('does not trigger change handler if readOnly', () => { const onChange = jest.fn(); const radioButton = renderRadioButton( - + ); radioButton.findLabel().click(); @@ -82,30 +113,83 @@ describe('events', () => { test('can be focused via API', () => { let checkboxRef: RadioButtonProps.Ref | null = null; - const radioButton = renderRadioButton( (checkboxRef = ref)} checked={false} />); + const radioButton = renderRadioButton( + (checkboxRef = ref)} checked={false} /> + ); expect(checkboxRef).toBeDefined(); checkboxRef!.focus(); expect(radioButton.findNativeInput().getElement()).toHaveFocus(); }); - test('does not trigger any change events when value is changed through api', () => { + test('does not trigger any change events when value is changed through API', () => { const onChange = jest.fn(); - const { container, rerender } = render(); + const { container, rerender } = render(); const radioButton = createWrapper(container).findRadioButton()!; expect(radioButton.findNativeInput().getElement()).not.toBeChecked(); - rerender(); + rerender(); expect(radioButton.findNativeInput().getElement()).toBeChecked(); - rerender(); + rerender(); expect(onChange).not.toHaveBeenCalled(); }); }); +test('style API', () => { + const wrapper = createWrapper( + render( + + Label + + ).container + ); + + const control = wrapper.findByClassName(abstractSwitchStyles.control)!.getElement(); + const label = wrapper.findByClassName(abstractSwitchStyles.label)!.getElement(); + const outerCircle = wrapper.findByClassName(styles['styled-circle-border'])!.getElement(); + const innerCircle = wrapper.findByClassName(styles['styled-circle-fill'])!.getElement(); + + expect(getComputedStyle(outerCircle).getPropertyValue('fill')).toBe('blue'); + expect(getComputedStyle(outerCircle).getPropertyValue('stroke')).toBe('green'); + expect(getComputedStyle(innerCircle).getPropertyValue('stroke')).toBe('blue'); + expect(getComputedStyle(control).getPropertyValue(customCssProps.styleFocusRingBorderColor)).toBe('orange'); + expect(getComputedStyle(control).getPropertyValue(customCssProps.styleFocusRingBorderRadius)).toBe('2px'); + expect(getComputedStyle(control).getPropertyValue(customCssProps.styleFocusRingBorderWidth)).toBe('1px'); + expect(getComputedStyle(label).getPropertyValue('color')).toBe('brown'); +}); + describe('test utils', () => { test('findRadioButton', () => { - const radioButton = renderRadioButton(); + const radioButton = renderRadioButton(); expect(radioButton).toBeTruthy(); }); @@ -113,8 +197,8 @@ describe('test utils', () => { const wrapper = createWrapper( render(
    - - + +
    ).container ); @@ -123,7 +207,7 @@ describe('test utils', () => { test('findLabel', () => { const radioButton = renderRadioButton( - + My radio button label ); @@ -132,13 +216,13 @@ describe('test utils', () => { test('findDescription', () => { const radioButton = renderRadioButton( - + ); expect(radioButton.findDescription()!.getElement().textContent).toBe('My radio button description'); }); test('findNativeInput', () => { - const radioButton = renderRadioButton(); + const radioButton = renderRadioButton(); expect(radioButton.findNativeInput()!.getElement().tagName).toBe('INPUT'); }); }); From 2dbb8616866aa993b8bf2949f3334b60003e96ac Mon Sep 17 00:00:00 2001 From: Joan Perals Tresserra Date: Wed, 19 Nov 2025 10:27:54 +0100 Subject: [PATCH 11/22] Replace onChange with onClick --- pages/radio-button/cards.page.tsx | 10 +--- pages/radio-button/common.tsx | 3 - pages/radio-button/focus-test.page.tsx | 2 +- pages/radio-button/table.page.tsx | 10 +--- pages/radio-button/test.page.tsx | 10 +--- .../__snapshots__/documenter.test.ts.snap | 4 +- .../components/radio-button/index.tsx | 4 +- .../components/radio-button/interfaces.ts | 4 +- .../__tests__/radio-button.test.tsx | 56 +++++++++---------- src/radio-group/internal.tsx | 6 +- 10 files changed, 45 insertions(+), 64 deletions(-) diff --git a/pages/radio-button/cards.page.tsx b/pages/radio-button/cards.page.tsx index ae5691025e..a076470d47 100644 --- a/pages/radio-button/cards.page.tsx +++ b/pages/radio-button/cards.page.tsx @@ -25,7 +25,7 @@ type RadioButtonDemoContext = React.Context< const Card = ({ label, checked, - onChange, + onClick, description, disabled, readOnly, @@ -45,7 +45,7 @@ const Card = ({ checked={checked} disabled={disabled} readOnly={readOnly} - onChange={onChange} + onClick={onClick} ref={ref} /> @@ -93,11 +93,7 @@ export default function RadioButtonsCardsPage() { label={option.label} description={option.description} checked={option.value === value} - onChange={({ detail }) => { - if (detail.checked) { - setValue(option.value); - } - }} + onClick={() => setValue(option.value)} disabled={option.allowDisabled && urlParams.disabled} readOnly={option.allowReadOnly && urlParams.readOnly} /> diff --git a/pages/radio-button/common.tsx b/pages/radio-button/common.tsx index c512abbfb3..39cab7547b 100644 --- a/pages/radio-button/common.tsx +++ b/pages/radio-button/common.tsx @@ -55,9 +55,6 @@ export const RadioButtonPermutation = ({ disabled, checked, name: `radio-group-${index}`, - onChange: () => { - /*empty handler to suppress react controlled property warning*/ - }, }; if (children) { return ; diff --git a/pages/radio-button/focus-test.page.tsx b/pages/radio-button/focus-test.page.tsx index 92f1604f17..cb26847bfa 100644 --- a/pages/radio-button/focus-test.page.tsx +++ b/pages/radio-button/focus-test.page.tsx @@ -18,7 +18,7 @@ export default function RadioButtonScenario() {

    - setChecked(detail.checked)}> + setChecked(true)}> Radio button label diff --git a/pages/radio-button/table.page.tsx b/pages/radio-button/table.page.tsx index b23f8b0aca..073005b8a0 100644 --- a/pages/radio-button/table.page.tsx +++ b/pages/radio-button/table.page.tsx @@ -28,7 +28,7 @@ const Row = ({ description, disabled, readOnly, - onChange, + onClick, }: Omit & { label: string }) => { const ref = useRef(null); return ( @@ -40,7 +40,7 @@ const Row = ({ checked={checked} disabled={disabled} readOnly={readOnly} - onChange={onChange} + onClick={onClick} ref={ref} /> @@ -101,11 +101,7 @@ export default function RadioButtonsTablePage() { label={option.label} description={option.description} checked={option.value === value} - onChange={({ detail }) => { - if (detail.checked) { - setValue(option.value); - } - }} + onClick={() => setValue(option.value)} disabled={option.allowDisabled && urlParams.disabled} readOnly={option.allowReadOnly && urlParams.readOnly} > diff --git a/pages/radio-button/test.page.tsx b/pages/radio-button/test.page.tsx index ab5f1ad637..497f7fe42d 100644 --- a/pages/radio-button/test.page.tsx +++ b/pages/radio-button/test.page.tsx @@ -42,7 +42,7 @@ const CustomRadioButton = ({ description, children, disabled, - onChange, + onClick, readOnly, }: Omit) => { const { urlParams } = useContext(AppContext as RadioButtonDemoContext); @@ -63,7 +63,7 @@ const CustomRadioButton = ({ checked={checked} disabled={disabled} readOnly={readOnly} - onChange={onChange} + onClick={onClick} description={fullDescription} style={urlParams.customStyle ? customStyle : undefined} > @@ -92,11 +92,7 @@ export default function RadioButtonsPage() { key={index} checked={option.value === value} description={option.description} - onChange={({ detail }) => { - if (detail.checked) { - setValue(option.value); - } - }} + onClick={() => setValue(option.value)} disabled={option.allowDisabled && urlParams.disabled} readOnly={option.allowReadOnly && urlParams.readOnly} > diff --git a/src/__tests__/snapshot-tests/__snapshots__/documenter.test.ts.snap b/src/__tests__/snapshot-tests/__snapshots__/documenter.test.ts.snap index 420237d14f..6ba6929511 100644 --- a/src/__tests__/snapshot-tests/__snapshots__/documenter.test.ts.snap +++ b/src/__tests__/snapshot-tests/__snapshots__/documenter.test.ts.snap @@ -20228,7 +20228,7 @@ exports[`Components definition for radio-button matches the snapshot: radio-butt "events": [ { "cancelable": false, - "description": "Called when the user changes the component state. The event \`detail\` contains the current value for the \`checked\` property.", + "description": "Called when the user clicks on the radio button and it is not disabled or read-only.", "detailInlineType": { "name": "RadioButtonProps.ChangeDetail", "properties": [ @@ -20241,7 +20241,7 @@ exports[`Components definition for radio-button matches the snapshot: radio-butt "type": "object", }, "detailType": "RadioButtonProps.ChangeDetail", - "name": "onChange", + "name": "onClick", }, ], "functions": [ diff --git a/src/internal/components/radio-button/index.tsx b/src/internal/components/radio-button/index.tsx index 1208abd5fe..83e954f37f 100644 --- a/src/internal/components/radio-button/index.tsx +++ b/src/internal/components/radio-button/index.tsx @@ -27,7 +27,7 @@ export default React.forwardRef(function RadioButton( description, disabled, controlId, - onChange, + onClick, readOnly, className, style, @@ -75,7 +75,7 @@ export default React.forwardRef(function RadioButton( )} onClick={() => { radioButtonRef.current?.focus(); - fireNonCancelableEvent(onChange, { checked: !checked }); + fireNonCancelableEvent(onClick, { checked: true }); }} styledControl={
  • -
    -

    - {label} -

    -

    {description}

    -
    - -
  • - ); -}; - -export default function RadioButtonsCardsPage() { - const { urlParams, setUrlParams } = useContext(AppContext as RadioButtonDemoContext); - - const [value, setValue] = useState(''); - - return ( -
    -

    Radio button — custom card selection

    - - - - - setUrlParams({ ...urlParams, disabled: detail.checked })} - description="Make one of the radio buttons disabled" - > - Disabled - - - setUrlParams({ ...urlParams, readOnly: detail.checked })} - description="Make one of the radio buttons read-only" - > - Read-only - - - - - -
    - -
      - {options.map((option, index) => ( - setValue(option.value)} - disabled={option.allowDisabled && urlParams.disabled} - readOnly={option.allowReadOnly && urlParams.readOnly} - /> - ))} -
    - - - -
    -
    - ); -} diff --git a/pages/radio-button/cards.scss b/pages/radio-button/cards.scss deleted file mode 100644 index e68caab42b..0000000000 --- a/pages/radio-button/cards.scss +++ /dev/null @@ -1,48 +0,0 @@ -/* - Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - SPDX-License-Identifier: Apache-2.0 -*/ - -@use '~design-tokens' as awsui; - -.cards { - display: flex; - flex-wrap: wrap; - gap: awsui.$space-scaled-xs; - list-style-type: none; - margin-block: 0; - margin-inline: 0; - padding-block: 0; - padding-inline: 0; -} - -.card { - $border-radius: awsui.$space-scaled-s; - align-items: start; - border-color: awsui.$color-border-control-default; - border-start-start-radius: $border-radius; - border-start-end-radius: $border-radius; - border-end-start-radius: $border-radius; - border-end-end-radius: $border-radius; - border-block-style: solid; - border-inline-style: solid; - border-block-width: 1px; - border-inline-width: 1px; - display: flex; - gap: awsui.$space-scaled-s; - inline-size: 300px; - justify-content: space-between; - padding-block: awsui.$space-scaled-l; - padding-inline: awsui.$space-scaled-l; - - &--selected { - border-color: awsui.$color-border-item-focused; - } -} - -.heading { - margin-block: 0; -} -.description { - margin-block: awsui.$space-scaled-xs 0; -} diff --git a/pages/radio-button/common.tsx b/pages/radio-button/common.tsx index 39cab7547b..44c99302c8 100644 --- a/pages/radio-button/common.tsx +++ b/pages/radio-button/common.tsx @@ -1,29 +1,12 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import React, { useState } from 'react'; +import React from 'react'; -import FormField from '~components/form-field'; -import Input from '~components/input'; import RadioButton, { RadioButtonProps } from '~components/radio-button'; import createPermutations from '../utils/permutations'; -export const options = [ - { value: 'email', label: 'E-Mail', description: 'First option' }, - { value: 'phone', label: 'Telephone', description: 'Second option', allowDisabled: true, allowReadOnly: true }, - { value: 'mail', label: 'Postal mail', description: 'Third option' }, -]; - -export const ExtraOptions = () => { - const [value, setValue] = useState(''); - return ( - - setValue(detail.value)} placeholder="Enter your address" /> - - ); -}; - -export const shortText = 'Short text'; +const shortText = 'Short text'; export const longText = 'Long text, long enough to wrap. Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Whatever.'; diff --git a/pages/radio-button/table.page.tsx b/pages/radio-button/table.page.tsx deleted file mode 100644 index d17fc17552..0000000000 --- a/pages/radio-button/table.page.tsx +++ /dev/null @@ -1,119 +0,0 @@ -// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 -import React, { useContext, useRef, useState } from 'react'; -import clsx from 'clsx'; - -import Box from '~components/box'; -import Checkbox from '~components/checkbox'; -import FormField from '~components/form-field'; -import RadioButton, { RadioButtonProps } from '~components/radio-button'; -import SpaceBetween from '~components/space-between'; - -import AppContext, { AppContextType } from '../app/app-context'; -import ScreenshotArea from '../utils/screenshot-area'; -import { ExtraOptions, options } from './common'; - -import styles from './table.scss'; - -type RadioButtonDemoContext = React.Context< - AppContextType<{ - disabled?: boolean; - readOnly?: boolean; - }> ->; - -const Row = ({ - label, - checked, - description, - disabled, - readOnly, - onSelect, -}: Omit & { label: string }) => { - const ref = useRef(null); - return ( - - - - - - {label} - - {description} - - ); -}; - -export default function RadioButtonsTablePage() { - const { urlParams, setUrlParams } = useContext(AppContext as RadioButtonDemoContext); - - const [value, setValue] = useState(''); - - return ( -
    -

    Radio button — custom table selection

    - - - - - setUrlParams({ ...urlParams, disabled: detail.checked })} - description="Make one of the radio buttons disabled" - > - Disabled - - - setUrlParams({ ...urlParams, readOnly: detail.checked })} - description="Make one of the radio buttons read-only" - > - Read-only - - - - - -
    - - - - - - - - - - - {options.map((option, index) => ( - setValue(option.value)} - disabled={option.allowDisabled && urlParams.disabled} - readOnly={option.allowReadOnly && urlParams.readOnly} - > - {option.label} - - ))} - -
    NameDescription
    - - - -
    -
    - ); -} diff --git a/pages/radio-button/table.scss b/pages/radio-button/table.scss deleted file mode 100644 index 21096dc117..0000000000 --- a/pages/radio-button/table.scss +++ /dev/null @@ -1,43 +0,0 @@ -/* - Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - SPDX-License-Identifier: Apache-2.0 -*/ - -@use '~design-tokens' as awsui; - -.table { - border-collapse: collapse; -} - -.head { - background-color: awsui.$color-background-cell-shaded; -} - -.cell, -.header-cell { - padding-inline: awsui.$space-scaled-s; -} - -.cell { - padding-block: awsui.$space-scaled-xs; -} - -.header-cell, -.row { - border-block-style: solid; - border-inline-style: solid; - border-block-width: 1px; - border-inline-width: 1px; - border-color: awsui.$color-border-divider-default; -} - -.header-cell { - padding-block: awsui.$space-scaled-xxs; - text-align: start; -} - -.row { - &--selected { - outline: 1px solid awsui.$color-border-item-focused; - } -} diff --git a/pages/radio-button/test.page.tsx b/pages/radio-button/test.page.tsx deleted file mode 100644 index 93a34253ed..0000000000 --- a/pages/radio-button/test.page.tsx +++ /dev/null @@ -1,229 +0,0 @@ -// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 -import React, { useContext, useState } from 'react'; -import clsx from 'clsx'; - -import Box from '~components/box'; -import Checkbox from '~components/checkbox'; -import ColumnLayout from '~components/column-layout'; -import FormField from '~components/form-field'; -import RadioButton, { RadioButtonProps } from '~components/radio-button'; -import RadioGroup from '~components/radio-group'; -import SpaceBetween from '~components/space-between'; - -import AppContext, { AppContextType } from '../app/app-context'; -import ScreenshotArea from '../utils/screenshot-area'; -import { ExtraOptions, options } from './common'; -import customStyle from './custom-style'; - -import styles from './styles.scss'; - -type Orientation = 'horizontal' | 'vertical'; -type Wrapper = 'form-field' | 'fieldset' | 'div'; - -type RadioButtonDemoContext = React.Context< - AppContextType<{ - customStyle?: boolean; - descriptions?: boolean; - disabled?: boolean; - interactiveElementsInDescriptions?: boolean; - interactiveElementsInLabels?: boolean; - orientation?: Orientation; - radioGroupRole?: boolean; - readOnly?: boolean; - wrapper: Wrapper; - }> ->; - -const label = 'Choose a quantity'; - -const CustomRadioButton = ({ - checked, - description, - children, - disabled, - onSelect, - readOnly, -}: Omit) => { - const { urlParams } = useContext(AppContext as RadioButtonDemoContext); - - const fullDescription = urlParams.descriptions ? ( - urlParams.interactiveElementsInDescriptions ? ( - <> - {description}. Learn more - - ) : ( - description - ) - ) : undefined; - - return ( - - {children} - {urlParams.interactiveElementsInLabels && ( - <> - {' '} - - - )} - - ); -}; - -export default function RadioButtonsPage() { - const { urlParams, setUrlParams } = useContext(AppContext as RadioButtonDemoContext); - - const [value, setValue] = useState(''); - - const className = clsx(styles['radio-group'], styles[`radio-group--${urlParams.orientation || 'vertical'}`]); - - const radioButtons = ( -
    - {options.map((option, index) => ( - setValue(option.value)} - disabled={option.allowDisabled && urlParams.disabled} - readOnly={option.allowReadOnly && urlParams.readOnly} - > - {option.label} - - ))} -
    - ); - return ( -
    -

    Radio button

    - - - - - setUrlParams({ ...urlParams, orientation: detail.value as Orientation })} - items={[ - { - label: 'Vertical', - value: 'vertical', - }, - { - label: 'Horizontal', - value: 'horizontal', - }, - ]} - /> - - - setUrlParams({ ...urlParams, wrapper: detail.value as Wrapper })} - items={[ - { - label: 'Cloudscape Form Field around div', - value: 'form-field', - }, - { - label: 'Native HTML fieldset around div', - value: 'fieldset', - }, - { - label: 'Div only', - value: 'div', - }, - ]} - /> - - - setUrlParams({ ...urlParams, radioGroupRole: detail.checked })} - description="Add `radiogroup` role to the wrapping div" - > - Use radiogroup role - - - - - - setUrlParams({ ...urlParams, disabled: detail.checked })} - description="Make one of the radio buttons disabled" - > - Disabled - - - setUrlParams({ ...urlParams, readOnly: detail.checked })} - description="Make one of the radio buttons read-only" - > - Read-only - - - setUrlParams({ ...urlParams, descriptions: detail.checked })} - description="Show descriptions" - > - Descriptions - - - {urlParams.descriptions && ( - - setUrlParams({ ...urlParams, interactiveElementsInDescriptions: detail.checked }) - } - > - Interactive elements in descriptions - - )} - - setUrlParams({ ...urlParams, interactiveElementsInLabels: detail.checked })} - > - Interactive elements in labels - - - setUrlParams({ ...urlParams, customStyle: detail.checked })} - description="Use the style API to customize the component styles" - > - Use custom style - - - - - -
    - - {urlParams.wrapper === 'form-field' ? ( - {radioButtons} - ) : urlParams.wrapper === 'fieldset' ? ( -
    - {label} - {radioButtons} -
    - ) : ( - radioButtons - )} - - - -
    -
    - ); -} From 80883b004dac34c999ef91ad777044a90bd0ec80 Mon Sep 17 00:00:00 2001 From: Joan Perals Tresserra Date: Wed, 19 Nov 2025 13:20:18 +0100 Subject: [PATCH 17/22] Restore system tag for style prop --- src/internal/components/radio-button/interfaces.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/internal/components/radio-button/interfaces.ts b/src/internal/components/radio-button/interfaces.ts index b9e850a06d..a5df3dc293 100644 --- a/src/internal/components/radio-button/interfaces.ts +++ b/src/internal/components/radio-button/interfaces.ts @@ -71,6 +71,9 @@ export interface RadioButtonProps extends BaseComponentProps { */ readOnly?: boolean; + /** + * @awsuiSystem core + */ style?: RadioButtonProps.Style; /** From 86bedb78414dbf3afe7c4162cb1a0108d3f06e26 Mon Sep 17 00:00:00 2001 From: Joan Perals Tresserra Date: Wed, 19 Nov 2025 13:32:46 +0100 Subject: [PATCH 18/22] Update Documenter snapshot --- .../__snapshots__/documenter.test.ts.snap | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/__tests__/snapshot-tests/__snapshots__/documenter.test.ts.snap b/src/__tests__/snapshot-tests/__snapshots__/documenter.test.ts.snap index ca5f3e82ab..0faf0e3242 100644 --- a/src/__tests__/snapshot-tests/__snapshots__/documenter.test.ts.snap +++ b/src/__tests__/snapshot-tests/__snapshots__/documenter.test.ts.snap @@ -20542,6 +20542,9 @@ This property should be set for either all or none of the radio buttons in a gro }, "name": "style", "optional": true, + "systemTags": [ + "core", + ], "type": "RadioButtonProps.Style", }, { @@ -20669,6 +20672,20 @@ is provided by its parent form field component.", "optional": true, "type": "string", }, + { + "description": "Defines the direction in which the radio buttons are laid out.", + "inlineType": { + "name": ""horizontal" | "vertical"", + "type": "union", + "values": [ + "horizontal", + "vertical", + ], + }, + "name": "direction", + "optional": true, + "type": "string", + }, { "deprecatedTag": "The usage of the \`id\` attribute is reserved for internal use cases. For testing and other use cases, use [data attributes](https://developer.mozilla.org/en-US/docs/Learn/HTML/Howto/Use_data_attributes). If you must From 9cc1109c5c1876a3592f0795e1268c3189c77c3f Mon Sep 17 00:00:00 2001 From: Joan Perals Tresserra Date: Wed, 19 Nov 2025 17:10:32 +0100 Subject: [PATCH 19/22] Refactor dev pages --- pages/radio-button/focus-test.page.tsx | 8 ++------ pages/radio-button/permutations.page.tsx | 17 +++++++---------- pages/radio-button/style-custom.page.tsx | 17 +++++++---------- 3 files changed, 16 insertions(+), 26 deletions(-) diff --git a/pages/radio-button/focus-test.page.tsx b/pages/radio-button/focus-test.page.tsx index 10b7468690..2c1de42d18 100644 --- a/pages/radio-button/focus-test.page.tsx +++ b/pages/radio-button/focus-test.page.tsx @@ -4,6 +4,7 @@ import React, { useState } from 'react'; import RadioButton from '~components/radio-button'; +import FocusTarget from '../common/focus-target'; import ScreenshotArea from '../utils/screenshot-area'; export default function RadioButtonScenario() { @@ -11,12 +12,7 @@ export default function RadioButtonScenario() { return (

    Radio buttons should be focused using the correct highlight

    -

    - Click here to focus so we can tab to the content below{' '} - -

    + setChecked(true)}> Radio button label diff --git a/pages/radio-button/permutations.page.tsx b/pages/radio-button/permutations.page.tsx index 18a4c00e53..ee7b829eda 100644 --- a/pages/radio-button/permutations.page.tsx +++ b/pages/radio-button/permutations.page.tsx @@ -2,20 +2,17 @@ // SPDX-License-Identifier: Apache-2.0 import React from 'react'; +import { SimplePage } from '../app/templates'; import PermutationsView from '../utils/permutations-view'; -import ScreenshotArea from '../utils/screenshot-area'; import { permutations, RadioButtonPermutation } from './common'; export default function RadioButtonPermutations() { return ( - <> -

    RadioButton permutations

    - - } - /> - - + + } + /> + ); } diff --git a/pages/radio-button/style-custom.page.tsx b/pages/radio-button/style-custom.page.tsx index 8be4d61604..13160cf1a5 100644 --- a/pages/radio-button/style-custom.page.tsx +++ b/pages/radio-button/style-custom.page.tsx @@ -2,21 +2,18 @@ // SPDX-License-Identifier: Apache-2.0 import React from 'react'; +import { SimplePage } from '../app/templates'; import PermutationsView from '../utils/permutations-view'; -import ScreenshotArea from '../utils/screenshot-area'; import { permutations, RadioButtonPermutation } from './common'; import customStyle from './custom-style'; export default function RadioButtonPermutations() { return ( - <> -

    RadioButton permutations with custom styles

    - - } - /> - - + + } + /> + ); } From 677d60e0f643fe1fd17bdf99cc9df9d9504a4195 Mon Sep 17 00:00:00 2001 From: Joan Perals Tresserra Date: Wed, 19 Nov 2025 17:11:51 +0100 Subject: [PATCH 20/22] Only call onSelect when not checked --- src/internal/components/radio-button/index.tsx | 4 +++- src/radio-button/__tests__/radio-button.test.tsx | 10 ++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/src/internal/components/radio-button/index.tsx b/src/internal/components/radio-button/index.tsx index d3c25f324d..9258f62934 100644 --- a/src/internal/components/radio-button/index.tsx +++ b/src/internal/components/radio-button/index.tsx @@ -75,7 +75,9 @@ export default React.forwardRef(function RadioButton( )} onClick={() => { radioButtonRef.current?.focus(); - fireNonCancelableEvent(onSelect); + if (!checked) { + fireNonCancelableEvent(onSelect); + } }} styledControl={
    + + +
    + ).container + ); + expect(wrapper.findAllRadioButtons()).toHaveLength(2); + }); + + test('findLabel', () => { + const radioButton = renderRadioButton( + + My radio button label + + ); + expect(radioButton.findLabel().getElement().textContent).toBe('My radio button label'); + }); + + test('findDescription', () => { + const radioButton = renderRadioButton( + + ); + expect(radioButton.findDescription()!.getElement().textContent).toBe('My radio button description'); + }); + + test('findNativeInput', () => { + const radioButton = renderRadioButton(); + expect(radioButton.findNativeInput()!.getElement().tagName).toBe('INPUT'); + }); +}); diff --git a/src/radio-button/__tests__/radio-button.test.tsx b/src/radio-button/__tests__/radio-button.test.tsx index 106c5c644e..46f99ac023 100644 --- a/src/radio-button/__tests__/radio-button.test.tsx +++ b/src/radio-button/__tests__/radio-button.test.tsx @@ -6,17 +6,9 @@ import { render } from '@testing-library/react'; import { RadioButtonProps } from '../../../lib/components/internal/components/radio-button/interfaces'; import RadioButton from '../../../lib/components/radio-button'; import createWrapper from '../../../lib/components/test-utils/dom'; -import customCssProps from '../../internal/generated/custom-css-properties'; +import { renderRadioButton } from './common'; -import abstractSwitchStyles from '../../../lib/components/internal/components/abstract-switch/styles.css.js'; -import styles from '../../../lib/components/internal/components/radio-button/styles.selectors.js'; - -function renderRadioButton(node: React.ReactNode) { - const wrapper = createWrapper(render(<>{node}).container); - return wrapper.findRadioButton()!; -} - -describe('native attributes from props', () => { +describe('Radio Button native attributes from props', () => { test('sets the `checked` attribute of the native element to true when `checked` is true', () => { const radioButton = renderRadioButton(); expect(radioButton.findNativeInput()!.getElement().checked).toBe(true); @@ -38,24 +30,7 @@ describe('native attributes from props', () => { }); }); -describe('native attributes API', () => { - it('adds native attributes', () => { - const { container } = render( - - ); - expect(container.querySelector('[data-testid="my-test-id"]')).not.toBeNull(); - }); - it('concatenates class names', () => { - const { container } = render( - - ); - const input = container.querySelector('input'); - expect(input).toHaveClass(abstractSwitchStyles['native-input']); - expect(input).toHaveClass('additional-class'); - }); -}); - -describe('events', () => { +describe('Radio Button events', () => { test('fires a single onSelect event on input click', () => { const onSelect = jest.fn(); const radioButton = renderRadioButton(); @@ -142,94 +117,3 @@ describe('events', () => { expect(onSelect).not.toHaveBeenCalled(); }); }); - -test('style API', () => { - const wrapper = createWrapper( - render( - - Label - - ).container - ); - - const control = wrapper.findByClassName(abstractSwitchStyles.control)!.getElement(); - const label = wrapper.findByClassName(abstractSwitchStyles.label)!.getElement(); - const outerCircle = wrapper.findByClassName(styles['styled-circle-border'])!.getElement(); - const innerCircle = wrapper.findByClassName(styles['styled-circle-fill'])!.getElement(); - - expect(getComputedStyle(outerCircle).getPropertyValue('fill')).toBe('blue'); - expect(getComputedStyle(outerCircle).getPropertyValue('stroke')).toBe('green'); - expect(getComputedStyle(innerCircle).getPropertyValue('stroke')).toBe('blue'); - expect(getComputedStyle(control).getPropertyValue(customCssProps.styleFocusRingBorderColor)).toBe('orange'); - expect(getComputedStyle(control).getPropertyValue(customCssProps.styleFocusRingBorderRadius)).toBe('2px'); - expect(getComputedStyle(control).getPropertyValue(customCssProps.styleFocusRingBorderWidth)).toBe('1px'); - expect(getComputedStyle(label).getPropertyValue('color')).toBe('brown'); -}); - -describe('test utils', () => { - test('findRadioButton', () => { - const radioButton = renderRadioButton(); - expect(radioButton).toBeTruthy(); - }); - - test('findAllRadioButtons', () => { - const wrapper = createWrapper( - render( -
    - - -
    - ).container - ); - expect(wrapper.findAllRadioButtons()).toHaveLength(2); - }); - - test('findLabel', () => { - const radioButton = renderRadioButton( - - My radio button label - - ); - expect(radioButton.findLabel().getElement().textContent).toBe('My radio button label'); - }); - - test('findDescription', () => { - const radioButton = renderRadioButton( - - ); - expect(radioButton.findDescription()!.getElement().textContent).toBe('My radio button description'); - }); - - test('findNativeInput', () => { - const radioButton = renderRadioButton(); - expect(radioButton.findNativeInput()!.getElement().tagName).toBe('INPUT'); - }); -}); From c8199e25861a972422ce97ec1eb5691d1bea69da Mon Sep 17 00:00:00 2001 From: Joan Perals Tresserra Date: Wed, 19 Nov 2025 17:42:23 +0100 Subject: [PATCH 22/22] Add missing screenshot areas --- pages/radio-button/permutations.page.tsx | 2 +- pages/radio-button/style-custom.page.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pages/radio-button/permutations.page.tsx b/pages/radio-button/permutations.page.tsx index ee7b829eda..b952259482 100644 --- a/pages/radio-button/permutations.page.tsx +++ b/pages/radio-button/permutations.page.tsx @@ -8,7 +8,7 @@ import { permutations, RadioButtonPermutation } from './common'; export default function RadioButtonPermutations() { return ( - + } diff --git a/pages/radio-button/style-custom.page.tsx b/pages/radio-button/style-custom.page.tsx index 13160cf1a5..dd028d7752 100644 --- a/pages/radio-button/style-custom.page.tsx +++ b/pages/radio-button/style-custom.page.tsx @@ -9,7 +9,7 @@ import customStyle from './custom-style'; export default function RadioButtonPermutations() { return ( - + }