Skip to content

feat: Add tokens mode with inline dismissible token pills - #5072

Open
srungta08 wants to merge 11 commits into
mainfrom
dev-v3-srungta-input-leading-content
Open

srungta08 wants to merge 11 commits into
mainfrom
dev-v3-srungta-input-leading-content

Conversation

@srungta08

@srungta08 srungta08 commented Sep 28, 2026 •

Copy link
Copy Markdown
Member

Description

Adds tokens mode to Autosuggest. When a tokens array is provided (including empty), accepted values display as inline dismissible token pills before the text input, with overflow into a +N pill that opens a focus-trapped portal dropdown.

New props:

  • tokens: ReadonlyArray<AutosuggestProps.Token> — activates tokens mode when provided (controlled)
  • i18nStrings.enteredTextLabel: (value: string) => string — replaces top-level enteredTextLabel; return Add "${value}" or Use: "${value}" based on your state
  • i18nStrings.tokenOverflowAriaLabel: (count: number) => string — accessible label for the +N overflow pill

Behaviour:

  • Selecting an item (Enter or dropdown click) adds it as a token
  • Tokens overflow into a +N pill; clicking opens a dropdown
  • Clear button (×) appears when input has typed text

Accessibility:

  • Keyboard: ArrowLeft/Right navigate between tokens and input; Backspace on empty input focuses last token; Backspace/Delete on a focused token dismisses it; focus returns to input after last token dismissed
  • Backspace focus uses imperative focusTokenAtIndex so it fires even when focusedTokenIndex hasn't changed (avoids React setState no-op)

Kick off Doc: https://chorus.aws.dev/doc/Op6bEowqHzMY/Kick-offScope-alignment-and-Design-SO-input-flexibility
API Proposal Doc: https://chorus.aws.dev/doc/qJ3Ttxk8R7CZ/API-Proposal-Autosuggest--Inline-Tokens-Mode

How has this been tested?

Unit tests (autosuggest-input-tokens.test.tsx) cover token rendering, add/dismiss, keyboard nav, overflow pill, ARIA, i18n resolution, and Backspace idempotency. Integration tests (autosuggest-tokens.test.ts) cover full add-token flows, overflow pill open/close/Escape, keyboard nav, and clear button.To review locally: run npm run start and open http://localhost:8080/autosuggest/tokens-mode. The interactive demo at the top lets you add/dismiss tokens and trigger overflow. Static scenario rows below cover all states.


By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.

@srungta08
srungta08 force-pushed the dev-v3-srungta-input-leading-content branch from e0dbbca to d241216 Compare September 28, 2026 18:21
@srungta08 srungta08 changed the title feat(autosuggest): Add tokens mode with inline dismissible token pills feat: Add tokens mode with inline dismissible token pills Sep 28, 2026
@codecov

codecov Bot commented Sep 28, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 97.58713% with 9 lines in your changes missing coverage. Please review.
✅ Project coverage is 97.71%. Comparing base (fdcc7a2) to head (a311d2f).
⚠️ Report is 2 commits behind head on main.

Files with missing lines Patch % Lines
...rc/internal/components/autosuggest-input/index.tsx 96.23% 5 Missing and 2 partials ⚠️
...components/autosuggest-input/use-token-overflow.ts 96.55% 2 Missing ⚠️
Additional details and impacted files
@@           Coverage Diff            @@
##             main    #5072    +/-   ##
========================================
  Coverage   97.70%   97.71%            
========================================
  Files         990      994     +4     
  Lines       31820    32170   +350     
  Branches    11754    11904   +150     
========================================
+ Hits        31091    31436   +345     
- Misses        683      686     +3     
- Partials       46       48     +2     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@srungta08
srungta08 force-pushed the dev-v3-srungta-input-leading-content branch from d241216 to d22f8cb Compare September 29, 2026 08:16
@srungta08

Copy link
Copy Markdown
Member Author

There will be a follow up PR for Visual Tests and Adding scenario to Control Segment Demo page

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Core overflow measurement, popup accessibility, localization, styling, and test-utils issues remain unresolved.

13 open findings
What changed in this PR

