+
@@ -253,6 +263,7 @@ const InternalAlert = React.forwardRef(
ariaLabel={dismissAriaLabel}
onClick={dismiss}
style={getDismissButtonStyles(style)}
+ classNames={{ button: classNames?.dismissButton }}
/>
)}
diff --git a/src/alert/styles.scss b/src/alert/styles.scss
index 0cb3e3bc5f..172c43b7f6 100644
--- a/src/alert/styles.scss
+++ b/src/alert/styles.scss
@@ -9,9 +9,15 @@
@use '@cloudscape-design/component-toolkit/internal/focus-visible' as focus-visible;
@use '../internal/generated/custom-css-properties/index.scss' as custom-props;
@use '../internal/styles/foundation' as foundation;
+@use '../internal/styles/style-api' as style-api;
@use './motion';
+$props: style-api.combine(style-api.$surface, (padding-block, padding-inline), style-api.$focus-ring);
+$tokens: style-api.resolve($props, carrier);
+@include style-api.register($tokens);
+@include style-api.docs('root', $tokens);
+
.root {
@include styles.styles-reset;
@include styles.text-flex-wrapping;
@@ -23,8 +29,9 @@
}
.alert {
+ @include style-api.carriers($tokens);
@include styles.styles-reset;
- color: awsui.$color-text-alert-default;
+ color: var(#{style-api.read($tokens, color)}, #{awsui.$color-text-alert-default});
position: relative;
display: flex;
flex-direction: row;
@@ -32,17 +39,13 @@
border-block-end: awsui.$border-width-alert-block-end solid;
border-inline-start: awsui.$border-width-alert-inline-start solid;
border-inline-end: awsui.$border-width-alert-inline-end solid;
- border-start-start-radius: awsui.$border-radius-alert;
- border-start-end-radius: awsui.$border-radius-alert;
- border-end-start-radius: awsui.$border-radius-alert;
- border-end-end-radius: awsui.$border-radius-alert;
- padding-block: awsui.$space-alert-vertical;
- padding-inline: awsui.$space-alert-horizontal;
- background-color: awsui.$color-background-container-content;
-
- #{custom-props.$alertFocusRingBoxShadow}: 0 0 0
- var(#{custom-props.$alertFocusRingBorderWidth}, foundation.$box-shadow-focused-width)
- var(#{custom-props.$alertFocusRingBorderColor}, awsui.$color-border-item-focused);
+ border-start-start-radius: var(#{style-api.read($tokens, border-radius)}, #{awsui.$border-radius-alert});
+ border-start-end-radius: var(#{style-api.read($tokens, border-radius)}, #{awsui.$border-radius-alert});
+ border-end-start-radius: var(#{style-api.read($tokens, border-radius)}, #{awsui.$border-radius-alert});
+ border-end-end-radius: var(#{style-api.read($tokens, border-radius)}, #{awsui.$border-radius-alert});
+ padding-block: var(#{style-api.read($tokens, padding-block)}, #{awsui.$space-alert-vertical});
+ padding-inline: var(#{style-api.read($tokens, padding-inline)}, #{awsui.$space-alert-horizontal});
+ background-color: var(#{style-api.read($tokens, background-color)}, #{awsui.$color-background-container-content});
}
.alert-wrapper {
@@ -86,14 +89,22 @@
outline: none;
}
@include focus-visible.when-visible {
- @include styles.focus-highlight(
- $gutter: awsui.$space-button-focus-outline-gutter,
- $border-radius: var(#{custom-props.$alertFocusRingBorderRadius}, awsui.$border-radius-control-default-focus-ring),
- $box-shadow: var(#{custom-props.$alertFocusRingBoxShadow})
+ @include style-api.focus-highlight(
+ $radius: var(#{custom-props.$alertFocusRingBorderRadius}, awsui.$border-radius-control-default-focus-ring),
+ $width: var(#{custom-props.$alertFocusRingBorderWidth}, foundation.$box-shadow-focused-width),
+ $color: var(#{custom-props.$alertFocusRingBorderColor}, awsui.$color-border-item-focused)
);
}
}
+$icon-props: (color);
+$icon-tokens: style-api.resolve($icon-props, carrier);
+@include style-api.docs('icon', $icon-tokens);
+
+.icon {
+ @include style-api.carriers($icon-tokens);
+}
+
.text {
min-inline-size: 0;
// To account for vertical misalignment due to button borders
@@ -160,10 +171,13 @@ $_text-colors: (
@each $type in map.keys($_text-colors) {
.type-#{$type} {
- border-color: #{map.get($_border-colors, $type)};
- background-color: #{map.get($_background-colors, $type)};
+ border-color: var(#{style-api.read($tokens, border-color)}, #{map.get($_border-colors, $type)});
+ background-color: var(#{style-api.read($tokens, background-color)}, #{map.get($_background-colors, $type)});
> .alert-wrapper > .alert-focus-wrapper > .icon {
- color: var(#{custom-props.$alertIconColor}, #{map.get($_text-colors, $type)});
+ color: var(
+ #{style-api.read($icon-tokens, color)},
+ var(#{custom-props.$alertIconColor}, #{map.get($_text-colors, $type)})
+ );
}
}
}
diff --git a/src/badge/__tests__/badge.test.tsx b/src/badge/__tests__/badge.test.tsx
index 4e548bda2b..fe805806c4 100644
--- a/src/badge/__tests__/badge.test.tsx
+++ b/src/badge/__tests__/badge.test.tsx
@@ -97,3 +97,16 @@ describe('native attributes', () => {
expect(container.firstChild).toHaveClass('additional-class');
});
});
+
+describe('Style API v2', () => {
+ test('applies classNames', () => {
+ const badge = renderBadge(
x);
+ expect(badge).toHaveClass('a');
+ expect(badge).not.toHaveClass('b');
+ });
+
+ test('does not leak the classNames prop to the DOM', () => {
+ const badge = renderBadge(
x);
+ expect(badge).not.toHaveAttribute('classNames');
+ });
+});
diff --git a/src/badge/index.tsx b/src/badge/index.tsx
index 357f199ca4..0acc94fd57 100644
--- a/src/badge/index.tsx
+++ b/src/badge/index.tsx
@@ -15,11 +15,17 @@ import styles from './styles.css.js';
export { BadgeProps };
+// Style API v2
+interface ClassNames {
+ root?: string;
+}
+
export default function Badge({ color = 'grey', children, style, nativeAttributes, ...rest }: BadgeProps) {
const { __internalRootRef } = useBaseComponent('Badge', { props: { color } });
const baseProps = getBaseProps(rest);
+ const { classNames } = rest as { classNames?: ClassNames };
- const className = clsx(baseProps.className, styles.badge, styles[`badge-color-${color}`]);
+ const className = clsx(baseProps.className, classNames?.root, styles.badge, styles[`badge-color-${color}`]);
return (
{
+ test('applies classNames', () => {
+ const link = renderButton(
+
+ );
+ expect(link).toHaveClass('a');
+ expect(link).not.toHaveClass('b');
+ expect(link).not.toHaveClass('unknown');
+
+ const button = renderButton();
+ expect(button).not.toHaveClass('a');
+ expect(button).toHaveClass('b');
+ expect(button).not.toHaveClass('unknown');
+ });
+
+ test('does not leak the classNames prop to the DOM', () => {
+ const link = renderButton();
+ expect(link).not.toHaveAttribute('classNames');
+
+ const button = renderButton();
+ expect(button).not.toHaveAttribute('classNames');
+ });
+});
diff --git a/src/button/index.tsx b/src/button/index.tsx
index 408a0db0e8..5d8f574b3b 100644
--- a/src/button/index.tsx
+++ b/src/button/index.tsx
@@ -7,7 +7,7 @@ import { getBaseProps } from '../internal/base-component';
import useBaseComponent from '../internal/hooks/use-base-component';
import { applyDisplayName } from '../internal/utils/apply-display-name';
import { ButtonProps } from './interfaces';
-import { InternalButton } from './internal';
+import { ClassNames, InternalButton } from './internal';
export { ButtonProps };
@@ -87,6 +87,7 @@ const Button = React.forwardRef(
form={form}
i18nStrings={i18nStrings}
style={style}
+ classNames={(props as { classNames?: ClassNames }).classNames}
nativeButtonAttributes={nativeButtonAttributes}
nativeAnchorAttributes={nativeAnchorAttributes}
__injectAnalyticsComponentMetadata={true}
diff --git a/src/button/internal.tsx b/src/button/internal.tsx
index b97f4d6500..a93893b931 100644
--- a/src/button/internal.tsx
+++ b/src/button/internal.tsx
@@ -59,8 +59,15 @@ export type InternalButtonProps = Omit & {
__emitPerformanceMarks?: boolean;
__skipNativeAttributesWarnings?: boolean;
__compact?: boolean;
+ classNames?: ClassNames;
} & InternalBaseComponentProps;
+// Style API v2
+export interface ClassNames {
+ button?: string;
+ anchor?: string;
+}
+
export const InternalButton = React.forwardRef(
(
{
@@ -105,6 +112,7 @@ export const InternalButton = React.forwardRef(
__skipNativeAttributesWarnings,
__compact = false,
analyticsAction = 'click',
+ classNames,
...props
}: InternalButtonProps,
ref: React.Ref
@@ -184,7 +192,8 @@ export const InternalButton = React.forwardRef(
buttonContext.onClick({ variant });
};
- const buttonClass = clsx(props.className, styles.button, styles[`variant-${variant}`], {
+ const stylingClassName = isAnchor ? classNames?.anchor : classNames?.button;
+ const buttonClass = clsx(props.className, stylingClassName, styles.button, styles[`variant-${variant}`], {
[styles.disabled]: isNotInteractive,
[styles['disabled-with-reason']]: isDisabledWithReason,
[styles['button-no-wrap']]: !wrapText,
diff --git a/src/button/styles.scss b/src/button/styles.scss
index fb3a52c3bd..9adbb00241 100644
--- a/src/button/styles.scss
+++ b/src/button/styles.scss
@@ -10,40 +10,87 @@
@use '../internal/generated/custom-css-properties/index.scss' as custom-props;
@use '@cloudscape-design/component-toolkit/internal/focus-visible' as focus-visible;
@use './constants' as constants;
+@use '../internal/styles/style-api' as style-api;
+
+$props: style-api.combine(style-api.$surface, (box-shadow, padding-block, padding-inline), style-api.$focus-ring);
+$tokens: style-api.resolve($props, carrier);
+@include style-api.register($tokens);
+@include style-api.docs('button', $tokens);
+@include style-api.docs('anchor', $tokens);
@mixin adjust-for-variant($variant) {
- background: var(#{custom-props.$styleBackgroundDefault}, map.get($variant, 'default-background'));
- color: var(#{custom-props.$styleColorDefault}, map.get($variant, 'default-color'));
- border-color: var(#{custom-props.$styleBorderColorDefault}, #{map.get($variant, 'default-border-color')});
- box-shadow: var(#{custom-props.$styleBoxShadowDefault});
- border-block-width: map.get($variant, 'border-width');
- border-inline-width: map.get($variant, 'border-width');
+ background: var(
+ #{style-api.read($tokens, background-color)},
+ var(#{custom-props.$styleBackgroundDefault}, map.get($variant, 'default-background'))
+ );
+ color: var(
+ #{style-api.read($tokens, color)},
+ var(#{custom-props.$styleColorDefault}, map.get($variant, 'default-color'))
+ );
+ border-color: var(
+ #{style-api.read($tokens, border-color)},
+ var(#{custom-props.$styleBorderColorDefault}, #{map.get($variant, 'default-border-color')})
+ );
+ box-shadow: var(#{style-api.read($tokens, box-shadow)}, var(#{custom-props.$styleBoxShadowDefault}));
+ @if map.get($variant, 'border-width') {
+ border-block-width: var(#{style-api.read($tokens, border-width)}, #{map.get($variant, 'border-width')});
+ border-inline-width: var(#{style-api.read($tokens, border-width)}, #{map.get($variant, 'border-width')});
+ }
position: relative;
text-decoration: none;
- padding-block: map.get($variant, 'padding');
- padding-inline: map.get($variant, 'padding');
+ @if map.get($variant, 'padding') {
+ padding-block: var(#{style-api.read($tokens, padding-block)}, #{map.get($variant, 'padding')});
+ padding-inline: var(#{style-api.read($tokens, padding-inline)}, #{map.get($variant, 'padding')});
+ }
font-weight: map.get($variant, 'font-weight');
&:hover {
- background: var(#{custom-props.$styleBackgroundHover}, map.get($variant, 'hover-background'));
- border-color: var(#{custom-props.$styleBorderColorHover}, map.get($variant, 'hover-border-color'));
- color: var(#{custom-props.$styleColorHover}, map.get($variant, 'hover-color'));
- box-shadow: var(#{custom-props.$styleBoxShadowHover});
+ background: var(
+ #{style-api.read($tokens, background-color)},
+ var(#{custom-props.$styleBackgroundHover}, map.get($variant, 'hover-background'))
+ );
+ border-color: var(
+ #{style-api.read($tokens, border-color)},
+ var(#{custom-props.$styleBorderColorHover}, map.get($variant, 'hover-border-color'))
+ );
+ color: var(
+ #{style-api.read($tokens, color)},
+ var(#{custom-props.$styleColorHover}, map.get($variant, 'hover-color'))
+ );
+ box-shadow: var(#{style-api.read($tokens, box-shadow)}, var(#{custom-props.$styleBoxShadowHover}));
text-decoration: none;
}
&:active {
- background: var(#{custom-props.$styleBackgroundActive}, map.get($variant, 'active-background'));
- border-color: var(#{custom-props.$styleBorderColorActive}, map.get($variant, 'active-border-color'));
- color: var(#{custom-props.$styleColorActive}, map.get($variant, 'active-color'));
- box-shadow: var(#{custom-props.$styleBoxShadowActive});
+ background: var(
+ #{style-api.read($tokens, background-color)},
+ var(#{custom-props.$styleBackgroundActive}, map.get($variant, 'active-background'))
+ );
+ border-color: var(
+ #{style-api.read($tokens, border-color)},
+ var(#{custom-props.$styleBorderColorActive}, map.get($variant, 'active-border-color'))
+ );
+ color: var(
+ #{style-api.read($tokens, color)},
+ var(#{custom-props.$styleColorActive}, map.get($variant, 'active-color'))
+ );
+ box-shadow: var(#{style-api.read($tokens, box-shadow)}, var(#{custom-props.$styleBoxShadowActive}));
}
&.disabled {
- background: var(#{custom-props.$styleBackgroundDisabled}, map.get($variant, 'disabled-background'));
- border-color: var(#{custom-props.$styleBorderColorDisabled}, map.get($variant, 'disabled-border-color'));
- color: var(#{custom-props.$styleColorDisabled}, map.get($variant, 'disabled-color'));
- box-shadow: var(#{custom-props.$styleBoxShadowDisabled});
+ background: var(
+ #{style-api.read($tokens, background-color)},
+ var(#{custom-props.$styleBackgroundDisabled}, map.get($variant, 'disabled-background'))
+ );
+ border-color: var(
+ #{style-api.read($tokens, border-color)},
+ var(#{custom-props.$styleBorderColorDisabled}, map.get($variant, 'disabled-border-color'))
+ );
+ color: var(
+ #{style-api.read($tokens, color)},
+ var(#{custom-props.$styleColorDisabled}, map.get($variant, 'disabled-color'))
+ );
+ box-shadow: var(#{style-api.read($tokens, box-shadow)}, var(#{custom-props.$styleBoxShadowDisabled}));
text-decoration: none;
cursor: auto;
}
@@ -54,24 +101,22 @@
}
.button {
+ @include style-api.carriers($tokens);
@include styles.styles-reset;
@include styles.text-wrapping;
@include styles.font-button;
letter-spacing: awsui.$font-button-letter-spacing;
- border-start-start-radius: awsui.$border-radius-button;
- border-start-end-radius: awsui.$border-radius-button;
- border-end-start-radius: awsui.$border-radius-button;
- border-end-end-radius: awsui.$border-radius-button;
- border-block: awsui.$border-width-button solid;
- border-inline: awsui.$border-width-button solid;
- padding-block: awsui.$space-button-vertical;
- padding-inline: awsui.$space-button-horizontal;
+ border-start-start-radius: var(#{style-api.read($tokens, border-radius)}, #{awsui.$border-radius-button});
+ border-start-end-radius: var(#{style-api.read($tokens, border-radius)}, #{awsui.$border-radius-button});
+ border-end-start-radius: var(#{style-api.read($tokens, border-radius)}, #{awsui.$border-radius-button});
+ border-end-end-radius: var(#{style-api.read($tokens, border-radius)}, #{awsui.$border-radius-button});
+ border-block: var(#{style-api.read($tokens, border-width)}, #{awsui.$border-width-button}) solid;
+ border-inline: var(#{style-api.read($tokens, border-width)}, #{awsui.$border-width-button}) solid;
+ padding-block: var(#{style-api.read($tokens, padding-block)}, #{awsui.$space-button-vertical});
+ padding-inline: var(#{style-api.read($tokens, padding-inline)}, #{awsui.$space-button-horizontal});
display: inline-block; // for as a button
text-decoration: none;
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);
@each $variant in map.keys(constants.$variants) {
&.variant-#{$variant} {
@@ -85,37 +130,37 @@
}
@include focus-visible.when-visible {
- @include styles.focus-highlight(
- $gutter: awsui.$space-button-focus-outline-gutter,
- $border-radius: var(#{custom-props.$styleFocusRingBorderRadius}, awsui.$border-radius-control-default-focus-ring),
- $box-shadow: var(#{custom-props.$styleFocusRingBoxShadow})
- );
+ $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);
+ @include style-api.focus-highlight($radius: $radius, $width: $width, $color: $color);
&.variant-icon,
&.variant-modal-dismiss,
&.variant-flashbar-icon {
- @include styles.focus-highlight(
- (
- 'vertical': awsui.$space-button-icon-focus-outline-gutter-vertical,
- 'horizontal': awsui.$space-button-focus-outline-gutter,
- ),
- $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: awsui.$space-button-icon-focus-outline-gutter-vertical,
+ $radius: $radius,
+ $width: $width,
+ $color: $color
);
}
&.variant-inline-icon,
&.variant-inline-icon-pointer-target {
- @include styles.focus-highlight(awsui.$space-button-inline-icon-focus-outline-gutter);
+ @include style-api.focus-highlight(
+ $gutter-block: awsui.$space-button-inline-icon-focus-outline-gutter,
+ $gutter-inline: awsui.$space-button-inline-icon-focus-outline-gutter,
+ $radius: $radius,
+ $width: $width,
+ $color: $color
+ );
}
}
&.button-no-text {
- padding-inline-start: awsui.$space-button-icon-only-horizontal;
- padding-inline-end: awsui.$space-button-icon-only-horizontal;
+ padding-inline-start: var(#{style-api.read($tokens, padding-inline)}, #{awsui.$space-button-icon-only-horizontal});
+ padding-inline-end: var(#{style-api.read($tokens, padding-inline)}, #{awsui.$space-button-icon-only-horizontal});
}
&.button-no-wrap {
@@ -133,12 +178,12 @@
&.variant-flashbar-icon {
// Icon has vertical padding, but no horizontal, we need to conpensate this
// in order to have equal height and width
- padding-inline: awsui.$space-xxs;
+ padding-inline: var(#{style-api.read($tokens, padding-inline)}, #{awsui.$space-xxs});
}
&.variant-modal-dismiss {
- padding-block: awsui.$space-button-modal-dismiss-vertical;
- padding-inline: awsui.$space-xxs;
+ padding-block: var(#{style-api.read($tokens, padding-block)}, #{awsui.$space-button-modal-dismiss-vertical});
+ padding-inline: var(#{style-api.read($tokens, padding-inline)}, #{awsui.$space-xxs});
margin-inline-end: calc(-1 * #{awsui.$space-xxs});
}
diff --git a/src/internal/styles/style-api.scss b/src/internal/styles/style-api.scss
new file mode 100644
index 0000000000..26b5ff4a72
--- /dev/null
+++ b/src/internal/styles/style-api.scss
@@ -0,0 +1,45 @@
+/*
+ Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ SPDX-License-Identifier: Apache-2.0
+*/
+
+@forward '@cloudscape-design/component-toolkit/internal/style-api';
+
+@use '@cloudscape-design/component-toolkit/internal/style-api' as toolkit;
+@use './tokens' as awsui;
+@use './foundation' as foundation;
+@use './forms/mixins' as forms;
+
+// Shared, component-agnostic Style API v2 token groups + the helpers that consume them. Components
+// combine these groups (see combine()), register() the public tokens, and re-anchor them with
+// carriers() on their boundary element so descendants / pseudo-elements read them per instance.
+
+// Common surface tokens (background / text / border / border-radius / border-width), shared by
+// badge, alert, button, etc.
+$surface: (color, background-color, border-color, border-width, border-radius);
+
+// Focus-outline tokens, shared by every focusable component (button, alert dismiss, ...).
+$focus-ring: (focus-ring-color, focus-ring-width, focus-ring-radius, focus-ring-gutter-block, focus-ring-gutter-inline);
+
+// Carrier map for the focus-outline group: reads resolve to the inherited --awsui-internal-style-*
+// carriers (re-anchored by carriers() on the component root), so the ring below reaches ::before.
+$_focus-ring-carriers: toolkit.resolve($focus-ring, carrier);
+
+// A wrapper for forms.focus-highlight with style API integration.
+@mixin focus-highlight(
+ $gutter-block: awsui.$space-button-focus-outline-gutter,
+ $gutter-inline: awsui.$space-button-focus-outline-gutter,
+ $radius: awsui.$border-radius-control-default-focus-ring,
+ $width: foundation.$box-shadow-focused-width,
+ $color: awsui.$color-border-item-focused
+) {
+ @include forms.focus-highlight(
+ $gutter: (
+ 'vertical': var(#{toolkit.read($_focus-ring-carriers, focus-ring-gutter-block)}, #{$gutter-block}),
+ 'horizontal': var(#{toolkit.read($_focus-ring-carriers, focus-ring-gutter-inline)}, #{$gutter-inline}),
+ ),
+ $border-radius: var(#{toolkit.read($_focus-ring-carriers, focus-ring-radius)}, #{$radius}),
+ $box-shadow: 0 0 0 var(#{toolkit.read($_focus-ring-carriers, focus-ring-width)}, #{$width})
+ var(#{toolkit.read($_focus-ring-carriers, focus-ring-color)}, #{$color})
+ );
+}