-
Notifications
You must be signed in to change notification settings - Fork 0
Review 4329
#4329 feat(Tokenizer): split delimited text into separate tokens on type and paste (#4175) by AKnassa (bucket: contributor)
54cd11e3b1e13ef890baa5242f45cfe347e404c6
LOOP VERSION: 1.6.0 AUDIT RUBRIC: 1.13
LANE: full WHY: contributor PR adds public API and core runtime behavior; the exact-head visual job is pending/cancelled, so local browser evidence is required.
WHY 1: A free-text Tokenizer currently treats a, b, c as one candidate value instead of a pasted list.
WHY 2: People adding email/tag/name lists must manually split common clipboard formats or ship a single messy token.
WHY 3: Tokenizer exists to build a set of values quickly; list paste failing makes the main data-entry job slower and error-prone.
USER-FACING PROBLEM: A user pasting several tags into a creatable Tokenizer cannot turn the list into separate tokens without manual cleanup.
PROBLEM SEVERITY: harmful friction — #4175 states the common email/tag paste case, and warm main creates one token from the whole comma list after Enter.
VERDICT: clear
Tokenizer adds a delimiter config for free-text creation and rewrites edited input text before the query is stored. The same path handles typing, paste, and composition end, so a list can become a batch of created tokens while ordinary search and Enter-to-create stay intact. PowerSearch opts out because its saved string-list values may legitimately contain commas.
SOLUTION (4 decisions · ~255 runtime/helper lines of 1423 changed lines)
-
delimiterscontrols create-on-delimiter for Tokenizer. -
transformQuerylets BaseTypeahead run the rewrite before search/query state. -
TokenizerChange.items?carries multi-token create metadata while preservingitem. - PowerSearch sets
delimiters={[]}to preserve comma-containing saved filters.
BURDEN: medium — one public prop, one public callback prop on BaseTypeahead, one optional callback payload field, a pure parser helper, no effects/listeners/observers.
BURDEN MATCH: mostly proportionate — the stated problem is a common list-entry flow, but BaseTypeahead exposing transformQuery is API surface created for internal Tokenizer plumbing.
VERDICT: note — public API shape still needs maintainer judgement after the correctness block is fixed.
OWNER: Tokenizer owns free-text token creation; BaseTypeahead owns the private query input and paste/composition event boundary.
TIER 1: reuses BaseTypeahead, SearchSource, useAnnounce, useTranslator for existing single-token announcements.
TIER 2: no theme/size/status architecture change.
SEAMS: search source, hasCreate, hasEntriesOnFocus, disabledMessage/readOnly, PowerSearch string_list, docs/template stories.
BEHAVIOR UNIT: pure utility (delimiters.ts) plus inline Tokenizer callback — testable for splitting; BaseTypeahead transformQuery should be judged as public plumbing.
| seam | driven result |
|---|---|
hasCreate + default delimiter story |
Chromium paste produced three tokens and cleared the input. |
delimiters={[]} story |
Chromium typing Smith, John kept the comma in the input and created no split token before Enter. |
| custom delimiter story | Chromium semicolon paste produced two tokens and cleared the input. |
| focusable-disabled story | Chromium paste did not mutate the disabled tokenizer. |
| PowerSearch string_list | Focused test proves Acme, Inc. remains one prospective value. |
VERDICT: note — implementation owner is plausible, but the public BaseTypeahead prop should be reviewed as API surface.
Every Tokenizer hasCreate gets comma/newline create behavior by default. Builders with comma-containing values must opt out with delimiters={[]}; PowerSearch does so internally for saved string-list filters. Assistive-technology users hear the same live-region path as single create, and that path currently hardcodes the new batch announcement.
VERDICT: BLOCKS — non-English screen-reader users hear a new English batch announcement.
Smallest real call site:
<Tokenizer
label="Tags"
searchSource={emptySource}
value={tags}
onChange={setTags}
hasCreate
delimiters={[]}
/>| change | public? | class | doc'd? | verdict | |
|---|---|---|---|---|---|
+ |
`Tokenizer.delimiters?: ReadonlyArray | RegExp = [',', '\n', '\r']` | public via core barrel | creatable token entry; no landed sibling with this prop | en+zh+dense |
~ |
TokenizerChange<T>.create + items?: T[] |
public via core barrel | batch change metadata | en+zh+dense | ok shape, but API judgement rides with delimiter feature |
+ |
BaseTypeahead.transformQuery?: (nextQuery: string) => string |
public via Typeahead barrel/docs | no external caller; added for Tokenizer | en+zh/dense | needs maintainer API judgement |
OSSIFICATION: delimiters is the exact surface Cindy's issue proposed for this class, with [] as the comma-safe escape hatch. items? is backward-compatible and avoids N stale onChange calls. transformQuery is the costly line: because BaseTypeaheadProps is exported, Tokenizer's private editing hook becomes a general public mutation point unless renamed internal or otherwise accepted.
VERDICT: [Needs human judgement] — contributor-added public API surface, especially BaseTypeahead.transformQuery.
No theme targets, tokens, variables, or style contracts are added or removed. The only story style conflict from the refresh kept --color-text-secondary for the explanatory text.
VERDICT: clear
BEHAVIOR: yes, intentionally — hasCreate now splits comma/newline-delimited text by default; PowerSearch pins the old comma behavior with delimiters={[]}.
API: additive — new optional prop and optional change metadata, no removed or required field.
VISUAL: yes, intentional state result — one pasted list now renders several tokens instead of one candidate string; captured in exact-head Storybook.
THEME: no target/token change.
VERDICT: note — behavior change is the stated feature and has a patch changeset.
EFFECTS: zero added/changed Effects.
| Effect + deps | external system | why render/handler cannot do it | measured render cost | lifetime + cleanup | focused test |
|---|---|---|---|---|---|
| none | none | n/a | n/a | n/a | n/a |
RENDER: no added render-time loops beyond compiling delimiter pattern in useMemo; split work runs only on user edit/paste/composition.
LISTENERS/OBSERVERS: no new persistent listener/observer; paste and composition are React input handlers.
LAYOUT: no layout reads/writes.
BUNDLE: no dependency change; pure helper file only.
VERDICT: clear
VISUAL CHECK: manual frames required WHY: the acceptance behavior is visible and the exact-head visual-acceptance status was pending/cancelled; local Storybook captured the changed state.
| Before | After |
|---|---|
![]() |
![]() |
SENSOR RECEIPT: build 54cd11e3b1e13ef890baa5242f45cfe347e404c6, story core-tokenizer--creatable, theme neutral, color mode light, direction ltr, viewport 900x420@1, hover pointer, reduced motion false, target .astryx-tokenizer count 1, fonts loaded, zero page/Storybook errors. After state: tokens a@x.com, b@y.com, c@z.com; input empty; live region Added 3 items.
VERDICT: clear
REMEDY SEARCH: not triggered — no proven visual defect
Auto-covered: CI lint failed on the exact head for @astryx/no-hardcoded-i18n-string at Tokenizer.tsx:774 and :775.
Checked by me: Chromium shows the new multi-token path writes Added 3 items to the polite live region. Source shows single add/remove announcements use t('@astryx.tokenizer.tokenAdded'), but the batch branch hardcodes English.
Browser-needed: keyboard/paste visible behavior was driven in Chromium for create, custom delimiter, comma-safe opt-out, and focusable-disabled paste.
VERDICT: BLOCKS — non-English screen-reader users hear the new batch-created announcement in English.
| slot | verdict |
|---|---|
| PROBLEM | clear |
| SOLUTION | note — public API shape still needs maintainer judgement after the correctness block is fixed |
| ARCHITECTURE | note — BaseTypeahead public plumbing needs API review |
| IMPACT | BLOCKS — non-English AT users hear English |
| API | [Needs human judgement] — contributor-added public API surface |
| THEMING | clear |
| BREAKING | note — intended behavior change |
| PERFORMANCE | clear |
| VISUAL | clear |
| A11Y & I18N | BLOCKS — hardcoded live-region announcement |
GOAL: met — warm main turns the comma list into one candidate/token; exact-head Storybook turns it into three tokens and focused Vitest passes 190 tests.
DISPOSITION: hardcoded batch announcement → blocks now; API surface → human judgement after the correctness block; stale minQueryLength JSDoc → evidence not spent, can be fixed with the API/doc pass.
ADVICE: proven existing pattern — the same file already routes single add/remove announcements through useTranslator() keys.
AUTHOR CAN PROCEED: yes — route the batch announcement through a catalog key with ICU count and update the localization test.
WORST OUTCOME: “non-English screen-reader users hear the new batch-created announcement in English” → request changes
JUDGEMENT NEEDED: API — BaseTypeahead.transformQuery and Tokenizer.delimiters are new public surface, but the blocking i18n defect comes first.
request changes
- The batch-created announcement is hardcoded English
→ non-English screen-reader users hear
Added 3 itemsinstead of a localized live-region update · packages/core/src/Tokenizer/Tokenizer.tsx:774
Thanks, this does make the list-paste path work: local Storybook turns a@x.com, b@y.com, c@z.com into three tokens, and the focused Tokenizer/Typeahead/PowerSearch tests are green.
One thing blocks merge: the new batch announcement is hardcoded English, so screen-reader users in non-English locales still hear Added 3 items while the single add/remove path uses catalog strings. Could you route the batch message through useTranslator() with an ICU count key and update the localization test? If you'd rather talk it through with someone, we're in Discord.
[Reviewed by Robohands]
- packages/core/src/Tokenizer/Tokenizer.tsx:774 — This live-region string needs the catalog, like the single add/remove announcements.
-
TokenizerProps.minQueryLengthJSDoc still says Create waits below threshold, but current docs/behavior say Create remains available. -
BaseTypeahead.transformQueryis public becauseBaseTypeaheadPropsis exported fromTypeahead/index.ts. - Exact-head CI lint is red for the same hardcoded announce string.
TIME total 10m setup 1m fast-install 40s + one build 2s + Storybook ready 0s; warm main reused: yes reading 4m rules, PR intent, source/types/tests/call-sites/history/API radius (prep phase) measuring 0m focused Vitest 8s + Chromium Storybook 4s + warm-main comparison 2s writing 4m presentation, gate, wiki record waste 0m first screenshot sensor expected default theme/mode incorrectly, then corrected to neutral/light
- CI was still in progress except lint, which had already failed on the reported issue.
Not posted to the PR. Drafted read-only; gate 1 passed with no rewrite.