Adds controlled inline-token support to Autosuggest, including overflow handling, keyboard interaction, localization, test utilities, and demos.

Changes:

  • Adds dismissible tokens, overflow popup, focus management, and styling.
  • Extends Autosuggest APIs and localized strings.
  • Adds unit/integration coverage and test-utils selectors.
File Description
src/​test-utils/​dom/​internal/​autosuggest-input.ts Adds internal token selectors.
src/​test-utils/​dom/​autosuggest/​index.ts Exposes token test utilities.
src/​internal/​components/​autosuggest-input/​use-token-overflow.ts Measures token overflow.
src/​internal/​components/​autosuggest-input/​use-token-list-focus.ts Manages token focus.
src/​internal/​components/​autosuggest-input/​test-classes/​styles.scss Adds stable test classes.
src/​internal/​components/​autosuggest-input/​styles.scss Styles token mode and overflow.
src/​internal/​components/​autosuggest-input/​overflow-dropdown.tsx Implements overflow popup.
src/​internal/​components/​autosuggest-input/​index.tsx Integrates token-mode behavior.
src/​internal/​components/​autosuggest-input/​__tests__/​autosuggest-input-tokens.test.tsx Tests token input behavior.
src/​input/​styles.scss Supports borderless embedded input.
src/​input/​internal.tsx Adds internal borderless mode.
src/​i18n/​messages/​all.zh-TW.json Adds token-related translations.
src/​i18n/​messages/​all.zh-CN.json Adds token-related translations.
src/​i18n/​messages/​all.tr.json Adds token-related translations.
src/​i18n/​messages/​all.pt-BR.json Adds token-related translations.
src/​i18n/​messages/​all.ko.json Adds token-related translations.
src/​i18n/​messages/​all.ja.json Adds token-related translations.
src/​i18n/​messages/​all.it.json Adds token-related translations.
src/​i18n/​messages/​all.id.json Adds token-related translations.
src/​i18n/​messages/​all.fr.json Adds token-related translations.
src/​i18n/​messages/​all.es.json Adds token-related translations.
src/​i18n/​messages/​all.en.json Adds English token messages.
src/​i18n/​messages/​all.en-GB.json Adds token-related translations.
src/​i18n/​messages/​all.de.json Adds token-related translations.
src/​i18n/​messages-types.ts Types new message arguments.
src/​autosuggest/​options-controller.ts Accepts resolved entered-text labels.
src/​autosuggest/​internal.tsx Integrates tokens and i18n.
src/​autosuggest/​interfaces.ts Adds public token APIs.
src/​autosuggest/​__tests__/​options-controller.test.ts Tests entered-text labels.
src/​autosuggest/​__tests__/​i18n.test.tsx Tests token localization.
src/​autosuggest/​__tests__/​autosuggest.test.tsx Tests token selection events.
src/​autosuggest/​__tests__/​autosuggest-tokens-mode.test.tsx Tests public token mode.
src/​autosuggest/​__integ__/​page-objects/​autosuggest-tokens-page.ts Adds integration helpers.
src/​autosuggest/​__integ__/​autosuggest-tokens.test.ts Tests browser interactions.
src/​__tests__/​snapshot-tests/​__snapshots__/​test-utils-selectors.test.tsx.snap Updates selector snapshots.
src/​__tests__/​snapshot-tests/​__snapshots__/​documenter.test.ts.snap Updates API snapshots.
pages/​autosuggest/​tokens-mode.page.tsx Adds a token-mode demo.

🧠 Review effort: Balanced


Give feedback about Copilot approvals in this survey to enter a drawing for a $150 gift card.

Comment thread src/test-utils/dom/autosuggest/index.ts
Comment thread src/autosuggest/__integ__/page-objects/autosuggest-tokens-page.ts
Comment thread src/autosuggest/internal.tsx
Comment thread src/internal/components/autosuggest-input/index.tsx
Comment thread src/internal/components/autosuggest-input/index.tsx
Comment thread src/internal/components/autosuggest-input/overflow-dropdown.tsx
Comment thread src/internal/components/autosuggest-input/use-token-overflow.ts Outdated
Comment thread src/internal/components/autosuggest-input/use-token-overflow.ts Outdated
Comment thread pages/autosuggest/tokens-mode.page.tsx
Comment thread src/autosuggest/interfaces.ts

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Token styling, overflow state, popup accessibility, and localization have unresolved functional issues.

