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 (
+
+
+
+
+ );
+};
+
+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
+
+
+
+
+
+
+
+
+
+
+ |
+ Name |
+ Description |
+
+
+
+ {options.map((option, index) => (
+ {
+ if (detail.checked) {
+ setValue(option.value);
+ }
+ }}
+ disabled={option.allowDisabled && urlParams.disabled}
+ readOnly={option.allowReadOnly && urlParams.readOnly}
+ >
+ {option.label}
+
+ ))}
+
+
+
+
+
+
+
+ );
+}
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' ? (
+
+ ) : (
+ 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={