@astryxdesign/core 0.7.7-canary.e629e7d → 0.7.7-canary.e68774f

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":3,"file":"PowerSearch.d.ts","sourceRoot":"","sources":["../../src/PowerSearch/PowerSearch.tsx"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"PowerSearch.d.ts","sourceRoot":"","sources":["../../src/PowerSearch/PowerSearch.tsx"],"names":[],"mappings":"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"}
@@ -20,6 +20,7 @@ import { TokenizerClearAllMessageContext } from "../Tokenizer/TokenizerClearAllM
20
20
  import { TokenizerItemRemovableContext } from "../Tokenizer/TokenizerItemRemovableContext.js";
21
21
  import { TypeaheadItem } from "../Typeahead/TypeaheadItem.js";
22
22
  import { Token } from "../Token/index.js";
23
+ import { TokenNamingContext } from "../Token/TokenNamingContext.js";
23
24
  import { Avatar } from "../Avatar/index.js";
24
25
  import { layerAnimations } from "../Layer/layerAnimations.stylex.js";
25
26
  import { Icon } from "../Icon/index.js";
@@ -41,7 +42,7 @@ import { useLocale } from "../i18n/useLocale.js";
41
42
  import { useMergedRefs } from "../hooks/useMergedRefs.js";
42
43
  import { registerEnglishFallback, useComponentTranslator } from "../i18n/useComponentTranslator.js";
43
44
  import english from "../i18n/generated-english/PowerSearch/PowerSearch.generated.js";
44
- import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
45
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
45
46
  registerEnglishFallback(english);
46
47
  // =============================================================================
47
48
  // Icon mapping for typeahead entries
