diff --git a/pages/radio-group/style-api-v2.page.tsx b/pages/radio-group/style-api-v2.page.tsx new file mode 100644 index 00000000000..0742a1dfa3f --- /dev/null +++ b/pages/radio-group/style-api-v2.page.tsx @@ -0,0 +1,62 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +import React, { useState } from 'react'; +import clsx from 'clsx'; + +import { RadioGroup, RadioGroupProps, SpaceBetween } from '~components'; + +import { SimplePage } from '../app/templates'; + +import styles from './style-api-v2.scss'; + +const plans: RadioGroupProps.RadioButtonDefinition[] = [ + { value: 'free', label: 'Free plan', description: 'For personal projects' }, + { value: 'pro', label: 'Pro plan', description: 'Best for growing teams' }, + { value: 'enterprise', label: 'Enterprise plan', description: 'Custom limits and support' }, +]; + +const plansWithDisabled: RadioGroupProps.RadioButtonDefinition[] = [ + plans[0], + plans[1], + { ...plans[2], disabled: true }, +]; + +interface StyledRadioGroupProps { + ariaLabel: string; + items: RadioGroupProps.RadioButtonDefinition[]; + readOnly?: boolean; + direction?: RadioGroupProps['direction']; +} + +function StyledRadioGroup({ ariaLabel, items, readOnly, direction }: StyledRadioGroupProps) { + const [value, setValue] = useState('pro'); + return ( + setValue(detail.value)} + {...{ + styleClassNames: { + radioButton: { control: clsx(styles['radio-control'], readOnly && styles['radio-control-readonly']) }, + }, + }} + /> + ); +} + +export default function () { + return ( + + + + + + + + + + ); +} diff --git a/pages/radio-group/style-api-v2.scss b/pages/radio-group/style-api-v2.scss new file mode 100644 index 00000000000..73763209265 --- /dev/null +++ b/pages/radio-group/style-api-v2.scss @@ -0,0 +1,37 @@ +/* + Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + SPDX-License-Identifier: Apache-2.0 +*/ + +@use '~components/styling/radio-button'; +@use '../style-api/palette' as palette; + +.radio-control { + @include radio-button.control( + $fill-color: palette.$control-bg, + $border-color: palette.$border-accent, + $focus-ring-color: palette.$accent, + $focus-ring-width: 3px, + $focus-ring-radius: 50% + ); + + &:has(:checked) { + @include radio-button.control($fill-color: palette.$control-bg-checked, $indicator-color: palette.$control-fg); + } + + &:has(:disabled) { + @include radio-button.control( + $fill-color: palette.$control-bg-disabled, + $border-color: palette.$border-disabled, + $indicator-color: palette.$control-fg-disabled + ); + } + + &.radio-control-readonly { + @include radio-button.control( + $fill-color: palette.$surface-alt, + $border-color: palette.$border, + $indicator-color: palette.$text-muted + ); + } +} diff --git a/src/internal/components/radio-button/index.tsx b/src/internal/components/radio-button/index.tsx index b8205a3d371..72b02e836bb 100644 --- a/src/internal/components/radio-button/index.tsx +++ b/src/internal/components/radio-button/index.tsx @@ -24,7 +24,7 @@ export interface InternalRadioButtonProps extends RadioButtonProps, InternalBase } // Style API v2 -interface StyleClassNames { +export interface StyleClassNames { control?: string; } diff --git a/src/internal/components/radio-button/styles.scss b/src/internal/components/radio-button/styles.scss index 9c7968c5acd..237df35501e 100644 --- a/src/internal/components/radio-button/styles.scss +++ b/src/internal/components/radio-button/styles.scss @@ -14,6 +14,8 @@ $radio-size: awsui.$size-control; $_control-props: style-api.combine((fill-color, border-color, indicator-color), style-api.$focus-ring); $_control-tokens: style-api.resolve($_control-props, carrier); +// Registered here, next to the carrier, so every component that renders this control loads the registration. +@include style-api.register($_control-tokens); .radio-control { @include style-api.carriers($_control-tokens); diff --git a/src/radio-button/styles.scss b/src/radio-button/styles.scss index 7234d46ed5b..1f0da938032 100644 --- a/src/radio-button/styles.scss +++ b/src/radio-button/styles.scss @@ -6,10 +6,9 @@ @use '../internal/styles' as styles; @use '../internal/styles/style-api' as style-api; -// The control slot is painted by the shared internal radio button, which reads the same token map. +// The control slot is painted by the shared internal radio button, which reads the same token map and registers it. $control-props: style-api.combine((fill-color, border-color, indicator-color), style-api.$focus-ring); $control-tokens: style-api.resolve($control-props, carrier); -@include style-api.register($control-tokens); @include style-api.docs('control', $control-tokens); .radio-button { diff --git a/src/radio-group/__tests__/style-api.test.tsx b/src/radio-group/__tests__/style-api.test.tsx new file mode 100644 index 00000000000..5c6a74a8827 --- /dev/null +++ b/src/radio-group/__tests__/style-api.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 RadioGroup, { RadioGroupProps } from '../../../lib/components/radio-group'; +import createWrapper from '../../../lib/components/test-utils/dom'; + +const items: RadioGroupProps.RadioButtonDefinition[] = [ + { value: 'free', label: 'Free plan' }, + { value: 'pro', label: 'Pro plan' }, + { value: 'enterprise', label: 'Enterprise plan', disabled: true }, +]; + +function renderRadioGroup(styleClassNames: Record) { + const { container } = render(); + return { container, wrapper: createWrapper(container).findRadioGroup()! }; +} + +describe('RadioGroup Style API v2', () => { + test('forwards the radio button classes to every radio', () => { + const { container, wrapper } = renderRadioGroup({ radioButton: { control: 'a', unknown: 'b' }, unknown: 'c' }); + const buttons = wrapper.findButtons(); + expect(buttons).toHaveLength(3); + for (const button of buttons) { + expect(button.findNativeInput().getElement().parentElement).toHaveClass('a'); + } + expect(container.querySelector('.b, .c')).toBeNull(); + }); + + test('does not leak the styleClassNames prop to the DOM', () => { + const { container } = renderRadioGroup({ radioButton: { control: 'a' } }); + expect(container.querySelector('[styleClassNames]')).toBeNull(); + }); +}); diff --git a/src/radio-group/internal.tsx b/src/radio-group/internal.tsx index d0011fef7a9..104424eee6a 100644 --- a/src/radio-group/internal.tsx +++ b/src/radio-group/internal.tsx @@ -7,7 +7,7 @@ import { useUniqueId } from '@cloudscape-design/component-toolkit/internal'; import { getAnalyticsMetadataAttribute } from '@cloudscape-design/component-toolkit/internal/analytics-metadata'; import { getBaseProps } from '../internal/base-component'; -import RadioButton from '../internal/components/radio-button'; +import RadioButton, { StyleClassNames as RadioButtonStyleClassNames } from '../internal/components/radio-button'; import { useFormFieldContext } from '../internal/context/form-field-context'; import { fireNonCancelableEvent } from '../internal/events'; import useRadioGroupForwardFocus from '../internal/hooks/forward-focus/radio-group'; @@ -19,7 +19,15 @@ import analyticsSelectors from './analytics-metadata/styles.css.js'; import styles from './styles.css.js'; import testUtilStyles from './test-classes/styles.css.js'; -type InternalRadioGroupProps = RadioGroupProps & InternalBaseComponentProps; +type InternalRadioGroupProps = RadioGroupProps & + InternalBaseComponentProps & { + styleClassNames?: StyleClassNames; + }; + +// Style API v2 +export interface StyleClassNames { + radioButton?: RadioButtonStyleClassNames; +} const InternalRadioGroup = React.forwardRef( ( @@ -35,6 +43,7 @@ const InternalRadioGroup = React.forwardRef( __internalRootRef, style, direction, + styleClassNames, ...props }: InternalRadioGroupProps, ref: React.Ref @@ -83,6 +92,7 @@ const InternalRadioGroup = React.forwardRef( controlId={item.controlId} readOnly={readOnly} style={style} + styleClassNames={styleClassNames?.radioButton} {...getAnalyticsMetadataAttribute( !item.disabled && !readOnly ? { diff --git a/src/radio-group/styles.scss b/src/radio-group/styles.scss index cdf6994923e..7edc284ff25 100644 --- a/src/radio-group/styles.scss +++ b/src/radio-group/styles.scss @@ -5,6 +5,9 @@ @use '../internal/styles' as styles; @use '../internal/styles/tokens' as awsui; +@use '../internal/styles/style-api' as style-api; + +@include style-api.docs-forward('radioButton', 'radio-button'); .radio-group { @include styles.styles-reset;