diff --git a/build-tools/utils/pluralize.js b/build-tools/utils/pluralize.js
index 3d0214431e..76078281f6 100644
--- a/build-tools/utils/pluralize.js
+++ b/build-tools/utils/pluralize.js
@@ -80,6 +80,7 @@ const pluralizationMap = {
TimeInput: 'TimeInputs',
Toggle: 'Toggles',
ToggleButton: 'ToggleButtons',
+ Token: 'Tokens',
TokenGroup: 'TokenGroups',
TopNavigation: 'TopNavigations',
TreeView: 'TreeViews',
diff --git a/pages/token-group/custom.page.tsx b/pages/token-group/custom.page.tsx
deleted file mode 100644
index 0d40977a92..0000000000
--- a/pages/token-group/custom.page.tsx
+++ /dev/null
@@ -1,88 +0,0 @@
-// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
-// SPDX-License-Identifier: Apache-2.0
-import React, { useState } from 'react';
-import { range } from 'lodash';
-
-import Box from '~components/box';
-import Icon from '~components/icon';
-import TokenList from '~components/internal/components/token-list';
-import SpaceBetween from '~components/space-between';
-import { TokenGroupProps } from '~components/token-group';
-import { Token } from '~components/token-group/token';
-
-import styles from './styles.scss';
-
-const i18nStrings: TokenGroupProps.I18nStrings = {
- limitShowMore: 'Show more chosen options',
- limitShowFewer: 'Show fewer chosen options',
-};
-
-export default function GenericTokenGroupPage() {
- const [files, setFiles] = useState(range(0, 4));
-
- const onDismiss = (itemIndex: number) => {
- const newItems = [...files];
- newItems.splice(itemIndex, 1);
- setFiles(newItems);
- };
-
- return (
-
- Generic token group
-
- Standalone token
-
-
- Standalone disabled token
-
-
- (
- onDismiss(fileIndex)}
- >
-
-
- )}
- />
-
-
- );
-}
-
-function FileOption({ file }: { file: number }) {
- const fileName = `agreement-${file + 1}.pdf`;
- return (
-
-
-
-
-
- {
-
- }
-
- application/pdf
-
-
- 313.03 KB
-
-
- 2022-01-01T12:02:02
-
-
-
-
- );
-}
diff --git a/pages/token/permutations.page.tsx b/pages/token/permutations.page.tsx
new file mode 100644
index 0000000000..1249b1b481
--- /dev/null
+++ b/pages/token/permutations.page.tsx
@@ -0,0 +1,73 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+import React from 'react';
+
+import Icon from '~components/icon';
+import Token, { TokenProps } from '~components/token';
+
+import createPermutations from '../utils/permutations';
+import PermutationsView from '../utils/permutations-view';
+import ScreenshotArea from '../utils/screenshot-area';
+
+const permutations = createPermutations([
+ {
+ label: ['token'],
+ icon: [undefined, ],
+ onDismiss: [undefined, () => {}],
+ readOnly: [false, true],
+ variant: ['inline'],
+ },
+ {
+ label: ['token'],
+ icon: [undefined, ],
+ onDismiss: [undefined, () => {}],
+ disabled: [true],
+ variant: ['inline'],
+ },
+ {
+ label: ['token'],
+ icon: [undefined, ],
+ onDismiss: [undefined, () => {}],
+ readOnly: [false, true],
+ variant: ['normal'],
+ },
+ {
+ label: ['token'],
+ icon: [undefined, ],
+ onDismiss: [undefined, () => {}],
+ disabled: [true],
+ variant: ['normal'],
+ },
+ {
+ label: ['token'],
+ description: [undefined, 'description'],
+ labelTag: ['label-tag', undefined],
+ tags: [['tag-1', 'tag-2'], undefined],
+ onDismiss: [undefined, () => {}],
+ variant: ['normal'],
+ },
+ {
+ label: ['token'],
+ icon: [ ],
+ description: ['description'],
+ labelTag: ['label-tag', undefined],
+ tags: [['tag-1', 'tag-2'], undefined],
+ readOnly: [false, true],
+ disabled: [true, false],
+ variant: ['normal'],
+ },
+]);
+
+export default function TokenPermutations() {
+ return (
+ <>
+ Token permutations
+
+ }
+ />
+
+ >
+ );
+}
diff --git a/pages/token/simple.page.tsx b/pages/token/simple.page.tsx
new file mode 100644
index 0000000000..0ba52ab3e1
--- /dev/null
+++ b/pages/token/simple.page.tsx
@@ -0,0 +1,218 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+import React, { useState } from 'react';
+import { range } from 'lodash';
+
+import { Popover } from '~components';
+import Box from '~components/box';
+import Icon from '~components/icon';
+import Input from '~components/input';
+import TokenList from '~components/internal/components/token-list';
+import SpaceBetween from '~components/space-between';
+import Token from '~components/token';
+
+import styles from './styles.scss';
+
+const i18nStrings = {
+ limitShowMore: 'Show more chosen options',
+ limitShowFewer: 'Show fewer chosen options',
+};
+
+const LONG_LABEL = `Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud
+ exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla
+ pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.`;
+
+export default function GenericTokenPage() {
+ const [files, setFiles] = useState(range(0, 4));
+
+ const onDismiss = (itemIndex: number) => {
+ const newItems = [...files];
+ newItems.splice(itemIndex, 1);
+ setFiles(newItems);
+ };
+
+ return (
+
+ Standalone token
+ Inline
+
+
+
+
+
+
+
+ {}}
+ />
+ {}}
+ />
+ {}}
+ />
+
+ }
+ readOnly={true}
+ onDismiss={() => {}}
+ />
+
+ }
+ disabled={true}
+ onDismiss={() => {}}
+ />
+
+
+
+
+
+
+
+ Normal
+
+
+ } />
+ {}} />}
+ >
+ Standalone token with popover
+
+ }
+ labelTag="Test"
+ onDismiss={() => {}}
+ dismissLabel="Dismiss normal token with popover"
+ />
+ {}} />}
+ >
+ Standalone token with icon and popover
+
+ }
+ icon={ }
+ onDismiss={() => {}}
+ />
+ {}}
+ />
+
+
+ }
+ onDismiss={() => {}}
+ />
+
+
+
+ {}}
+ dismissLabel="Dismiss normal readonly token"
+ />
+
+
+
+ {}}
+ dismissLabel="Dismiss normal disabled token"
+ />
+
+ (
+ }
+ disabled={file === 0}
+ dismissLabel={`Remove file ${fileIndex + 1}`}
+ onDismiss={() => onDismiss(fileIndex)}
+ />
+ )}
+ />
+
+
+ );
+}
+
+function FileOption({ file }: { file: number }) {
+ const fileName = `agreement-${file + 1}.pdf`;
+ return (
+
+
+
+
+
+ {
+
+ }
+
+ application/pdf
+
+
+ 313.03 KB
+
+
+ 2022-01-01T12:02:02
+
+
+
+
+ );
+}
diff --git a/pages/token-group/styles.scss b/pages/token/styles.scss
similarity index 100%
rename from pages/token-group/styles.scss
rename to pages/token/styles.scss
diff --git a/src/__tests__/snapshot-tests/__snapshots__/documenter.test.ts.snap b/src/__tests__/snapshot-tests/__snapshots__/documenter.test.ts.snap
index f383c5d49e..c007578a5f 100644
--- a/src/__tests__/snapshot-tests/__snapshots__/documenter.test.ts.snap
+++ b/src/__tests__/snapshot-tests/__snapshots__/documenter.test.ts.snap
@@ -3104,6 +3104,11 @@ Instead, use \`onSelect\` in combination with the \`onChange\` handler only as a
"optional": true,
"type": "string",
},
+ {
+ "name": "labelContent",
+ "optional": true,
+ "type": "React.ReactNode",
+ },
{
"name": "labelTag",
"optional": true,
@@ -21113,6 +21118,11 @@ The event \`detail\` contains the current \`selectedOption\`.",
"optional": true,
"type": "string",
},
+ {
+ "name": "labelContent",
+ "optional": true,
+ "type": "React.ReactNode",
+ },
{
"name": "labelTag",
"optional": true,
@@ -21698,6 +21708,11 @@ If you want to clear the selection, use \`null\`.",
"optional": true,
"type": "string",
},
+ {
+ "name": "labelContent",
+ "optional": true,
+ "type": "React.ReactNode",
+ },
{
"name": "labelTag",
"optional": true,
@@ -26341,6 +26356,130 @@ In most cases, they aren't needed, as the \`svg\` element inherits styles from t
}
`;
+exports[`Components definition for token matches the snapshot: token 1`] = `
+{
+ "dashCaseName": "token",
+ "events": [
+ {
+ "cancelable": false,
+ "description": "Called when the user clicks on the dismiss button.
+
+Make sure that you add a listener to this event to update your application state.",
+ "name": "onDismiss",
+ },
+ ],
+ "functions": [],
+ "name": "Token",
+ "properties": [
+ {
+ "description": "Adds an \`aria-label\` to the token.
+
+Use this if the label is not plain text.",
+ "name": "ariaLabel",
+ "optional": true,
+ "type": "string",
+ },
+ {
+ "deprecatedTag": "Custom CSS is not supported. For testing and other use cases, use [data attributes](https://developer.mozilla.org/en-US/docs/Learn/HTML/Howto/Use_data_attributes).",
+ "description": "Adds the specified classes to the root element of the component.",
+ "name": "className",
+ "optional": true,
+ "type": "string",
+ },
+ {
+ "description": "Further information about the token that appears below the label.",
+ "name": "description",
+ "optional": true,
+ "type": "string",
+ },
+ {
+ "description": "Determines whether the token is disabled.",
+ "name": "disabled",
+ "optional": true,
+ "type": "boolean",
+ },
+ {
+ "description": "Adds an \`aria-label\` to the dismiss button.",
+ "name": "dismissLabel",
+ "optional": true,
+ "type": "string",
+ },
+ {
+ "deprecatedTag": "The usage of the \`id\` attribute is reserved for internal use cases. For testing and other use cases,
+use [data attributes](https://developer.mozilla.org/en-US/docs/Learn/HTML/Howto/Use_data_attributes). If you must
+use the \`id\` attribute, consider setting it on a parent element instead.",
+ "description": "Adds the specified ID to the root element of the component.",
+ "name": "id",
+ "optional": true,
+ "type": "string",
+ },
+ {
+ "description": "A label tag that provides additional guidance, shown next to the label.",
+ "name": "labelTag",
+ "optional": true,
+ "type": "string",
+ },
+ {
+ "description": "Specifies if the control is read-only. A read-only control is still focusable.",
+ "name": "readOnly",
+ "optional": true,
+ "type": "boolean",
+ },
+ {
+ "description": "A list of tags giving further guidance about the token.",
+ "name": "tags",
+ "optional": true,
+ "type": "ReadonlyArray",
+ },
+ {
+ "description": "Content to display in the tooltip when \`variant="inline"\`. The tooltip appears when the token label is truncated due to insufficient space.
+
+Only applies to plain text labels.",
+ "name": "tooltipContent",
+ "optional": true,
+ "type": "string",
+ },
+ {
+ "description": "Specifies the token's visual style and functionality.
+
+For \`inline\` only label, icon and dismiss button are displayed.
+
+Defaults to \`normal\` if not specified.",
+ "inlineType": {
+ "name": "TokenProps.Variant",
+ "type": "union",
+ "values": [
+ "inline",
+ "normal",
+ ],
+ },
+ "name": "variant",
+ "optional": true,
+ "type": "string",
+ },
+ ],
+ "regions": [
+ {
+ "description": "An icon at the start of the token.
+
+When \`variant="normal"\`, if a description or tags are set, icon size should be \`normal\`.
+
+When \`variant="inline"\`, icon size should be \`small\`.",
+ "isDefault": false,
+ "name": "icon",
+ },
+ {
+ "description": "Slot for the label of the token as text or an element.
+
+For \`variant="inline"\`, only plain text is supported, for example, strings or numbers.",
+ "isDefault": false,
+ "name": "label",
+ },
+ ],
+ "releaseStatus": "stable",
+}
+`;
+
exports[`Components definition for token-group matches the snapshot: token-group 1`] = `
{
"dashCaseName": "token-group",
@@ -65434,7 +65573,7 @@ Note: This function returns the specified component's wrapper even if the specif
],
"returnType": {
"isNullable": true,
- "name": "TokenWrapper",
+ "name": "TokenGroupItemWrapper",
},
},
{
@@ -65445,7 +65584,7 @@ Note: This function returns the specified component's wrapper even if the specif
"name": "Array",
"typeArguments": [
{
- "name": "TokenWrapper",
+ "name": "TokenGroupItemWrapper",
},
],
},
@@ -66106,7 +66245,7 @@ Note: This function returns the specified component's wrapper even if the specif
},
},
],
- "name": "TokenWrapper",
+ "name": "TokenGroupItemWrapper",
},
{
"methods": [
@@ -78454,51 +78593,464 @@ Note: This function returns the specified component's wrapper even if the specif
},
},
{
- "name": "findInputByValue",
- "parameters": [
- {
- "flags": {
- "isOptional": false,
- },
- "name": "value",
- "typeName": "string",
- },
- ],
+ "name": "findInputByValue",
+ "parameters": [
+ {
+ "flags": {
+ "isOptional": false,
+ },
+ "name": "value",
+ "typeName": "string",
+ },
+ ],
+ "returnType": {
+ "isNullable": true,
+ "name": "ElementWrapper",
+ "typeArguments": [
+ {
+ "name": "HTMLInputElement",
+ },
+ ],
+ },
+ },
+ {
+ "name": "findItemByValue",
+ "parameters": [
+ {
+ "flags": {
+ "isOptional": false,
+ },
+ "name": "value",
+ "typeName": "string",
+ },
+ ],
+ "returnType": {
+ "isNullable": true,
+ "name": "TileWrapper",
+ },
+ },
+ {
+ "name": "findItems",
+ "parameters": [],
+ "returnType": {
+ "isNullable": false,
+ "name": "Array",
+ "typeArguments": [
+ {
+ "name": "TileWrapper",
+ },
+ ],
+ },
+ },
+ {
+ "inheritedFrom": {
+ "name": "AbstractWrapper.fireEvent",
+ },
+ "name": "fireEvent",
+ "parameters": [
+ {
+ "flags": {
+ "isOptional": false,
+ },
+ "name": "event",
+ "typeName": "Event",
+ },
+ ],
+ "returnType": {
+ "isNullable": false,
+ "name": "void",
+ },
+ },
+ {
+ "inheritedFrom": {
+ "name": "AbstractWrapper.focus",
+ },
+ "name": "focus",
+ "parameters": [],
+ "returnType": {
+ "isNullable": false,
+ "name": "void",
+ },
+ },
+ {
+ "inheritedFrom": {
+ "name": "AbstractWrapper.getElement",
+ },
+ "name": "getElement",
+ "parameters": [],
+ "returnType": {
+ "isNullable": false,
+ "name": "ElementType",
+ },
+ },
+ {
+ "inheritedFrom": {
+ "name": "AbstractWrapper.keydown",
+ },
+ "name": "keydown",
+ "parameters": [
+ {
+ "flags": {
+ "isOptional": false,
+ },
+ "name": "keyCode",
+ "typeName": "KeyCode",
+ },
+ ],
+ "returnType": {
+ "isNullable": false,
+ "name": "void",
+ },
+ },
+ {
+ "inheritedFrom": {
+ "name": "AbstractWrapper.keydown",
+ },
+ "name": "keydown",
+ "parameters": [
+ {
+ "flags": {
+ "isOptional": false,
+ },
+ "name": "keyboardEventProps",
+ "typeName": "KeyboardEventInit",
+ },
+ ],
+ "returnType": {
+ "isNullable": false,
+ "name": "void",
+ },
+ },
+ {
+ "inheritedFrom": {
+ "name": "AbstractWrapper.keypress",
+ },
+ "name": "keypress",
+ "parameters": [
+ {
+ "flags": {
+ "isOptional": false,
+ },
+ "name": "keyCode",
+ "typeName": "KeyCode",
+ },
+ ],
+ "returnType": {
+ "isNullable": false,
+ "name": "void",
+ },
+ },
+ {
+ "inheritedFrom": {
+ "name": "AbstractWrapper.keyup",
+ },
+ "name": "keyup",
+ "parameters": [
+ {
+ "flags": {
+ "isOptional": false,
+ },
+ "name": "keyCode",
+ "typeName": "KeyCode",
+ },
+ ],
+ "returnType": {
+ "isNullable": false,
+ "name": "void",
+ },
+ },
+ {
+ "inheritedFrom": {
+ "name": "AbstractWrapper.matches",
+ },
+ "name": "matches",
+ "parameters": [
+ {
+ "flags": {
+ "isOptional": false,
+ },
+ "name": "selector",
+ "typeName": "string",
+ },
+ ],
+ "returnType": {
+ "isNullable": true,
+ "name": "this",
+ },
+ },
+ ],
+ "name": "TilesWrapper",
+ },
+ {
+ "methods": [
+ {
+ "inheritedFrom": {
+ "name": "AbstractWrapper.blur",
+ },
+ "name": "blur",
+ "parameters": [],
+ "returnType": {
+ "isNullable": false,
+ "name": "void",
+ },
+ },
+ {
+ "description": "Performs a click by triggering a mouse event.
+Note that programmatic events ignore disabled attribute and will trigger listeners even if the element is disabled.",
+ "inheritedFrom": {
+ "name": "AbstractWrapper.click",
+ },
+ "name": "click",
+ "parameters": [
+ {
+ "flags": {
+ "isOptional": true,
+ },
+ "name": "params",
+ "typeName": "MouseEventInit",
+ },
+ ],
+ "returnType": {
+ "isNullable": false,
+ "name": "void",
+ },
+ },
+ {
+ "inheritedFrom": {
+ "name": "AbstractWrapper.find",
+ },
+ "name": "find",
+ "parameters": [
+ {
+ "flags": {
+ "isOptional": false,
+ },
+ "name": "selector",
+ "typeName": "string",
+ },
+ ],
+ "returnType": {
+ "isNullable": true,
+ "name": "ElementWrapper",
+ "typeArguments": [
+ {
+ "name": "NewElementType",
+ },
+ ],
+ },
+ },
+ {
+ "inheritedFrom": {
+ "name": "AbstractWrapper.findAll",
+ },
+ "name": "findAll",
+ "parameters": [
+ {
+ "flags": {
+ "isOptional": false,
+ },
+ "name": "selector",
+ "typeName": "string",
+ },
+ ],
+ "returnType": {
+ "isNullable": false,
+ "name": "Array",
+ "typeArguments": [
+ {
+ "name": "ElementWrapper",
+ },
+ ],
+ },
+ },
+ {
+ "inheritedFrom": {
+ "name": "AbstractWrapper.findAllByClassName",
+ },
+ "name": "findAllByClassName",
+ "parameters": [
+ {
+ "flags": {
+ "isOptional": false,
+ },
+ "name": "className",
+ "typeName": "string",
+ },
+ ],
+ "returnType": {
+ "isNullable": false,
+ "name": "Array",
+ "typeArguments": [
+ {
+ "name": "ElementWrapper",
+ },
+ ],
+ },
+ },
+ {
+ "description": "Returns the wrappers of all components that match the specified component type and the specified CSS selector.
+If no CSS selector is specified, returns all of the components that match the specified component type.
+If no matching component is found, returns an empty array.",
+ "inheritedFrom": {
+ "name": "AbstractWrapper.findAllComponents",
+ },
+ "name": "findAllComponents",
+ "parameters": [
+ {
+ "description": "Component's wrapper class",
+ "flags": {
+ "isOptional": false,
+ },
+ "name": "ComponentClass",
+ "typeName": "ComponentWrapperClass",
+ },
+ {
+ "description": "CSS selector",
+ "flags": {
+ "isOptional": true,
+ },
+ "name": "selector",
+ "typeName": "string",
+ },
+ ],
+ "returnType": {
+ "isNullable": false,
+ "name": "Array",
+ "typeArguments": [
+ {
+ "name": "Wrapper",
+ },
+ ],
+ },
+ },
+ {
+ "inheritedFrom": {
+ "name": "AbstractWrapper.findAny",
+ },
+ "name": "findAny",
+ "parameters": [
+ {
+ "flags": {
+ "isOptional": false,
+ },
+ "name": "selectors",
+ "typeName": "Array",
+ },
+ ],
+ "returnType": {
+ "isNullable": true,
+ "name": "ElementWrapper",
+ "typeArguments": [
+ {
+ "name": "NewElementType",
+ },
+ ],
+ },
+ },
+ {
+ "inheritedFrom": {
+ "name": "AbstractWrapper.findByClassName",
+ },
+ "name": "findByClassName",
+ "parameters": [
+ {
+ "flags": {
+ "isOptional": false,
+ },
+ "name": "className",
+ "typeName": "string",
+ },
+ ],
+ "returnType": {
+ "isNullable": true,
+ "name": "ElementWrapper",
+ "typeArguments": [
+ {
+ "name": "NewElementType",
+ },
+ ],
+ },
+ },
+ {
+ "description": "Returns the component wrapper matching the specified selector.
+If the specified selector doesn't match any element, it returns \`null\`.
+
+Note: This function returns the specified component's wrapper even if the specified selector points to a different component type.",
+ "inheritedFrom": {
+ "name": "AbstractWrapper.findComponent",
+ },
+ "name": "findComponent",
+ "parameters": [
+ {
+ "description": "CSS selector",
+ "flags": {
+ "isOptional": false,
+ },
+ "name": "selector",
+ "typeName": "string",
+ },
+ {
+ "description": "Component's wrapper class",
+ "flags": {
+ "isOptional": false,
+ },
+ "name": "ComponentClass",
+ "typeName": "WrapperClass",
+ },
+ ],
+ "returnType": {
+ "isNullable": true,
+ "name": "Wrapper",
+ },
+ },
+ {
+ "name": "findDescription",
+ "parameters": [],
"returnType": {
"isNullable": true,
"name": "ElementWrapper",
"typeArguments": [
{
- "name": "HTMLInputElement",
+ "name": "HTMLElement",
},
],
},
},
{
- "name": "findItemByValue",
- "parameters": [
- {
- "flags": {
- "isOptional": false,
+ "name": "findImage",
+ "parameters": [],
+ "returnType": {
+ "isNullable": false,
+ "name": "ElementWrapper",
+ "typeArguments": [
+ {
+ "name": "HTMLElement",
},
- "name": "value",
- "typeName": "string",
- },
- ],
+ ],
+ },
+ },
+ {
+ "name": "findLabel",
+ "parameters": [],
"returnType": {
- "isNullable": true,
- "name": "TileWrapper",
+ "isNullable": false,
+ "name": "ElementWrapper",
+ "typeArguments": [
+ {
+ "name": "HTMLElement",
+ },
+ ],
},
},
{
- "name": "findItems",
+ "name": "findNativeInput",
"parameters": [],
"returnType": {
"isNullable": false,
- "name": "Array",
+ "name": "ElementWrapper",
"typeArguments": [
{
- "name": "TileWrapper",
+ "name": "HTMLElement",
},
],
},
@@ -78640,13 +79192,13 @@ Note: This function returns the specified component's wrapper even if the specif
},
},
],
- "name": "TilesWrapper",
+ "name": "TileWrapper",
},
{
"methods": [
{
"inheritedFrom": {
- "name": "AbstractWrapper.blur",
+ "name": "BaseInputWrapper.blur",
},
"name": "blur",
"parameters": [],
@@ -78865,45 +79417,9 @@ Note: This function returns the specified component's wrapper even if the specif
},
},
{
- "name": "findDescription",
- "parameters": [],
- "returnType": {
- "isNullable": true,
- "name": "ElementWrapper",
- "typeArguments": [
- {
- "name": "HTMLElement",
- },
- ],
- },
- },
- {
- "name": "findImage",
- "parameters": [],
- "returnType": {
- "isNullable": false,
- "name": "ElementWrapper",
- "typeArguments": [
- {
- "name": "HTMLElement",
- },
- ],
- },
- },
- {
- "name": "findLabel",
- "parameters": [],
- "returnType": {
- "isNullable": false,
- "name": "ElementWrapper",
- "typeArguments": [
- {
- "name": "HTMLElement",
- },
- ],
+ "inheritedFrom": {
+ "name": "BaseInputWrapper.findNativeInput",
},
- },
- {
"name": "findNativeInput",
"parameters": [],
"returnType": {
@@ -78911,7 +79427,7 @@ Note: This function returns the specified component's wrapper even if the specif
"name": "ElementWrapper",
"typeArguments": [
{
- "name": "HTMLElement",
+ "name": "HTMLInputElement",
},
],
},
@@ -78937,7 +79453,7 @@ Note: This function returns the specified component's wrapper even if the specif
},
{
"inheritedFrom": {
- "name": "AbstractWrapper.focus",
+ "name": "BaseInputWrapper.focus",
},
"name": "focus",
"parameters": [],
@@ -78957,6 +79473,31 @@ Note: This function returns the specified component's wrapper even if the specif
"name": "ElementType",
},
},
+ {
+ "description": "Gets the value of the component.
+
+Returns the current value of the input.",
+ "inheritedFrom": {
+ "name": "BaseInputWrapper.getInputValue",
+ },
+ "name": "getInputValue",
+ "parameters": [],
+ "returnType": {
+ "isNullable": false,
+ "name": "string",
+ },
+ },
+ {
+ "inheritedFrom": {
+ "name": "BaseInputWrapper.isDisabled",
+ },
+ "name": "isDisabled",
+ "parameters": [],
+ "returnType": {
+ "isNullable": false,
+ "name": "boolean",
+ },
+ },
{
"inheritedFrom": {
"name": "AbstractWrapper.keydown",
@@ -79052,14 +79593,35 @@ Note: This function returns the specified component's wrapper even if the specif
"name": "this",
},
},
+ {
+ "description": "Sets the value of the component and calls the \`onChange\` handler",
+ "inheritedFrom": {
+ "name": "BaseInputWrapper.setInputValue",
+ },
+ "name": "setInputValue",
+ "parameters": [
+ {
+ "description": "The value the input is set to.",
+ "flags": {
+ "isOptional": false,
+ },
+ "name": "value",
+ "typeName": "string",
+ },
+ ],
+ "returnType": {
+ "isNullable": false,
+ "name": "void",
+ },
+ },
],
- "name": "TileWrapper",
+ "name": "TimeInputWrapper",
},
{
"methods": [
{
"inheritedFrom": {
- "name": "BaseInputWrapper.blur",
+ "name": "AbstractWrapper.blur",
},
"name": "blur",
"parameters": [],
@@ -79278,17 +79840,71 @@ Note: This function returns the specified component's wrapper even if the specif
},
},
{
- "inheritedFrom": {
- "name": "BaseInputWrapper.findNativeInput",
+ "description": "Returns the token description.",
+ "name": "findDescription",
+ "parameters": [],
+ "returnType": {
+ "isNullable": true,
+ "name": "ElementWrapper",
+ "typeArguments": [
+ {
+ "name": "HTMLElement",
+ },
+ ],
},
- "name": "findNativeInput",
+ },
+ {
+ "description": "Returns the token dismiss button.",
+ "name": "findDismiss",
+ "parameters": [],
+ "returnType": {
+ "isNullable": true,
+ "name": "ElementWrapper",
+ "typeArguments": [
+ {
+ "name": "HTMLElement",
+ },
+ ],
+ },
+ },
+ {
+ "description": "Returns the token label.",
+ "name": "findLabel",
"parameters": [],
"returnType": {
"isNullable": false,
"name": "ElementWrapper",
"typeArguments": [
{
- "name": "HTMLInputElement",
+ "name": "HTMLElement",
+ },
+ ],
+ },
+ },
+ {
+ "description": "Returns the token label tag.",
+ "name": "findLabelTag",
+ "parameters": [],
+ "returnType": {
+ "isNullable": true,
+ "name": "ElementWrapper",
+ "typeArguments": [
+ {
+ "name": "HTMLElement",
+ },
+ ],
+ },
+ },
+ {
+ "description": "Returns the token tags.",
+ "name": "findTags",
+ "parameters": [],
+ "returnType": {
+ "isNullable": true,
+ "name": "Array",
+ "typeArguments": [
+ {
+ "name": "ElementWrapper",
},
],
},
@@ -79314,7 +79930,7 @@ Note: This function returns the specified component's wrapper even if the specif
},
{
"inheritedFrom": {
- "name": "BaseInputWrapper.focus",
+ "name": "AbstractWrapper.focus",
},
"name": "focus",
"parameters": [],
@@ -79334,31 +79950,6 @@ Note: This function returns the specified component's wrapper even if the specif
"name": "ElementType",
},
},
- {
- "description": "Gets the value of the component.
-
-Returns the current value of the input.",
- "inheritedFrom": {
- "name": "BaseInputWrapper.getInputValue",
- },
- "name": "getInputValue",
- "parameters": [],
- "returnType": {
- "isNullable": false,
- "name": "string",
- },
- },
- {
- "inheritedFrom": {
- "name": "BaseInputWrapper.isDisabled",
- },
- "name": "isDisabled",
- "parameters": [],
- "returnType": {
- "isNullable": false,
- "name": "boolean",
- },
- },
{
"inheritedFrom": {
"name": "AbstractWrapper.keydown",
@@ -79454,29 +80045,8 @@ Returns the current value of the input.",
"name": "this",
},
},
- {
- "description": "Sets the value of the component and calls the \`onChange\` handler",
- "inheritedFrom": {
- "name": "BaseInputWrapper.setInputValue",
- },
- "name": "setInputValue",
- "parameters": [
- {
- "description": "The value the input is set to.",
- "flags": {
- "isOptional": false,
- },
- "name": "value",
- "typeName": "string",
- },
- ],
- "returnType": {
- "isNullable": false,
- "name": "void",
- },
- },
],
- "name": "TimeInputWrapper",
+ "name": "TokenWrapper",
},
{
"methods": [
@@ -79715,7 +80285,7 @@ Note: This function returns the specified component's wrapper even if the specif
],
"returnType": {
"isNullable": true,
- "name": "TokenWrapper",
+ "name": "TokenGroupItemWrapper",
},
},
{
@@ -79726,7 +80296,7 @@ Note: This function returns the specified component's wrapper even if the specif
"name": "Array",
"typeArguments": [
{
- "name": "TokenWrapper",
+ "name": "TokenGroupItemWrapper",
},
],
},
@@ -107419,7 +107989,7 @@ Note: This function returns the specified component's wrapper even if the specif
],
"returnType": {
"isNullable": false,
- "name": "TokenWrapper",
+ "name": "TokenGroupItemWrapper",
},
},
{
@@ -107430,7 +108000,7 @@ Note: This function returns the specified component's wrapper even if the specif
"name": "MultiElementWrapper",
"typeArguments": [
{
- "name": "TokenWrapper",
+ "name": "TokenGroupItemWrapper",
},
],
},
@@ -107734,7 +108304,7 @@ Note: This function returns the specified component's wrapper even if the specif
},
},
],
- "name": "TokenWrapper",
+ "name": "TokenGroupItemWrapper",
},
{
"methods": [
@@ -116150,6 +116720,272 @@ If no CSS selector is specified, returns a multi-element wrapper that matches th
{
"description": "Returns a wrapper that matches the specified component type with the specified CSS selector.
+Note: This function returns the specified component's wrapper even if the specified selector points to a different component type.",
+ "inheritedFrom": {
+ "name": "AbstractWrapper.findComponent",
+ },
+ "name": "findComponent",
+ "parameters": [
+ {
+ "description": "CSS selector",
+ "flags": {
+ "isOptional": false,
+ },
+ "name": "selector",
+ "typeName": "string",
+ },
+ {
+ "description": "Component's wrapper class",
+ "flags": {
+ "isOptional": false,
+ },
+ "name": "ComponentClass",
+ "typeName": "WrapperClass",
+ },
+ ],
+ "returnType": {
+ "isNullable": false,
+ "name": "Wrapper",
+ },
+ },
+ {
+ "description": "Returns the token description.",
+ "name": "findDescription",
+ "parameters": [],
+ "returnType": {
+ "isNullable": false,
+ "name": "ElementWrapper",
+ },
+ },
+ {
+ "description": "Returns the token dismiss button.",
+ "name": "findDismiss",
+ "parameters": [],
+ "returnType": {
+ "isNullable": false,
+ "name": "ElementWrapper",
+ },
+ },
+ {
+ "description": "Returns the token label.",
+ "name": "findLabel",
+ "parameters": [],
+ "returnType": {
+ "isNullable": false,
+ "name": "ElementWrapper",
+ },
+ },
+ {
+ "description": "Returns the token label tag.",
+ "name": "findLabelTag",
+ "parameters": [],
+ "returnType": {
+ "isNullable": false,
+ "name": "ElementWrapper",
+ },
+ },
+ {
+ "description": "Returns the token tags.",
+ "name": "findTags",
+ "parameters": [],
+ "returnType": {
+ "isNullable": false,
+ "name": "MultiElementWrapper",
+ "typeArguments": [
+ {
+ "name": "ElementWrapper",
+ },
+ ],
+ },
+ },
+ {
+ "inheritedFrom": {
+ "name": "AbstractWrapper.getElement",
+ },
+ "name": "getElement",
+ "parameters": [],
+ "returnType": {
+ "isNullable": false,
+ "name": "string",
+ },
+ },
+ {
+ "inheritedFrom": {
+ "name": "AbstractWrapper.matches",
+ },
+ "name": "matches",
+ "parameters": [
+ {
+ "flags": {
+ "isOptional": false,
+ },
+ "name": "selector",
+ "typeName": "string",
+ },
+ ],
+ "returnType": {
+ "isNullable": false,
+ "name": "ElementWrapper",
+ },
+ },
+ {
+ "inheritedFrom": {
+ "name": "AbstractWrapper.toSelector",
+ },
+ "name": "toSelector",
+ "parameters": [],
+ "returnType": {
+ "isNullable": false,
+ "name": "string",
+ },
+ },
+ ],
+ "name": "TokenWrapper",
+ },
+ {
+ "methods": [
+ {
+ "inheritedFrom": {
+ "name": "AbstractWrapper.find",
+ },
+ "name": "find",
+ "parameters": [
+ {
+ "flags": {
+ "isOptional": false,
+ },
+ "name": "selector",
+ "typeName": "string",
+ },
+ ],
+ "returnType": {
+ "isNullable": false,
+ "name": "ElementWrapper",
+ },
+ },
+ {
+ "inheritedFrom": {
+ "name": "AbstractWrapper.findAll",
+ },
+ "name": "findAll",
+ "parameters": [
+ {
+ "flags": {
+ "isOptional": false,
+ },
+ "name": "selector",
+ "typeName": "string",
+ },
+ ],
+ "returnType": {
+ "isNullable": false,
+ "name": "MultiElementWrapper",
+ "typeArguments": [
+ {
+ "name": "ElementWrapper",
+ },
+ ],
+ },
+ },
+ {
+ "inheritedFrom": {
+ "name": "AbstractWrapper.findAllByClassName",
+ },
+ "name": "findAllByClassName",
+ "parameters": [
+ {
+ "flags": {
+ "isOptional": false,
+ },
+ "name": "className",
+ "typeName": "string",
+ },
+ ],
+ "returnType": {
+ "isNullable": false,
+ "name": "MultiElementWrapper",
+ "typeArguments": [
+ {
+ "name": "ElementWrapper",
+ },
+ ],
+ },
+ },
+ {
+ "description": "Returns a multi-element wrapper that matches the specified component type with the specified CSS selector.
+If no CSS selector is specified, returns a multi-element wrapper that matches the specified component type.",
+ "inheritedFrom": {
+ "name": "AbstractWrapper.findAllComponents",
+ },
+ "name": "findAllComponents",
+ "parameters": [
+ {
+ "flags": {
+ "isOptional": false,
+ },
+ "name": "ComponentClass",
+ "typeName": "ComponentWrapperClass",
+ },
+ {
+ "description": "CSS Selector",
+ "flags": {
+ "isOptional": true,
+ },
+ "name": "selector",
+ "typeName": "string",
+ },
+ ],
+ "returnType": {
+ "isNullable": false,
+ "name": "MultiElementWrapper",
+ "typeArguments": [
+ {
+ "name": "Wrapper",
+ },
+ ],
+ },
+ },
+ {
+ "inheritedFrom": {
+ "name": "AbstractWrapper.findAny",
+ },
+ "name": "findAny",
+ "parameters": [
+ {
+ "flags": {
+ "isOptional": false,
+ },
+ "name": "selectors",
+ "typeName": "Array",
+ },
+ ],
+ "returnType": {
+ "isNullable": false,
+ "name": "ElementWrapper",
+ },
+ },
+ {
+ "inheritedFrom": {
+ "name": "AbstractWrapper.findByClassName",
+ },
+ "name": "findByClassName",
+ "parameters": [
+ {
+ "flags": {
+ "isOptional": false,
+ },
+ "name": "className",
+ "typeName": "string",
+ },
+ ],
+ "returnType": {
+ "isNullable": false,
+ "name": "ElementWrapper",
+ },
+ },
+ {
+ "description": "Returns a wrapper that matches the specified component type with the specified CSS selector.
+
Note: This function returns the specified component's wrapper even if the specified selector points to a different component type.",
"inheritedFrom": {
"name": "AbstractWrapper.findComponent",
@@ -116193,7 +117029,7 @@ Note: This function returns the specified component's wrapper even if the specif
],
"returnType": {
"isNullable": false,
- "name": "TokenWrapper",
+ "name": "TokenGroupItemWrapper",
},
},
{
@@ -116204,7 +117040,7 @@ Note: This function returns the specified component's wrapper even if the specif
"name": "MultiElementWrapper",
"typeArguments": [
{
- "name": "TokenWrapper",
+ "name": "TokenGroupItemWrapper",
},
],
},
diff --git a/src/__tests__/snapshot-tests/__snapshots__/test-utils-selectors.test.tsx.snap b/src/__tests__/snapshot-tests/__snapshots__/test-utils-selectors.test.tsx.snap
index 8760d2cc2f..c46eb4c924 100644
--- a/src/__tests__/snapshot-tests/__snapshots__/test-utils-selectors.test.tsx.snap
+++ b/src/__tests__/snapshot-tests/__snapshots__/test-utils-selectors.test.tsx.snap
@@ -652,6 +652,10 @@ exports[`test-utils selectors 1`] = `
"toggle": [
"awsui_root_4yi2u",
],
+ "token": [
+ "awsui_dismiss-button_1epxo",
+ "awsui_root_1epxo",
+ ],
"token-group": [
"awsui_dismiss-button_dm8gx",
"awsui_root_dm8gx",
diff --git a/src/__tests__/snapshot-tests/__snapshots__/test-utils-wrappers.test.tsx.snap b/src/__tests__/snapshot-tests/__snapshots__/test-utils-wrappers.test.tsx.snap
index 328e7f612e..cb85907a8b 100644
--- a/src/__tests__/snapshot-tests/__snapshots__/test-utils-wrappers.test.tsx.snap
+++ b/src/__tests__/snapshot-tests/__snapshots__/test-utils-wrappers.test.tsx.snap
@@ -87,6 +87,7 @@ import TilesWrapper from './tiles';
import TimeInputWrapper from './time-input';
import ToggleWrapper from './toggle';
import ToggleButtonWrapper from './toggle-button';
+import TokenWrapper from './token';
import TokenGroupWrapper from './token-group';
import TopNavigationWrapper from './top-navigation';
import TreeViewWrapper from './tree-view';
@@ -172,6 +173,7 @@ export { TilesWrapper };
export { TimeInputWrapper };
export { ToggleWrapper };
export { ToggleButtonWrapper };
+export { TokenWrapper };
export { TokenGroupWrapper };
export { TopNavigationWrapper };
export { TreeViewWrapper };
@@ -1663,6 +1665,25 @@ findToggleButton(selector?: string): ToggleButtonWrapper | null;
* @returns {Array}
*/
findAllToggleButtons(selector?: string): Array;
+/**
+ * Returns the wrapper of the first Token that matches the specified CSS selector.
+ * If no CSS selector is specified, returns the wrapper of the first Token.
+ * If no matching Token is found, returns \`null\`.
+ *
+ * @param {string} [selector] CSS Selector
+ * @returns {TokenWrapper | null}
+ */
+findToken(selector?: string): TokenWrapper | null;
+
+/**
+ * Returns an array of Token wrapper that matches the specified CSS selector.
+ * If no CSS selector is specified, returns all of the Tokens inside the current wrapper.
+ * If no matching Token is found, returns an empty array.
+ *
+ * @param {string} [selector] CSS Selector
+ * @returns {Array}
+ */
+findAllTokens(selector?: string): Array;
/**
* Returns the wrapper of the first TokenGroup that matches the specified CSS selector.
* If no CSS selector is specified, returns the wrapper of the first TokenGroup.
@@ -2776,6 +2797,19 @@ ElementWrapper.prototype.findToggleButton = function(selector) {
ElementWrapper.prototype.findAllToggleButtons = function(selector) {
return this.findAllComponents(ToggleButtonWrapper, selector);
};
+ElementWrapper.prototype.findToken = function(selector) {
+ let rootSelector = \`.\${TokenWrapper.rootSelector}\`;
+ if("legacyRootSelector" in TokenWrapper && TokenWrapper.legacyRootSelector){
+ rootSelector = \`:is(.\${TokenWrapper.rootSelector}, .\${TokenWrapper.legacyRootSelector})\`;
+ }
+ // casting to 'any' is needed to avoid this issue with generics
+ // https://github.com/microsoft/TypeScript/issues/29132
+ return (this as any).findComponent(selector ? appendSelector(selector, rootSelector) : rootSelector, TokenWrapper);
+};
+
+ElementWrapper.prototype.findAllTokens = function(selector) {
+ return this.findAllComponents(TokenWrapper, selector);
+};
ElementWrapper.prototype.findTokenGroup = function(selector) {
let rootSelector = \`.\${TokenGroupWrapper.rootSelector}\`;
if("legacyRootSelector" in TokenGroupWrapper && TokenGroupWrapper.legacyRootSelector){
@@ -2939,6 +2973,7 @@ import TilesWrapper from './tiles';
import TimeInputWrapper from './time-input';
import ToggleWrapper from './toggle';
import ToggleButtonWrapper from './toggle-button';
+import TokenWrapper from './token';
import TokenGroupWrapper from './token-group';
import TopNavigationWrapper from './top-navigation';
import TreeViewWrapper from './tree-view';
@@ -3024,6 +3059,7 @@ export { TilesWrapper };
export { TimeInputWrapper };
export { ToggleWrapper };
export { ToggleButtonWrapper };
+export { TokenWrapper };
export { TokenGroupWrapper };
export { TopNavigationWrapper };
export { TreeViewWrapper };
@@ -4359,6 +4395,23 @@ findToggleButton(selector?: string): ToggleButtonWrapper;
* @returns {MultiElementWrapper}
*/
findAllToggleButtons(selector?: string): MultiElementWrapper;
+/**
+ * Returns a wrapper that matches the Tokens with the specified CSS selector.
+ * If no CSS selector is specified, returns a wrapper that matches Tokens.
+ *
+ * @param {string} [selector] CSS Selector
+ * @returns {TokenWrapper}
+ */
+findToken(selector?: string): TokenWrapper;
+
+/**
+ * Returns a multi-element wrapper that matches Tokens with the specified CSS selector.
+ * If no CSS selector is specified, returns a multi-element wrapper that matches Tokens.
+ *
+ * @param {string} [selector] CSS Selector
+ * @returns {MultiElementWrapper}
+ */
+findAllTokens(selector?: string): MultiElementWrapper;
/**
* Returns a wrapper that matches the TokenGroups with the specified CSS selector.
* If no CSS selector is specified, returns a wrapper that matches TokenGroups.
@@ -5462,6 +5515,19 @@ ElementWrapper.prototype.findToggleButton = function(selector) {
ElementWrapper.prototype.findAllToggleButtons = function(selector) {
return this.findAllComponents(ToggleButtonWrapper, selector);
};
+ElementWrapper.prototype.findToken = function(selector) {
+ let rootSelector = \`.\${TokenWrapper.rootSelector}\`;
+ if("legacyRootSelector" in TokenWrapper && TokenWrapper.legacyRootSelector){
+ rootSelector = \`:is(.\${TokenWrapper.rootSelector}, .\${TokenWrapper.legacyRootSelector})\`;
+ }
+ // casting to 'any' is needed to avoid this issue with generics
+ // https://github.com/microsoft/TypeScript/issues/29132
+ return (this as any).findComponent(selector ? appendSelector(selector, rootSelector) : rootSelector, TokenWrapper);
+};
+
+ElementWrapper.prototype.findAllTokens = function(selector) {
+ return this.findAllComponents(TokenWrapper, selector);
+};
ElementWrapper.prototype.findTokenGroup = function(selector) {
let rootSelector = \`.\${TokenGroupWrapper.rootSelector}\`;
if("legacyRootSelector" in TokenGroupWrapper && TokenGroupWrapper.legacyRootSelector){
diff --git a/src/file-token-group/file-token.tsx b/src/file-token-group/file-token.tsx
index e19d4e9940..1d59348de5 100644
--- a/src/file-token-group/file-token.tsx
+++ b/src/file-token-group/file-token.tsx
@@ -12,7 +12,7 @@ import { BaseComponentProps } from '../internal/base-component/index.js';
import Tooltip from '../internal/components/tooltip/index';
import InternalSpaceBetween from '../space-between/internal.js';
import InternalSpinner from '../spinner/internal.js';
-import DismissButton from '../token-group/dismiss-button';
+import DismissButton from '../token/dismiss-button.js';
import { TokenGroupProps } from '../token-group/interfaces.js';
import * as defaultFormatters from './default-formatters.js';
import { FileOptionThumbnail } from './thumbnail.js';
diff --git a/src/file-token-group/styles.scss b/src/file-token-group/styles.scss
index 785ecf68b5..d8da57058e 100644
--- a/src/file-token-group/styles.scss
+++ b/src/file-token-group/styles.scss
@@ -6,7 +6,7 @@
@use '../internal/styles/tokens' as awsui;
@use '../internal/styles' as styles;
@use './constants' as constants;
-@use '../token-group/mixins.scss' as mixins;
+@use '../token/mixins.scss' as mixins;
@use '@cloudscape-design/component-toolkit/internal/focus-visible' as focus-visible;
@mixin token-box-validation {
diff --git a/src/internal/components/button-trigger/styles.scss b/src/internal/components/button-trigger/styles.scss
index 30f785e579..60e35e5315 100644
--- a/src/internal/components/button-trigger/styles.scss
+++ b/src/internal/components/button-trigger/styles.scss
@@ -6,7 +6,7 @@
@use '../../styles' as styles;
@use '../../styles/tokens' as awsui;
@use '@cloudscape-design/component-toolkit/internal/focus-visible' as focus-visible;
-@use '../../../token-group/constants' as tokenGroup;
+@use '../../../token/constants' as token;
@use './motion';
@@ -40,7 +40,7 @@ $padding-block-inner-filtering-token: 0px;
border-block-width: awsui.$border-width-token;
border-inline-width: awsui.$border-width-token;
- border-color: tokenGroup.$token-border-color;
+ border-color: token.$token-border-color;
border-start-end-radius: 0;
border-end-end-radius: 0;
block-size: 100%;
diff --git a/src/internal/components/option/highlight-match.tsx b/src/internal/components/option/highlight-match.tsx
index efc4a39671..2ff8304002 100644
--- a/src/internal/components/option/highlight-match.tsx
+++ b/src/internal/components/option/highlight-match.tsx
@@ -23,19 +23,24 @@ const splitOnFiltering = (str: string, highlightText: string) => {
interface HighlightMatchProps {
str?: string;
highlightText?: string;
+ labelRef?: React.RefObject;
}
-function Highlight({ str }: HighlightMatchProps) {
- return str ? {str} : null;
+function Highlight({ str, labelRef }: HighlightMatchProps) {
+ return (
+
+ {str}
+
+ );
}
-export default function HighlightMatch({ str, highlightText }: HighlightMatchProps) {
+export default function HighlightMatch({ str, highlightText, labelRef }: HighlightMatchProps) {
if (!str || !highlightText) {
- return {str} ;
+ return {str} ;
}
if (str === highlightText) {
- return ;
+ return ;
}
const { noMatches, matches } = splitOnFiltering(str, highlightText);
@@ -50,5 +55,5 @@ export default function HighlightMatch({ str, highlightText }: HighlightMatchPro
}
});
- return {highlighted} ;
+ return {highlighted} ;
}
diff --git a/src/internal/components/option/index.tsx b/src/internal/components/option/index.tsx
index e1f7288a95..7917f0c318 100644
--- a/src/internal/components/option/index.tsx
+++ b/src/internal/components/option/index.tsx
@@ -10,6 +10,7 @@ import { isDevelopment } from '../../is-development';
import { OptionProps } from './interfaces';
import { Description, FilteringTags, Label, LabelTag, OptionIcon, Tags } from './option-parts';
+import analyticsSelectors from './analytics-metadata/styles.css.js';
import styles from './styles.css.js';
export { OptionProps };
@@ -30,6 +31,10 @@ const Option = ({
isGroupOption = false,
highlightedOption = false,
selectedOption = false,
+ disableTitleTooltip = false,
+ labelContainerRef,
+ labelRef,
+ labelId,
...restProps
}: OptionProps) => {
if (!option) {
@@ -37,6 +42,7 @@ const Option = ({
}
const { disabled } = option;
const baseProps = getBaseProps(restProps);
+ const SpanOrDivTag = option.labelContent ? 'div' : 'span';
if (isDevelopment) {
validateStringValue(option.label, 'label');
@@ -54,7 +60,8 @@ const Option = ({
styles.option,
disabled && styles.disabled,
isGroupOption && styles.parent,
- highlightedOption && styles.highlighted
+ highlightedOption && styles.highlighted,
+ baseProps.className
);
const icon = option.__customIcon || (
@@ -69,24 +76,31 @@ const Option = ({
);
return (
-
{icon}
-
-
-
+
+
+ {option.labelContent ? (
+ {option.labelContent}
+ ) : (
+
+ )}
-
+
-
-
+
+
);
};
diff --git a/src/internal/components/option/interfaces.ts b/src/internal/components/option/interfaces.ts
index b7952f7384..628986b7a4 100644
--- a/src/internal/components/option/interfaces.ts
+++ b/src/internal/components/option/interfaces.ts
@@ -8,6 +8,7 @@ import { BaseComponentProps } from '../../base-component';
interface BaseOption {
value?: string;
label?: string;
+ labelContent?: React.ReactNode;
lang?: string;
description?: string;
disabled?: boolean;
@@ -49,4 +50,8 @@ export interface OptionProps extends BaseComponentProps {
highlightedOption?: boolean;
selectedOption?: boolean;
isGroupOption?: boolean;
+ disableTitleTooltip?: boolean;
+ labelContainerRef?: React.RefObject;
+ labelRef?: React.RefObject;
+ labelId?: string;
}
diff --git a/src/internal/components/option/option-parts.tsx b/src/internal/components/option/option-parts.tsx
index 2c043264a1..3048c5f9c8 100644
--- a/src/internal/components/option/option-parts.tsx
+++ b/src/internal/components/option/option-parts.tsx
@@ -11,19 +11,36 @@ import analyticsSelectors from './analytics-metadata/styles.css.js';
import styles from './styles.css.js';
interface LabelProps {
+ labelContainerRef?: React.RefObject;
+ labelRef?: React.RefObject;
+ labelId?: string;
label?: string;
prefix?: string;
highlightText?: string;
triggerVariant: boolean;
}
-export const Label = ({ label, prefix, highlightText, triggerVariant }: LabelProps) => (
-
- {prefix && (
- {prefix}
- )}
-
-
-);
+export const Label = ({
+ labelContainerRef,
+ labelRef,
+ labelId,
+ label,
+ prefix,
+ highlightText,
+ triggerVariant,
+}: LabelProps) => {
+ return (
+
+ {prefix && (
+ {prefix}
+ )}
+
+
+ );
+};
interface LabelTagProps {
labelTag?: string;
diff --git a/src/property-filter/__tests__/extended-wrapper.ts b/src/property-filter/__tests__/extended-wrapper.ts
index 9ca8a0ef33..5cd13e2251 100644
--- a/src/property-filter/__tests__/extended-wrapper.ts
+++ b/src/property-filter/__tests__/extended-wrapper.ts
@@ -11,7 +11,7 @@ import createWrapper, {
import itemStyles from '../../../lib/components/internal/components/selectable-item/styles.css.js';
import selectableStyles from '../../../lib/components/internal/components/selectable-item/styles.selectors.js';
import propertyFilterStyles from '../../../lib/components/property-filter/styles.selectors.js';
-import selectStyles from '../../../lib/components/select/parts/styles.selectors.js';
+import tokenStyles from '../../../lib/components/token/test-classes/styles.selectors.js';
export function createExtendedWrapper() {
const wrapper = createWrapper().findPropertyFilter()!;
@@ -270,7 +270,7 @@ function printField(wrapper: null | FormFieldWrapper, type: 'property' | 'operat
}
const multiselect = wrapper.findControl()!.findMultiselect();
if (multiselect) {
- const tokens = multiselect.findAllByClassName(selectStyles['inline-token']);
+ const tokens = multiselect.findAllByClassName(tokenStyles.root);
const value = tokens.map(w => w.getElement().textContent).join(', ');
return `${formFieldLabel}[${value}]`;
}
diff --git a/src/property-filter/__tests__/property-filter-stories-token-edit-single.test.tsx b/src/property-filter/__tests__/property-filter-stories-token-edit-single.test.tsx
index 4f8c076226..e3f0aae99b 100644
--- a/src/property-filter/__tests__/property-filter-stories-token-edit-single.test.tsx
+++ b/src/property-filter/__tests__/property-filter-stories-token-edit-single.test.tsx
@@ -245,7 +245,7 @@ describe('Property filter stories: tokens editing, single', () => {
expect(wrapper.editor.form()).toEqual(['Property[Status] Operator[=Equals] Value[Ready, Go]']);
expect(wrapper.editor.value().multiselect().options()).toEqual(['Go']);
- wrapper.editor.value().multiselect().value([null]);
+ wrapper.editor.value().multiselect().value(['Go']);
expect(wrapper.editor.form()).toEqual(['Property[Status] Operator[=Equals] Value[Ready]']);
wrapper.editor.submit();
diff --git a/src/property-filter/filtering-token/styles.scss b/src/property-filter/filtering-token/styles.scss
index 17f1462ba6..f96c3eda88 100644
--- a/src/property-filter/filtering-token/styles.scss
+++ b/src/property-filter/filtering-token/styles.scss
@@ -6,7 +6,7 @@
@use '@cloudscape-design/component-toolkit/internal/focus-visible' as focus-visible;
@use '../../internal/styles' as styles;
@use '../../internal/styles/tokens' as awsui;
-@use '../../token-group/constants' as constants;
+@use '../../token/constants' as constants;
$token-padding-block: styles.$control-padding-vertical;
$token-padding-inline: styles.$control-padding-horizontal;
diff --git a/src/select/parts/styles.scss b/src/select/parts/styles.scss
index 5cdc87809e..bf26b7fa2f 100644
--- a/src/select/parts/styles.scss
+++ b/src/select/parts/styles.scss
@@ -88,31 +88,6 @@ $inlineLabel-border-radius: 2px;
@include styles.with-direction('rtl') {
mask-image: linear-gradient(-270deg, transparent, white 20px, white);
}
-
- > .inline-token {
- display: flex;
- align-items: center;
- min-inline-size: max-content;
- block-size: 18px;
-
- border-block: awsui.$border-width-token solid awsui.$color-border-item-selected;
- border-inline: awsui.$border-width-token solid awsui.$color-border-item-selected;
- padding-block: 0;
- padding-inline: awsui.$space-xxs;
- background: awsui.$color-background-item-selected;
- border-start-start-radius: awsui.$border-radius-token;
- border-start-end-radius: awsui.$border-radius-token;
- border-end-start-radius: awsui.$border-radius-token;
- border-end-end-radius: awsui.$border-radius-token;
- color: awsui.$color-text-body-default;
- }
-}
-
-.visual-refresh > .inline-token-list > .inline-token {
- border-start-start-radius: awsui.$border-radius-badge;
- border-start-end-radius: awsui.$border-radius-badge;
- border-end-start-radius: awsui.$border-radius-badge;
- border-end-end-radius: awsui.$border-radius-badge;
}
.inline-token-hidden-placeholder {
@@ -122,13 +97,6 @@ $inlineLabel-border-radius: 2px;
.inline-token-counter {
white-space: nowrap;
}
-.inline-token-trigger--disabled {
- > .inline-token-list > .inline-token {
- border-color: awsui.$color-border-control-disabled;
- background-color: awsui.$color-background-container-content;
- color: awsui.$color-text-disabled;
- }
-}
.inline-label-trigger-wrapper {
margin-block-start: -7px;
diff --git a/src/select/parts/trigger.tsx b/src/select/parts/trigger.tsx
index 401156a0f5..8c8c376242 100644
--- a/src/select/parts/trigger.tsx
+++ b/src/select/parts/trigger.tsx
@@ -12,6 +12,7 @@ import { FormFieldValidationControlProps } from '../../internal/context/form-fie
import { useVisualRefresh } from '../../internal/hooks/use-visual-mode';
import { joinStrings } from '../../internal/utils/strings';
import { MultiselectProps } from '../../multiselect/interfaces';
+import InternalToken from '../../token/internal';
import { SelectProps } from '../interfaces';
import { SelectTriggerProps } from '../utils/use-select';
@@ -71,9 +72,7 @@ const Trigger = React.forwardRef(
>
{selectedOptions.map(({ label }, i) => (
-
- {label}
-
+
))}
diff --git a/src/test-utils/dom/file-token-group/index.ts b/src/test-utils/dom/file-token-group/index.ts
index 0faf3501c2..a525207677 100644
--- a/src/test-utils/dom/file-token-group/index.ts
+++ b/src/test-utils/dom/file-token-group/index.ts
@@ -5,7 +5,7 @@ import { ComponentWrapper, ElementWrapper } from '@cloudscape-design/test-utils-
import selectors from '../../../file-token-group/styles.selectors.js';
import testSelectors from '../../../file-token-group/test-classes/styles.selectors.js';
import formFieldStyles from '../../../form-field/styles.selectors.js';
-import tokenGroupSelectors from '../../../token-group/styles.selectors.js';
+import tokenSelectors from '../../../token/test-classes/styles.selectors.js';
export default class FileTokenGroupWrapper extends ComponentWrapper {
static rootSelector: string = testSelectors.root;
@@ -54,6 +54,6 @@ export class FileTokenWrapper extends ComponentWrapper {
}
findRemoveButton(): ElementWrapper {
- return this.findByClassName(tokenGroupSelectors['dismiss-button'])!;
+ return this.findByClassName(tokenSelectors['dismiss-button'])!;
}
}
diff --git a/src/test-utils/dom/multiselect/index.ts b/src/test-utils/dom/multiselect/index.ts
index 6dc01809e3..f08e033fd1 100644
--- a/src/test-utils/dom/multiselect/index.ts
+++ b/src/test-utils/dom/multiselect/index.ts
@@ -5,7 +5,7 @@ import { ElementWrapper, usesDom } from '@cloudscape-design/test-utils-core/dom'
import InputWrapper from '../input';
import DropdownHostComponentWrapper from '../internal/dropdown-host';
import TokenGroupWrapper from '../token-group';
-import TokenWrapper from '../token-group/token';
+import { TokenGroupItemWrapper } from '../token-group/token';
import inputStyles from '../../../input/styles.selectors.js';
import buttonTriggerStyles from '../../../internal/components/button-trigger/styles.selectors.js';
@@ -56,7 +56,7 @@ export default class MultiselectWrapper extends DropdownHostComponentWrapper {
*
* @param tokenIndex 1-based index of the token to return
*/
- findToken(tokenIndex: number): TokenWrapper | null {
+ findToken(tokenIndex: number): TokenGroupItemWrapper | null {
const tokenGroup = this.findComponent(`.${tokenGroupStyles.root}`, TokenGroupWrapper);
return tokenGroup!.findToken(tokenIndex);
}
@@ -69,7 +69,7 @@ export default class MultiselectWrapper extends DropdownHostComponentWrapper {
return tokenGroup!.findTokenToggle();
}
- findTokens(): Array {
+ findTokens(): Array {
const tokenGroup = this.findComponent(`.${tokenGroupStyles.root}`, TokenGroupWrapper);
return tokenGroup?.findTokens() || [];
}
diff --git a/src/test-utils/dom/token-group/index.ts b/src/test-utils/dom/token-group/index.ts
index 6b309bf375..e9321f41cf 100644
--- a/src/test-utils/dom/token-group/index.ts
+++ b/src/test-utils/dom/token-group/index.ts
@@ -2,18 +2,19 @@
// SPDX-License-Identifier: Apache-2.0
import { ComponentWrapper, ElementWrapper } from '@cloudscape-design/test-utils-core/dom';
-import TokenWrapper from './token';
+import { TokenGroupItemWrapper } from './token';
import tokenListSelectors from '../../../internal/components/token-list/styles.selectors.js';
+import newTokenSelectors from '../../../token/test-classes/styles.selectors.js';
import selectors from '../../../token-group/styles.selectors.js';
export default class TokenGroupWrapper extends ComponentWrapper {
static rootSelector: string = selectors.root;
- findTokens(): Array {
- return this.findAllByClassName(TokenWrapper.rootSelector).map(
- tokenElement => new TokenWrapper(tokenElement.getElement())
- );
+ findTokens(): Array {
+ const tokens = this.findAll(`:is(.${selectors.token}, .${newTokenSelectors.root})`);
+
+ return tokens.map(tokenElement => new TokenGroupItemWrapper(tokenElement.getElement()));
}
/**
@@ -21,10 +22,10 @@ export default class TokenGroupWrapper extends ComponentWrapper {
*
* @param tokenIndex 1-based index of the token to return.
*/
- findToken(tokenIndex: number): TokenWrapper | null {
+ findToken(tokenIndex: number): TokenGroupItemWrapper | null {
return this.findComponent(
- `.${tokenListSelectors['list-item']}:nth-child(${tokenIndex}) > .${TokenWrapper.rootSelector}`,
- TokenWrapper
+ `.${tokenListSelectors['list-item']}:nth-child(${tokenIndex}) > :is(.${selectors.token}, .${newTokenSelectors.root})`,
+ TokenGroupItemWrapper
);
}
diff --git a/src/test-utils/dom/token-group/token.ts b/src/test-utils/dom/token-group/token.ts
index 512909f421..5fa960ef98 100644
--- a/src/test-utils/dom/token-group/token.ts
+++ b/src/test-utils/dom/token-group/token.ts
@@ -4,10 +4,10 @@ import { ComponentWrapper, ElementWrapper } from '@cloudscape-design/test-utils-
import OptionWrapper from '../internal/option';
-import selectors from '../../../token-group/styles.selectors.js';
+import selectors from '../../../token/test-classes/styles.selectors.js';
+import legacySelectors from '../../../token-group/styles.selectors.js';
-export default class TokenWrapper extends ComponentWrapper {
- static rootSelector: string = selectors.token;
+export class TokenGroupItemWrapper extends ComponentWrapper {
findOption(): OptionWrapper {
return this.findComponent(`.${OptionWrapper.rootSelector}`, OptionWrapper)!;
}
@@ -17,6 +17,9 @@ export default class TokenWrapper extends ComponentWrapper {
}
findDismiss(): ElementWrapper {
- return this.findByClassName(selectors['dismiss-button'])!;
+ const selector = selectors['dismiss-button'];
+ const legacySelector = legacySelectors['dismiss-button'];
+
+ return this.find(`:is(.${legacySelector}, .${selector})`)!;
}
}
diff --git a/src/test-utils/dom/token/index.ts b/src/test-utils/dom/token/index.ts
new file mode 100644
index 0000000000..26e8e2f9b0
--- /dev/null
+++ b/src/test-utils/dom/token/index.ts
@@ -0,0 +1,50 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+import { ComponentWrapper, ElementWrapper } from '@cloudscape-design/test-utils-core/dom';
+
+import OptionWrapper from '../internal/option';
+
+import selectors from '../../../token/test-classes/styles.selectors.js';
+
+export default class TokenWrapper extends ComponentWrapper {
+ static rootSelector: string = selectors.root;
+
+ protected findOption(): OptionWrapper {
+ return this.findComponent(`.${OptionWrapper.rootSelector}`, OptionWrapper)!;
+ }
+
+ /**
+ * Returns the token label.
+ */
+ findLabel(): ElementWrapper {
+ return this.findOption().findLabel();
+ }
+
+ /**
+ * Returns the token label tag.
+ */
+ findLabelTag(): ElementWrapper | null {
+ return this.findOption().findLabelTag();
+ }
+
+ /**
+ * Returns the token description.
+ */
+ findDescription(): ElementWrapper | null {
+ return this.findOption().findDescription();
+ }
+
+ /**
+ * Returns the token tags.
+ */
+ findTags(): Array | null {
+ return this.findOption().findTags();
+ }
+
+ /**
+ * Returns the token dismiss button.
+ */
+ findDismiss(): ElementWrapper | null {
+ return this.findByClassName(selectors['dismiss-button']);
+ }
+}
diff --git a/src/token-group/__tests__/token-group.test.tsx b/src/token-group/__tests__/token-group.test.tsx
index 5f6a5fe21d..7fb21c439e 100644
--- a/src/token-group/__tests__/token-group.test.tsx
+++ b/src/token-group/__tests__/token-group.test.tsx
@@ -90,19 +90,19 @@ describe('TokenGroup', () => {
const wrapper = renderTokenGroup({ items, onDismiss });
expect(findToken(wrapper)!.findDismiss()).not.toBeNull();
- expect(findToken(wrapper)!.findDismiss()!.findByClassName(IconWrapper.rootSelector)).not.toBeNull();
+ expect(findToken(wrapper)!.findDismiss().findByClassName(IconWrapper.rootSelector)).not.toBeNull();
});
test('sets no alternative text on the dismiss area by default', () => {
const wrapper = renderTokenGroup({ items, onDismiss });
- expect(findToken(wrapper)!.findDismiss()!.getElement()).not.toHaveAttribute('aria-label');
+ expect(findToken(wrapper)!.findDismiss().getElement()).not.toHaveAttribute('aria-label');
});
test('sets the alternative text on the dismiss area', () => {
const wrapper = renderTokenGroup({ items: [{ ...items[0], dismissLabel: 'dismiss' }], onDismiss });
- expect(findToken(wrapper)!.findDismiss()!.getElement()).toHaveAttribute('aria-label', 'dismiss');
+ expect(findToken(wrapper)!.findDismiss().getElement()).toHaveAttribute('aria-label', 'dismiss');
});
test('correctly disables the option when disabled', () => {
@@ -138,7 +138,7 @@ describe('TokenGroup', () => {
const onDismissSpy = jest.fn();
const wrapper = renderTokenGroup({ items, readOnly: true, onDismiss: onDismissSpy });
- findToken(wrapper)!.findDismiss()!.click();
+ findToken(wrapper)!.findDismiss().click();
expect(onDismissSpy).not.toHaveBeenCalled();
});
@@ -147,7 +147,7 @@ describe('TokenGroup', () => {
const onDismissSpy = jest.fn();
const wrapper = renderTokenGroup({ items, onDismiss: onDismissSpy });
- findToken(wrapper)!.findDismiss()!.click();
+ findToken(wrapper)!.findDismiss().click();
expect(onDismissSpy).toHaveBeenCalledWith(
expect.objectContaining({
@@ -160,7 +160,7 @@ describe('TokenGroup', () => {
const onDismissSpy = jest.fn();
const wrapper = renderTokenGroup({ items: [{ ...items[0], disabled: true }], onDismiss });
- findToken(wrapper)!.findDismiss()!.click();
+ findToken(wrapper)!.findDismiss().click();
expect(onDismissSpy).not.toHaveBeenCalled();
});
@@ -168,7 +168,7 @@ describe('TokenGroup', () => {
test('does not automatically remove the token after firing dismiss event', () => {
const wrapper = renderTokenGroup({ items, onDismiss: onDismiss });
- findToken(wrapper)!.findDismiss()!.click();
+ findToken(wrapper)!.findDismiss().click();
expect(findToken(wrapper)).not.toBeNull();
});
diff --git a/src/token-group/analytics-metadata/interfaces.ts b/src/token-group/analytics-metadata/interfaces.ts
index 69370ea1cd..d8c2f22fbb 100644
--- a/src/token-group/analytics-metadata/interfaces.ts
+++ b/src/token-group/analytics-metadata/interfaces.ts
@@ -1,20 +1,10 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
-import { LabelIdentifier } from '@cloudscape-design/component-toolkit/internal/analytics-metadata';
-
import {
GeneratedAnalyticsMetadataTokenListShowLess,
GeneratedAnalyticsMetadataTokenListShowMore,
} from '../../internal/components/token-list/analytics-metadata/interfaces';
-export interface GeneratedAnalyticsMetadataTokenGroupDismiss {
- action: 'dismiss';
- detail: {
- label: LabelIdentifier;
- position?: string;
- };
-}
-
export type GeneratedAnalyticsMetadataTokenGroupShowMore = GeneratedAnalyticsMetadataTokenListShowMore;
export type GeneratedAnalyticsMetadataTokenGroupShowLess = GeneratedAnalyticsMetadataTokenListShowLess;
diff --git a/src/token-group/internal.tsx b/src/token-group/internal.tsx
index 531bcb8094..5e75b0e676 100644
--- a/src/token-group/internal.tsx
+++ b/src/token-group/internal.tsx
@@ -6,16 +6,16 @@ import clsx from 'clsx';
import { useMergeRefs } from '@cloudscape-design/component-toolkit/internal';
import { getAnalyticsMetadataAttribute } from '@cloudscape-design/component-toolkit/internal/analytics-metadata';
+import InternalIcon from '../icon/internal';
import { getBaseProps } from '../internal/base-component';
-import Option from '../internal/components/option';
import TokenList from '../internal/components/token-list';
import { fireNonCancelableEvent } from '../internal/events';
import checkControlled from '../internal/hooks/check-controlled';
import { InternalBaseComponentProps } from '../internal/hooks/use-base-component';
import { useListFocusController } from '../internal/hooks/use-list-focus-controller';
import { SomeRequired } from '../internal/types';
+import InternalToken from '../token/internal';
import { TokenGroupProps } from './interfaces';
-import { Token } from './token';
import tokenListStyles from '../internal/components/token-list/styles.css.js';
import styles from './styles.css.js';
@@ -71,21 +71,31 @@ export default function InternalTokenGroup({
items={items}
limit={limit}
renderItem={(item, itemIndex) => (
- {
fireNonCancelableEvent(onDismiss, { itemIndex });
setNextFocusIndex(itemIndex);
}}
- disabled={item.disabled}
readOnly={readOnly || isItemReadOnly?.(item)}
+ variant={'normal'}
+ icon={
+ item.iconName || item.iconUrl || item.iconSvg ? (
+
+ ) : undefined
+ }
{...(item.disabled || readOnly
? {}
: getAnalyticsMetadataAttribute({ detail: { position: `${itemIndex + 1}` } }))}
- >
-
-
+ />
)}
i18nStrings={i18nStrings}
limitShowFewerAriaLabel={limitShowFewerAriaLabel}
diff --git a/src/token-group/styles.scss b/src/token-group/styles.scss
index b8c85efd43..f84a72d1d6 100644
--- a/src/token-group/styles.scss
+++ b/src/token-group/styles.scss
@@ -5,9 +5,11 @@
@use '../internal/styles' as styles;
@use '../internal/styles/tokens' as awsui;
-@use '@cloudscape-design/component-toolkit/internal/focus-visible' as focus-visible;
-@use './constants' as constants;
-@use './mixins.scss' as mixins;
+
+.dismiss-button,
+.token {
+ /* used in test-utils */
+}
.root {
@include styles.styles-reset;
@@ -16,72 +18,3 @@
padding-block-start: awsui.$space-xs;
}
}
-
-.dismiss-button {
- margin-block-start: -1px;
- margin-block-end: 0;
- margin-inline-start: awsui.$space-xxs;
- margin-inline-end: -1px;
- border-block: 1px solid transparent;
- border-inline: 1px solid transparent;
- padding-block: 0;
- padding-inline: awsui.$space-xxs;
- color: awsui.$color-text-button-inline-icon-default;
- background-color: transparent;
-
- @include focus-visible.when-visible {
- @include styles.focus-highlight(0px);
- }
-
- &:focus {
- outline: none;
- text-decoration: none;
- }
-
- &:hover {
- cursor: pointer;
- color: awsui.$color-text-button-inline-icon-hover;
- }
-}
-
-.token {
- block-size: 100%;
- display: flex;
- flex-direction: column;
- gap: awsui.$space-xxs;
-}
-
-.token-box {
- @include mixins.token-box-styles();
-}
-
-.token-box-readonly {
- border-color: awsui.$color-border-input-disabled;
- background-color: awsui.$color-background-container-content;
- pointer-events: none;
-
- > .dismiss-button {
- color: awsui.$color-text-button-inline-icon-disabled;
-
- &:hover {
- /* stylelint-disable-next-line plugin/no-unsupported-browser-features */
- cursor: initial;
- color: awsui.$color-text-button-inline-icon-disabled;
- }
- }
-}
-.token-box-disabled.token-box-disabled {
- border-color: awsui.$color-border-control-disabled;
- background-color: awsui.$color-background-container-content;
- color: awsui.$color-text-disabled;
- pointer-events: none;
-
- > .dismiss-button {
- color: awsui.$color-text-button-inline-icon-disabled;
-
- &:hover {
- cursor: initial;
- color: awsui.$color-text-button-inline-icon-disabled;
- }
- }
-}
diff --git a/src/token-group/token.tsx b/src/token-group/token.tsx
deleted file mode 100644
index 6d741da18f..0000000000
--- a/src/token-group/token.tsx
+++ /dev/null
@@ -1,47 +0,0 @@
-// 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 { getBaseProps } from '../internal/base-component';
-import DismissButton from './dismiss-button';
-
-import styles from './styles.css.js';
-
-interface TokenProps {
- children: React.ReactNode;
- ariaLabel?: string;
- dismissLabel?: string;
- onDismiss?: () => void;
- disabled?: boolean;
- readOnly?: boolean;
- className?: string;
-}
-
-export function Token({ ariaLabel, disabled, readOnly, dismissLabel, onDismiss, children, ...restProps }: TokenProps) {
- const baseProps = getBaseProps(restProps);
-
- return (
-
-
- {children}
- {onDismiss && (
-
- )}
-
-
- );
-}
diff --git a/src/token/__integ__/token.test.ts b/src/token/__integ__/token.test.ts
new file mode 100644
index 0000000000..ac06929ede
--- /dev/null
+++ b/src/token/__integ__/token.test.ts
@@ -0,0 +1,156 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+import { BasePageObject } from '@cloudscape-design/browser-test-tools/page-objects';
+import useBrowser from '@cloudscape-design/browser-test-tools/use-browser';
+
+import createWrapper from '../../../lib/components/test-utils/selectors';
+
+class TokenPage extends BasePageObject {
+ getTokenWrapper(selector: string) {
+ return createWrapper().findToken(selector);
+ }
+
+ isTokenDismissButtonPresent(selector: string) {
+ const tokenWrapper = this.getTokenWrapper(selector);
+ const dismissButton = tokenWrapper.findDismiss();
+ return this.isExisting(dismissButton.toSelector());
+ }
+
+ isIconVisible(iconTestId: string) {
+ return this.isExisting(iconTestId);
+ }
+
+ getTokenLabelTag(selector: string) {
+ const tokenWrapper = this.getTokenWrapper(selector);
+ return tokenWrapper.findLabelTag();
+ }
+
+ getTokenLabel(selector: string) {
+ const tokenWrapper = this.getTokenWrapper(selector);
+ return tokenWrapper.findLabel();
+ }
+
+ getTokenDescription(selector: string) {
+ const tokenWrapper = this.getTokenWrapper(selector);
+ return tokenWrapper.findDescription();
+ }
+
+ getTokenTags(selector: string) {
+ const tokenWrapper = this.getTokenWrapper(selector);
+ return tokenWrapper.findTags();
+ }
+}
+
+function setupTest(testFn: (page: TokenPage) => Promise) {
+ return useBrowser(async browser => {
+ await browser.url('#/light/token/simple');
+ const page = new TokenPage(browser);
+ await page.waitForVisible('h1');
+ await testFn(page);
+ });
+}
+
+describe('Token component', () => {
+ describe('Basic rendering', () => {
+ test(
+ 'inline token displays correct label',
+ setupTest(async page => {
+ const label = page.getTokenLabel('[data-testid="basic-inline-token"]');
+ const labelText = await page.getText(label.toSelector());
+ expect(labelText).toBe('Inline token');
+ })
+ );
+
+ test(
+ 'dismissable token has dismiss button',
+ setupTest(async page => {
+ const hasDismissButton = await page.isTokenDismissButtonPresent('[data-testid="inline-token-dismissable"]');
+ expect(hasDismissButton).toBeTruthy();
+ })
+ );
+
+ test(
+ 'token with icon displays icon correctly',
+ setupTest(async page => {
+ // Check if the token with icon exists
+ const tokenSelector = '[data-testid="normal-token-with-icon-dismissable"]';
+ const tokenWrapper = page.getTokenWrapper(tokenSelector);
+ const tokenExists = await page.isExisting(tokenWrapper.toSelector());
+ expect(tokenExists).toBeTruthy();
+
+ // Use the isIconVisible method to check for the specific icon
+ const iconExists = await page.isIconVisible('[data-testid="token-bug-icon"]');
+ expect(iconExists).toBeTruthy();
+ })
+ );
+
+ test(
+ 'token displays label tag',
+ setupTest(async page => {
+ const labelTag = page.getTokenLabelTag('[data-testid="normal-token-dismissable"]');
+ const labelTagText = await page.getText(labelTag.toSelector());
+ expect(labelTagText).toBe('test');
+ })
+ );
+
+ test(
+ 'token displays description',
+ setupTest(async page => {
+ const description = page.getTokenDescription('[data-testid="normal-token-with-icon-dismissable"]');
+ const descriptionText = await page.getText(description.toSelector());
+ expect(descriptionText).toBe('some description');
+ })
+ );
+
+ test(
+ 'token displays tags correctly',
+ setupTest(async page => {
+ const tokenSelector = '[data-testid="normal-token-with-icon-dismissable"]';
+ const tags = page.getTokenTags(tokenSelector);
+ const tagsExist = await page.isExisting(tags.toSelector());
+ expect(tagsExist).toBeTruthy();
+
+ // Check the text content of the tags
+ const tagsText = await page.getText(tags.toSelector());
+ expect(tagsText).toContain('tag');
+ })
+ );
+ });
+
+ describe('Accessibility', () => {
+ test(
+ 'truncated inline token is focusable',
+ setupTest(async page => {
+ const selector = '[data-testid="inline-token-long-text"]';
+ const tokenElement = page.getTokenWrapper(selector).toSelector();
+ const tabIndex = await page.getElementAttribute(tokenElement, 'tabindex');
+ expect(tabIndex).toBe('0');
+ })
+ );
+
+ test(
+ 'short inline tokens are not focusable',
+ setupTest(async page => {
+ const selector = '[data-testid="basic-inline-token"]';
+ const tokenElement = page.getTokenWrapper(selector).toSelector();
+ const tabIndex = await page.getElementAttribute(tokenElement, 'tabindex');
+ expect(tabIndex).toBeNull();
+ })
+ );
+
+ test(
+ 'tokens have proper accessibility attributes',
+ setupTest(async page => {
+ const selector = '[data-testid="basic-inline-token"]';
+ const tokenElement = page.getTokenWrapper(selector).toSelector();
+ const role = await page.getElementAttribute(tokenElement, 'role');
+ const ariaDisabled = await page.getElementAttribute(tokenElement, 'aria-disabled');
+ const ariaLabelledby = await page.getElementAttribute(tokenElement, 'aria-labelledby');
+
+ expect(role).toBe('group');
+ expect(ariaDisabled).toBe('false');
+ expect(ariaLabelledby).toBeTruthy();
+ })
+ );
+ });
+});
diff --git a/src/token/__tests__/analytics-metadata.test.tsx b/src/token/__tests__/analytics-metadata.test.tsx
new file mode 100644
index 0000000000..4b29f33d41
--- /dev/null
+++ b/src/token/__tests__/analytics-metadata.test.tsx
@@ -0,0 +1,89 @@
+// 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 {
+ activateAnalyticsMetadata,
+ GeneratedAnalyticsMetadataFragment,
+} from '@cloudscape-design/component-toolkit/internal/analytics-metadata';
+import { getGeneratedAnalyticsMetadata } from '@cloudscape-design/component-toolkit/internal/analytics-metadata/utils';
+
+import createWrapper from '../../../lib/components/test-utils/dom';
+import Token, { TokenProps } from '../../../lib/components/token';
+import InternalToken from '../../../lib/components/token/internal';
+
+import analyticsSelectors from '../../../lib/components/token/analytics-metadata/styles.css.js';
+
+function renderToken(props: TokenProps) {
+ const renderResult = render( );
+ return createWrapper(renderResult.container).findToken()!;
+}
+
+const tokenMetadata: GeneratedAnalyticsMetadataFragment = {
+ contexts: [
+ {
+ type: 'component',
+ detail: {
+ name: 'awsui.Token',
+ label: '',
+ },
+ },
+ ],
+};
+
+describe('Token analytics metadata', () => {
+ beforeEach(() => {
+ activateAnalyticsMetadata(true);
+ });
+
+ test('Token renders correct analytics metadata', () => {
+ const wrapper = renderToken({ label: 'test' });
+
+ const simpleToken = wrapper.getElement();
+
+ expect(wrapper.findDismiss()).toBe(null);
+ expect(getGeneratedAnalyticsMetadata(simpleToken)).toEqual(tokenMetadata);
+ });
+
+ test('Internal Token does not render "component" metadata', () => {
+ const renderResult = render( );
+ const wrapper = createWrapper(renderResult.container).findToken();
+ expect(getGeneratedAnalyticsMetadata(wrapper!.getElement())).toEqual({});
+ });
+
+ test('adds analytics metadata to the token', () => {
+ const wrapper = renderToken({ label: 'Test token' });
+ expect(wrapper.getElement()).toHaveClass(analyticsSelectors.token);
+
+ const metadata = wrapper.getElement().getAttribute('data-awsui-analytics');
+ expect(metadata).toBeTruthy();
+ });
+
+ test('adds analytics metadata to the dismiss button', () => {
+ const onDismiss = jest.fn();
+ const wrapper = renderToken({ label: 'Test token', onDismiss });
+
+ const dismissButton = wrapper.findDismiss()!.getElement();
+ const metadata = dismissButton.getAttribute('data-awsui-analytics');
+ expect(metadata).toBeTruthy();
+ });
+
+ 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();
+ });
+
+ 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();
+ });
+});
diff --git a/src/token/__tests__/token-tooltip.test.tsx b/src/token/__tests__/token-tooltip.test.tsx
new file mode 100644
index 0000000000..f29a7f843c
--- /dev/null
+++ b/src/token/__tests__/token-tooltip.test.tsx
@@ -0,0 +1,207 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+import React from 'react';
+import { act, fireEvent, render, screen } from '@testing-library/react';
+
+import createWrapper from '../../../lib/components/test-utils/dom';
+import Token from '../../../lib/components/token';
+
+// Mock ResizeObserver
+let mockResizeObserverCallback: any;
+const mockResizeObserver = jest.fn().mockImplementation(callback => {
+ mockResizeObserverCallback = callback;
+ return {
+ observe: jest.fn(),
+ unobserve: jest.fn(),
+ disconnect: jest.fn(),
+ };
+});
+
+global.ResizeObserver = mockResizeObserver;
+
+describe('Token Tooltip behavior', () => {
+ beforeEach(() => {
+ mockResizeObserver.mockClear();
+ mockResizeObserverCallback = null;
+ });
+
+ const mockEllipsisActive = (labelElement: HTMLElement, isActive: boolean) => {
+ const innerSpan = labelElement.querySelector('span');
+ if (innerSpan) {
+ Object.defineProperty(innerSpan, 'offsetWidth', {
+ configurable: true,
+ value: isActive ? 300 : 100,
+ });
+ Object.defineProperty(labelElement, 'offsetWidth', {
+ configurable: true,
+ value: 200,
+ });
+ }
+ };
+
+ const triggerResizeObserver = (labelElement: HTMLElement) => {
+ act(() => {
+ if (mockResizeObserverCallback) {
+ mockResizeObserverCallback([
+ {
+ target: labelElement.parentElement,
+ contentRect: { width: 200, height: 20 },
+ borderBoxSize: [{ inlineSize: 200, blockSize: 20 }],
+ contentBoxSize: [{ inlineSize: 200, blockSize: 20 }],
+ devicePixelContentBoxSize: [{ inlineSize: 200, blockSize: 20 }],
+ },
+ ]);
+ }
+ });
+ };
+
+ test('shows tooltip on mouse enter when text overflows', () => {
+ const { container } = render(
+
+ );
+
+ const wrapper = createWrapper(container).findToken()!;
+ const tokenElement = wrapper.getElement();
+ const labelElement = wrapper.findLabel().getElement();
+
+ mockEllipsisActive(labelElement, true);
+ triggerResizeObserver(labelElement);
+
+ fireEvent.mouseEnter(tokenElement);
+
+ expect(screen.queryByTestId('tooltip-live-region-content')).toBeInTheDocument();
+ expect(screen.getByTestId('tooltip-live-region-content')).toHaveTextContent(
+ 'Very long text that should be truncated'
+ );
+ });
+
+ test('hides tooltip on mouse leave', () => {
+ const { container } = render(
+
+ );
+
+ const wrapper = createWrapper(container).findToken()!;
+ const tokenElement = wrapper.getElement();
+ const labelElement = wrapper.findLabel().getElement();
+
+ mockEllipsisActive(labelElement, true);
+ triggerResizeObserver(labelElement);
+
+ fireEvent.mouseEnter(tokenElement);
+ expect(screen.queryByTestId('tooltip-live-region-content')).toBeInTheDocument();
+
+ fireEvent.mouseLeave(tokenElement);
+ expect(screen.queryByTestId('tooltip-live-region-content')).not.toBeInTheDocument();
+ });
+
+ test('shows tooltip on focus when text overflows', () => {
+ const { container } = render(
+
+ );
+
+ const wrapper = createWrapper(container).findToken()!;
+ const tokenElement = wrapper.getElement();
+ const labelElement = wrapper.findLabel().getElement();
+
+ mockEllipsisActive(labelElement, true);
+ triggerResizeObserver(labelElement);
+
+ fireEvent.focus(tokenElement);
+
+ expect(screen.queryByTestId('tooltip-live-region-content')).toBeInTheDocument();
+ });
+
+ test('hides tooltip on blur', () => {
+ const { container } = render(
+
+ );
+
+ const wrapper = createWrapper(container).findToken()!;
+ const tokenElement = wrapper.getElement();
+ const labelElement = wrapper.findLabel().getElement();
+
+ mockEllipsisActive(labelElement, true);
+ triggerResizeObserver(labelElement);
+
+ fireEvent.focus(tokenElement);
+ expect(screen.queryByTestId('tooltip-live-region-content')).toBeInTheDocument();
+
+ fireEvent.blur(tokenElement);
+ expect(screen.queryByTestId('tooltip-live-region-content')).not.toBeInTheDocument();
+ });
+
+ test('does not show tooltip when text does not overflow', () => {
+ const { container } = render( );
+
+ const wrapper = createWrapper(container).findToken()!;
+ const tokenElement = wrapper.getElement();
+ const labelElement = wrapper.findLabel().getElement();
+
+ mockEllipsisActive(labelElement, false);
+ act(() => {
+ fireEvent(window, new Event('resize'));
+ });
+
+ fireEvent.mouseEnter(tokenElement);
+
+ expect(screen.queryByTestId('tooltip-live-region-content')).not.toBeInTheDocument();
+ });
+
+ test('sets tabIndex for focusable tokens with tooltips', () => {
+ const { container } = render(
+
+ );
+
+ const wrapper = createWrapper(container).findToken()!;
+ const tokenElement = wrapper.getElement();
+ const labelElement = wrapper.findLabel().getElement();
+
+ mockEllipsisActive(labelElement, true);
+ triggerResizeObserver(labelElement);
+
+ expect(tokenElement).toHaveAttribute('tabindex', '0');
+ });
+
+ test('tooltip is accessible via live region', () => {
+ const { container } = render(
+
+ );
+
+ const wrapper = createWrapper(container).findToken()!;
+ const tokenElement = wrapper.getElement();
+ const labelElement = wrapper.findLabel().getElement();
+
+ mockEllipsisActive(labelElement, true);
+ triggerResizeObserver(labelElement);
+
+ fireEvent.mouseEnter(tokenElement);
+
+ const liveRegionContent = screen.queryByTestId('tooltip-live-region-content');
+ expect(liveRegionContent).toBeInTheDocument();
+ expect(liveRegionContent).toHaveTextContent('Accessible tooltip content');
+ });
+});
diff --git a/src/token/__tests__/token.test.tsx b/src/token/__tests__/token.test.tsx
new file mode 100644
index 0000000000..7576c4d7d0
--- /dev/null
+++ b/src/token/__tests__/token.test.tsx
@@ -0,0 +1,230 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+import React from 'react';
+import { render, screen } from '@testing-library/react';
+
+import Icon from '../../../lib/components/icon/internal';
+import createWrapper from '../../../lib/components/test-utils/dom';
+import Token, { TokenProps } from '../../../lib/components/token';
+import InternalToken from '../../../lib/components/token/internal';
+
+import styles from '../../../lib/components/token/styles.selectors.js';
+
+function renderToken(props: TokenProps) {
+ const renderResult = render( );
+ return createWrapper(renderResult.container).findToken()!;
+}
+
+describe('Token', () => {
+ describe('Basic rendering', () => {
+ test('renders with minimal props', () => {
+ const wrapper = renderToken({ label: 'Test token' });
+ expect(wrapper.getElement()).toHaveClass(styles['token-normal']);
+ expect(wrapper.findLabel()!.getElement()).toHaveTextContent('Test token');
+ });
+
+ test('renders with all props', () => {
+ const onDismiss = jest.fn();
+ const wrapper = renderToken({
+ label: 'Test token',
+ labelTag: 'Tag',
+ description: 'Description',
+ tags: ['Tag1', 'Tag2'],
+ icon: ,
+ dismissLabel: 'Dismiss',
+ onDismiss,
+ });
+
+ expect(wrapper.findLabel()!.getElement()).toHaveTextContent('Test token');
+ expect(wrapper.findLabelTag()!.getElement()).toHaveTextContent('Tag');
+ expect(wrapper.findDescription()!.getElement()).toHaveTextContent('Description');
+ expect(wrapper.findTags()).toHaveLength(2);
+ expect(wrapper.findTags()![0].getElement()).toHaveTextContent('Tag1');
+ expect(wrapper.findTags()![1].getElement()).toHaveTextContent('Tag2');
+ expect(screen.getByTestId('custom-icon')).toBeInTheDocument();
+ expect(wrapper.findDismiss()!.getElement()).toHaveAttribute('aria-label', 'Dismiss');
+ });
+
+ test('renders with ReactNode label', () => {
+ renderToken({
+ label: Custom label
,
+ });
+ expect(screen.getByTestId('custom-label')).toBeInTheDocument();
+ });
+ });
+
+ describe('Variants', () => {
+ test('renders normal variant by default', () => {
+ const wrapper = renderToken({ label: 'Test token' });
+ expect(wrapper.getElement()).toHaveClass(styles['token-normal']);
+ expect(wrapper.getElement()).not.toHaveClass(styles['token-inline']);
+ });
+
+ test('renders inline variant', () => {
+ const wrapper = renderToken({ label: 'Test token', variant: 'inline' });
+ expect(wrapper.getElement()).toHaveClass(styles['token-inline']);
+ expect(wrapper.getElement()).not.toHaveClass(styles['token-normal']);
+ });
+
+ test('applies correct CSS classes for inline variant', () => {
+ const inlineWrapper = renderToken({ label: 'Test token', variant: 'inline' });
+ expect(inlineWrapper.getElement()).toHaveClass(styles['token-inline']);
+
+ const normalWrapper = renderToken({ label: 'Test token' });
+ expect(normalWrapper.getElement()).toHaveClass(styles['token-normal']);
+ });
+ });
+
+ describe('States', () => {
+ test('applies disabled state', () => {
+ const wrapper = renderToken({ label: 'Test token', disabled: true });
+ expect(wrapper.getElement().querySelector(`.${styles['token-box']}`)).toHaveClass(styles['token-box-disabled']);
+ expect(wrapper.getElement()).toHaveAttribute('aria-disabled', 'true');
+ });
+
+ test('applies readonly state', () => {
+ const wrapper = renderToken({ label: 'Test token', readOnly: true });
+ expect(wrapper.getElement().querySelector(`.${styles['token-box']}`)).toHaveClass(styles['token-box-readonly']);
+ });
+ });
+
+ describe('Dismiss button', () => {
+ test('renders when onDismiss is provided', () => {
+ const onDismiss = jest.fn();
+ const wrapper = renderToken({ label: 'Test token', onDismiss });
+ expect(wrapper.findDismiss()).toBeTruthy();
+ });
+
+ test('does not render when onDismiss is not provided', () => {
+ const wrapper = renderToken({ label: 'Test token' });
+ expect(wrapper.findDismiss()).toBeNull();
+ });
+
+ test('calls onDismiss when clicked', () => {
+ const onDismiss = jest.fn();
+ const wrapper = renderToken({ label: 'Test token', onDismiss });
+ wrapper.findDismiss()!.click();
+ expect(onDismiss).toHaveBeenCalledTimes(1);
+ });
+
+ test('shows for inline readonly tokens', () => {
+ const onDismiss = jest.fn();
+ const wrapper = renderToken({
+ label: 'Test token',
+ variant: 'inline',
+ onDismiss,
+ readOnly: true,
+ });
+ expect(wrapper.findDismiss()).toBeTruthy();
+ });
+
+ test('sets accessibility attributes', () => {
+ const onDismiss = jest.fn();
+ const wrapper = renderToken({
+ label: 'Test token',
+ onDismiss,
+ dismissLabel: 'Remove token',
+ });
+
+ const dismissButton = wrapper.findDismiss()!.getElement();
+ expect(dismissButton).toHaveAttribute('aria-label', 'Remove token');
+ });
+ });
+
+ describe('Icons', () => {
+ test('renders icon with correct styling for normal variant', () => {
+ renderToken({
+ label: 'Test token',
+ icon: ,
+ });
+ const normalIcon = screen.getByTestId('normal-icon').parentElement;
+ expect(normalIcon).toHaveClass(styles.icon);
+ expect(normalIcon).not.toHaveClass(styles['icon-inline']);
+ });
+
+ test('renders icon with correct styling for inline variant', () => {
+ renderToken({
+ label: 'Test token',
+ variant: 'inline',
+ icon: ,
+ });
+ const inlineIcon = screen.getByTestId('inline-icon').parentElement;
+ expect(inlineIcon).toHaveClass(styles.icon);
+ expect(inlineIcon).toHaveClass(styles['icon-inline']);
+ });
+ });
+
+ describe('Error handling for inline variants', () => {
+ test('React elements trigger a dev warning', () => {
+ // Mock console.warn to capture the warning
+ const consoleSpy = jest.spyOn(console, 'warn').mockImplementation(() => {});
+
+ // Positive case: React element with inline variant should trigger warning
+ renderToken({
+ variant: 'inline',
+ label: React element ,
+ });
+
+ expect(consoleSpy).toHaveBeenCalledWith(
+ expect.stringContaining(
+ '[AwsUi] [Label] Only plain text (strings or numbers) are supported when variant="inline"'
+ )
+ );
+
+ consoleSpy.mockRestore();
+ });
+
+ test('strings do not throw a dev warning', () => {
+ // Mock console.warn to capture any warnings
+ const consoleSpy = jest.spyOn(console, 'warn').mockImplementation(() => {});
+
+ // Negative case: string with inline variant should not trigger warning
+ renderToken({
+ variant: 'inline',
+ label: 'String label',
+ });
+
+ expect(consoleSpy).not.toHaveBeenCalled();
+
+ consoleSpy.mockRestore();
+ });
+ });
+
+ describe('Accessibility', () => {
+ test('applies default group role and aria attributes', () => {
+ const wrapper = renderToken({ label: 'Test token' });
+ expect(wrapper.getElement()).toHaveAttribute('role', 'group');
+ expect(wrapper.getElement()).toHaveAttribute('aria-disabled', 'false');
+ expect(wrapper.getElement()).toHaveAttribute('aria-labelledby');
+ });
+
+ test('allows custom role override', () => {
+ const { container } = render( );
+ const wrapper = createWrapper(container).findToken()!;
+ expect(wrapper.getElement()).toHaveAttribute('role', 'menuitem');
+ });
+
+ test('aria-labelledby matches label element ID', () => {
+ const wrapper = renderToken({ label: 'Test token' });
+ const tokenElement = wrapper.getElement();
+ const labelElement = wrapper.findLabel().getElement();
+
+ const ariaLabelledby = tokenElement.getAttribute('aria-labelledby');
+ const labelId = labelElement.getAttribute('id');
+
+ expect(ariaLabelledby).toBe(labelId);
+ expect(ariaLabelledby).toBeTruthy();
+ });
+
+ test('uses ariaLabel when provided instead of aria-labelledby', () => {
+ const wrapper = renderToken({
+ label: 'Test token',
+ ariaLabel: 'Custom aria label',
+ });
+ const tokenElement = wrapper.getElement();
+
+ expect(tokenElement).toHaveAttribute('aria-label', 'Custom aria label');
+ expect(tokenElement).not.toHaveAttribute('aria-labelledby');
+ });
+ });
+});
diff --git a/src/token/analytics-metadata/interfaces.ts b/src/token/analytics-metadata/interfaces.ts
new file mode 100644
index 0000000000..3a0dc32178
--- /dev/null
+++ b/src/token/analytics-metadata/interfaces.ts
@@ -0,0 +1,16 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+import { LabelIdentifier } from '@cloudscape-design/component-toolkit/internal/analytics-metadata';
+
+export interface GeneratedAnalyticsMetadataTokenDismiss {
+ action: 'dismiss';
+ detail: {
+ label: LabelIdentifier;
+ position?: string;
+ };
+}
+
+export interface GeneratedAnalyticsMetadataTokenComponent {
+ name: 'awsui.Token';
+ label: string;
+}
diff --git a/src/token/analytics-metadata/styles.scss b/src/token/analytics-metadata/styles.scss
new file mode 100644
index 0000000000..c2cd2afd5b
--- /dev/null
+++ b/src/token/analytics-metadata/styles.scss
@@ -0,0 +1,8 @@
+/*
+ Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ SPDX-License-Identifier: Apache-2.0
+*/
+
+.token {
+ /* used in analytics metadata */
+}
diff --git a/src/token-group/constants.scss b/src/token/constants.scss
similarity index 85%
rename from src/token-group/constants.scss
rename to src/token/constants.scss
index 6ed92a762d..d77ea9c646 100644
--- a/src/token-group/constants.scss
+++ b/src/token/constants.scss
@@ -8,3 +8,5 @@
$token-background: awsui.$color-background-item-selected;
$token-border-color: awsui.$color-border-item-selected;
+$token-max-height-inline: 20px;
+$icon-width: 16px;
diff --git a/src/token-group/dismiss-button.tsx b/src/token/dismiss-button.tsx
similarity index 61%
rename from src/token-group/dismiss-button.tsx
rename to src/token/dismiss-button.tsx
index 91037853f6..fcbad1b5d3 100644
--- a/src/token-group/dismiss-button.tsx
+++ b/src/token/dismiss-button.tsx
@@ -1,28 +1,32 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
import React, { forwardRef, Ref } from 'react';
+import clsx from 'clsx';
import { getAnalyticsMetadataAttribute } from '@cloudscape-design/component-toolkit/internal/analytics-metadata';
import InternalIcon from '../icon/internal';
-import { GeneratedAnalyticsMetadataTokenGroupDismiss } from './analytics-metadata/interfaces';
+import { fireNonCancelableEvent, NonCancelableEventHandler } from '../internal/events';
+import { GeneratedAnalyticsMetadataTokenDismiss } from './analytics-metadata/interfaces';
import styles from './styles.css.js';
+import testUtilStyles from './test-classes/styles.css.js';
interface DismissButtonProps {
disabled?: boolean;
readOnly?: boolean;
- onDismiss?: () => void;
+ onDismiss?: NonCancelableEventHandler;
dismissLabel?: string;
+ inline?: boolean;
}
export default forwardRef(DismissButton);
function DismissButton(
- { disabled, dismissLabel, onDismiss, readOnly }: DismissButtonProps,
+ { disabled, dismissLabel, onDismiss, readOnly, inline }: DismissButtonProps,
ref: Ref
) {
- const analyticsMetadata: GeneratedAnalyticsMetadataTokenGroupDismiss = {
+ const analyticsMetadata: GeneratedAnalyticsMetadataTokenDismiss = {
action: 'dismiss',
detail: {
label: { root: 'self' },
@@ -32,14 +36,18 @@ function DismissButton(
{
if (disabled || readOnly || !onDismiss) {
return;
}
- onDismiss();
+ fireNonCancelableEvent(onDismiss);
}}
aria-label={dismissLabel}
{...(disabled || readOnly ? {} : getAnalyticsMetadataAttribute(analyticsMetadata))}
diff --git a/src/token/index.tsx b/src/token/index.tsx
new file mode 100644
index 0000000000..ffbba14e8b
--- /dev/null
+++ b/src/token/index.tsx
@@ -0,0 +1,35 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+'use client';
+import React from 'react';
+
+import { getAnalyticsMetadataAttribute } from '@cloudscape-design/component-toolkit/internal/analytics-metadata';
+
+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 analyticsSelectors from './analytics-metadata/styles.css.js';
+
+export { TokenProps };
+
+export default function Token(props: TokenProps) {
+ const baseComponentProps = useBaseComponent('Token');
+
+ const componentAnalyticsMetadata: GeneratedAnalyticsMetadataTokenComponent = {
+ name: 'awsui.Token',
+ label: `.${analyticsSelectors.token}`,
+ };
+
+ return (
+
+ );
+}
+
+applyDisplayName(Token, 'Token');
diff --git a/src/token/interfaces.ts b/src/token/interfaces.ts
new file mode 100644
index 0000000000..47bfa0d75a
--- /dev/null
+++ b/src/token/interfaces.ts
@@ -0,0 +1,76 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+import React from 'react';
+
+import { BaseComponentProps } from '../internal/base-component';
+import { NonCancelableEventHandler } from '../internal/events';
+
+export interface TokenProps extends BaseComponentProps {
+ /** Slot for the label of the token as text or an element.
+ *
+ * For `variant="inline"`, only plain text is supported, for example, strings or numbers.
+ */
+ label: React.ReactNode;
+
+ /**
+ * Adds an `aria-label` to the token.
+ *
+ * Use this if the label is not plain text.
+ */
+ ariaLabel?: string;
+
+ /** A label tag that provides additional guidance, shown next to the label. */
+ labelTag?: string;
+
+ /** Further information about the token that appears below the label. */
+ description?: string;
+
+ /** A list of tags giving further guidance about the token. */
+ tags?: ReadonlyArray;
+
+ /** An icon at the start of the token.
+ *
+ * When `variant="normal"`, if a description or tags are set, icon size should be `normal`.
+ *
+ * When `variant="inline"`, icon size should be `small`.
+ */
+ icon?: React.ReactNode;
+
+ /**
+ * Specifies the token's visual style and functionality.
+ *
+ * For `inline` only label, icon and dismiss button are displayed.
+ *
+ * Defaults to `normal` if not specified.
+ */
+ variant?: TokenProps.Variant;
+
+ /** Determines whether the token is disabled. */
+ disabled?: boolean;
+
+ /**
+ * Specifies if the control is read-only. A read-only control is still focusable.
+ */
+ readOnly?: boolean;
+
+ /** Adds an `aria-label` to the dismiss button. */
+ dismissLabel?: string;
+
+ /**
+ * Called when the user clicks on the dismiss button.
+ *
+ * Make sure that you add a listener to this event to update your application state.
+ */
+ onDismiss?: NonCancelableEventHandler;
+
+ /**
+ * Content to display in the tooltip when `variant="inline"`. The tooltip appears when the token label is truncated due to insufficient space.
+ *
+ * Only applies to plain text labels.
+ */
+ tooltipContent?: string;
+}
+
+export namespace TokenProps {
+ export type Variant = 'normal' | 'inline';
+}
diff --git a/src/token/internal.tsx b/src/token/internal.tsx
new file mode 100644
index 0000000000..b21df924fa
--- /dev/null
+++ b/src/token/internal.tsx
@@ -0,0 +1,175 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+import React, { useRef, useState } from 'react';
+import clsx from 'clsx';
+
+import { useResizeObserver, useUniqueId, warnOnce } from '@cloudscape-design/component-toolkit/internal';
+
+import { getBaseProps } from '../internal/base-component';
+import Option from '../internal/components/option';
+import Tooltip from '../internal/components/tooltip';
+import { InternalBaseComponentProps } from '../internal/hooks/use-base-component';
+import LiveRegion from '../live-region/internal';
+import DismissButton from './dismiss-button';
+import { TokenProps } from './interfaces';
+
+import analyticsSelectors from './analytics-metadata/styles.css.js';
+import styles from './styles.css.js';
+import testUtilStyles from './test-classes/styles.css.js';
+
+type InternalTokenProps = TokenProps &
+ InternalBaseComponentProps & {
+ role?: string;
+ disableInnerPadding?: boolean;
+ };
+
+function InternalToken({
+ // External
+ label,
+ ariaLabel,
+ labelTag,
+ description,
+ variant = 'normal',
+ disabled,
+ readOnly,
+ icon,
+ tags,
+ dismissLabel,
+ onDismiss,
+ tooltipContent,
+
+ // Internal
+ role,
+ disableInnerPadding,
+
+ // Base
+ __internalRootRef,
+ ...restProps
+}: InternalTokenProps) {
+ const baseProps = getBaseProps(restProps);
+ const labelContainerRef = useRef(null);
+ const labelRef = useRef(null);
+ const [showTooltip, setShowTooltip] = useState(false);
+ const [isEllipsisActive, setIsEllipsisActive] = useState(false);
+ const isInline = variant === 'inline';
+ const ariaLabelledbyId = useUniqueId();
+
+ const isLabelOverflowing = () => {
+ const labelContent = labelRef.current;
+ const labelContainer = labelContainerRef.current;
+
+ if (labelContent && labelContainer) {
+ return labelContent.offsetWidth > labelContainer.offsetWidth;
+ }
+ };
+
+ useResizeObserver(labelContainerRef, () => {
+ if (isInline) {
+ setIsEllipsisActive(isLabelOverflowing() ?? false);
+ }
+ });
+
+ const buildOptionDefinition = () => {
+ const isLabelStringOrNumber = typeof label === 'string' || typeof label === 'number';
+ const labelObject = isLabelStringOrNumber ? { label: String(label) } : { labelContent: label };
+
+ if (isInline) {
+ if (!isLabelStringOrNumber) {
+ warnOnce('Label', `Only plain text (strings or numbers) are supported when variant="inline".`);
+ }
+
+ return {
+ ...labelObject,
+ disabled,
+ __customIcon: icon && {icon} ,
+ };
+ } else {
+ return {
+ ...labelObject,
+ disabled,
+ labelTag,
+ description,
+ tags,
+ __customIcon: icon && {icon} ,
+ };
+ }
+ };
+
+ return (
+ {
+ setShowTooltip(true);
+ }}
+ onBlur={() => {
+ setShowTooltip(false);
+ }}
+ onMouseEnter={() => {
+ setShowTooltip(true);
+ }}
+ onMouseLeave={() => {
+ setShowTooltip(false);
+ }}
+ tabIndex={!!tooltipContent && isInline && isEllipsisActive ? 0 : undefined}
+ >
+
+
+ {onDismiss && (
+
+ )}
+
+ {!!tooltipContent && isInline && isEllipsisActive && showTooltip && (
+
+ {tooltipContent}
+
+ }
+ size="medium"
+ onDismiss={() => {
+ setShowTooltip(false);
+ }}
+ />
+ )}
+
+ );
+}
+
+export default InternalToken;
diff --git a/src/token-group/mixins.scss b/src/token/mixins.scss
similarity index 56%
rename from src/token-group/mixins.scss
rename to src/token/mixins.scss
index b7916cf207..057560077b 100644
--- a/src/token-group/mixins.scss
+++ b/src/token/mixins.scss
@@ -25,3 +25,23 @@
color: awsui.$color-text-body-default;
box-sizing: border-box;
}
+
+@mixin token-box-inline-styles {
+ position: relative;
+ block-size: constants.$token-max-height-inline;
+ max-block-size: constants.$token-max-height-inline;
+ border-block: awsui.$border-width-field solid constants.$token-border-color;
+ border-inline: awsui.$border-width-field solid constants.$token-border-color;
+ padding-inline-start: awsui.$space-scaled-xxs;
+ padding-inline-end: awsui.$space-scaled-xxs;
+ display: flex;
+ align-items: center;
+ background: constants.$token-background;
+ border-start-start-radius: awsui.$space-scaled-xxs;
+ border-start-end-radius: awsui.$space-scaled-xxs;
+ border-end-start-radius: awsui.$space-scaled-xxs;
+ border-end-end-radius: awsui.$space-scaled-xxs;
+ color: awsui.$color-text-body-default;
+ box-sizing: border-box;
+ max-inline-size: 100%;
+}
diff --git a/src/token/styles.scss b/src/token/styles.scss
new file mode 100644
index 0000000000..b00b7cfa2e
--- /dev/null
+++ b/src/token/styles.scss
@@ -0,0 +1,119 @@
+/*
+ Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ SPDX-License-Identifier: Apache-2.0
+*/
+
+@use '../internal/styles' as styles;
+@use '../internal/styles/tokens' as awsui;
+@use '@cloudscape-design/component-toolkit/internal/focus-visible' as focus-visible;
+@use './constants' as constants;
+@use './mixins.scss' as mixins;
+
+.root {
+ @include styles.styles-reset;
+}
+
+.dismiss-button {
+ align-self: flex-start;
+ margin-block-end: 0;
+ margin-inline-start: awsui.$space-xxs;
+ 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: awsui.$color-text-button-inline-icon-default;
+ background-color: transparent;
+ cursor: pointer;
+
+ @include focus-visible.when-visible {
+ @include styles.focus-highlight(0px);
+ }
+
+ &:focus {
+ outline: none;
+ text-decoration: none;
+ }
+
+ &:hover {
+ color: awsui.$color-text-button-inline-icon-hover;
+ }
+
+ &-inline {
+ padding-inline: 0;
+ display: flex;
+ align-items: center;
+ align-self: center;
+ }
+}
+
+.icon {
+ padding-inline-end: awsui.$space-xs;
+ align-self: flex-start;
+ display: flex;
+ flex-shrink: 0;
+
+ &-inline {
+ padding-inline-end: awsui.$space-xxs;
+ align-self: center;
+ }
+}
+
+.token-normal {
+ block-size: 100%;
+ display: flex;
+ flex-direction: column;
+ gap: awsui.$space-xxs;
+}
+
+.token-inline {
+ display: inline-flex;
+ max-inline-size: 100%;
+
+ @include focus-visible.when-visible {
+ @include styles.focus-highlight(0px);
+ }
+}
+
+.token-option-inline {
+ max-block-size: constants.$token-max-height-inline;
+}
+
+.token-box {
+ @include mixins.token-box-styles();
+
+ &-without-dismiss {
+ padding-inline-end: styles.$control-padding-horizontal;
+ }
+}
+
+.token-box-inline {
+ @include mixins.token-box-inline-styles();
+}
+
+.disable-padding {
+ padding-block-start: 0;
+ padding-block-end: 0;
+ padding-inline-start: 0;
+ padding-inline-end: 0;
+}
+
+.token-box-readonly,
+.token-box-disabled {
+ border-color: awsui.$color-border-input-disabled;
+ background-color: awsui.$color-background-container-content;
+ pointer-events: none;
+
+ > .dismiss-button {
+ color: awsui.$color-text-button-inline-icon-disabled;
+ cursor: initial;
+
+ &:hover {
+ color: awsui.$color-text-button-inline-icon-disabled;
+ }
+ }
+}
+
+.token-box-disabled {
+ border-color: awsui.$color-border-control-disabled;
+ color: awsui.$color-text-disabled;
+}
diff --git a/src/token/test-classes/styles.scss b/src/token/test-classes/styles.scss
new file mode 100644
index 0000000000..180aff3483
--- /dev/null
+++ b/src/token/test-classes/styles.scss
@@ -0,0 +1,8 @@
+/*
+ Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ SPDX-License-Identifier: Apache-2.0
+*/
+.root,
+.dismiss-button {
+ /* used in test-utils */
+}