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 ( -
- - -
- - { -
-
- {fileName} -
-
- } - - 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 ( +
+ + +
+ + { +
+
+ {fileName} +
+
+ } + + 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} - - - + - - + + ); }; 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(