3 open findings
11 resolved since last review
Previously missed (5)

In code that hasn't changed since last review

Medium severity Add Arabic token-mode entered-text translation

src/​autosuggest/​internal.tsx:99

The Arabic autosuggest catalog was not given i18nStrings.enteredTextLabel, so token mode falls through to its legacy enteredTextLabel translation (“Use”) and labels the add-token action incorrectly. Add the missing Arabic token-mode message and avoid using the legacy key when isTokenMode is true.

Medium severity Add Arabic overflow label translation

src/​autosuggest/​internal.tsx:104

The Arabic catalog also lacks this new overflow key, so Arabic users receive the hardcoded English “N more items” label. Add the Arabic catalog entry and remove the English fallback in accordance with the repository i18n convention.

Medium severity Add autosuggest translation key to all locale catalogs

src/​autosuggest/​internal.tsx:109

Only the English autosuggest catalog defines this new key; the similarly named localized entries belong to prompt-input, a different component namespace. For every non-English locale this lookup returns undefined and the hardcoded English fallback is announced, violating the repository i18n rule. Add the autosuggest key to all locale catalogs and let the provider supply the default instead of embedding English here.

Medium severity Track icon identity in overflow measurement dependencies

src/​internal/​components/​autosuggest-input/​index.tsx:151

String interpolation does not track icon changes: React elements stringify to the same [object Object]. Replacing a token icon with a differently sized icon while preserving labels/count therefore leaves overflow measurements stale. Pass an identity-bearing dependency (such as the token array) to the measurement hook instead of serializing React nodes.

Medium severity Reset overflow state when all tokens become visible

src/​internal/​components/​autosuggest-input/​index.tsx:254

When a resize or controlled token update makes every token visible, the overflow panel unmounts but overflowOpen remains true. handleFocus then continues suppressing the suggestions dropdown even though no overflow UI exists. Reset overflowOpen whenever hiddenTokens becomes empty.

🧠 Review effort: Balanced

Comment thread src/internal/components/autosuggest-input/index.tsx
Comment thread src/internal/components/autosuggest-input/index.tsx Outdated
Comment thread src/internal/components/autosuggest-input/index.tsx
Comment thread src/internal/components/autosuggest-input/index.tsx

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

Token mode has unresolved accessibility, localization, disabled-state, styling, and overflow-layout defects.

0 open findings

3 resolved since last review
Previously missed (6)

In code that hasn't changed since last review

Medium severity Hardcoded English fallbacks break Autosuggest localization

src/​autosuggest/​internal.tsx:99

Cloudscape i18n convention 1000000 prohibits hardcoded English fallbacks. These fallbacks also mask incomplete catalogs: Arabic and Thai omit all three new Autosuggest keys, and every non-English Autosuggest catalog omits tokenInsertedAriaLabel, so those locales announce English text. Add every key to every locale and rely on useInternalI18n rather than these literals.

Medium severity Icon changes do not trigger measurement updates

src/​internal/​components/​autosuggest-input/​index.tsx:151

String interpolation does not distinguish React icon elements: different <Icon>/SVG nodes all stringify to [object Object]. Replacing a token icon with a differently sized icon while keeping the same labels/count therefore does not rerun measurement, leaving stale overflow layout. Depend on the token array/icon identity or observe the measurement elements instead.

This issue also appears on line 199 of the same file.

Medium severity Token validation errors omit aria-invalid

src/​internal/​components/​autosuggest-input/​index.tsx:682

Overriding invalid to false removes aria-invalid from the actual combobox; the outer group only receives invalid styling and has no equivalent ARIA state. Consequently, token-mode validation errors are not communicated to assistive technology. Suppress only the input's visual invalid treatment while preserving aria-invalid on the combobox.

Medium severity Extra gap incorrectly rejects tokens that fit

