@tristan2828/ui-foundation 3.30.0 → 3.30.1

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.
@@ -688,6 +688,12 @@ Save button, no Undo:
688
688
  | `choice`: a select, a switch, a single reference | the pick (its list opens with the field, non-modal) | closes the list with nothing picked: gives up |
689
689
  | `multi`: a multi reference | leaving it, or Ctrl/Cmd+Enter (Enter picks) | closes the list first; Esc again gives up |
690
690
 
691
+ In either multi picker (`MultiChoice`, `MultiReference`), on a form, in
692
+ a toolbar filter or in place, Escape never removes a pick: it closes the
693
+ list when it's open and leaves the value alone when it's closed (Base UI
694
+ would empty it). A pick comes off only by its chip's remove button,
695
+ Backspace in the empty input, or unpicking it in the list.
696
+
691
697
  Leaving means clicking or tabbing anywhere else. Unchanged closes with no
692
698
  request; a value the form's schema refuses never leaves the browser and
693
699
  shows the schema's message. Opening another value saves the open one
@@ -13,5 +13,17 @@ import * as React from 'react';
13
13
  import { Combobox, ComboboxChip, ComboboxChips, ComboboxChipsInput, ComboboxContent, ComboboxEmpty, ComboboxItem, ComboboxList, ComboboxValue, useComboboxAnchor, } from '../ui/combobox.js';
14
14
  export function MultiChoice({ options, value, onValueChange, getLabel = String, id, placeholder, 'aria-label': ariaLabel, 'aria-invalid': ariaInvalid, className, }) {
15
15
  const anchor = useComboboxAnchor();
16
- return (_jsxs(Combobox, { multiple: true, autoHighlight: true, items: options, value: value, onValueChange: (next) => onValueChange(next), itemToStringLabel: getLabel, children: [_jsx(ComboboxChips, { ref: anchor, className: className, children: _jsx(ComboboxValue, { children: (selected) => (_jsxs(React.Fragment, { children: [selected.map((option) => (_jsx(ComboboxChip, { children: getLabel(option) }, option))), _jsx(ComboboxChipsInput, { id: id, "aria-label": ariaLabel, "aria-invalid": ariaInvalid, placeholder: selected.length === 0 ? placeholder : undefined })] })) }) }), _jsxs(ComboboxContent, { anchor: anchor, children: [_jsx(ComboboxEmpty, { children: "No options found." }), _jsx(ComboboxList, { children: (option) => (_jsx(ComboboxItem, { value: option, children: getLabel(option) }, option)) })] })] }));
16
+ return (_jsxs(Combobox, { multiple: true, autoHighlight: true, items: options, value: value, onValueChange: (next, details) => {
17
+ // Base UI empties the value on Escape while the list is closed, so
18
+ // a second Escape after a pick wiped every chip (issue #123). Escape
19
+ // never removes picks here: a chip's remove button, Backspace or
20
+ // unpicking in the list does. Let the key go on, so a dialog or an
21
+ // edit in place gets its own Escape.
22
+ if (details.reason === 'escape-key') {
23
+ details.cancel();
24
+ details.allowPropagation();
25
+ return;
26
+ }
27
+ onValueChange(next);
28
+ }, itemToStringLabel: getLabel, children: [_jsx(ComboboxChips, { ref: anchor, className: className, children: _jsx(ComboboxValue, { children: (selected) => (_jsxs(React.Fragment, { children: [selected.map((option) => (_jsx(ComboboxChip, { children: getLabel(option) }, option))), _jsx(ComboboxChipsInput, { id: id, "aria-label": ariaLabel, "aria-invalid": ariaInvalid, placeholder: selected.length === 0 ? placeholder : undefined })] })) }) }), _jsxs(ComboboxContent, { anchor: anchor, children: [_jsx(ComboboxEmpty, { children: "No options found." }), _jsx(ComboboxList, { children: (option) => (_jsx(ComboboxItem, { value: option, children: getLabel(option) }, option)) })] })] }));
17
29
  }
@@ -27,7 +27,19 @@ export function MultiReference({ options, value, onValueChange, getLabel, onSear
27
27
  return (_jsxs(Combobox, { multiple: true, autoHighlight: true,
28
28
  // Ids, not {id,label} objects: Base UI compares items to the value
29
29
  // with Object.is, so the items must have the value's own shape.
30
- items: options.map((option) => option.id), value: value, onValueChange: (next) => onValueChange(next), onInputValueChange: onSearchChange, itemToStringLabel: label, filter: null, readOnly: readOnly, children: [_jsx(ComboboxChips, { ref: anchor, className: className, children: _jsx(ComboboxValue, { children: (selected) => (_jsxs(React.Fragment, { children: [selected.map((option) => (_jsx(ComboboxChip, { children: label(option) }, option))), _jsx(ComboboxChipsInput, { id: id, onKeyDown: (event) => {
30
+ items: options.map((option) => option.id), value: value, onValueChange: (next, details) => {
31
+ // Base UI empties the value on Escape while the list is closed, so
32
+ // a second Escape after a pick wiped every chip (issue #123). Escape
33
+ // never removes picks here: a chip's remove button, Backspace or
34
+ // unpicking in the list does. Let the key go on, so a dialog or an
35
+ // edit in place gets its own Escape.
36
+ if (details.reason === 'escape-key') {
37
+ details.cancel();
38
+ details.allowPropagation();
39
+ return;
40
+ }
41
+ onValueChange(next);
42
+ }, onInputValueChange: onSearchChange, itemToStringLabel: label, filter: null, readOnly: readOnly, children: [_jsx(ComboboxChips, { ref: anchor, className: className, children: _jsx(ComboboxValue, { children: (selected) => (_jsxs(React.Fragment, { children: [selected.map((option) => (_jsx(ComboboxChip, { children: label(option) }, option))), _jsx(ComboboxChipsInput, { id: id, onKeyDown: (event) => {
31
43
  if (searching && event.key === 'Enter') {
32
44
  event.preventDefault();
33
45
  event.preventBaseUIHandler();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tristan2828/ui-foundation",
3
- "version": "3.30.0",
3
+ "version": "3.30.1",
4
4
  "description": "Shared UI foundation for contract-first, database-backed React apps: shadcn primitives, DataTable/EntityForm composites, auth, the gateway error seam, design tokens, lint rules and the AI playbook.",
5
5
  "license": "MIT",
6
6
  "repository": {