diff --git a/pages/token/style-api-v2.page.tsx b/pages/token/style-api-v2.page.tsx new file mode 100644 index 0000000000..fb20e8630a --- /dev/null +++ b/pages/token/style-api-v2.page.tsx @@ -0,0 +1,50 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +import React from 'react'; +import clsx from 'clsx'; + +import { KeyValuePairs, Token } from '~components'; + +import { SimplePage } from '../app/templates'; + +import styles from './style-api-v2.scss'; + +const themed = { root: styles.root, dismissButton: styles.dismiss }; +const muted = { + root: clsx(styles.root, styles['root-muted']), + dismissButton: clsx(styles.dismiss, styles['dismiss-muted']), +}; + +export default function () { + const shared = { dismissLabel: 'Remove', onDismiss: () => {} }; + return ( + + + ), + }, + { + type: 'pair', + label: 'Inline variant', + value: , + }, + { + type: 'pair', + label: 'Disabled', + value: , + }, + { + type: 'pair', + label: 'Read-only', + value: , + }, + ]} + /> + + ); +} diff --git a/pages/token/style-api-v2.scss b/pages/token/style-api-v2.scss new file mode 100644 index 0000000000..c845dd3245 --- /dev/null +++ b/pages/token/style-api-v2.scss @@ -0,0 +1,50 @@ +/* + Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + SPDX-License-Identifier: Apache-2.0 +*/ + +@use '@cloudscape-design/component-toolkit/styling' as styling; +@use '../style-api/palette' as palette; + +.root { + @include styling.override { + background-color: palette.$genai-surface; + border-color: palette.$genai; + border-width: 2px; + border-start-start-radius: 4px; + border-start-end-radius: 4px; + border-end-start-radius: 4px; + border-end-end-radius: 4px; + padding-block: 4px; + padding-inline: 4px 0px; + } +} + +.dismiss { + --awsui-style-focus-ring-color: #{palette.$genai}; + --awsui-style-focus-ring-width: 2px; + --awsui-style-focus-ring-radius: 4px; + + @include styling.override { + color: palette.$genai; + + &:hover { + color: palette.$genai-hover; + } + } +} + +// Disabled and read-only are applied as extra classes, because they are not element states the +// consumer can target (double selectors bump specificity above the rules above). +.root.root-muted { + @include styling.override { + background-color: palette.$surface-alt; + border-color: palette.$border-disabled; + } +} + +.dismiss.dismiss-muted { + @include styling.override { + color: palette.$text-disabled; + } +} diff --git a/src/__tests__/snapshot-tests/__snapshots__/style-docs.test.ts.snap b/src/__tests__/snapshot-tests/__snapshots__/style-docs.test.ts.snap index 0ec1499e58..cebb43af89 100644 --- a/src/__tests__/snapshot-tests/__snapshots__/style-docs.test.ts.snap +++ b/src/__tests__/snapshot-tests/__snapshots__/style-docs.test.ts.snap @@ -237,6 +237,42 @@ exports[`Style API docs definition for toggle matches the snapshot: toggle 1`] = } `; +exports[`Style API docs definition for token matches the snapshot: token 1`] = ` +{ + "slots": [ + { + "name": "root", + "properties": [ + "background-color", + "border-color", + "border-width", + "border-radius", + "padding-block", + "padding-inline", + ], + "propertyDescriptions": {}, + "tokenDescriptions": {}, + "tokens": [], + }, + { + "name": "dismissButton", + "properties": [ + "color", + ], + "propertyDescriptions": {}, + "tokenDescriptions": {}, + "tokens": [ + "focus-ring-color", + "focus-ring-width", + "focus-ring-radius", + "focus-ring-gutter-block", + "focus-ring-gutter-inline", + ], + }, + ], +} +`; + exports[`Style API docs list of components with a Style API doc matches the snapshot 1`] = ` [ "alert", @@ -247,5 +283,6 @@ exports[`Style API docs list of components with a Style API doc matches the snap "link", "radio-button", "toggle", + "token", ] `; diff --git a/src/internal/styles/style-api.scss b/src/internal/styles/style-api.scss index a245639edf..8e3a8eddfd 100644 --- a/src/internal/styles/style-api.scss +++ b/src/internal/styles/style-api.scss @@ -51,9 +51,9 @@ $_focus-ring-carriers: toolkit.resolve($focus-ring, carrier); } } -@mixin docs($name, $map) { +@mixin docs($name, $tokens: null, $properties: null, $descriptions: null) { @if enabled() { - @include toolkit.docs($name, $map); + @include toolkit.docs($name, $tokens, $properties, $descriptions); } } diff --git a/src/token/__tests__/style-api.test.tsx b/src/token/__tests__/style-api.test.tsx new file mode 100644 index 0000000000..305c2ed5c7 --- /dev/null +++ b/src/token/__tests__/style-api.test.tsx @@ -0,0 +1,50 @@ +// 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 createWrapper from '../../../lib/components/test-utils/dom'; +import Token from '../../../lib/components/token'; + +const slots = { root: 'a', dismissButton: 'b', unknown: 'c' }; + +function renderToken(element: React.ReactElement) { + const { container } = render(element); + return { container, wrapper: createWrapper(container).findToken()! }; +} + +describe('Token Style API v2 (implicit styleClassNames)', () => { + test('applies styleClassNames to the root and dismissButton slots', () => { + const { container, wrapper } = renderToken( + {}} {...({ styleClassNames: slots } as any)} /> + ); + + const dismissButton = wrapper.findDismiss()!.getElement(); + expect(dismissButton).toHaveClass('b'); + // the root slot lands on the token box, the element that carries the token's own styling + expect(dismissButton.parentElement).toHaveClass('a'); + expect(container.querySelector('.c')).toBeNull(); + }); + + test('applies the slots to the inline variant', () => { + const { wrapper } = renderToken( + {}} + {...({ styleClassNames: slots } as any)} + /> + ); + const dismissButton = wrapper.findDismiss()!.getElement(); + expect(dismissButton).toHaveClass('b'); + expect(dismissButton.parentElement).toHaveClass('a'); + }); + + test('does not leak the styleClassNames prop to the DOM', () => { + const { container } = renderToken( + {}} {...({ styleClassNames: slots } as any)} /> + ); + expect(container.querySelector('[styleClassNames]')).toBeNull(); + }); +}); diff --git a/src/token/dismiss-button.tsx b/src/token/dismiss-button.tsx index f751760eb6..0cdfcf68a0 100644 --- a/src/token/dismiss-button.tsx +++ b/src/token/dismiss-button.tsx @@ -16,6 +16,7 @@ import styles from './styles.css.js'; import testUtilStyles from './test-classes/styles.css.js'; interface DismissButtonProps { + className?: string; disabled?: boolean; readOnly?: boolean; onDismiss?: NonCancelableEventHandler; @@ -26,7 +27,7 @@ interface DismissButtonProps { export default forwardRef(DismissButton); function DismissButton( - { disabled, dismissLabel, onDismiss, readOnly, inline }: DismissButtonProps, + { className, disabled, dismissLabel, onDismiss, readOnly, inline }: DismissButtonProps, ref: Ref ) { const isOneTheme = isThemeActive(Theme.OneTheme); @@ -44,7 +45,8 @@ function DismissButton( styles['dismiss-button'], legacyTestingStyles['dismiss-button'], testUtilStyles['dismiss-button'], - inline && styles['dismiss-button-inline'] + inline && styles['dismiss-button-inline'], + className )} aria-disabled={disabled || readOnly ? true : undefined} onClick={() => { diff --git a/src/token/index.tsx b/src/token/index.tsx index ffbba14e8b..3fa037676d 100644 --- a/src/token/index.tsx +++ b/src/token/index.tsx @@ -9,7 +9,7 @@ import useBaseComponent from '../internal/hooks/use-base-component'; import { applyDisplayName } from '../internal/utils/apply-display-name'; import { GeneratedAnalyticsMetadataTokenComponent } from './analytics-metadata/interfaces'; import { TokenProps } from './interfaces'; -import InternalToken from './internal'; +import InternalToken, { StyleClassNames } from './internal'; import analyticsSelectors from './analytics-metadata/styles.css.js'; @@ -26,6 +26,7 @@ export default function Token(props: TokenProps) { return ( diff --git a/src/token/internal.tsx b/src/token/internal.tsx index 9bc1df0256..06e666968d 100644 --- a/src/token/internal.tsx +++ b/src/token/internal.tsx @@ -28,8 +28,15 @@ import analyticsSelectors from './analytics-metadata/styles.css.js'; import styles from './styles.css.js'; import testUtilStyles from './test-classes/styles.css.js'; +// Style API v2 +export interface StyleClassNames { + root?: string; + dismissButton?: string; +} + type InternalTokenProps = Omit & InternalBaseComponentProps & { + styleClassNames?: StyleClassNames; /** Token label. Required unless `__customContent` replaces the option layout entirely. */ label?: React.ReactNode; /** @@ -69,6 +76,7 @@ function InternalToken({ disableInnerPadding, __tokenBoxClassName, __customContent, + styleClassNames, // Base __internalRootRef, @@ -187,7 +195,8 @@ function InternalToken({ readOnly && styles['token-box-readonly'], !isInline && !onDismiss && !__customContent && styles['token-box-without-dismiss'], disableInnerPadding && styles['disable-padding'], - __tokenBoxClassName + __tokenBoxClassName, + styleClassNames?.root )} style={tokenRootStyleProps} > @@ -203,6 +212,7 @@ function InternalToken({ /> {onDismiss && (