@tangible/ui 0.0.9 → 0.0.10
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.
|
@@ -152,8 +152,18 @@ function ComboboxRoot({ value: controlledValue, defaultValue, onValueChange, inp
|
|
|
152
152
|
role,
|
|
153
153
|
listNavigation,
|
|
154
154
|
]);
|
|
155
|
+
// Flag: first registerOption call after open clears stale entries.
|
|
156
|
+
const prevOpenForFlush = useRef(open);
|
|
157
|
+
const needsFlushRef = useRef(false);
|
|
158
|
+
if (open && !prevOpenForFlush.current)
|
|
159
|
+
needsFlushRef.current = true;
|
|
160
|
+
prevOpenForFlush.current = open;
|
|
155
161
|
// Register option
|
|
156
162
|
const registerOption = useCallback((option) => {
|
|
163
|
+
if (needsFlushRef.current) {
|
|
164
|
+
optionsRef.current.clear();
|
|
165
|
+
needsFlushRef.current = false;
|
|
166
|
+
}
|
|
157
167
|
optionsRef.current.set(toKey(option.value), option);
|
|
158
168
|
setRegistryVersion((v) => v + 1);
|
|
159
169
|
}, []);
|
|
@@ -166,14 +176,6 @@ function ComboboxRoot({ value: controlledValue, defaultValue, onValueChange, inp
|
|
|
166
176
|
optionsRef.current.delete(toKey(optionValue));
|
|
167
177
|
setRegistryVersion((v) => v + 1);
|
|
168
178
|
}, []);
|
|
169
|
-
// Flush stale registry on open. Options that were registered before close
|
|
170
|
-
// may no longer exist (parent changed children while closed). Clearing
|
|
171
|
-
// before the new options mount ensures no orphaned entries accumulate.
|
|
172
|
-
useLayoutEffect(() => {
|
|
173
|
-
if (open) {
|
|
174
|
-
optionsRef.current.clear();
|
|
175
|
-
}
|
|
176
|
-
}, [open]);
|
|
177
179
|
// Active option ID for aria-activedescendant (hash-based for stability during filtering)
|
|
178
180
|
const activeOptionId = activeIndex >= 0 && orderedOptions[activeIndex]
|
|
179
181
|
? `${listboxId}-opt-${hashForId(toKey(orderedOptions[activeIndex].value))}`
|
|
@@ -384,7 +386,7 @@ function ComboboxRoot({ value: controlledValue, defaultValue, onValueChange, inp
|
|
|
384
386
|
return (_jsx(ComboboxActionsContext.Provider, { value: actionsValue, children: _jsx(ComboboxStateContext.Provider, { value: stateValue, children: _jsxs("div", { className: "tui-combobox", children: [_jsxs("div", { className: "tui-combobox__input-wrapper", children: [_jsx("input", { ref: (node) => {
|
|
385
387
|
inputRef.current = node;
|
|
386
388
|
refs.setReference(node);
|
|
387
|
-
}, type: "text", id: inputId, className: cx('tui-combobox__input', size !== 'md' && `is-size-${size}`, inputClassName), role: "combobox", "aria-expanded": open, "aria-controls": listboxId, "aria-autocomplete": "list", "aria-activedescendant": activeOptionId, "aria-label": ariaLabel, "aria-labelledby": ariaLabelledBy, disabled: disabled, placeholder: placeholder, value: inputValue, autoComplete: "off", ...referenceProps, onChange: handleInputChange }), clearable && inputValue && !disabled && (_jsx("button", { type: "button", className: "tui-combobox__clear", onClick: handleClear, onMouseDown: (e) => e.preventDefault(), "aria-label": labels.clear, tabIndex: -1, children: _jsx(Icon, { name: "system/close", size: "sm" }) })), _jsx("span", { className: "tui-combobox__icon", "aria-hidden": "true", children: _jsx(Icon, { name: "system/chevron-down", size: "sm" }) })] }), children] }) }) }));
|
|
389
|
+
}, type: "text", id: inputId, className: cx('tui-combobox__input', size !== 'md' && `is-size-${size}`, inputClassName), role: "combobox", "aria-expanded": open, "aria-controls": open ? listboxId : undefined, "aria-autocomplete": "list", "aria-activedescendant": activeOptionId, "aria-label": ariaLabel, "aria-labelledby": ariaLabelledBy, disabled: disabled, placeholder: placeholder, value: inputValue, autoComplete: "off", ...referenceProps, onChange: handleInputChange }), clearable && inputValue && !disabled && (_jsx("button", { type: "button", className: "tui-combobox__clear", onClick: handleClear, onMouseDown: (e) => e.preventDefault(), "aria-label": labels.clear, tabIndex: -1, children: _jsx(Icon, { name: "system/close", size: "sm" }) })), _jsx("span", { className: "tui-combobox__icon", "aria-hidden": "true", children: _jsx(Icon, { name: "system/chevron-down", size: "sm" }) })] }), children] }) }) }));
|
|
388
390
|
}
|
|
389
391
|
ComboboxRoot.displayName = 'Combobox';
|
|
390
392
|
// =============================================================================
|
|
@@ -213,7 +213,17 @@ function MultiSelectRoot({ id: triggerIdProp, value: controlledValue, defaultVal
|
|
|
213
213
|
typeahead,
|
|
214
214
|
]);
|
|
215
215
|
// Register option
|
|
216
|
+
// Flag: first registerOption call after open clears stale entries.
|
|
217
|
+
const prevOpenForFlush = useRef(open);
|
|
218
|
+
const needsFlushRef = useRef(false);
|
|
219
|
+
if (open && !prevOpenForFlush.current)
|
|
220
|
+
needsFlushRef.current = true;
|
|
221
|
+
prevOpenForFlush.current = open;
|
|
216
222
|
const registerOption = useCallback((option) => {
|
|
223
|
+
if (needsFlushRef.current) {
|
|
224
|
+
optionsRef.current.clear();
|
|
225
|
+
needsFlushRef.current = false;
|
|
226
|
+
}
|
|
217
227
|
optionsRef.current.set(toKey(option.value), option);
|
|
218
228
|
setRegistryVersion((v) => v + 1);
|
|
219
229
|
}, []);
|
|
@@ -225,14 +235,6 @@ function MultiSelectRoot({ id: triggerIdProp, value: controlledValue, defaultVal
|
|
|
225
235
|
optionsRef.current.delete(toKey(optionValue));
|
|
226
236
|
setRegistryVersion((v) => v + 1);
|
|
227
237
|
}, []);
|
|
228
|
-
// Flush stale registry on open. Options that were registered before close
|
|
229
|
-
// may no longer exist (parent changed children while closed). Clearing
|
|
230
|
-
// before the new options mount ensures no orphaned entries accumulate.
|
|
231
|
-
useLayoutEffect(() => {
|
|
232
|
-
if (open) {
|
|
233
|
-
optionsRef.current.clear();
|
|
234
|
-
}
|
|
235
|
-
}, [open]);
|
|
236
238
|
// Get selected options for trigger display
|
|
237
239
|
const getSelectedOptions = useCallback(() => {
|
|
238
240
|
return value
|
|
@@ -462,7 +464,7 @@ function MultiSelectTriggerComponent({ asChild = false, className, children, })
|
|
|
462
464
|
'aria-describedby': ariaDescribedBy,
|
|
463
465
|
'aria-haspopup': 'listbox',
|
|
464
466
|
'aria-expanded': open,
|
|
465
|
-
'aria-controls': listboxId,
|
|
467
|
+
'aria-controls': open ? listboxId : undefined,
|
|
466
468
|
'aria-keyshortcuts': hasSelection && !open ? 'Delete' : undefined,
|
|
467
469
|
'data-state': open ? 'open' : 'closed',
|
|
468
470
|
...floatingProps,
|
|
@@ -494,7 +496,7 @@ function MultiSelectTriggerComponent({ asChild = false, className, children, })
|
|
|
494
496
|
ref: refs.setReference,
|
|
495
497
|
'aria-haspopup': 'listbox',
|
|
496
498
|
'aria-expanded': open,
|
|
497
|
-
'aria-controls': listboxId,
|
|
499
|
+
'aria-controls': open ? listboxId : undefined,
|
|
498
500
|
'aria-activedescendant': floatingProps['aria-activedescendant'],
|
|
499
501
|
'aria-describedby': ariaDescribedBy,
|
|
500
502
|
// asChild: use aria-disabled + data-disabled since element may not support native disabled
|
|
@@ -522,7 +524,7 @@ function MultiSelectTriggerComponent({ asChild = false, className, children, })
|
|
|
522
524
|
}
|
|
523
525
|
// Default: render button with optional custom content
|
|
524
526
|
const triggerContent = children ?? defaultTriggerContent;
|
|
525
|
-
return (_jsxs(_Fragment, { children: [_jsx("button", { ref: refs.setReference, type: "button", id: triggerId, className: cx('tui-multiselect__trigger', sizeClass, className), disabled: disabled, "aria-label": ariaLabel, "aria-labelledby": ariaLabelledBy, "aria-describedby": ariaDescribedBy, "aria-haspopup": "listbox", "aria-expanded": open, "aria-controls": listboxId, "data-state": open ? 'open' : 'closed', ...floatingProps,
|
|
527
|
+
return (_jsxs(_Fragment, { children: [_jsx("button", { ref: refs.setReference, type: "button", id: triggerId, className: cx('tui-multiselect__trigger', sizeClass, className), disabled: disabled, "aria-label": ariaLabel, "aria-labelledby": ariaLabelledBy, "aria-describedby": ariaDescribedBy, "aria-haspopup": "listbox", "aria-expanded": open, "aria-controls": open ? listboxId : undefined, "data-state": open ? 'open' : 'closed', ...floatingProps,
|
|
526
528
|
// Handle Backspace/Delete AFTER floatingProps to ensure we catch it
|
|
527
529
|
// (typeahead may intercept these for its buffer)
|
|
528
530
|
onKeyDown: (e) => {
|
|
@@ -185,8 +185,21 @@ function SelectRoot({ id: triggerIdProp, value: controlledValue, defaultValue, o
|
|
|
185
185
|
listNavigation,
|
|
186
186
|
typeahead,
|
|
187
187
|
]);
|
|
188
|
+
// Flag: when the dropdown opens, the first registerOption call clears the
|
|
189
|
+
// registry before adding. This ensures stale entries from options that changed
|
|
190
|
+
// while closed don't persist — and avoids a useLayoutEffect ordering issue
|
|
191
|
+
// where a parent clear would run after children register.
|
|
192
|
+
const prevOpenForFlush = useRef(open);
|
|
193
|
+
const needsFlushRef = useRef(false);
|
|
194
|
+
if (open && !prevOpenForFlush.current)
|
|
195
|
+
needsFlushRef.current = true;
|
|
196
|
+
prevOpenForFlush.current = open;
|
|
188
197
|
// Register option (stable callback - no value/displayText deps)
|
|
189
198
|
const registerOption = useCallback((option) => {
|
|
199
|
+
if (needsFlushRef.current) {
|
|
200
|
+
optionsRef.current.clear();
|
|
201
|
+
needsFlushRef.current = false;
|
|
202
|
+
}
|
|
190
203
|
optionsRef.current.set(toKey(option.value), option);
|
|
191
204
|
setRegistryVersion((v) => v + 1);
|
|
192
205
|
}, []);
|
|
@@ -201,14 +214,6 @@ function SelectRoot({ id: triggerIdProp, value: controlledValue, defaultValue, o
|
|
|
201
214
|
}, []);
|
|
202
215
|
// Highlighted value for keyboard navigation
|
|
203
216
|
const highlightedValue = activeIndex !== null ? orderedOptions[activeIndex]?.value ?? null : null;
|
|
204
|
-
// Flush stale registry on open. Options that were registered before close
|
|
205
|
-
// may no longer exist (parent changed children while closed). Clearing
|
|
206
|
-
// before the new options mount ensures no orphaned entries accumulate.
|
|
207
|
-
useLayoutEffect(() => {
|
|
208
|
-
if (open) {
|
|
209
|
-
optionsRef.current.clear();
|
|
210
|
-
}
|
|
211
|
-
}, [open]);
|
|
212
217
|
// Reset active index when closing
|
|
213
218
|
useEffect(() => {
|
|
214
219
|
if (!open) {
|
|
@@ -371,6 +376,10 @@ function SelectTriggerComponent({ asChild = false, className, children, }) {
|
|
|
371
376
|
'aria-keyshortcuts': clearable && hasValue ? 'Delete' : undefined,
|
|
372
377
|
'data-state': open ? 'open' : 'closed',
|
|
373
378
|
...floatingProps,
|
|
379
|
+
// Override Floating UI's aria-controls — only reference the listbox when
|
|
380
|
+
// it's actually in the DOM. After first open, the listbox unmounts when
|
|
381
|
+
// closed to save hooks. A phantom aria-controls violates ARIA 1.2.
|
|
382
|
+
'aria-controls': open ? floatingProps['aria-controls'] : undefined,
|
|
374
383
|
};
|
|
375
384
|
if (asChild && isValidElement(children)) {
|
|
376
385
|
const childProps = children.props;
|
|
@@ -398,7 +407,7 @@ function SelectTriggerComponent({ asChild = false, className, children, }) {
|
|
|
398
407
|
ref: refs.setReference,
|
|
399
408
|
'aria-haspopup': floatingProps['aria-haspopup'],
|
|
400
409
|
'aria-expanded': floatingProps['aria-expanded'],
|
|
401
|
-
'aria-controls': floatingProps['aria-controls'],
|
|
410
|
+
'aria-controls': open ? floatingProps['aria-controls'] : undefined,
|
|
402
411
|
'aria-activedescendant': floatingProps['aria-activedescendant'],
|
|
403
412
|
'aria-describedby': ariaDescribedBy,
|
|
404
413
|
// asChild: use aria-disabled + data-disabled since element may not support native disabled
|
package/package.json
CHANGED
package/tui-manifest.json
CHANGED