@@ -90,45 +91,25 @@ function truncateString(value, limit) {
90
91
  function getEnumLabel(values, value) {
91
92
  return values.find(v => v.value === value)?.label ?? value;
92
93
  }
93
- function PowerSearchTokenValue({
94
- operatorValue,
95
- filterValue,
96
- maxLength,
97
- locale
98
- }) {
94
+
95
+ /**
96
+ * The visible text of a filter token's value. The token renders it, and the
97
+ * token's remove button and editor dialog are named by the same text.
98
+ */
99
+ function tokenValueText(operatorValue, filterValue, maxLength, locale) {
99
100
  switch (filterValue.type) {
100
101
  case 'empty':
101
102
  return null;
102
103
  case 'string':
103
- return /*#__PURE__*/_jsx("span", {
104
- ...{
105
- className: "x1lvx875"
106
- },
107
- children: truncateString(filterValue.value, maxLength)
108
- });
104
+ return truncateString(filterValue.value, maxLength);
109
105
  case 'integer':
110
106
  case 'float':
111
- return /*#__PURE__*/_jsx("span", {
112
- ...{
113
- className: "x1lvx875"
114
- },
115
- children: filterValue.value
116
- });
107
+ return String(filterValue.value);
117
108
  case 'enum':
118
109
  if (operatorValue.type === 'enum') {
119
- return /*#__PURE__*/_jsx("span", {
120
- ...{
121
- className: "x1lvx875"
122
- },
123
- children: truncateString(getEnumLabel(operatorValue.values, filterValue.value), maxLength)
124
- });
110
+ return truncateString(getEnumLabel(operatorValue.values, filterValue.value), maxLength);
125
111
  }
126
- return /*#__PURE__*/_jsx("span", {
127
- ...{
128
- className: "x1lvx875"
129
- },
130
- children: truncateString(filterValue.value, maxLength)
131
- });
112
+ return truncateString(filterValue.value, maxLength);
132
113
  case 'string_list':
133
114
  {
134
115
  const items = filterValue.value;
@@ -136,28 +117,13 @@ function PowerSearchTokenValue({
136
117
  return null;
137
118
  }
138
119
  if (items.length === 1) {
139
- return /*#__PURE__*/_jsx("span", {
140
- ...{
141
- className: "x1lvx875"
142
- },
143
- children: truncateString(items[0], maxLength)
144
- });
120
+ return truncateString(items[0], maxLength);
145
121
  }
146
122
  const totalLength = items.reduce((sum, s) => sum + s.length, 0);
147
123
  if (totalLength > maxLength) {
148
- return /*#__PURE__*/_jsxs("span", {
149
- ...{
150
- className: "x1lvx875"
151
- },
152
- children: [items.length, " items"]
153
- });
124
+ return `${items.length} items`;
154
125
  }
155
- return /*#__PURE__*/_jsx("span", {
156
- ...{
157
- className: "x1lvx875"
158
- },
159
- children: items.join(', ')
160
- });
126
+ return items.join(', ');
161
127
  }
162
128
  case 'enum_list':
163
129
  {
@@ -168,43 +134,18 @@ function PowerSearchTokenValue({
168
134
  if (operatorValue.type === 'enum_list') {
169
135
  const labels = items.map(v => getEnumLabel(operatorValue.values, v));
170
136
  if (labels.length === 1) {
171
- return /*#__PURE__*/_jsx("span", {
172
- ...{
173
- className: "x1lvx875"
174
- },
175
- children: truncateString(labels[0], maxLength)
176
- });
137
+ return truncateString(labels[0], maxLength);
177
138
  }
178
139
  const totalLength = labels.reduce((sum, s) => sum + s.length, 0);
179
140
  if (totalLength > maxLength) {
180
- return /*#__PURE__*/_jsxs("span", {
181
- ...{
182
- className: "x1lvx875"
183
- },
184
- children: [labels.length, " items"]
185
- });
141
+ return `${labels.length} items`;
186
142
  }
187
- return /*#__PURE__*/_jsx("span", {
188
- ...{
189
- className: "x1lvx875"
190
- },
191
- children: labels.join(', ')
192
- });
143
+ return labels.join(', ');
193
144
  }
194
145
  if (items.length === 1) {
195
- return /*#__PURE__*/_jsx("span", {
196
- ...{
197
- className: "x1lvx875"
198
- },
199
- children: truncateString(items[0], maxLength)
200
- });
146
+ return truncateString(items[0], maxLength);
201
147
  }
202
- return /*#__PURE__*/_jsxs("span", {
203
- ...{
204
- className: "x1lvx875"
205
- },
206
- children: [items.length, " items"]
207
- });
148
+ return `${items.length} items`;
208
149
  }
209
150
  case 'entity_list':
210
151
  {
@@ -213,89 +154,46 @@ function PowerSearchTokenValue({
213
154
  return null;
214
155
  }
215
156
  if (entities.length === 1) {
216
- return /*#__PURE__*/_jsx("span", {
217
- ...{
218
- className: "x1lvx875"
219
- },
220
- children: truncateString(entities[0].label, maxLength)
221
- });
157
+ return truncateString(entities[0].label, maxLength);
222
158
  }
223
159
  const totalLength = entities.reduce((sum, e) => sum + e.label.length, 0);
224
160
  if (totalLength > maxLength) {
225
- return /*#__PURE__*/_jsxs("span", {
226
- ...{
227
- className: "x1lvx875"
228
- },
229
- children: [entities.length, " items"]
230
- });
161
+ return `${entities.length} items`;
231
162
  }
232
- return /*#__PURE__*/_jsx("span", {
233
- ...{
234
- className: "x1lvx875"
235
- },
236
- children: entities.map(e => e.label).join(', ')
237
- });
163
+ return entities.map(e => e.label).join(', ');
238
164
  }
239
165
  case 'time':
240
- return /*#__PURE__*/_jsx("span", {
241
- ...{
242
- className: "x1lvx875"
243
- },
244
- children: filterValue.value
245
- });
166
+ return filterValue.value;
246
167
  case 'date_absolute':
247
- {
248
- const formatted = formatDateAbsoluteCompact(filterValue.unixSeconds, locale);
249
- return /*#__PURE__*/_jsx("span", {
250
- ...{
251
- className: "x1lvx875"
252
- },
253
- children: truncateString(formatted, maxLength)
254
- });
255
- }
168
+ return truncateString(formatDateAbsoluteCompact(filterValue.unixSeconds, locale), maxLength);
256
169
  case 'date_relative':
257
- return /*#__PURE__*/_jsx("span", {
258
- ...{
259
- className: "x1lvx875"
260
- },
261
- children: truncateString(filterValue.value, maxLength)
262
- });
170
+ return truncateString(filterValue.value, maxLength);
263
171
  case 'date_range':
264
- return /*#__PURE__*/_jsx("span", {
265
- ...{
266
- className: "x1lvx875"
267
- },
268
- children: "date range"
269
- });
172
+ return 'date range';
270
173
  case 'custom':
271
174
  if (operatorValue.type === 'custom') {
272
- return /*#__PURE__*/_jsx("span", {
273
- ...{
274
- className: "x1lvx875"
275
- },
276
- children: truncateString(operatorValue.getString(filterValue.value), maxLength)
277
- });
175
+ return truncateString(operatorValue.getString(filterValue.value), maxLength);
278
176
  }
279
- return /*#__PURE__*/_jsx("span", {
280
- ...{
281
- className: "x1lvx875"
282
- },
283
- children: filterValue.value
284
- });
177
+ return filterValue.value;
285
178
  case 'nested':
286
179
  {
287
180
  const count = filterValue.value.length;
288
- return /*#__PURE__*/_jsx("span", {
289
- ...{
290
- className: "x1lvx875"
291
- },
292
- children: count === 1 ? '1 filter' : `${count} filters`
293
- });
181
+ return count === 1 ? '1 filter' : `${count} filters`;
294
182
  }
295
183
  default:
296
184
  return null;
297
185
  }
298
186
  }
187
+ function PowerSearchTokenValue({
188
+ text
189
+ }) {
190
+ return /*#__PURE__*/_jsx("span", {
191
+ ...{
192
+ className: "x1lvx875"
193
+ },
194
+ children: text
195
+ });
196
+ }
299
197
 
300
198
  // =============================================================================
301
199
  // Types
@@ -309,6 +207,14 @@ function PowerSearchTokenValue({
309
207
  // Component
310
208
  // =============================================================================
311
209
 
210
+ /**
211
+ * Whether a press on a token with this operator commits the filter instead of
212
+ * opening its editor: an `empty` operator with no `empty` Editor override.
213
+ */
214
+ function commitsWithoutEditor(operator, componentOverrides) {
215
+ return operator?.value.type === 'empty' && componentOverrides?.empty?.Editor == null;
216
+ }
217
+
312
218
  /**
313
219
  * Structured filter bar where each token represents a filter (field + operator + value).
314
220
  *
@@ -423,6 +329,28 @@ export function PowerSearch({
423
329
  type: 'idle'
424
330
  });
425
331
 
332
+ // A default filter token's visible text: its label (field and operator)
333
+ // and its value. The token's remove button and editor dialog are named by
334
+ // the same text.
335
+ const tokenText = useCallback(filter => {
336
+ const field = config.getField(filter.field);
337
+ const operator = config.getOperator(filter.field, filter.operator);
338
+ const fieldLabel = field?.label ?? '';
339
+ const operatorLabel = operator ? resolveOperatorLabel(operator, t) : '';
340
+ const label = `${fieldLabel}: ${operatorLabel}`.trim();
341
+ const value = operator ? tokenValueText(operator.value, filter.value, Math.max(maxTokenLength - fieldLabel.length - operatorLabel.length, 10), locale) : null;
342
+ return {
343
+ label,
344
+ value,
345
+ full: value != null ? `${label} ${value}` : label
346
+ };
347
+ }, [config, maxTokenLength, t, locale]);
348
+ // The built-in token being edited. A token from a `components` override
349
+ // names itself, so its editor keeps the plain popover.
350
+ const editedFilter = popoverState.type === 'editing' ? filters[popoverState.filterIndex] : undefined;
351
+ const editedOperator = editedFilter != null ? config.getOperator(editedFilter.field, editedFilter.operator) : undefined;
352
+ const editedBuiltInFilter = editedOperator == null || componentOverrides?.[editedOperator.value.type]?.Token == null ? editedFilter : undefined;
353
+
426
354
  // Layer for positioning the edit popover anchored to the tokenizer
427
355
  const handleLayerHide = useCallback(() => {
428
356
  setPopoverStateRaw({
@@ -434,9 +362,13 @@ export function PowerSearch({
434
362
  hasLightDismiss: true,
435
363
  hasCloseButton: false,
436
364
  hasAutoFocus: false,
437
- // The popup's own listbox/menu content is the exposed semantics; focus
438
- // stays on the tokenizer input, so a modal dialog wrapper is incorrect.
439
- role: 'none'
365
+ // Adding: the popup's own listbox/menu content is the exposed semantics,
366
+ // and focus stays on the tokenizer input, so a dialog wrapper is
367
+ // incorrect. Editing a token: focus moves into the editor's controls, so
368
+ // the editor is a non-modal dialog named by the token's visible text.
369
+ role: editedBuiltInFilter != null ? 'dialog' : 'none',
370
+ isModal: false,
371
+ dialogLabel: editedBuiltInFilter != null ? tokenText(editedBuiltInFilter).full : undefined
440
372
  });
441
373
 
442
374
  // Wrapper that manages layer visibility and tokenizer focus alongside state
@@ -567,7 +499,7 @@ export function PowerSearch({
567
499
  // edit: commit it as that editor's save would, the way adding one does,
568
500
  // instead of opening it. A consumer's `empty` Editor override still opens.
569
501
  const operator = config.getOperator(filter.field, filter.operator);
570
- if (operator?.value.type === 'empty' && componentOverrides?.empty?.Editor == null) {
502
+ if (commitsWithoutEditor(operator, componentOverrides)) {
571
503
  const newFilters = [...filters];
572
504
  newFilters[index] = {
573
505
  field: filter.field,
@@ -652,13 +584,14 @@ export function PowerSearch({
652
584
  // Default token rendering
653
585
  const fieldLabel = field?.label ?? '';
654
586
  const operatorLabel = operator ? resolveOperatorLabel(operator, t) : '';
655
- const tokenLabel = `${fieldLabel}: ${operatorLabel}`.trim();
656
- const adjustedMaxLength = Math.max(maxTokenLength - fieldLabel.length - operatorLabel.length, 10);
657
- const valueContent = operator && filter ? /*#__PURE__*/_jsx(PowerSearchTokenValue, {
658
- operatorValue: operator.value,
659
- filterValue: filter.value,
660
- maxLength: adjustedMaxLength,
661
- locale: locale
587
+ const text = filter != null ? tokenText(filter) : {
588
+ label: `${fieldLabel}: ${operatorLabel}`.trim(),
589
+ value: null
590
+ };
591
+ const tokenLabel = text.label;
592
+ const fullText = text.value != null ? `${text.label} ${text.value}` : text.label;
593
+ const valueContent = text.value != null ? /*#__PURE__*/_jsx(PowerSearchTokenValue, {
594
+ text: text.value
662
595
  }) : undefined;
663
596
 
664
597
  // Show entity photo as token icon for single-entity filters
@@ -671,24 +604,38 @@ export function PowerSearch({
671
604
  size: 16
672
605
  });
673
606
  }
674
- return /*#__PURE__*/_jsx(Token, {
675
- label: tokenLabel,
676
- size: size,
677
- icon: tokenIcon,
678
- endContent: valueContent,
679
- onMouseDown: handleClick ? e => {
680
- // Keep a token press from entering Tokenizer's input-focus
681
- // redirect. Click still edits/removes; Tab entry is unchanged.
682
- e.preventDefault();
683
- } : undefined,
684
- onClick: handleClick ? e => {
685
- e.stopPropagation();
686
- handleClick();
687
- } : undefined,
688
- onRemove: handleRemove,
689
- isDisabled: isDisabled
607
+
608
+ // A token that opens its editor says that it opens a dialog, and the
609
+ // naming context names it by all its visible text.
610
+ const editorProps = handleClick != null && !commitsWithoutEditor(operator, componentOverrides) ? {
611
+ 'aria-haspopup': 'dialog',
612
+ 'aria-expanded': popoverState.type === 'editing' && popoverState.filterIndex === filterIndex
613
+ } : null;
614
+ return /*#__PURE__*/_jsx(TokenNamingContext, {
615
+ value: {
616
+ actionName: fullText,
617
+ removeName: fullText
618
+ },
619
+ children: /*#__PURE__*/_jsx(Token, {
620
+ label: tokenLabel,
621
+ size: size,
622
+ icon: tokenIcon,
623
+ ...editorProps,
624
+ endContent: valueContent,
625
+ onMouseDown: handleClick ? e => {
626
+ // Keep a token press from entering Tokenizer's input-focus
627
+ // redirect. Click still edits/removes; Tab entry is unchanged.
628
+ e.preventDefault();
629
+ } : undefined,
630
+ onClick: handleClick ? e => {
631
+ e.stopPropagation();
632
+ handleClick();
633
+ } : undefined,
634
+ onRemove: handleRemove,
635
+ isDisabled: isDisabled
636
+ })
690
637
  });
691
- }, [filters, config, configProp, maxTokenLength, locale, size, isReadOnly, isDisabled, handleTokenClick, componentOverrides, t]);
638
+ }, [filters, config, configProp, maxTokenLength, size, isReadOnly, isDisabled, handleTokenClick, componentOverrides, t, popoverState, tokenText]);
692
639
 
693
640
  // Custom typeahead item renderer — adds field icon and description
694
641
  const renderItem = useCallback(item => {
@@ -12,7 +12,7 @@
12
12
  * - /apps/storybook/stories/Token.stories.tsx (storybook stories)
13
13
  * - /packages/cli/assets/templates/blocks/components/Token/ (showcase blocks)
14
14
  */
15
- import type { ReactNode } from 'react';
15
+ import { type ReactNode } from 'react';
16
16
  import type { BaseProps } from '../BaseProps.js';
17
17
  import type { TokenColorMap } from './index.js';
18
18
  /**
@@ -95,7 +95,7 @@ export interface TokenProps extends BaseProps<HTMLElement> {
95
95
  * <Token label="Link" href="/path" />
96
96
  * ```
97
97
  */
98
- export declare function Token({ label, size, color, icon, isDisabled, onRemove, onClick, href, description, endContent, isLabelHidden, xstyle, className, style, 'data-testid': testId, 'aria-labelledby': ariaLabelledBy, 'aria-describedby': ariaDescribedBy, ref, ...rest }: TokenProps): import("react").JSX.Element;
98
+ export declare function Token({ label, size, color, icon, isDisabled, onRemove, onClick, href, description, endContent, isLabelHidden, xstyle, className, style, 'data-testid': testId, 'aria-labelledby': ariaLabelledBy, 'aria-describedby': ariaDescribedBy, 'aria-haspopup': ariaHasPopup, 'aria-expanded': ariaExpanded, ref, ...rest }: TokenProps): import("react").JSX.Element;
99
99
  export declare namespace Token {
100
100
  var displayName: string;
101
101
  }
@@ -1 +1 @@
1
- {"version":3,"file":"Token.d.ts","sourceRoot":"","sources":["../../src/Token/Token.tsx"],"names":[],"mappings":"AAIA;;;;;;;;;;;;;GAaG;AAEH,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,OAAO,CAAC;AAiBrC,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,cAAc,CAAC;AAK5C,OAAO,KAAK,EAAC,aAAa,EAAC,MAAM,SAAS,CAAC;AAc3C;;;GAGG;AACH,MAAM,MAAM,UAAU,GAAG,MAAM,aAAa,CAAC;AAE7C,MAAM,MAAM,SAAS,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,CAAC;AAE3C,MAAM,WAAW,UAAW,SAAQ,SAAS,CAAC,WAAW,CAAC;IACxD,wCAAwC;IACxC,GAAG,CAAC,EAAE,KAAK,CAAC,GAAG,CAAC,WAAW,CAAC,CAAC;IAC7B;;OAEG;IACH,KAAK,EAAE,MAAM,CAAC;IACd;;;OAGG;IACH,IAAI,CAAC,EAAE,SAAS,CAAC;IACjB;;;OAGG;IACH,KAAK,CAAC,EAAE,UAAU,CAAC;IACnB;;OAEG;IACH,IAAI,CAAC,EAAE,SAAS,CAAC;IACjB;;;OAGG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB;;;OAGG;IACH,QAAQ,CAAC,EAAE,CAAC,CAAC,EAAE,KAAK,CAAC,UAAU,KAAK,IAAI,CAAC;IACzC;;;OAGG;IACH,OAAO,CAAC,EAAE,CAAC,CAAC,EAAE,KAAK,CAAC,UAAU,KAAK,IAAI,CAAC;IACxC;;OAEG;IACH,IAAI,CAAC,EAAE,MAAM,CAAC;IACd;;OAEG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;OAEG;IACH,UAAU,CAAC,EAAE,SAAS,CAAC;IACvB;;;OAGG;IACH,aAAa,CAAC,EAAE,OAAO,CAAC;CACzB;AA6JD;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,KAAK,CAAC,EACpB,KAAK,EACL,IAAW,EACX,KAAiB,EACjB,IAAI,EACJ,UAAkB,EAClB,QAAQ,EACR,OAAO,EACP,IAAI,EACJ,WAAW,EACX,UAAU,EACV,aAAqB,EACrB,MAAM,EACN,SAAS,EACT,KAAK,EACL,aAAa,EAAE,MAAM,EACrB,iBAAiB,EAAE,cAAc,EACjC,kBAAkB,EAAE,eAAe,EACnC,GAAG,EACH,GAAG,IAAI,EACR,EAAE,UAAU,+BAwQZ;yBA5Re,KAAK"}
1
+ {"version":3,"file":"Token.d.ts","sourceRoot":"","sources":["../../src/Token/Token.tsx"],"names":[],"mappings":"AAIA;;;;;;;;;;;;;GAaG;AAEH,OAAO,EAAM,KAAK,SAAS,EAAC,MAAM,OAAO,CAAC;AAiB1C,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,cAAc,CAAC;AAK5C,OAAO,KAAK,EAAC,aAAa,EAAC,MAAM,SAAS,CAAC;AAe3C;;;GAGG;AACH,MAAM,MAAM,UAAU,GAAG,MAAM,aAAa,CAAC;AAE7C,MAAM,MAAM,SAAS,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,CAAC;AAE3C,MAAM,WAAW,UAAW,SAAQ,SAAS,CAAC,WAAW,CAAC;IACxD,wCAAwC;IACxC,GAAG,CAAC,EAAE,KAAK,CAAC,GAAG,CAAC,WAAW,CAAC,CAAC;IAC7B;;OAEG;IACH,KAAK,EAAE,MAAM,CAAC;IACd;;;OAGG;IACH,IAAI,CAAC,EAAE,SAAS,CAAC;IACjB;;;OAGG;IACH,KAAK,CAAC,EAAE,UAAU,CAAC;IACnB;;OAEG;IACH,IAAI,CAAC,EAAE,SAAS,CAAC;IACjB;;;OAGG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB;;;OAGG;IACH,QAAQ,CAAC,EAAE,CAAC,CAAC,EAAE,KAAK,CAAC,UAAU,KAAK,IAAI,CAAC;IACzC;;;OAGG;IACH,OAAO,CAAC,EAAE,CAAC,CAAC,EAAE,KAAK,CAAC,UAAU,KAAK,IAAI,CAAC;IACxC;;OAEG;IACH,IAAI,CAAC,EAAE,MAAM,CAAC;IACd;;OAEG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;OAEG;IACH,UAAU,CAAC,EAAE,SAAS,CAAC;IACvB;;;OAGG;IACH,aAAa,CAAC,EAAE,OAAO,CAAC;CACzB;AA6JD;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,KAAK,CAAC,EACpB,KAAK,EACL,IAAW,EACX,KAAiB,EACjB,IAAI,EACJ,UAAkB,EAClB,QAAQ,EACR,OAAO,EACP,IAAI,EACJ,WAAW,EACX,UAAU,EACV,aAAqB,EACrB,MAAM,EACN,SAAS,EACT,KAAK,EACL,aAAa,EAAE,MAAM,EACrB,iBAAiB,EAAE,cAAc,EACjC,kBAAkB,EAAE,eAAe,EACnC,eAAe,EAAE,YAAY,EAC7B,eAAe,EAAE,YAAY,EAC7B,GAAG,EACH,GAAG,IAAI,EACR,EAAE,UAAU,+BA8QZ;yBApSe,KAAK"}
@@ -16,6 +16,7 @@
16
16
  * - /apps/storybook/stories/Token.stories.tsx (storybook stories)
17
17
  * - /packages/cli/assets/templates/blocks/components/Token/ (showcase blocks)
18
18
  */
19
+ import { use } from 'react';
19
20
  import * as stylex from '@stylexjs/stylex';
20
21
  import "../theme/tokens.stylex.js";
21
22
  import { colorVars, spacingVars, radiusVars, sizeVars, durationVars, easeVars, fontWeightVars, typeScaleVars } from "../theme/tokens.stylex.js";
@@ -29,6 +30,7 @@ import { focusOutlineProps } from "../utils/focusOutline.stylex.js";
29
30
  import { interactionOverlayStyles } from "../utils/interactionOverlay.stylex.js";
30
31
  import { usePressFeedback } from "../hooks/usePressFeedback.js";
31
32
  import { disabledInteractionProps } from "../utils/disabledInteraction.js";
33
+ import { TokenNamingContext } from "./TokenNamingContext.js";
32
34
  import { registerEnglishFallback, useComponentTranslator } from "../i18n/useComponentTranslator.js";
33
35
  import english from "../i18n/generated-english/Token/Token.generated.js";
34
36
  import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
@@ -216,6 +218,8 @@ export function Token({
216
218
  'data-testid': testId,
217
219
  'aria-labelledby': ariaLabelledBy,
218
220
  'aria-describedby': ariaDescribedBy,
221
+ 'aria-haspopup': ariaHasPopup,
222
+ 'aria-expanded': ariaExpanded,
219
223
  ref,
220
224
  ...rest
221
225
  }) {
@@ -230,6 +234,7 @@ export function Token({
230
234
  onClick,
231
235
  isDisabled
232
236
  });
237
+ const naming = use(TokenNamingContext);
233
238
 
234
239
  // When role is 'button' via context (no explicit onClick), treat as
235
240
  // if onClick was provided — the popover attaches its own handler.
@@ -237,7 +242,7 @@ export function Token({
237
242
  const removeButton = onRemove != null && /*#__PURE__*/_jsx("button", {
238
243
  type: "button",
239
244
  "aria-label": t('@astryx.token.remove', {
240
- label
245
+ label: naming?.removeName ?? label
241
246
  }),
242
247
  onClick: e => {
243
248
  e.stopPropagation();
@@ -268,12 +273,14 @@ export function Token({
268
273
  }), endContent, removeButton]
269
274
  });
270
275
 
271
- // Naming inputs reach the element that owns the token's action: the inner
272
- // button of a clickable token, otherwise the root
276
+ // Naming and popup inputs reach the element that owns the token's action:
277
+ // the inner button of a clickable token, otherwise the root
273
278
  // (architecture:public-component-api INV5).
274
279
  const labellingProps = {
275
280
  'aria-labelledby': ariaLabelledBy,
276
- 'aria-describedby': ariaDescribedBy
281
+ 'aria-describedby': ariaDescribedBy,
282
+ 'aria-haspopup': ariaHasPopup,
283
+ 'aria-expanded': ariaExpanded
277
284
  };
278
285
  const sharedProps = {
279
286
  'data-testid': testId,
@@ -398,6 +405,7 @@ export function Token({
398
405
  children: [icon, /*#__PURE__*/_jsx("button", {
399
406
  type: "button",
400
407
  onClick: effectiveOnClick,
408
+ "aria-label": ariaLabelledBy == null ? naming?.actionName : undefined,
401
409
  ...labellingProps,
402
410
  "aria-disabled": isDisabled || undefined,
403
411
  ...(isDisabled ? disabledInteractionProps() : null),
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Names for a Token whose visible text runs past its label. PowerSearch
3
+ * provides them: its tokens show the value as `endContent`.
4
+ *
5
+ * - `actionName` names a clickable token's inner button in place of `label`.
6
+ * - `removeName` names the remove button in place of `label`. The remove word
7
+ * still comes from the catalog, so each locale keeps its word order.
8
+ */
9
+ export interface TokenNaming {
10
+ actionName?: string;
11
+ removeName?: string;
12
+ }
13
+ export declare const TokenNamingContext: import("react").Context<TokenNaming | null>;
14
+ //# sourceMappingURL=TokenNamingContext.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"TokenNamingContext.d.ts","sourceRoot":"","sources":["../../src/Token/TokenNamingContext.ts"],"names":[],"mappings":"AAaA;;;;;;;GAOG;AACH,MAAM,WAAW,WAAW;IAC1B,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,eAAO,MAAM,kBAAkB,6CAA0C,CAAC"}
@@ -0,0 +1,23 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ 'use client';
4
+
5
+ /**
6
+ * @file TokenNamingContext.ts
7
+ * @input Naming parts from a caller whose token value renders outside its label
8
+ * @output Internal context: the names of a token's action and remove button
9
+ * @position Internal to Core; not exported from the Token entry point
10
+ */
11
+ import { createContext } from 'react';
12
+
13
+ /**
14
+ * Names for a Token whose visible text runs past its label. PowerSearch
15
+ * provides them: its tokens show the value as `endContent`.
16
+ *
17
+ * - `actionName` names a clickable token's inner button in place of `label`.
18
+ * - `removeName` names the remove button in place of `label`. The remove word
19
+ * still comes from the catalog, so each locale keeps its word order.
20
+ */
21
+
22
+ export const TokenNamingContext = /*#__PURE__*/createContext(null);
23
+ TokenNamingContext.displayName = 'TokenNamingContext';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@astryxdesign/core",
3
- "version": "0.7.7-canary.e629e7d",
3
+ "version": "0.7.7-canary.e68774f",
4
4
  "displayName": "Astryx Core",
5
5
  "description": "The component library. Accessible, themeable React components with built-in spacing, dark mode, and StyleX styling.",
6
6
  "author": "Meta Open Source",
@@ -865,9 +865,9 @@
865
865
  "react-dom": ">=19.0.0"
866
866
  },
867
867
  "devDependencies": {
868
- "@astryxdesign/a11y-spec": "0.7.7-canary.e629e7d",
869
- "@astryxdesign/cli": "0.7.7-canary.e629e7d",
870
- "@astryxdesign/interaction-harness": "0.7.7-canary.e629e7d",
868
+ "@astryxdesign/a11y-spec": "0.7.7-canary.e68774f",
869
+ "@astryxdesign/cli": "0.7.7-canary.e68774f",
870
+ "@astryxdesign/interaction-harness": "0.7.7-canary.e68774f",
871
871
  "@babel/cli": "^7.29.7",
872
872
  "@babel/core": "^7.29.7",
873
873
  "@babel/preset-react": "^7.29.7",
@@ -34,6 +34,7 @@ import {TokenizerClearAllMessageContext} from '../Tokenizer/TokenizerClearAllMes
34
34
  import {TokenizerItemRemovableContext} from '../Tokenizer/TokenizerItemRemovableContext';
35
35
  import {TypeaheadItem} from '../Typeahead/TypeaheadItem';
36
36
  import {Token} from '../Token';
37
+ import {TokenNamingContext} from '../Token/TokenNamingContext';
37
38
  import {Avatar} from '../Avatar';
38
39
  import {layerAnimations} from '../Layer/layerAnimations.stylex';
39
40
  import {Icon} from '../Icon';
@@ -75,6 +76,7 @@ import type {
75
76
  OperatorValue,
76
77
  EnumItem,
77
78
  PowerSearchComponents,
79
+ PowerSearchOperator,
78
80
  } from './types';
79
81
 
80
82
  import {useMergedRefs} from '../hooks/useMergedRefs';
@@ -158,52 +160,35 @@ function getEnumLabel(values: ReadonlyArray<EnumItem>, value: string): string {
158
160
  return values.find(v => v.value === value)?.label ?? value;
159
161
  }
160
162
 
161
- function PowerSearchTokenValue({
162
- operatorValue,
163
- filterValue,
164
- maxLength,
165
- locale,
166
- }: {
167
- operatorValue: OperatorValue;
168
- filterValue: FilterValue;
169
- maxLength: number;
170
- locale: Locale;
171
- }) {
163
+ /**
164
+ * The visible text of a filter token's value. The token renders it, and the
165
+ * token's remove button and editor dialog are named by the same text.
166
+ */
167
+ function tokenValueText(
168
+ operatorValue: OperatorValue,
169
+ filterValue: FilterValue,
170
+ maxLength: number,
171
+ locale: Locale,
172
+ ): string | null {
172
173
  switch (filterValue.type) {
173
174
  case 'empty':
174
175
  return null;
175
176
 
176
177
  case 'string':
177
- return (
178
- <span {...stylex.props(tokenValueStyles.value)}>
179
- {truncateString(filterValue.value, maxLength)}
180
- </span>
181
- );
178
+ return truncateString(filterValue.value, maxLength);
182
179
 
183
180
  case 'integer':
184
181
  case 'float':
185
- return (
186
- <span {...stylex.props(tokenValueStyles.value)}>
187
- {filterValue.value}
188
- </span>
189
- );
182
+ return String(filterValue.value);
190
183
 
191
184
  case 'enum':
192
185
  if (operatorValue.type === 'enum') {
193
- return (
194
- <span {...stylex.props(tokenValueStyles.value)}>
195
- {truncateString(
196
- getEnumLabel(operatorValue.values, filterValue.value),
197
- maxLength,
198
- )}
199
- </span>
186
+ return truncateString(
187
+ getEnumLabel(operatorValue.values, filterValue.value),
188
+ maxLength,
200
189
  );
201
190
  }
202
- return (
203
- <span {...stylex.props(tokenValueStyles.value)}>
204
- {truncateString(filterValue.value, maxLength)}
205
- </span>
206
- );
191
+ return truncateString(filterValue.value, maxLength);
207
192
 
208
193
  case 'string_list': {
209
194
  const items = filterValue.value;
@@ -211,25 +196,13 @@ function PowerSearchTokenValue({
211
196
  return null;
212
197
  }
213
198
  if (items.length === 1) {
214
- return (
215
- <span {...stylex.props(tokenValueStyles.value)}>
216
- {truncateString(items[0], maxLength)}
217
- </span>
218
- );
199
+ return truncateString(items[0], maxLength);
219
200
  }
220
201
  const totalLength = items.reduce((sum, s) => sum + s.length, 0);
221
202
  if (totalLength > maxLength) {
222
- return (
223
- <span {...stylex.props(tokenValueStyles.value)}>
224
- {items.length} items
225
- </span>
226
- );
203
+ return `${items.length} items`;
227
204
  }
228
- return (
229
- <span {...stylex.props(tokenValueStyles.value)}>
230
- {items.join(', ')}
231
- </span>
232
- );
205
+ return items.join(', ');
233
206
  }
234
207
 
235
208
  case 'enum_list': {
@@ -240,38 +213,18 @@ function PowerSearchTokenValue({
240
213
  if (operatorValue.type === 'enum_list') {
241
214
  const labels = items.map(v => getEnumLabel(operatorValue.values, v));
242
215
  if (labels.length === 1) {
243
- return (
244
- <span {...stylex.props(tokenValueStyles.value)}>
245
- {truncateString(labels[0], maxLength)}
246
- </span>
247
- );
216
+ return truncateString(labels[0], maxLength);
248
217
  }
249
218
  const totalLength = labels.reduce((sum, s) => sum + s.length, 0);
250
219
  if (totalLength > maxLength) {
251
- return (
252
- <span {...stylex.props(tokenValueStyles.value)}>
253
- {labels.length} items
254
- </span>
255
- );
220
+ return `${labels.length} items`;
256
221
  }
257
- return (
258
- <span {...stylex.props(tokenValueStyles.value)}>
259
- {labels.join(', ')}
260
- </span>
261
- );
222
+ return labels.join(', ');
262
223
  }
263
224
  if (items.length === 1) {
264
- return (
265
- <span {...stylex.props(tokenValueStyles.value)}>
266
- {truncateString(items[0], maxLength)}
267
- </span>
268
- );
225
+ return truncateString(items[0], maxLength);
269
226
  }
270
- return (
271
- <span {...stylex.props(tokenValueStyles.value)}>
272
- {items.length} items
273
- </span>
274
- );
227
+ return `${items.length} items`;
275
228
  }
276
229
 
277
230
  case 'entity_list': {
@@ -280,80 +233,42 @@ function PowerSearchTokenValue({
280
233
  return null;
281
234
  }
282
235
  if (entities.length === 1) {
283
- return (
284
- <span {...stylex.props(tokenValueStyles.value)}>
285
- {truncateString(entities[0].label, maxLength)}
286
- </span>
287
- );
236
+ return truncateString(entities[0].label, maxLength);
288
237
  }
289
238
  const totalLength = entities.reduce((sum, e) => sum + e.label.length, 0);
290
239
  if (totalLength > maxLength) {
291
- return (
292
- <span {...stylex.props(tokenValueStyles.value)}>
293
- {entities.length} items
294
- </span>
295
- );
240
+ return `${entities.length} items`;
296
241
  }
297
- return (
298
- <span {...stylex.props(tokenValueStyles.value)}>
299
- {entities.map(e => e.label).join(', ')}
300
- </span>
301
- );
242
+ return entities.map(e => e.label).join(', ');
302
243
  }
303
244
 
304
245
  case 'time':
305
- return (
306
- <span {...stylex.props(tokenValueStyles.value)}>
307
- {filterValue.value}
308
- </span>
309
- );
246
+ return filterValue.value;
310
247
 
311
- case 'date_absolute': {
312
- const formatted = formatDateAbsoluteCompact(
313
- filterValue.unixSeconds,
314
- locale,
248
+ case 'date_absolute':
249
+ return truncateString(
250
+ formatDateAbsoluteCompact(filterValue.unixSeconds, locale),
251
+ maxLength,
315
252
  );
316
- return (
317
- <span {...stylex.props(tokenValueStyles.value)}>
318
- {truncateString(formatted, maxLength)}
319
- </span>
320
- );
321
- }
322
253
 
323
254
  case 'date_relative':
324
- return (
325
- <span {...stylex.props(tokenValueStyles.value)}>
326
- {truncateString(filterValue.value, maxLength)}
327
- </span>
328
- );
255
+ return truncateString(filterValue.value, maxLength);
329
256
 
330
257
  case 'date_range':
331
- return <span {...stylex.props(tokenValueStyles.value)}>date range</span>;
258
+ return 'date range';
332
259
 
333
260
  case 'custom':
334
261
  if (operatorValue.type === 'custom') {
335
- return (
336
- <span {...stylex.props(tokenValueStyles.value)}>
337
- {truncateString(
338
- operatorValue.getString(filterValue.value),
339
- maxLength,
340
- )}
341
- </span>
262
+ return truncateString(
263
+ operatorValue.getString(filterValue.value),
264
+ maxLength,
342
265
  );
343
266
  }
344
- return (
345
- <span {...stylex.props(tokenValueStyles.value)}>
346
- {filterValue.value}
347
- </span>
348
- );
267
+ return filterValue.value;
349
268
 
350
269
  case 'nested': {
351
270
  const count = filterValue.value.length;
352
- return (
353
- <span {...stylex.props(tokenValueStyles.value)}>
354
- {count === 1 ? '1 filter' : `${count} filters`}
355
- </span>
356
- );
271
+ return count === 1 ? '1 filter' : `${count} filters`;
357
272
  }
358
273
 
359
274
  default:
@@ -361,6 +276,10 @@ function PowerSearchTokenValue({
361
276
  }
362
277
  }
363
278
 
279
+ function PowerSearchTokenValue({text}: {text: string}) {
280
+ return <span {...stylex.props(tokenValueStyles.value)}>{text}</span>;
281
+ }
282
+
364
283
  // =============================================================================
365
284
  // Types
366
285
  // =============================================================================
@@ -491,6 +410,20 @@ type PopoverState =
491
410
  // Component
492
411
  // =============================================================================
493
412
 
413
+ /**
414
+ * Whether a press on a token with this operator commits the filter instead of
415
+ * opening its editor: an `empty` operator with no `empty` Editor override.
416
+ */
417
+ function commitsWithoutEditor(
418
+ operator: PowerSearchOperator | undefined,
419
+ componentOverrides: PowerSearchComponents | undefined,
420
+ ): boolean {
421
+ return (
422
+ operator?.value.type === 'empty' &&
423
+ componentOverrides?.empty?.Editor == null
424
+ );
425
+ }
426
+
494
427
  /**
495
428
  * Structured filter bar where each token represents a filter (field + operator + value).
496
429
  *
@@ -608,6 +541,47 @@ export function PowerSearch({
608
541
  type: 'idle',
609
542
  });
610
543
 
544
+ // A default filter token's visible text: its label (field and operator)
545
+ // and its value. The token's remove button and editor dialog are named by
546
+ // the same text.
547
+ const tokenText = useCallback(
548
+ (filter: PowerSearchFilter) => {
549
+ const field = config.getField(filter.field);
550
+ const operator = config.getOperator(filter.field, filter.operator);
551
+ const fieldLabel = field?.label ?? '';
552
+ const operatorLabel = operator ? resolveOperatorLabel(operator, t) : '';
553
+ const label = `${fieldLabel}: ${operatorLabel}`.trim();
554
+ const value = operator
555
+ ? tokenValueText(
556
+ operator.value,
557
+ filter.value,
558
+ Math.max(
559
+ maxTokenLength - fieldLabel.length - operatorLabel.length,
560
+ 10,
561
+ ),
562
+ locale,
563
+ )
564
+ : null;
565
+ return {label, value, full: value != null ? `${label} ${value}` : label};
566
+ },
567
+ [config, maxTokenLength, t, locale],
568
+ );
569
+ // The built-in token being edited. A token from a `components` override
570
+ // names itself, so its editor keeps the plain popover.
571
+ const editedFilter =
572
+ popoverState.type === 'editing'
573
+ ? filters[popoverState.filterIndex]
574
+ : undefined;
575
+ const editedOperator =
576
+ editedFilter != null
577
+ ? config.getOperator(editedFilter.field, editedFilter.operator)
578
+ : undefined;
579
+ const editedBuiltInFilter =
580
+ editedOperator == null ||
581
+ componentOverrides?.[editedOperator.value.type]?.Token == null
582
+ ? editedFilter
583
+ : undefined;
584
+
611
585
  // Layer for positioning the edit popover anchored to the tokenizer
612
586
  const handleLayerHide = useCallback(() => {
613
587
  setPopoverStateRaw({type: 'idle'});
@@ -618,9 +592,16 @@ export function PowerSearch({
618
592
  hasLightDismiss: true,
619
593
  hasCloseButton: false,
620
594
  hasAutoFocus: false,
621
- // The popup's own listbox/menu content is the exposed semantics; focus
622
- // stays on the tokenizer input, so a modal dialog wrapper is incorrect.
623
- role: 'none',
595
+ // Adding: the popup's own listbox/menu content is the exposed semantics,
596
+ // and focus stays on the tokenizer input, so a dialog wrapper is
597
+ // incorrect. Editing a token: focus moves into the editor's controls, so
598
+ // the editor is a non-modal dialog named by the token's visible text.
599
+ role: editedBuiltInFilter != null ? 'dialog' : 'none',
600
+ isModal: false,
601
+ dialogLabel:
602
+ editedBuiltInFilter != null
603
+ ? tokenText(editedBuiltInFilter).full
604
+ : undefined,
624
605
  });
625
606
 
626
607
  // Wrapper that manages layer visibility and tokenizer focus alongside state
@@ -789,10 +770,7 @@ export function PowerSearch({
789
770
  // edit: commit it as that editor's save would, the way adding one does,
790
771
  // instead of opening it. A consumer's `empty` Editor override still opens.
791
772
  const operator = config.getOperator(filter.field, filter.operator);
792
- if (
793
- operator?.value.type === 'empty' &&
794
- componentOverrides?.empty?.Editor == null
795
- ) {
773
+ if (commitsWithoutEditor(operator, componentOverrides)) {
796
774
  const newFilters = [...filters];
797
775
  newFilters[index] = {
798
776
  field: filter.field,
@@ -897,20 +875,16 @@ export function PowerSearch({
897
875
  // Default token rendering
898
876
  const fieldLabel = field?.label ?? '';
899
877
  const operatorLabel = operator ? resolveOperatorLabel(operator, t) : '';
900
- const tokenLabel = `${fieldLabel}: ${operatorLabel}`.trim();
901
- const adjustedMaxLength = Math.max(
902
- maxTokenLength - fieldLabel.length - operatorLabel.length,
903
- 10,
904
- );
905
-
878
+ const text =
879
+ filter != null
880
+ ? tokenText(filter)
881
+ : {label: `${fieldLabel}: ${operatorLabel}`.trim(), value: null};
882
+ const tokenLabel = text.label;
883
+ const fullText =
884
+ text.value != null ? `${text.label} ${text.value}` : text.label;
906
885
  const valueContent =
907
- operator && filter ? (
908
- <PowerSearchTokenValue
909
- operatorValue={operator.value}
910
- filterValue={filter.value}
911
- maxLength={adjustedMaxLength}
912
- locale={locale}
913
- />
886
+ text.value != null ? (
887
+ <PowerSearchTokenValue text={text.value} />
914
888
  ) : undefined;
915
889
 
916
890
  // Show entity photo as token icon for single-entity filters
@@ -924,32 +898,49 @@ export function PowerSearch({
924
898
  tokenIcon = <Avatar src={entity.photo} name={entity.label} size={16} />;
925
899
  }
926
900
 
901
+ // A token that opens its editor says that it opens a dialog, and the
902
+ // naming context names it by all its visible text.
903
+ const editorProps =
904
+ handleClick != null &&
905
+ !commitsWithoutEditor(operator, componentOverrides)
906
+ ? {
907
+ 'aria-haspopup': 'dialog' as const,
908
+ 'aria-expanded':
909
+ popoverState.type === 'editing' &&
910
+ popoverState.filterIndex === filterIndex,
911
+ }
912
+ : null;
913
+
927
914
  return (
928
- <Token
929
- label={tokenLabel}
930
- size={size}
931
- icon={tokenIcon}
932
- endContent={valueContent}
933
- onMouseDown={
934
- handleClick
935
- ? e => {
936
- // Keep a token press from entering Tokenizer's input-focus
937
- // redirect. Click still edits/removes; Tab entry is unchanged.
938
- e.preventDefault();
939
- }
940
- : undefined
941
- }
942
- onClick={
943
- handleClick
944
- ? (e: React.MouseEvent) => {
945
- e.stopPropagation();
946
- handleClick();
947
- }
948
- : undefined
949
- }
950
- onRemove={handleRemove}
951
- isDisabled={isDisabled}
952
- />
915
+ <TokenNamingContext
916
+ value={{actionName: fullText, removeName: fullText}}>
917
+ <Token
918
+ label={tokenLabel}
919
+ size={size}
920
+ icon={tokenIcon}
921
+ {...editorProps}
922
+ endContent={valueContent}
923
+ onMouseDown={
924
+ handleClick
925
+ ? e => {
926
+ // Keep a token press from entering Tokenizer's input-focus
927
+ // redirect. Click still edits/removes; Tab entry is unchanged.
928
+ e.preventDefault();
929
+ }
930
+ : undefined
931
+ }
932
+ onClick={
933
+ handleClick
934
+ ? (e: React.MouseEvent) => {
935
+ e.stopPropagation();
936
+ handleClick();
937
+ }
938
+ : undefined
939
+ }
940
+ onRemove={handleRemove}
941
+ isDisabled={isDisabled}
942
+ />
943
+ </TokenNamingContext>
953
944
  );
954
945
  },
955
946
  [
@@ -957,13 +948,14 @@ export function PowerSearch({
957
948
  config,
958
949
  configProp,
959
950
  maxTokenLength,
960
- locale,
961
951
  size,
962
952
  isReadOnly,
963
953
  isDisabled,
964
954
  handleTokenClick,
965
955
  componentOverrides,
966
956
  t,
957
+ popoverState,
958
+ tokenText,
967
959
  ],
968
960
  );
969
961
 
@@ -17,7 +17,7 @@
17
17
  * - /packages/cli/assets/templates/blocks/components/Token/ (showcase blocks)
18
18
  */
19
19
 
20
- import type {ReactNode} from 'react';
20
+ import {use, type ReactNode} from 'react';
21
21
  import * as stylex from '@stylexjs/stylex';
22
22
  import {
23
23
  colorVars,
@@ -41,6 +41,7 @@ import {interactionOverlayStyles} from '../utils/interactionOverlay.stylex';
41
41
  import {usePressFeedback} from '../hooks/usePressFeedback';
42
42
  import type {TokenColorMap} from './index';
43
43
  import {disabledInteractionProps} from '../utils/disabledInteraction';
44
+ import {TokenNamingContext} from './TokenNamingContext';
44
45
  import {
45
46
  registerEnglishFallback,
46
47
  useComponentTranslator,
@@ -309,6 +310,8 @@ export function Token({
309
310
  'data-testid': testId,
310
311
  'aria-labelledby': ariaLabelledBy,
311
312
  'aria-describedby': ariaDescribedBy,
313
+ 'aria-haspopup': ariaHasPopup,
314
+ 'aria-expanded': ariaExpanded,
312
315
  ref,
313
316
  ...rest
314
317
  }: TokenProps) {
@@ -319,6 +322,7 @@ export function Token({
319
322
  const t = useComponentTranslator();
320
323
  const LinkComponent = useLinkComponent();
321
324
  const role = useInteractiveRole({href, onClick, isDisabled});
325
+ const naming = use(TokenNamingContext);
322
326
 
323
327
  // When role is 'button' via context (no explicit onClick), treat as
324
328
  // if onClick was provided — the popover attaches its own handler.
@@ -327,7 +331,9 @@ export function Token({
327
331
  const removeButton = onRemove != null && (
328
332
  <button
329
333
  type="button"
330
- aria-label={t('@astryx.token.remove', {label})}
334
+ aria-label={t('@astryx.token.remove', {
335
+ label: naming?.removeName ?? label,
336
+ })}
331
337
  onClick={e => {
332
338
  e.stopPropagation();
333
339
  onRemove(e);
@@ -352,12 +358,14 @@ export function Token({
352
358
  </>
353
359
  );
354
360
 
355
- // Naming inputs reach the element that owns the token's action: the inner
356
- // button of a clickable token, otherwise the root
361
+ // Naming and popup inputs reach the element that owns the token's action:
362
+ // the inner button of a clickable token, otherwise the root
357
363
  // (architecture:public-component-api INV5).
358
364
  const labellingProps = {
359
365
  'aria-labelledby': ariaLabelledBy,
360
366
  'aria-describedby': ariaDescribedBy,
367
+ 'aria-haspopup': ariaHasPopup,
368
+ 'aria-expanded': ariaExpanded,
361
369
  };
362
370
 
363
371
  const sharedProps = {
@@ -532,6 +540,7 @@ export function Token({
532
540
  <button
533
541
  type="button"
534
542
  onClick={effectiveOnClick}
543
+ aria-label={ariaLabelledBy == null ? naming?.actionName : undefined}
535
544
  {...labellingProps}
536
545
  aria-disabled={isDisabled || undefined}
537
546
  {...(isDisabled ? disabledInteractionProps() : null)}
@@ -0,0 +1,28 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ 'use client';
4
+
5
+ /**
6
+ * @file TokenNamingContext.ts
7
+ * @input Naming parts from a caller whose token value renders outside its label
8
+ * @output Internal context: the names of a token's action and remove button
9
+ * @position Internal to Core; not exported from the Token entry point
10
+ */
11
+
12
+ import {createContext} from 'react';
13
+
14
+ /**
15
+ * Names for a Token whose visible text runs past its label. PowerSearch
16
+ * provides them: its tokens show the value as `endContent`.
17
+ *
18
+ * - `actionName` names a clickable token's inner button in place of `label`.
19
+ * - `removeName` names the remove button in place of `label`. The remove word
20
+ * still comes from the catalog, so each locale keeps its word order.
21
+ */
22
+ export interface TokenNaming {
23
+ actionName?: string;
24
+ removeName?: string;
25
+ }
26
+
27
+ export const TokenNamingContext = createContext<TokenNaming | null>(null);
28
+ TokenNamingContext.displayName = 'TokenNamingContext';