Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions src/internal/styles/forms/mixins.scss
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
7 changes: 0 additions & 7 deletions src/token/__tests__/__snapshots__/styles.test.tsx.snap
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
10 changes: 4 additions & 6 deletions src/token/__tests__/analytics-metadata.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -65,25 +65,23 @@ 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', () => {
const onDismiss = jest.fn();
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', () => {
const onDismiss = jest.fn();
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({});
});
});
31 changes: 30 additions & 1 deletion src/token/__tests__/styles.test.tsx
Original file line number Diff line number Diff line change
@@ -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',
Expand Down Expand Up @@ -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();
});
});
4 changes: 2 additions & 2 deletions src/token/__tests__/token.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down
74 changes: 27 additions & 47 deletions src/token/dismiss-button.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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<HTMLButtonElement>
) {
export default function DismissButton({
disabled,
dismissLabel,
onDismiss,
readOnly,
inline,
style,
}: DismissButtonProps) {
const isOneTheme = isThemeActive(Theme.OneTheme);
const buttonRef = useRef<HTMLButtonElement>(null);
const mergedRef = useMergeRefs(ref, buttonRef);
const { tabIndex } = useSingleTabStopNavigation(buttonRef);
const analyticsMetadata: GeneratedAnalyticsMetadataTokenDismiss = {
action: 'dismiss',
detail: {
label: { root: 'self' },
},
};
return (
<button
ref={mergedRef}
type="button"
tabIndex={tabIndex}
<InternalButton
variant="inline-icon"
iconName="close"
__iconSize={isOneTheme ? 'x-small' : undefined}
formAction="none"
ariaLabel={dismissLabel}
__title=""
disabled={disabled || readOnly}
__focusable={true}
analyticsAction="dismiss"
className={clsx(
styles['dismiss-button'],
legacyTestingStyles['dismiss-button'],
testUtilStyles['dismiss-button'],
inline && styles['dismiss-button-inline']
)}
aria-disabled={disabled || readOnly ? true : undefined}
onClick={() => {
if (disabled || readOnly || !onDismiss) {
return;
}

fireNonCancelableEvent(onDismiss);
}}
aria-label={dismissLabel}
data-awsui-motion-trigger="hover"
{...(disabled || readOnly ? {} : getAnalyticsMetadataAttribute(analyticsMetadata))}
>
<InternalIcon
name="close"
size={isOneTheme ? 'x-small' : undefined}
nativeAttributes={{ 'data-awsui-motion-target': '' }}
/>
</button>
style={getDismissButtonStyles(style, { disabled, readOnly })}
onClick={() => fireNonCancelableEvent(onDismiss)}
/>
);
}
1 change: 1 addition & 0 deletions src/token/internal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,7 @@ function InternalToken({
/>
{onDismiss && (
<DismissButton
style={restProps.style}
disabled={disabled}
dismissLabel={dismissLabel}
onDismiss={onDismiss}
Expand Down
62 changes: 25 additions & 37 deletions src/token/styles.scss
Original file line number Diff line number Diff line change
Expand Up @@ -11,54 +11,51 @@
@use './constants' as constants;
@use './mixins.scss' as mixins;
@use '../internal/styles/utils/theming' as theming;
@use '../internal/styles/style-api' as style-api;

.root {
@include styles.styles-reset;
}

.dismiss-button {
// The button's icon is absolutely positioned, so without this the button shrinks below the icon width.
flex-shrink: 0;
align-self: flex-start;
margin-block-end: 0;
margin-inline-start: awsui.$space-xxs;

&-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});
display: flex;
align-items: center;
align-self: center;
}
}

// Keeps the token geometry on top of the button's inline-icon variant, which has no border, no focus ring gutter
// in visual refresh only, and horizontal padding. The token box ancestor gives the selector enough specificity.
:is(.token-box, .token-box-inline) > .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;

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good comment. The new behaviour is indeed different from the previous custom dismiss button implementation. At the same time - it is consistent with icon button's default implementation - so also reproducible in standalone buttons or e.g. in Alert/Flashbar dismiss. I believe we should keep it therefore for consistency, and if we want a design change - apply it for all icon buttons, not just token dismiss.


@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;
}
Expand Down Expand Up @@ -127,20 +124,11 @@
.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 {
pointer-events: none;
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);
}
}
35 changes: 26 additions & 9 deletions src/token/styles.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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,
},
};
}
Loading