Skip to content
Draft
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
50 changes: 50 additions & 0 deletions pages/token/style-api-v2.page.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<SimplePage title="Token - Style API v2" screenshotArea={{}}>
<KeyValuePairs
items={[
{
type: 'pair',
label: 'Default',
value: (
<Token label="Generated" description="From your prompt" {...shared} {...{ styleClassNames: themed }} />
),
},
{
type: 'pair',
label: 'Inline variant',
value: <Token label="Inline" variant="inline" {...shared} {...{ styleClassNames: themed }} />,
},
{
type: 'pair',
label: 'Disabled',
value: <Token label="Disabled" disabled={true} {...shared} {...{ styleClassNames: muted }} />,
},
{
type: 'pair',
label: 'Read-only',
value: <Token label="Read-only" readOnly={true} {...shared} {...{ styleClassNames: muted }} />,
},
]}
/>
</SimplePage>
);
}
50 changes: 50 additions & 0 deletions pages/token/style-api-v2.scss
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -247,5 +283,6 @@ exports[`Style API docs list of components with a Style API doc matches the snap
"link",
"radio-button",
"toggle",
"token",
]
`;
4 changes: 2 additions & 2 deletions src/internal/styles/style-api.scss
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}

Expand Down
50 changes: 50 additions & 0 deletions src/token/__tests__/style-api.test.tsx
Original file line number Diff line number Diff line change
@@ -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(
<Token label="Token" dismissLabel="Remove" onDismiss={() => {}} {...({ 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(
<Token
label="Token"
variant="inline"
dismissLabel="Remove"
onDismiss={() => {}}
{...({ 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(
<Token label="Token" dismissLabel="Remove" onDismiss={() => {}} {...({ styleClassNames: slots } as any)} />
);
expect(container.querySelector('[styleClassNames]')).toBeNull();
});
});
6 changes: 4 additions & 2 deletions src/token/dismiss-button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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<HTMLButtonElement>
) {
const isOneTheme = isThemeActive(Theme.OneTheme);
Expand All @@ -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={() => {
Expand Down
3 changes: 2 additions & 1 deletion src/token/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -26,6 +26,7 @@ export default function Token(props: TokenProps) {
return (
<InternalToken
{...props}
styleClassNames={(props as { styleClassNames?: StyleClassNames }).styleClassNames}
{...baseComponentProps}
{...getAnalyticsMetadataAttribute({ component: componentAnalyticsMetadata })}
/>
Expand Down
12 changes: 11 additions & 1 deletion src/token/internal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<TokenProps, 'label'> &
InternalBaseComponentProps & {
styleClassNames?: StyleClassNames;
/** Token label. Required unless `__customContent` replaces the option layout entirely. */
label?: React.ReactNode;
/**
Expand Down Expand Up @@ -69,6 +76,7 @@ function InternalToken({
disableInnerPadding,
__tokenBoxClassName,
__customContent,
styleClassNames,

// Base
__internalRootRef,
Expand Down Expand Up @@ -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}
>
Expand All @@ -203,6 +212,7 @@ function InternalToken({
/>
{onDismiss && (
<DismissButton
className={styleClassNames?.dismissButton}
disabled={disabled}
dismissLabel={dismissLabel}
onDismiss={onDismiss}
Expand Down
27 changes: 19 additions & 8 deletions src/token/styles.scss
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,25 @@
@use './constants' as constants;
@use './mixins.scss' as mixins;
@use '../internal/styles/utils/theming' as theming;
@use '../internal/styles/style-api' as style-api;

// The token box and the dismiss button carry the consumer classes, so consumers set their properties
// directly. The dismiss button's focus ring is drawn in a ::before pseudo-element, so it is themed through
// tokens instead. Carrier layer: ::before can't see a non-inheriting public property set on the button.
$dismiss-tokens: style-api.resolve(style-api.$focus-ring, carrier);
@include style-api.register($dismiss-tokens);
@include style-api.docs(
'root',
$properties: (background-color, border-color, border-width, border-radius, padding-block, padding-inline)
);
@include style-api.docs('dismissButton', $dismiss-tokens, $properties: (color));

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

.dismiss-button {
@include style-api.carriers($dismiss-tokens);
align-self: flex-start;
margin-block-end: 0;
margin-inline-start: awsui.$space-xxs;
Expand All @@ -28,15 +41,13 @@
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 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)
);
}

Expand Down
Loading