src/​internal/​components/​autosuggest-input/​use-token-overflow.ts:104

The first visible token is charged an extra gap even though budgetWithPill already reserves the gap between the pill and tokens. For example, a 48px token is rejected from a 50px token budget when the gap is 4px, so the component hides a token that actually fits.

Low severity Generated class substring used as integration selector

src/​autosuggest/​__integ__/​page-objects/​autosuggest-tokens-page.ts:15

Cloudscape SCSS/test-utils convention 1000003 forbids integration selectors based on generated class substrings. [class*="overflow-panel"] can match unrelated elements or break when styles are renamed; expose the panel through a test class and a static test-utils selector instead.

Low severity Documentation incorrectly limits enteredTextLabel to token mode

src/​autosuggest/​interfaces.ts:194

This says the whole object is token-mode-only, but i18nStrings.enteredTextLabel is intentionally resolved and tested in default mode as the replacement for the deprecated top-level prop. The public documentation would incorrectly tell default-mode consumers not to use the replacement.

🧠 Review effort: Balanced

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment thread src/autosuggest/__integ__/page-objects/autosuggest-tokens-page.ts
Comment thread src/autosuggest/internal.tsx
Comment thread src/autosuggest/internal.tsx
Comment thread src/autosuggest/internal.tsx
Comment thread src/i18n/messages/all.en.json
Comment on lines +206 to +207
const buildAnnouncementMessage = (label: string) =>
tokenInsertedAriaLabel ? tokenInsertedAriaLabel(label) : `${label} inserted`;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should remove hardcoded strings.

Comment thread src/internal/components/autosuggest-input/index.tsx
Comment thread src/internal/components/autosuggest-input/use-token-overflow.ts
Comment thread src/internal/components/autosuggest-input/use-token-overflow.ts
Comment thread src/internal/components/autosuggest-input/index.tsx
<li key={i} className={styles['overflow-panel-item']}>
<InternalToken
label={token.value}
dismissLabel={token.dismissLabel ?? token.value}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we default this to token.value?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just in case dismissLabel is not there

