From 7d105c04d4a448dcf8c9135f847ec256d8c0bfed Mon Sep 17 00:00:00 2001 From: Andrei Zhaleznichenka Date: Mon, 26 Oct 2026 11:27:18 +0100 Subject: [PATCH] refactor: Refactors token to use internal button for dismiss --- src/internal/styles/forms/mixins.scss | 28 +++++++ .../__snapshots__/styles.test.tsx.snap | 7 -- .../__tests__/analytics-metadata.test.tsx | 10 +-- src/token/__tests__/styles.test.tsx | 31 +++++++- src/token/__tests__/token.test.tsx | 4 +- src/token/dismiss-button.tsx | 74 +++++++------------ src/token/internal.tsx | 1 + src/token/styles.scss | 62 +++++++--------- src/token/styles.tsx | 35 ++++++--- 9 files changed, 143 insertions(+), 109 deletions(-) diff --git a/src/internal/styles/forms/mixins.scss b/src/internal/styles/forms/mixins.scss index 7188bf455a1..268b676163a 100644 --- a/src/internal/styles/forms/mixins.scss +++ b/src/internal/styles/forms/mixins.scss @@ -410,3 +410,31 @@ $inline-label-border-radius: 2px; awsui.$color-background-input-disabled ); } + +// Resets the generic v1 style properties that the button reads. They inherit, so a button nested in another +// styled component picks them up; the button's own v1 style is set inline and still applies. +@mixin reset-v1-button-styles { + @each $property + in ( + custom-props.$styleBackgroundActive, + custom-props.$styleBackgroundDefault, + custom-props.$styleBackgroundDisabled, + custom-props.$styleBackgroundHover, + custom-props.$styleBorderColorActive, + custom-props.$styleBorderColorDefault, + custom-props.$styleBorderColorDisabled, + custom-props.$styleBorderColorHover, + custom-props.$styleBoxShadowActive, + custom-props.$styleBoxShadowDefault, + custom-props.$styleBoxShadowDisabled, + custom-props.$styleBoxShadowFocus, + custom-props.$styleBoxShadowHover, + custom-props.$styleColorActive, + custom-props.$styleColorDefault, + custom-props.$styleColorDisabled, + custom-props.$styleColorHover + ) + { + #{$property}: initial; + } +} diff --git a/src/token/__tests__/__snapshots__/styles.test.tsx.snap b/src/token/__tests__/__snapshots__/styles.test.tsx.snap index dc0879616c0..8a5cf5a302e 100644 --- a/src/token/__tests__/__snapshots__/styles.test.tsx.snap +++ b/src/token/__tests__/__snapshots__/styles.test.tsx.snap @@ -2,19 +2,12 @@ exports[`getTokenRootStyles handles all possible style configurations 1`] = ` { - "--awsui-style-focus-ring-border-color-ka4djm": "#6366f1", - "--awsui-style-focus-ring-border-radius-ka4djm": "12px", - "--awsui-style-focus-ring-border-width-ka4djm": "2px", "--awsui-token-style-background-default-ka4djm": "#eef2ff", "--awsui-token-style-background-disabled-ka4djm": "#f1f5f9", "--awsui-token-style-background-read-only-ka4djm": "#f8fafc", "--awsui-token-style-border-color-default-ka4djm": "#c7d2fe", "--awsui-token-style-border-color-disabled-ka4djm": "#e2e8f0", "--awsui-token-style-border-color-read-only-ka4djm": "#cbd5e1", - "--awsui-token-style-dismiss-color-default-ka4djm": "#6366f1", - "--awsui-token-style-dismiss-color-disabled-ka4djm": "#cbd5e1", - "--awsui-token-style-dismiss-color-hover-ka4djm": "#4338ca", - "--awsui-token-style-dismiss-color-read-only-ka4djm": "#94a3b8", "borderRadius": "24px", "borderWidth": "2px", "paddingBlock": "4px", diff --git a/src/token/__tests__/analytics-metadata.test.tsx b/src/token/__tests__/analytics-metadata.test.tsx index 4b29f33d413..a16083e71b3 100644 --- a/src/token/__tests__/analytics-metadata.test.tsx +++ b/src/token/__tests__/analytics-metadata.test.tsx @@ -65,8 +65,8 @@ describe('Token analytics metadata', () => { const wrapper = renderToken({ label: 'Test token', onDismiss }); const dismissButton = wrapper.findDismiss()!.getElement(); - const metadata = dismissButton.getAttribute('data-awsui-analytics'); - expect(metadata).toBeTruthy(); + const metadata = JSON.parse(dismissButton.getAttribute('data-awsui-analytics')!); + expect(metadata).toEqual({ action: 'dismiss', detail: { label: { root: 'self' } } }); }); test('does not add analytics metadata to dismiss button when disabled', () => { @@ -74,8 +74,7 @@ describe('Token analytics metadata', () => { const wrapper = renderToken({ label: 'Test token', onDismiss, disabled: true }); const dismissButton = wrapper.findDismiss()!.getElement(); - const metadata = dismissButton.getAttribute('data-awsui-analytics'); - expect(metadata).toBeFalsy(); + expect(JSON.parse(dismissButton.getAttribute('data-awsui-analytics') ?? '{}')).toEqual({}); }); test('does not add analytics metadata to dismiss button when readOnly', () => { @@ -83,7 +82,6 @@ describe('Token analytics metadata', () => { const wrapper = renderToken({ label: 'Test token', onDismiss, readOnly: true }); const dismissButton = wrapper.findDismiss()!.getElement(); - const metadata = dismissButton.getAttribute('data-awsui-analytics'); - expect(metadata).toBeFalsy(); + expect(JSON.parse(dismissButton.getAttribute('data-awsui-analytics') ?? '{}')).toEqual({}); }); }); diff --git a/src/token/__tests__/styles.test.tsx b/src/token/__tests__/styles.test.tsx index 87726b0bbd2..86c64d5fc6f 100644 --- a/src/token/__tests__/styles.test.tsx +++ b/src/token/__tests__/styles.test.tsx @@ -1,7 +1,7 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import { getTokenRootStyles } from '../styles'; +import { getDismissButtonStyles, getTokenRootStyles } from '../styles'; jest.mock('../../internal/environment', () => ({ SYSTEM: 'core', @@ -63,3 +63,32 @@ describe('getTokenRootStyles', () => { expect(fn({ root: { background: { default: '#fff' } } })).toMatchSnapshot(); }); }); + +describe('getDismissButtonStyles', () => { + const dismissButton = { + color: { default: 'default', hover: 'hover', disabled: 'disabled', readOnly: 'readOnly' }, + focusRing: { borderColor: 'ring', borderRadius: '4px', borderWidth: '2px' }, + }; + + test('maps the token dismiss button style to the button style', () => { + expect(getDismissButtonStyles({ dismissButton }, {})).toEqual({ + root: { + color: { default: 'default', hover: 'hover', active: 'hover', disabled: 'disabled' }, + focusRing: dismissButton.focusRing, + }, + }); + }); + + test('uses the read-only color for the disabled state of a read-only token', () => { + expect(getDismissButtonStyles({ dismissButton }, { readOnly: true })!.root!.color!.disabled).toBe('readOnly'); + }); + + test('uses the disabled color when a token is both disabled and read-only', () => { + const styles = getDismissButtonStyles({ dismissButton }, { disabled: true, readOnly: true }); + expect(styles!.root!.color!.disabled).toBe('disabled'); + }); + + test('returns undefined without a dismiss button style', () => { + expect(getDismissButtonStyles({ root: {} }, {})).toBeUndefined(); + }); +}); diff --git a/src/token/__tests__/token.test.tsx b/src/token/__tests__/token.test.tsx index 2e4b723f536..7c1df1fef2a 100644 --- a/src/token/__tests__/token.test.tsx +++ b/src/token/__tests__/token.test.tsx @@ -108,11 +108,11 @@ describe('Token', () => { expect(onDismiss).toHaveBeenCalledTimes(1); }); - test('is a hover-motion trigger and its close icon carries the motion target', () => { + test('is a hover-motion trigger and target', () => { const wrapper = renderToken({ label: 'Test token', onDismiss: jest.fn() }); const dismiss = wrapper.findDismiss()!.getElement(); expect(dismiss).toHaveAttribute('data-awsui-motion-trigger', 'hover'); - expect(dismiss.querySelector('[data-awsui-motion-target]')).toBeTruthy(); + expect(dismiss).toHaveAttribute('data-awsui-motion-target'); }); test('shows for inline readonly tokens', () => { diff --git a/src/token/dismiss-button.tsx b/src/token/dismiss-button.tsx index ddd33d90ff6..6ef1627a8c5 100644 --- a/src/token/dismiss-button.tsx +++ b/src/token/dismiss-button.tsx @@ -1,20 +1,15 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import React, { forwardRef, Ref, useRef } from 'react'; +import React from 'react'; import clsx from 'clsx'; -import { - isThemeActive, - Theme, - useMergeRefs, - useSingleTabStopNavigation, -} from '@cloudscape-design/component-toolkit/internal'; -import { getAnalyticsMetadataAttribute } from '@cloudscape-design/component-toolkit/internal/analytics-metadata'; +import { isThemeActive, Theme } from '@cloudscape-design/component-toolkit/internal'; -import InternalIcon from '../icon/internal'; +import { InternalButton } from '../button/internal'; import { fireNonCancelableEvent } from '../internal/events'; import { NonCancelableEventHandler } from '../types/events'; -import { GeneratedAnalyticsMetadataTokenDismiss } from './analytics-metadata/interfaces'; +import { TokenProps } from './interfaces'; +import { getDismissButtonStyles } from './styles'; import legacyTestingStyles from '../token-group/styles.css.js'; import styles from './styles.css.js'; @@ -26,52 +21,37 @@ interface DismissButtonProps { onDismiss?: NonCancelableEventHandler; dismissLabel?: string; inline?: boolean; + style?: TokenProps['style']; } -export default forwardRef(DismissButton); - -function DismissButton( - { disabled, dismissLabel, onDismiss, readOnly, inline }: DismissButtonProps, - ref: Ref -) { +export default function DismissButton({ + disabled, + dismissLabel, + onDismiss, + readOnly, + inline, + style, +}: DismissButtonProps) { const isOneTheme = isThemeActive(Theme.OneTheme); - const buttonRef = useRef(null); - const mergedRef = useMergeRefs(ref, buttonRef); - const { tabIndex } = useSingleTabStopNavigation(buttonRef); - const analyticsMetadata: GeneratedAnalyticsMetadataTokenDismiss = { - action: 'dismiss', - detail: { - label: { root: 'self' }, - }, - }; return ( - + style={getDismissButtonStyles(style, { disabled, readOnly })} + onClick={() => fireNonCancelableEvent(onDismiss)} + /> ); } diff --git a/src/token/internal.tsx b/src/token/internal.tsx index ad094379993..1b3d04e7087 100644 --- a/src/token/internal.tsx +++ b/src/token/internal.tsx @@ -207,6 +207,7 @@ function InternalToken({ /> {onDismiss && ( .dismiss-button.dismiss-button { border-block: awsui.$border-width-field solid transparent; border-inline: awsui.$border-width-field solid transparent; - padding-block: 0; - padding-inline: awsui.$space-xxs; - color: var(#{custom-props.$tokenStyleDismissColorDefault}, awsui.$color-text-button-inline-icon-default); - background-color: transparent; - cursor: pointer; - #{custom-props.$styleFocusRingBoxShadow}: 0 0 0 - var(#{custom-props.$styleFocusRingBorderWidth}, foundation.$box-shadow-focused-width) - var(#{custom-props.$styleFocusRingBorderColor}, awsui.$color-border-item-focused); + @include styles.reset-v1-button-styles; @include focus-visible.when-visible { - @include styles.focus-highlight( - $gutter: 0px, - $border-radius: var(#{custom-props.$styleFocusRingBorderRadius}, awsui.$border-radius-control-default-focus-ring), - $box-shadow: var(#{custom-props.$styleFocusRingBoxShadow}) + @include style-api.focus-highlight( + $gutter-block: 0px, + $gutter-inline: 0px, + $radius: var(#{custom-props.$styleFocusRingBorderRadius}, awsui.$border-radius-control-default-focus-ring), + $width: var(#{custom-props.$styleFocusRingBorderWidth}, foundation.$box-shadow-focused-width), + $color: var(#{custom-props.$styleFocusRingBorderColor}, awsui.$color-border-item-focused) ); } - &:focus { - outline: none; - text-decoration: none; - } - - &:hover { - color: var(#{custom-props.$tokenStyleDismissColorHover}, awsui.$color-text-button-inline-icon-hover); - } - - &-inline { - // Extra space keeps the dismiss button clear of small interactive labels (WCAG target spacing). - margin-inline-start: calc(#{awsui.$space-xxs} + #{awsui.$border-width-field}); + &.dismiss-button-inline { padding-inline: 0; - display: flex; - align-items: center; - align-self: center; } - &:not(&-inline) { + &:not(.dismiss-button-inline) { @include theming.one-theme-only { padding-block-start: 1px; } @@ -127,10 +124,6 @@ .token-box-readonly { border-color: var(#{custom-props.$tokenStyleBorderColorReadOnly}, awsui.$color-border-input-disabled); background: var(#{custom-props.$tokenStyleBackgroundReadOnly}, awsui.$color-background-container-content); - - > .dismiss-button { - color: var(#{custom-props.$tokenStyleDismissColorReadOnly}, awsui.$color-text-button-inline-icon-disabled); - } } .token-box-disabled { @@ -138,9 +131,4 @@ border-color: var(#{custom-props.$tokenStyleBorderColorDisabled}, awsui.$color-border-control-disabled); background: var(#{custom-props.$tokenStyleBackgroundDisabled}, awsui.$color-background-container-content); color: awsui.$color-text-disabled; - - // stylelint-disable-next-line no-descending-specificity - > .dismiss-button { - color: var(#{custom-props.$tokenStyleDismissColorDisabled}, awsui.$color-text-button-inline-icon-disabled); - } } diff --git a/src/token/styles.tsx b/src/token/styles.tsx index c45129471be..52dde3bf8e8 100644 --- a/src/token/styles.tsx +++ b/src/token/styles.tsx @@ -1,5 +1,6 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 +import { ButtonProps } from '../button/interfaces'; import { SYSTEM } from '../internal/environment'; import customCssProps from '../internal/generated/custom-css-properties'; import { TokenProps } from './interfaces'; @@ -20,14 +21,30 @@ export function getTokenRootStyles(style: TokenProps['style']) { [customCssProps.tokenStyleBorderColorDefault]: style?.root?.borderColor?.default, [customCssProps.tokenStyleBorderColorDisabled]: style?.root?.borderColor?.disabled, [customCssProps.tokenStyleBorderColorReadOnly]: style?.root?.borderColor?.readOnly, - [customCssProps.tokenStyleDismissColorDefault]: style?.dismissButton?.color?.default, - [customCssProps.tokenStyleDismissColorDisabled]: style?.dismissButton?.color?.disabled, - [customCssProps.tokenStyleDismissColorHover]: style?.dismissButton?.color?.hover, - [customCssProps.tokenStyleDismissColorReadOnly]: style?.dismissButton?.color?.readOnly, - ...(style?.dismissButton?.focusRing && { - [customCssProps.styleFocusRingBorderColor]: style.dismissButton.focusRing.borderColor, - [customCssProps.styleFocusRingBorderRadius]: style.dismissButton.focusRing.borderRadius, - [customCssProps.styleFocusRingBorderWidth]: style.dismissButton.focusRing.borderWidth, - }), + }; +} + +// Maps the token's dismiss button style to the button style. Button has no read-only state: a read-only token +// shows its dismiss button as disabled, and the disabled color wins when both apply. The token never had an +// active color, so pressing keeps the hover color. +export function getDismissButtonStyles( + style: TokenProps['style'], + { disabled, readOnly }: { disabled?: boolean; readOnly?: boolean } +): ButtonProps.Style | undefined { + const dismissButton = style?.dismissButton; + if (SYSTEM !== 'core' || !dismissButton) { + return undefined; + } + const color = dismissButton.color; + return { + root: { + color: color && { + default: color.default, + hover: color.hover, + active: color.hover, + disabled: readOnly && !disabled ? color.readOnly : color.disabled, + }, + focusRing: dismissButton.focusRing, + }, }; }