+ {renderDropdownShell(
- }
- onMouseDown={handleDropdownMouseDown}
- open={open && (!!dropdownContent || !!dropdownFooter)}
- footer={
- dropdownFooterRef && (
-
-
- {open && dropdownFooter ? dropdownFooter : null}
-
-
- )
- }
- expandToViewport={expandToViewport}
- loopFocus={loopFocus}
- content={
- open && dropdownContent ? (
-
- {dropdownContent}
+ />,
+ true
+ )}
+
+ );
+ }
+
+ return (
+
+
+ {tokenInsertedAriaLabel}
+
+
+ {tokenList.map((token, i) => (
+
+ undefined} />
+
+ ))}
+
+
+
+ {renderDropdownShell(
+
+
+
+
+
+ {tokenList.length > 0 && (
+
+ {hiddenTokens.length > 0 && (
+ -
+
+
+ )}
+
+ {visibleTokens.map((token, i) => {
+ const realIndex = tokenList.length - effectiveVisible + i;
+ return (
+ - {
+ const comingFromOutside =
+ !e.relatedTarget || !tokenListRef.current?.contains(e.relatedTarget as Node);
+ if (comingFromOutside) {
+ preventOpenOnFocusRef.current = true;
+ }
+ }}
+ onKeyDown={(e: React.KeyboardEvent) => {
+ if (e.keyCode === KeyCode.backspace || e.keyCode === KeyCodeDelete) {
+ e.preventDefault();
+ if (!readOnly && !disabled) {
+ removeToken(realIndex);
+ }
+ } else if (e.keyCode === KeyCode.left) {
+ e.preventDefault();
+ if (i === 0) {
+ if (hiddenTokens.length > 0) {
+ overflowPillRef.current?.focus();
+ }
+ } else {
+ focusTokenAtIndex(realIndex - 1);
+ }
+ } else if (e.keyCode === KeyCode.right) {
+ e.preventDefault();
+ if (realIndex < tokenList.length - 1) {
+ focusTokenAtIndex(realIndex + 1);
+ } else {
+ inputRef.current?.focus();
+ }
+ }
+ }}
+ >
+ removeToken(realIndex)}
+ />
+
+ );
+ })}
+
+ )}
+
+
+ handleChange(event.detail.value)}
+ __onDelayedInput={event => handleDelayedInput(event.detail.value)}
+ onKeyDown={handleKeyDown}
+ onKeyUp={onKeyUp}
+ disabled={disabled}
+ disableBrowserAutocorrect={disableBrowserAutocorrect}
+ readOnly={readOnly}
+ ariaRequired={ariaRequired}
+ clearAriaLabel={clearAriaLabel}
+ ref={inputRef}
+ autoComplete={false}
+ nativeInputAttributes={processAttributes(nativeAttributes, nativeInputAttributes, 'Autosuggest')}
+ __skipNativeAttributesWarnings={Object.keys(nativeAttributes)}
+ __noBorder={true}
+ {...(!disabled && !readOnly && value
+ ? {
+ __endIcon: 'close' as const,
+ __onEndIconClick: () => {
+ inputRef.current?.focus();
+ fireNonCancelableEvent(onChange, { value: '', tokens: tokenList });
+ },
+ }
+ : {})}
+ {...{ ...formFieldContext, invalid: false, warning: false }}
+ />
- ) : null
- }
- />
+
+ )}
+ {hiddenTokens.length > 0 && (
+
{
+ if (hiddenTokens.length === 1) {
+ setTokensOverflowOpen(false);
+ if (tokenList.length === 1) {
+ inputRef.current?.focus();
+ } else {
+ setFocusFirstVisibleAfterOverflow(true);
+ }
+ }
+ removeToken(i, true);
+ }}
+ onClose={() => {
+ setTokensOverflowOpen(false);
+ overflowPillRef.current?.focus();
+ }}
+ />
+ )}
+
);
}
diff --git a/src/internal/components/autosuggest-input/overflow-dropdown.tsx b/src/internal/components/autosuggest-input/overflow-dropdown.tsx
new file mode 100644
index 0000000000..41f82c7177
--- /dev/null
+++ b/src/internal/components/autosuggest-input/overflow-dropdown.tsx
@@ -0,0 +1,134 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+import React, { useEffect, useRef } from 'react';
+import clsx from 'clsx';
+
+import { AutosuggestProps } from '../../../autosuggest/interfaces';
+import InternalToken from '../../../token/internal';
+
+import styles from './styles.css.js';
+import testUtilStyles from './test-classes/styles.selectors.js';
+
+export interface OverflowDropdownProps {
+ tokens: ReadonlyArray
;
+ triggerRef: React.RefObject;
+ open: boolean;
+ onDismiss: (index: number) => void;
+ onClose: () => void;
+ tokenDismissLabel?: (value: string) => string;
+ readOnly?: boolean;
+}
+
+export function OverflowDropdown({
+ tokens,
+ triggerRef,
+ open,
+ onDismiss,
+ onClose,
+ tokenDismissLabel,
+ readOnly,
+}: OverflowDropdownProps) {
+ const pendingFocusAfterDismissRef = useRef(null);
+ const listRef = useRef(null);
+ const panelRef = useRef(null);
+
+ useEffect(() => {
+ if (!open) {
+ return;
+ }
+ const first = listRef.current?.querySelector('button');
+ first?.focus();
+ }, [open]);
+
+ // No deps: runs after every render to handle focus restoration after a token dismiss.
+ useEffect(() => {
+ if (pendingFocusAfterDismissRef.current === null) {
+ return;
+ }
+ const dismissedIndex = pendingFocusAfterDismissRef.current;
+ pendingFocusAfterDismissRef.current = null;
+ if (tokens.length === 0) {
+ return;
+ }
+ const nextPosition = Math.min(dismissedIndex, tokens.length - 1);
+ const buttons = listRef.current?.querySelectorAll('button');
+ if (buttons && buttons[nextPosition]) {
+ buttons[nextPosition].focus();
+ }
+ });
+
+ const handlePanelKeyDown = (e: React.KeyboardEvent) => {
+ if (e.key === 'Escape') {
+ e.stopPropagation();
+ onClose();
+ triggerRef.current?.focus();
+ } else if (e.key === 'Tab') {
+ onClose();
+ } else if (e.key === 'ArrowUp' || e.key === 'ArrowDown') {
+ e.preventDefault();
+ e.stopPropagation();
+ const buttons = Array.from(listRef.current?.querySelectorAll('button') ?? []);
+ if (buttons.length === 0) {
+ return;
+ }
+ const focused = buttons.indexOf(document.activeElement as HTMLButtonElement);
+ if (e.key === 'ArrowUp') {
+ const prev = focused > 0 ? focused - 1 : buttons.length - 1;
+ buttons[prev].focus();
+ } else {
+ const next = focused < buttons.length - 1 ? focused + 1 : 0;
+ buttons[next].focus();
+ }
+ }
+ };
+
+ useEffect(() => {
+ if (!open) {
+ return;
+ }
+ const handleClick = (e: MouseEvent) => {
+ if (
+ panelRef.current &&
+ !panelRef.current.contains(e.target as Node) &&
+ triggerRef.current &&
+ !triggerRef.current.contains(e.target as Node)
+ ) {
+ onClose();
+ }
+ };
+ document.addEventListener('mousedown', handleClick);
+ return () => document.removeEventListener('mousedown', handleClick);
+ }, [open, onClose, triggerRef]);
+
+ if (!open) {
+ return null;
+ }
+
+ return (
+ e.preventDefault()}
+ onKeyDown={handlePanelKeyDown}
+ className={clsx(styles['overflow-panel'], testUtilStyles['overflow-panel'])}
+ >
+
+ {tokens.map((token, i) => (
+ -
+ {
+ pendingFocusAfterDismissRef.current = i;
+ onDismiss(i);
+ }}
+ />
+
+ ))}
+
+
+ );
+}
diff --git a/src/internal/components/autosuggest-input/styles.scss b/src/internal/components/autosuggest-input/styles.scss
index d8e3587abc..174dac7f9c 100644
--- a/src/internal/components/autosuggest-input/styles.scss
+++ b/src/internal/components/autosuggest-input/styles.scss
@@ -3,6 +3,11 @@
SPDX-License-Identifier: Apache-2.0
*/
+@use '../../../internal/styles/tokens' as awsui;
+@use '../../../internal/styles' as styles;
+@use '../../../internal/styles/foundation' as foundation;
+@use '@cloudscape-design/component-toolkit/internal/focus-visible' as focus-visible;
+
.root {
// Used in test-utils.
}
@@ -15,3 +20,172 @@
.dropdown-content {
display: contents;
}
+
+.token-trigger {
+ display: flex;
+ align-items: center;
+ flex-wrap: nowrap;
+ overflow: visible;
+ inline-size: 100%;
+ box-sizing: border-box;
+ min-block-size: awsui.$size-vertical-input;
+ padding-inline-start: styles.$control-padding-horizontal;
+ padding-inline-end: 0;
+ gap: awsui.$space-xxs;
+
+ background-color: awsui.$color-background-input-default;
+ border-block: awsui.$border-width-field solid awsui.$color-border-input-default;
+ border-inline: awsui.$border-width-field solid awsui.$color-border-input-default;
+ border-start-start-radius: styles.$control-border-radius;
+ border-start-end-radius: styles.$control-border-radius;
+ border-end-start-radius: styles.$control-border-radius;
+ border-end-end-radius: styles.$control-border-radius;
+
+ &:focus-within {
+ @include styles.form-focus-element();
+ }
+
+ &.token-trigger-invalid {
+ @include styles.form-invalid-control();
+ &:focus-within {
+ box-shadow: foundation.$box-shadow-focused-light-invalid;
+ }
+ }
+
+ &.token-trigger-warning {
+ @include styles.form-warning-control();
+ &:focus-within {
+ box-shadow: foundation.$box-shadow-focused-light-invalid;
+ }
+ }
+
+ &.token-trigger-disabled {
+ @include styles.form-disabled-element();
+ &:focus-within {
+ box-shadow: none;
+ }
+ }
+
+ &.token-trigger-readonly {
+ @include styles.form-readonly-element();
+ }
+
+ &.grouped {
+ @include styles.grouped-control;
+ }
+}
+
+.token-list {
+ display: flex;
+ flex-wrap: nowrap;
+ align-items: center;
+ gap: awsui.$space-xxs;
+ flex-shrink: 1;
+ min-inline-size: 0;
+ overflow: visible;
+ list-style: none;
+ margin-block: 0;
+ margin-inline: 0;
+ padding-block: 0;
+ padding-inline: 0;
+
+ > .token-list-item {
+ display: contents;
+ }
+}
+
+.token-input-wrapper {
+ margin-inline-start: calc(-1 * #{awsui.$space-xxs});
+}
+
+.search-icon {
+ display: flex;
+ align-items: center;
+ flex-shrink: 0;
+}
+
+.pill-icon {
+ display: flex;
+ align-items: center;
+ @include styles.with-motion {
+ transition: transform awsui.$motion-duration-rotate-180 awsui.$motion-easing-rotate-180;
+ }
+}
+
+.pill-icon-open {
+ transform: rotate(-180deg);
+}
+
+.token-trigger-container {
+ position: relative;
+ inline-size: 100%;
+}
+
+.measurement-container {
+ @include styles.measurement-ghost;
+ display: flex;
+ flex-wrap: nowrap;
+ gap: awsui.$space-xxs;
+}
+
+.overflow-panel {
+ position: absolute;
+ inset-block-start: calc(100% - #{awsui.$border-divider-list-width});
+ inset-inline-start: 0;
+ inline-size: max-content;
+ max-inline-size: min(320px, 90vw);
+ z-index: 2000;
+ background-color: awsui.$color-background-dropdown-item-default;
+
+ @include styles.dropdown-shadow();
+}
+
+.overflow-panel-list {
+ list-style: none;
+ padding-block: awsui.$space-xxs;
+ padding-inline: 0;
+ margin-block: 0;
+ margin-inline: 0;
+ display: flex;
+ flex-direction: column;
+}
+
+.overflow-panel-item {
+ display: flex;
+ align-items: center;
+ padding-block: awsui.$space-xxs;
+ padding-inline: awsui.$space-xs;
+}
+
+.token-overflow-pill {
+ @include styles.styles-reset;
+
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ flex-shrink: 0;
+ padding-block: 1px;
+ padding-inline: awsui.$space-xs;
+ border-block: awsui.$border-width-field solid awsui.$color-border-control-default;
+ border-inline: awsui.$border-width-field solid awsui.$color-border-control-default;
+ border-start-start-radius: awsui.$border-radius-token-inline;
+ border-start-end-radius: awsui.$border-radius-token-inline;
+ border-end-start-radius: awsui.$border-radius-token-inline;
+ border-end-end-radius: awsui.$border-radius-token-inline;
+ background-color: awsui.$color-background-control-default;
+ color: awsui.$color-text-body-default;
+ white-space: nowrap;
+ gap: awsui.$space-xxs;
+ @include styles.font-body-s;
+
+ @include focus-visible.when-visible {
+ @include styles.focus-element-without-border(awsui.$border-radius-control-default-focus-ring);
+ }
+
+ &:disabled {
+ border-color: awsui.$color-border-control-disabled;
+ background-color: awsui.$color-background-control-disabled;
+ color: awsui.$color-text-disabled;
+ cursor: auto;
+ }
+}
diff --git a/src/internal/components/autosuggest-input/test-classes/styles.scss b/src/internal/components/autosuggest-input/test-classes/styles.scss
new file mode 100644
index 0000000000..88863d7ea3
--- /dev/null
+++ b/src/internal/components/autosuggest-input/test-classes/styles.scss
@@ -0,0 +1,27 @@
+/*
+ Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ SPDX-License-Identifier: Apache-2.0
+*/
+.root {
+ /* used in test-utils */
+}
+
+.token-trigger {
+ /* used in test-utils */
+}
+
+.token-list {
+ /* used in test-utils */
+}
+
+.token-list-item {
+ /* used in test-utils */
+}
+
+.token-overflow-pill {
+ /* used in test-utils */
+}
+
+.overflow-panel {
+ /* used in test-utils */
+}
diff --git a/src/internal/components/autosuggest-input/use-token-list-focus.ts b/src/internal/components/autosuggest-input/use-token-list-focus.ts
new file mode 100644
index 0000000000..afeef65f5b
--- /dev/null
+++ b/src/internal/components/autosuggest-input/use-token-list-focus.ts
@@ -0,0 +1,53 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+import { useRef } from 'react';
+
+interface UseTokenListFocusOptions {
+ tokenListRef: React.RefObject;
+ inputRef: React.RefObject;
+ tokenListLength: number;
+ effectiveVisible: number;
+}
+
+interface UseTokenListFocusResult {
+ focusTokenAtIndex: (absoluteIndex: number) => void;
+}
+
+/**
+ * Provides imperative focus management for the inline token list.
+ *
+ * `focusTokenAtIndex` translates an absolute token index (0-based within the full
+ * token array) to a position-in-visible index, then focuses the corresponding dismiss
+ * button. If the token is not visible (index falls before the visible window), focus
+ * is sent to the input instead.
+ *
+ * The snapshot ref is updated on every render so that keyboard handlers, which close
+ * over a stale `effectiveVisible`, always see the latest value.
+ */
+export function useTokenListFocus({
+ tokenListRef,
+ inputRef,
+ tokenListLength,
+ effectiveVisible,
+}: UseTokenListFocusOptions): UseTokenListFocusResult {
+ const snapshotRef = useRef({ tokenListLength, effectiveVisible });
+ // Mutate every render so keyboard-handler callbacks capture the live values.
+ snapshotRef.current = { tokenListLength, effectiveVisible };
+
+ const focusTokenAtIndex = (absoluteIndex: number) => {
+ const { tokenListLength: len, effectiveVisible: ev } = snapshotRef.current;
+ const visibleStart = len - ev;
+ const positionInVisible = absoluteIndex - visibleStart;
+ if (positionInVisible < 0) {
+ inputRef.current?.focus();
+ return;
+ }
+ const dismissButtons = tokenListRef.current?.querySelectorAll('[data-token-item] button');
+ if (dismissButtons && dismissButtons[positionInVisible]) {
+ dismissButtons[positionInVisible].focus();
+ }
+ };
+
+ return { focusTokenAtIndex };
+}
diff --git a/src/internal/components/autosuggest-input/use-token-overflow.ts b/src/internal/components/autosuggest-input/use-token-overflow.ts
new file mode 100644
index 0000000000..1ea5cb742a
--- /dev/null
+++ b/src/internal/components/autosuggest-input/use-token-overflow.ts
@@ -0,0 +1,119 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+import { useLayoutEffect, useRef, useState } from 'react';
+
+import { useResizeObserver } from '@cloudscape-design/component-toolkit/internal';
+
+interface UseTokenOverflowResult {
+ containerRef: React.RefObject;
+ visibleCount: number | undefined;
+ tokenListMaxWidth: number | undefined;
+}
+
+/**
+ * Measures token and container widths to determine how many tokens can be shown
+ * inline before the rest must collapse into the overflow "+N" pill.
+ *
+ * @param measureRef - ref to the invisible measurement container
+ * @param triggerRowRef - ref to the visible token trigger row (for padding/gap CSS)
+ * @param tokenCount - current total token count; changing it re-triggers measurement
+ * @param tokenContentKey - stable string derived from token labels/icons; changing it re-triggers measurement when content changes without count changing
+ * @param enabled - set to false in non-token mode to skip all measurement work
+ */
+export function useTokenOverflow(
+ measureRef: React.RefObject,
+ triggerRowRef: React.RefObject,
+ tokenCount: number,
+ tokenContentKey: string,
+ enabled: boolean
+): UseTokenOverflowResult {
+ const containerRef = useRef(null);
+ const [visibleCount, setVisibleCount] = useState(undefined);
+ const [tokenListMaxWidth, setTokenListMaxWidth] = useState(undefined);
+ const lastOverflowResultRef = useRef<{ count: number | undefined; maxWidth: number | undefined }>({
+ count: undefined,
+ maxWidth: undefined,
+ });
+
+ const [containerResizeCount, setContainerResizeCount] = useState(0);
+ const lastContainerWidthRef = useRef(0);
+ useResizeObserver(
+ () => (enabled ? containerRef.current : null),
+ entry => {
+ if (entry.borderBoxWidth > 0 && entry.borderBoxWidth !== lastContainerWidthRef.current) {
+ lastContainerWidthRef.current = entry.borderBoxWidth;
+ setContainerResizeCount(n => n + 1);
+ }
+ }
+ );
+
+ useLayoutEffect(() => {
+ if (!enabled || !measureRef.current || !containerRef.current || !triggerRowRef.current) {
+ return;
+ }
+ measureRef.current.inert = true;
+ const containerWidth = containerRef.current.offsetWidth ?? 0;
+ if (containerWidth <= 0) {
+ return;
+ }
+
+ const tokenEls = Array.from(measureRef.current.querySelectorAll('[data-measure-token]'));
+ if (tokenEls.length === 0) {
+ if (lastOverflowResultRef.current.count !== 0) {
+ lastOverflowResultRef.current = { count: 0, maxWidth: undefined };
+ setVisibleCount(0);
+ setTokenListMaxWidth(undefined);
+ }
+ return;
+ }
+
+ const pillEl = measureRef.current.querySelector('[data-measure-pill]');
+ const pillWidth = pillEl?.offsetWidth ?? 0;
+
+ const triggerEl = triggerRowRef.current!;
+ const triggerStyle = getComputedStyle(triggerEl);
+ const paddingInline = parseFloat(triggerStyle.paddingInlineStart) + parseFloat(triggerStyle.paddingInlineEnd);
+ const gap = parseFloat(triggerStyle.gap) || parseFloat(triggerStyle.columnGap);
+
+ const iconEl = triggerEl.querySelector('[data-search-icon]')!;
+ const iconWidth = iconEl.offsetWidth;
+
+ const inputMinWidth = Math.floor(containerWidth * 0.2);
+
+ const tokenWidths = tokenEls.map(el => el.offsetWidth);
+ const totalAll = tokenWidths.reduce((s, w) => s + w, 0) + gap * Math.max(tokenWidths.length - 1, 0);
+ const budgetWithoutPill = containerWidth - paddingInline - iconWidth - inputMinWidth - gap;
+ const budgetWithPill = budgetWithoutPill - pillWidth - gap;
+
+ let newCount: number;
+ let newMaxWidth: number | undefined;
+
+ if (totalAll <= budgetWithoutPill) {
+ newCount = tokenCount;
+ newMaxWidth = undefined;
+ } else {
+ let used = 0;
+ let count = 0;
+ for (let i = tokenWidths.length - 1; i >= 0; i--) {
+ const cost = count === 0 ? tokenWidths[i] : gap + tokenWidths[i];
+ if (used + cost > budgetWithPill) {
+ break;
+ }
+ used += cost;
+ count++;
+ }
+ newCount = count;
+ newMaxWidth = budgetWithPill + gap + pillWidth;
+ }
+
+ if (newCount !== lastOverflowResultRef.current.count || newMaxWidth !== lastOverflowResultRef.current.maxWidth) {
+ lastOverflowResultRef.current = { count: newCount, maxWidth: newMaxWidth };
+ setVisibleCount(newCount);
+ setTokenListMaxWidth(newMaxWidth);
+ }
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [enabled, tokenCount, tokenContentKey, containerResizeCount]);
+
+ return { containerRef, visibleCount, tokenListMaxWidth };
+}
diff --git a/src/internal/styles/utils/mixins.scss b/src/internal/styles/utils/mixins.scss
index 350b06ee56..c9cd8eb2dc 100644
--- a/src/internal/styles/utils/mixins.scss
+++ b/src/internal/styles/utils/mixins.scss
@@ -10,6 +10,16 @@
inset-inline-start: -9999px !important;
}
+@mixin measurement-ghost {
+ position: absolute;
+ inset-block-start: 0;
+ inset-inline-start: 0;
+ block-size: 0;
+ overflow: hidden;
+ visibility: hidden;
+ pointer-events: none;
+}
+
@mixin text-wrapping {
// When using with Flexbox, a flex item has min-width set to "auto" by default, which
// prevents the text wrapping. We need to override the min-width by setting it to "0"
diff --git a/src/test-utils/dom/autosuggest/index.ts b/src/test-utils/dom/autosuggest/index.ts
index a90acb5dd6..5984083832 100644
--- a/src/test-utils/dom/autosuggest/index.ts
+++ b/src/test-utils/dom/autosuggest/index.ts
@@ -8,9 +8,11 @@ import InputWrapper from '../input';
import DropdownWrapper from '../internal/dropdown';
import OptionWrapper from '../internal/option';
import OptionsListWrapper from '../internal/options-list';
+import TokenWrapper from '../token';
import mainStyles from '../../../autosuggest/styles.selectors.js';
import dropdownStyles from '../../../dropdown/styles.selectors.js';
+import autosuggestInputTestUtilStyles from '../../../internal/components/autosuggest-input/test-classes/styles.selectors.js';
import dropdownStatusStyles from '../../../internal/components/dropdown-status/styles.selectors.js';
import footerStyles from '../../../internal/components/dropdown-status/styles.selectors.js';
import optionStyles from '../../../internal/components/option/styles.selectors.js';
@@ -177,4 +179,55 @@ export default class AutosuggestWrapper extends InputWrapper {
.fireEvent(new MouseEvent('mouseup', { bubbles: true }));
});
}
+
+ /**
+ * Returns the token trigger row — the container element that holds the overflow pill,
+ * visible tokens, and text input when the component is in tokens mode.
+ * Returns `null` when the component is not in tokens mode (the `tokens` prop is absent).
+ */
+ findTokenTrigger(): ElementWrapper | null {
+ return this.findByClassName(autosuggestInputTestUtilStyles['token-trigger']);
+ }
+
+ /**
+ * Returns all visible inline token pills. Only available when the component is in tokens mode
+ * (the `tokens` prop is provided).
+ */
+ findInlineTokens(): Array {
+ return this.findAllByClassName(autosuggestInputTestUtilStyles['token-list-item']).map(
+ (elementWrapper: ElementWrapper) =>
+ new TokenWrapper(elementWrapper.findComponent(`.${TokenWrapper.rootSelector}`, TokenWrapper)!.getElement())
+ );
+ }
+
+ /**
+ * Returns the inline token at a 1-based index. Only available when the component is in tokens mode
+ * (the `tokens` prop is provided).
+ *
+ * @param index 1-based index of the token to return.
+ */
+ findInlineToken(index: number): TokenWrapper | null {
+ return this.findComponent(
+ `.${autosuggestInputTestUtilStyles['token-list']} > li[data-token-index="${index}"] .${TokenWrapper.rootSelector}`,
+ TokenWrapper
+ );
+ }
+
+ /**
+ * Returns the +N overflow pill shown when tokens overflow the available width.
+ * Returns `null` when all tokens are visible or the component is not in tokens mode.
+ */
+ findOverflowPill(): ElementWrapper | null {
+ return this.find(
+ `.${autosuggestInputTestUtilStyles['token-list']} .${autosuggestInputTestUtilStyles['token-overflow-pill']}:not([data-measure-pill])`
+ );
+ }
+
+ /**
+ * Returns the overflow panel shown when the +N pill is clicked.
+ * Returns `null` when the overflow panel is closed.
+ */
+ findOverflowPanel(): ElementWrapper | null {
+ return this.findByClassName(autosuggestInputTestUtilStyles['overflow-panel']);
+ }
}
diff --git a/src/test-utils/dom/internal/autosuggest-input.ts b/src/test-utils/dom/internal/autosuggest-input.ts
index 96f69ab986..d582320490 100644
--- a/src/test-utils/dom/internal/autosuggest-input.ts
+++ b/src/test-utils/dom/internal/autosuggest-input.ts
@@ -1,16 +1,16 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
-import { ComponentWrapper } from '@cloudscape-design/test-utils-core/dom';
+import { ComponentWrapper, ElementWrapper } from '@cloudscape-design/test-utils-core/dom';
import InputWrapper from '../input';
import DropdownWrapper from './dropdown.js';
import dropdownStyles from '../../../dropdown/styles.selectors.js';
import inputStyles from '../../../input/styles.selectors.js';
-import styles from '../../../internal/components/autosuggest-input/styles.selectors.js';
+import testUtilStyles from '../../../internal/components/autosuggest-input/test-classes/styles.selectors.js';
export default class AutosuggestInputWrapper extends ComponentWrapper {
- static rootSelector = styles.root;
+ static rootSelector = testUtilStyles.root;
findInput(): InputWrapper {
return this.findComponent(`.${inputStyles['input-container']}`, InputWrapper)!;
@@ -19,4 +19,30 @@ export default class AutosuggestInputWrapper extends ComponentWrapper {
findDropdown(): DropdownWrapper {
return this.findComponent(`.${dropdownStyles.root}`, DropdownWrapper)!;
}
+
+ findTokenTrigger(): ElementWrapper | null {
+ return this.findByClassName(testUtilStyles['token-trigger']);
+ }
+
+ findInlineTokens(): Array {
+ const tokenList = this.findByClassName(testUtilStyles['token-list']);
+ if (!tokenList) {
+ return [];
+ }
+ return tokenList.findAll('span[role]');
+ }
+
+ findInlineToken(index: number): ElementWrapper | null {
+ return this.find(`.${testUtilStyles['token-list']} > li[data-token-index="${index}"] span[role]`);
+ }
+
+ findOverflowPill(): ElementWrapper | null {
+ return this.find(
+ `.${testUtilStyles['token-list']} .${testUtilStyles['token-overflow-pill']}:not([data-measure-pill])`
+ );
+ }
+
+ findOverflowPanel(): ElementWrapper | null {
+ return this.findByClassName(testUtilStyles['overflow-panel']);
+ }
}