Comment thread src/autosuggest/interfaces.ts
const selectedValue = option.value || '';
if (isTokenMode && selectedValue) {
const currentTokens = tokens ?? [];
const updated = [...currentTokens, { value: selectedValue, dismissLabel: selectedValue }];

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We do we set dismissLabel to selectedValue?

/** Specifies the text value displayed in the token pill. */
value: string;
/** Adds an `aria-label` to the dismiss button. Defaults to `value` if not provided. */
dismissLabel?: string;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need explicit dismiss label here? Dismiss labels are meta-info, which can be supported with i18n, e.g.:

<Autosuggest
  tokens={[{ value: "first" }, { value: "second" }]}
  i18nStrings={{
    // Optional, we will inject default i18n when explicit func is not provided
    tokenDismissAriaLabel: (value) => `Remove token ${value}`
  }}
/>

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I missed this will fix

if (isTokenMode && selectedValue) {
const currentTokens = tokens ?? [];
const updated = [...currentTokens, { value: selectedValue, dismissLabel: selectedValue }];
autosuggestInputRef.current?.announceTokenAdded(selectedValue);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we use a reference? Can we use react state instead, e.g.

const [announcement, setAnnouncement] = useState({})

onSelectItem: () => {
  // ...
  setAnnouncement({ tokenAdded: selectedValue })
}

return <AutosuggestInput announcements={announcement} />


const { items, getItemGroup, getItemParent } = useMemo(() => createItems(options), [options]);

const enteredItemLabel = i18n('enteredTextLabel', enteredTextLabel?.(filterValue), format =>

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The useAutosuggestItems() is also used by the property filter, but the PR does not include any changes to property filter. Is this change safe?

Comment thread src/input/internal.tsx
@@ -0,0 +1,3308 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.

@pan-kot pan-kot Oct 9, 2026 •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A 3.3k lines of code in a single test file is too much: the tests can serve as code documentation when written well, but they should be discoverable. I recommend splitting these tests into multiple files - grouped by features that we test (e.g. tokens, tokens overflow, tokens keyboard nav., etc.), move the shared code to some common.tsx helper, and try to reduce the number of tests by avoiding duplication, unnecessary assertions, etc.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here is what my review agent says on this file:

  • The file is large because it was written to hit coverage lines rather than to check behaviour.
  • It was written in coverage passes. 67 test or describe titles cite source line numbers ("(lines 591, 595)"), and blocks are labelled "fourth-pass" and "FIFTH-PASS". The FIFTH-PASS block alone is about 670 lines. The line numbers are already stale: the code those tests target is now at lines 374–381.
  • Many tests check nothing. 64 of the 188 tests only assert that an element exists, and 14 create a jest.fn() that is never asserted. "mousedown outside the component closes the dropdown" checks that the combobox still exists.
  • Some tests pass without running their assertion. "overflow pill button has tabIndex=-1" wraps its expect in if (pill), and in practice pill is null, so nothing is checked. The source sets the pill to tabIndex={0}, so the assertion would fail if it ran. Four tests use expect(true).toBe(true) as a fallback.
  • The layout mocks are copied over and over. There are 10 overflow render helpers, and 6 of them are identical (token 120px, pill 40px, container 400px). Across the file there are 22 HTMLElement.prototype.offsetWidth overrides and 27 getComputedStyle mocks. One override, at line 3016, is never restored, so it leaks into every later test in the file.
  • It reaches into the DOM directly. It uses 206 querySelector calls, 68 hashed-class substring selectors ([class*="overflow-panel"]), and hand-built dispatchEvent(new KeyboardEvent…) calls. The PR already adds findInlineToken, findOverflowPill and findTokenTrigger to the test utils.

My rough estimate is 120–130 tests in about 900–1,100 lines, with coverage at or above today's. I haven't written the refactor, so treat the size as an estimate. Also, if the pill tabIndex test is fixed so it actually renders the overflow state, it will fail. The PR then has to decide whether the pill should be a tab stop: the source sets tabIndex={0}, while the test expects -1.

return { container, rerender, wrapper, onChange };
}

describe('FIFTH-PASS: reliable overflow-state tests', () => {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is FIFTH-PASS here?

style?: InputProps['style'];
tokens?: ReadonlyArray<AutosuggestProps.Token>;
tokenOverflowAriaLabel?: (hiddenCount: number) => string;
tokenInsertedAriaLabel?: (value: string) => string;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

let's use consistent terminology - "added" or "inserted". Right now it is not consistent: tokenInsertedAriaLabel, but announceTokenAdded

announceTokenAdded(label: string): void;
}

const AutosuggestInput = React.forwardRef(

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Autosuggest input is an internal component reused between autosuggest and property filter. The tokens feature is needed for autosuggest, but not for property filter - so why are we adding it here and not inside autosuggest? Can autosuggest input expose a slot for token and some props required to make responsiveness work?


const baseProps = getBaseProps(restProps);
const formFieldContext = useFormFieldContext(restProps);
const { invalid, warning, ariaLabelledby } = formFieldContext;

@pan-kot pan-kot Oct 9, 2026 •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we integrate with form field's ariaLabelledby as part of this change?

const preventCloseOnBlurRef = useRef(false);

const [triggerWidth, setTriggerWidth] = useState<number | null>(null);
const [overflowOpen, setOverflowOpen] = useState(false);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: the "overflowOpen" sounds generic - we can call it e.g. "tokensOverflowOpen" so that it cannot be mistaken with the options dropdown

if (!isTokenMode || !tokenListRef.current) {
return;
}
const dismissButtons = tokenListRef.current.querySelectorAll<HTMLButtonElement>('[data-token-item] button');

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we use the existing single tab stop navigation mechanism here instead? We use it in e.g. button group, tabs, and a few other components.

It might even be possible to use the public NavigableGroup components here: https://cloudscape.design/components/navigable-group/

This branch was successfully deployed

3 active deployments
dev-pages-react16 — a311d2fa Deployed Oct 8, 2026 by srungta08 via deploy (React 16) / deploy #2646
dev-pages-react18 — a311d2fa Deployed Oct 8, 2026 by srungta08 via deploy (React 18) / deploy #2646
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants