diff --git a/pages/progress-bar/style-api-v2.page.tsx b/pages/progress-bar/style-api-v2.page.tsx new file mode 100644 index 00000000000..19475c39f4c --- /dev/null +++ b/pages/progress-bar/style-api-v2.page.tsx @@ -0,0 +1,21 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +import React from 'react'; + +import { ProgressBar } from '~components'; + +import { SimplePage } from '../app/templates'; + +import styles from './style-api-v2.scss'; + +const brand = { styleClassNames: { root: styles['bar-brand'], percentage: styles['percentage-brand'] } }; +const genai = { styleClassNames: { root: styles['bar-genai'], percentage: styles['percentage-genai'] } }; + +export default function () { + return ( + + + + + ); +} diff --git a/pages/progress-bar/style-api-v2.scss b/pages/progress-bar/style-api-v2.scss new file mode 100644 index 00000000000..78511c97861 --- /dev/null +++ b/pages/progress-bar/style-api-v2.scss @@ -0,0 +1,33 @@ +/* + Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + SPDX-License-Identifier: Apache-2.0 +*/ + +@use '~components/styling/progress-bar'; +@use '../style-api/palette' as palette; + +.bar-brand { + @include progress-bar.root( + $track-color: palette.$subtle, + $value-color: palette.$accent, + $bar-border-radius: 2px, + $bar-block-size: 8px + ); +} + +.bar-genai { + @include progress-bar.root( + $track-color: palette.$genai-surface, + $value-color: palette.$genai, + $bar-border-radius: 0, + $bar-block-size: 12px + ); +} + +.percentage-brand { + @include progress-bar.percentage($color: palette.$accent, $font-size: 14px, $font-weight: 700); +} + +.percentage-genai { + @include progress-bar.percentage($color: palette.$genai-text, $font-size: 16px, $font-weight: 600); +} diff --git a/pages/slider/style-api-v2.page.tsx b/pages/slider/style-api-v2.page.tsx new file mode 100644 index 00000000000..7ab09b15e09 --- /dev/null +++ b/pages/slider/style-api-v2.page.tsx @@ -0,0 +1,69 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +import React, { useState } from 'react'; + +import { KeyValuePairs, Slider } from '~components'; + +import { SimplePage } from '../app/templates'; + +import styles from './style-api-v2.scss'; + +const styleClassNames = { root: styles.slider, handle: styles.handle }; + +export default function () { + const [value, setValue] = useState(40); + return ( + + setValue(detail.value)} + min={0} + max={100} + step={20} + tickMarks={true} + ariaLabel="With tick marks" + {...{ styleClassNames }} + /> + ), + }, + { + type: 'pair', + label: 'Disabled', + value: ( + setValue(detail.value)} + min={0} + max={100} + disabled={true} + ariaLabel="Disabled" + {...{ styleClassNames }} + /> + ), + }, + { + type: 'pair', + label: 'Read-only', + value: ( + setValue(detail.value)} + min={0} + max={100} + readOnly={true} + ariaLabel="Read-only" + {...{ styleClassNames }} + /> + ), + }, + ]} + /> + + ); +} diff --git a/pages/slider/style-api-v2.scss b/pages/slider/style-api-v2.scss new file mode 100644 index 00000000000..167fe3299bd --- /dev/null +++ b/pages/slider/style-api-v2.scss @@ -0,0 +1,39 @@ +/* + Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + SPDX-License-Identifier: Apache-2.0 +*/ + +@use '~components/styling/slider'; +@use '../style-api/palette' as palette; + +.slider { + @include slider.root($track-color: palette.$surface-alt, $value-color: palette.$accent); +} + +// The handle is the native , so its states are regular selectors. +.handle { + @include slider.handle($fill-color: palette.$accent, $halo-color: palette.$genai, $border-radius: 4px); + + &:hover { + @include slider.handle($fill-color: palette.$accent-hover); + } + + &:is(:focus, :active) { + @include slider.handle($fill-color: palette.$accent-active); + } + + &:disabled, + &[aria-disabled='true'] { + @include slider.handle($fill-color: palette.$control-bg-disabled, $halo-color: palette.$control-bg-disabled); + } +} + +// The value fill follows the handle state through :has() on the root slot. +.slider:has(.handle:is(:focus, :active)) { + @include slider.root($value-color: palette.$accent-active); +} + +// After the focus rule: a read-only slider stays focusable, and both rules have the same specificity. +.slider:has(.handle:disabled, .handle[aria-disabled='true']) { + @include slider.root($value-color: palette.$control-bg-disabled); +} diff --git a/src/progress-bar/__tests__/style-api.test.tsx b/src/progress-bar/__tests__/style-api.test.tsx new file mode 100644 index 00000000000..76ede0d1b48 --- /dev/null +++ b/src/progress-bar/__tests__/style-api.test.tsx @@ -0,0 +1,36 @@ +// 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 ProgressBar, { ProgressBarProps } from '../../../lib/components/progress-bar'; +import createWrapper from '../../../lib/components/test-utils/dom'; + +function renderProgressBar(props: Partial & Record = {}) { + const { container } = render(); + return { container, wrapper: createWrapper(container).findProgressBar()! }; +} + +describe('ProgressBar Style API v2', () => { + test.each(['standalone', 'flash', 'key-value'])( + 'applies styleClassNames (variant=%s)', + variant => { + const { wrapper } = renderProgressBar({ + variant, + styleClassNames: { root: 'a', percentage: 'b', unknown: 'c' }, + }); + expect(wrapper.getElement()).toHaveClass('a'); + expect(wrapper.getElement()).not.toHaveClass('c'); + + const percentage = wrapper.findPercentageText()!.getElement(); + expect(percentage).not.toHaveClass('a'); + expect(percentage).toHaveClass('b'); + expect(percentage).not.toHaveClass('c'); + } + ); + + test('does not leak the styleClassNames prop to the DOM', () => { + const { container } = renderProgressBar({ styleClassNames: { root: 'a', percentage: 'b' } }); + expect(container.querySelector('[styleClassNames]')).toBeNull(); + }); +}); diff --git a/src/progress-bar/index.tsx b/src/progress-bar/index.tsx index 5c1737f059f..24e0b7a575b 100644 --- a/src/progress-bar/index.tsx +++ b/src/progress-bar/index.tsx @@ -14,7 +14,7 @@ import { applyDisplayName } from '../internal/utils/apply-display-name'; import { joinStrings } from '../internal/utils/strings'; import InternalLiveRegion from '../live-region/internal'; import { ProgressBarProps } from './interfaces'; -import { Progress, ResultState, SmallText } from './internal'; +import { Progress, ResultState, SmallText, StyleClassNames } from './internal'; import styles from './styles.css.js'; @@ -41,7 +41,8 @@ export default function ProgressBar({ const { __internalRootRef } = useBaseComponent('ProgressBar', { props: { variant }, }); - const baseProps = getBaseProps(rest); + const { styleClassNames, ...restProps } = rest as typeof rest & { styleClassNames?: StyleClassNames }; + const baseProps = getBaseProps(restProps); const generatedName = useUniqueId('awsui-progress-bar-'); const labelId = `${generatedName}-label`; @@ -75,7 +76,7 @@ export default function ProgressBar({ return (
@@ -101,6 +102,7 @@ export default function ProgressBar({ )} isInFlash={isInFlash} style={style} + styleClassNames={styleClassNames} />
); diff --git a/src/progress-bar/styles.scss b/src/progress-bar/styles.scss index 49bd3e65c11..e0320c5c8aa 100644 --- a/src/progress-bar/styles.scss +++ b/src/progress-bar/styles.scss @@ -6,20 +6,47 @@ @use '../internal/styles/tokens' as awsui; @use '../internal/styles' as styles; @use '../internal/generated/custom-css-properties/index.scss' as custom-props; +@use '../internal/styles/style-api' as style-api; @use './motion'; -$progress-height: var(#{custom-props.$progressBarHeight}, 0.4 * styles.$base-size); -$progress-border-radius: var(#{custom-props.$progressBarBorderRadius}, 10px); -$progress-background-color: var( - #{custom-props.$progressBarBackgroundColor}, - awsui.$color-background-progress-bar-default +$progress-bar-tokens: style-api.resolve((track-color, value-color, bar-block-size, bar-border-radius), carrier); +@include style-api.register($progress-bar-tokens); +@include style-api.docs('root', $progress-bar-tokens); +@include style-api.docs('percentage', $properties: (color, font-family, font-size, font-weight)); + +$progress-height: style-api.value( + $progress-bar-tokens, + bar-block-size, + var(#{custom-props.$progressBarHeight}, 0.4 * styles.$base-size) +); +$progress-border-radius: style-api.value( + $progress-bar-tokens, + bar-border-radius, + var(#{custom-props.$progressBarBorderRadius}, 10px) +); +$progress-background-color: style-api.value( + $progress-bar-tokens, + track-color, + var(#{custom-props.$progressBarBackgroundColor}, awsui.$color-background-progress-bar-default) +); +$progress-value-background-color: style-api.value( + $progress-bar-tokens, + value-color, + var(#{custom-props.$progressValueBackgroundColor}, awsui.$color-background-progress-bar-value-default) ); -$progress-value-background-color: var( - #{custom-props.$progressValueBackgroundColor}, - awsui.$color-background-progress-bar-value-default +$progress-flash-background-color: style-api.value( + $progress-bar-tokens, + track-color, + var(#{custom-props.$progressBarBackgroundColor}, awsui.$color-background-progress-bar-flash) +); +$progress-flash-value-background-color: style-api.value( + $progress-bar-tokens, + value-color, + var(#{custom-props.$progressValueBackgroundColor}, awsui.$color-background-progress-bar-value-flash) ); .root { + @include style-api.carriers($progress-bar-tokens); @include styles.styles-reset; } @@ -79,7 +106,19 @@ $progress-value-background-color: var( } .percentage { - /* used in test-utils */ + display: inline-block; + font-family: awsui.$font-family-base; + -webkit-font-smoothing: auto; + -moz-osx-font-smoothing: auto; + line-height: awsui.$line-height-body-s; + letter-spacing: awsui.$letter-spacing-body-s; + color: awsui.$color-text-small; + font-size: awsui.$font-size-body-s; + font-weight: styles.$font-weight-normal; + + &.percentage-flash { + color: currentColor; + } } @mixin general-progress-background-style { @@ -131,22 +170,16 @@ $progress-value-background-color: var( } &.progress-flash { - background-color: var(#{custom-props.$progressBarBackgroundColor}, awsui.$color-background-progress-bar-flash); + background-color: $progress-flash-background-color; &::-webkit-progress-bar { - background-color: var(#{custom-props.$progressBarBackgroundColor}, awsui.$color-background-progress-bar-flash); + background-color: $progress-flash-background-color; } &::-webkit-progress-value { - background-color: var( - #{custom-props.$progressValueBackgroundColor}, - awsui.$color-background-progress-bar-value-flash - ); + background-color: $progress-flash-value-background-color; } &::-moz-progress-bar { - background-color: var( - #{custom-props.$progressValueBackgroundColor}, - awsui.$color-background-progress-bar-value-flash - ); + background-color: $progress-flash-value-background-color; } } } diff --git a/src/slider/__tests__/style-api.test.tsx b/src/slider/__tests__/style-api.test.tsx new file mode 100644 index 00000000000..765ef577458 --- /dev/null +++ b/src/slider/__tests__/style-api.test.tsx @@ -0,0 +1,44 @@ +// 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 Slider from '../../../lib/components/slider'; +import createWrapper from '../../../lib/components/test-utils/dom'; + +import styles from '../../../lib/components/slider/styles.css.js'; + +function renderSlider(element: React.ReactElement) { + const { container } = render(element); + return { container, wrapper: createWrapper(container).findSlider()! }; +} + +const slots = { root: 'a', handle: 'b', track: 'c', range: 'd' }; + +describe('Slider Style API v2 (implicit styleClassNames)', () => { + test('applies styleClassNames to the root and handle slots only', () => { + const { container, wrapper } = renderSlider( + {}} + {...({ styleClassNames: slots } as any)} + /> + ); + + expect(wrapper.getElement()).toHaveClass('a'); + expect(container.querySelector(`.${styles.thumb}`)).toHaveClass('b'); + expect(container.querySelector('.c')).toBeNull(); + expect(container.querySelector('.d')).toBeNull(); + }); + + test('does not leak the styleClassNames prop to the DOM', () => { + const { container } = renderSlider( + {}} {...({ styleClassNames: slots } as any)} /> + ); + expect(container.querySelector('[styleClassNames]')).toBeNull(); + }); +}); diff --git a/src/slider/internal.tsx b/src/slider/internal.tsx index e1de93e7e92..08fc4e56d8a 100644 --- a/src/slider/internal.tsx +++ b/src/slider/internal.tsx @@ -27,7 +27,14 @@ import { import styles from './styles.css.js'; +// Style API v2 +export interface StyleClassNames { + root?: string; + handle?: string; +} + interface InternalSliderProps extends SliderProps, InternalBaseComponentProps { + styleClassNames?: StyleClassNames; style?: SliderProps['style']; } @@ -47,6 +54,7 @@ export default function InternalSlider({ valueFormatter, i18nStrings, style, + styleClassNames, __internalRootRef, ...rest }: InternalSliderProps) { @@ -132,7 +140,7 @@ export default function InternalSlider({