@lumx/react 4.11.0-next.5 → 4.11.0-next.6

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.
package/index.d.ts CHANGED
@@ -1954,13 +1954,22 @@ interface ComboboxInputProps extends TextFieldProps {
1954
1954
  value: string;
1955
1955
  }) => void;
1956
1956
  /**
1957
- * When true (default), the combobox automatically filters options as the user types.
1958
- * Each `Combobox.Option` registers itself and hides when it doesn't match the input value.
1957
+ * Controls how the combobox filters options as the user types.
1959
1958
  *
1960
- * Set to false when you handle filtering yourself (e.g. async search, consumer-side
1961
- * pre-filtering). Options will not be auto-filtered.
1959
+ * - `'auto'` (default) Options are automatically filtered client-side.
1960
+ * - `'manual'` Filtering is the consumer's responsibility.
1961
+ * - `'off'` — Like `'manual'`, but the input is rendered as `readOnly`
1962
+ * and `openOnFocus` defaults to `true`.
1962
1963
  */
1963
- autoFilter?: boolean;
1964
+ filter?: 'auto' | 'manual' | 'off';
1965
+ /**
1966
+ * When true, the combobox opens automatically when the input receives focus.
1967
+ * When false (default, unless `filter` is `'off'`), the combobox only opens
1968
+ * on click, typing, or keyboard navigation.
1969
+ *
1970
+ * @default false (true when filter is 'off')
1971
+ */
1972
+ openOnFocus?: boolean;
1964
1973
  }
1965
1974
 
1966
1975
  /**
package/index.js CHANGED
@@ -3371,6 +3371,7 @@ function setupListbox(handle, signal, notify) {
3371
3371
  const trigger = handle.trigger;
3372
3372
  const listbox = handle.listbox;
3373
3373
  const isGrid = listbox.getAttribute('role') === 'grid';
3374
+ const itemSelector = '[role="option"]';
3374
3375
 
3375
3376
  // ── Focus navigation ──────────────────────────────────────
3376
3377
 
@@ -3378,13 +3379,25 @@ function setupListbox(handle, signal, notify) {
3378
3379
  onActivate: item => {
3379
3380
  item.setAttribute('data-focus-visible-added', 'true');
3380
3381
  trigger.setAttribute('aria-activedescendant', item.id);
3381
- // Scroll to the element in listbox or else the item
3382
- const toScrollTo = item.closest('[role=listbox] > *') || item;
3383
- toScrollTo.scrollIntoView({
3384
- behavior: 'smooth',
3385
- block: 'nearest'
3386
- });
3387
3382
  notify('activeDescendantChange', item.id);
3383
+ requestAnimationFrame(() => {
3384
+ // Last item in listbox
3385
+ const lastItem = !isGrid &&
3386
+ // Last item: find last element containing itemSelector and not followed by elements containing itemSelector and then get the itemSelector inside
3387
+ listbox.querySelector(`:scope > :has(${itemSelector}):not(:has(~ * ${itemSelector})) ${itemSelector}`);
3388
+ if (item === lastItem) {
3389
+ // Scroll to the end of the listbox (shouldsnap to the end of the scroll container thanks to CSS scroll snap)
3390
+ listbox.lastElementChild?.scrollIntoView({
3391
+ block: 'nearest'
3392
+ });
3393
+ } else {
3394
+ // Scroll to the element in listbox or else the item
3395
+ const toScrollTo = item.closest('[role=listbox] > *') || item;
3396
+ toScrollTo.scrollIntoView({
3397
+ block: 'nearest'
3398
+ });
3399
+ }
3400
+ });
3388
3401
  },
3389
3402
  onDeactivate: item => {
3390
3403
  item.removeAttribute('data-focus-visible-added');
@@ -3406,9 +3419,7 @@ function setupListbox(handle, signal, notify) {
3406
3419
  } else {
3407
3420
  focusNav = createListFocusNavigation({
3408
3421
  container: listbox,
3409
- // Filtered options don't render [role="option"] at all (they render only a
3410
- // hidden placeholder), so no :not([data-filtered]) filter is needed here.
3411
- itemSelector: '[role="option"]',
3422
+ itemSelector,
3412
3423
  getActiveItem: () => {
3413
3424
  const id = trigger.getAttribute('aria-activedescendant');
3414
3425
  return id ? document.getElementById(id) : null;
@@ -6876,9 +6887,11 @@ const ComboboxButton = Object.assign(forwardRefPolymorphic((props, ref) => {
6876
6887
  */
6877
6888
  function setupComboboxInput(input, options) {
6878
6889
  const {
6879
- autoFilter = true,
6890
+ filter = 'auto',
6880
6891
  onSelect: optionOnSelect
6881
6892
  } = options;
6893
+ const openOnFocus = options.openOnFocus ?? filter === 'off';
6894
+ const autoFilter = filter === 'auto';
6882
6895
 
6883
6896
  /** Check if the input is disabled (native `disabled` attribute or `aria-disabled="true"`). */
6884
6897
  const isDisabled = () => input.disabled || input.getAttribute('aria-disabled') === 'true';
@@ -6931,11 +6944,13 @@ function setupComboboxInput(input, options) {
6931
6944
  signal
6932
6945
  });
6933
6946
 
6934
- // Open on focus.
6947
+ // Open on focus (only when openOnFocus is enabled).
6935
6948
  input.addEventListener('focus', () => {
6936
6949
  if (isDisabled()) return;
6937
6950
  combobox.focusNav?.clear();
6938
- combobox.setIsOpen(true);
6951
+ if (openOnFocus) {
6952
+ combobox.setIsOpen(true);
6953
+ }
6939
6954
  }, {
6940
6955
  signal
6941
6956
  });
@@ -7040,6 +7055,7 @@ const ComboboxInput$1 = (props, {
7040
7055
  textFieldRef,
7041
7056
  toggleButtonProps,
7042
7057
  handleToggle,
7058
+ filter,
7043
7059
  theme,
7044
7060
  ...forwardedProps
7045
7061
  } = props;
@@ -7049,6 +7065,7 @@ const ComboboxInput$1 = (props, {
7049
7065
  const isAnyDisabled = disabledState.disabled || disabledState['aria-disabled'] || undefined;
7050
7066
  return /*#__PURE__*/jsx(TextField, {
7051
7067
  autoComplete: "off",
7068
+ readOnly: filter === 'off' || undefined,
7052
7069
  ...forwardedProps,
7053
7070
  ref: ref,
7054
7071
  role: "combobox",
@@ -7624,7 +7641,8 @@ const ComboboxInput = forwardRef((props, ref) => {
7624
7641
  inputRef: externalInputRef,
7625
7642
  toggleButtonProps,
7626
7643
  onSelect,
7627
- autoFilter,
7644
+ filter,
7645
+ openOnFocus,
7628
7646
  ...otherProps
7629
7647
  } = props;
7630
7648
  const internalInputRef = useRef(null);
@@ -7646,14 +7664,15 @@ const ComboboxInput = forwardRef((props, ref) => {
7646
7664
  onChangeRef.current?.(option.value);
7647
7665
  onSelectRef.current?.(option);
7648
7666
  },
7649
- autoFilter
7667
+ filter,
7668
+ openOnFocus
7650
7669
  });
7651
7670
  setHandle(handle);
7652
7671
  return () => {
7653
7672
  handle.destroy();
7654
7673
  setHandle(null);
7655
7674
  };
7656
- }, [autoFilter, setHandle]);
7675
+ }, [filter, openOnFocus, setHandle]);
7657
7676
  const handleToggle = useCallback(() => {
7658
7677
  setIsOpen(!isOpen);
7659
7678
  internalInputRef.current?.focus();
@@ -7663,6 +7682,7 @@ const ComboboxInput = forwardRef((props, ref) => {
7663
7682
  ref,
7664
7683
  listboxId,
7665
7684
  isOpen,
7685
+ filter,
7666
7686
  inputRef: mergedInputRef,
7667
7687
  textFieldRef: anchorRef,
7668
7688
  toggleButtonProps,
@@ -8156,7 +8176,7 @@ function useComboboxOptionContext() {
8156
8176
  /**
8157
8177
  * Combobox.Option component - wraps ListItem with option role and data-value.
8158
8178
  *
8159
- * When autoFilter is enabled on the parent Combobox.Input, each option registers itself
8179
+ * When filter="auto" is enabled on the parent Combobox.Input, each option registers itself
8160
8180
  * with the combobox handle (via an internal ref to its root <li>). When the filter changes,
8161
8181
  * the handle calls back with the new match state. When filtered out, the core template renders
8162
8182
  * a bare `<li hidden>` — no ARIA roles, no CSS classes — keeping the element in the DOM so
@@ -9602,7 +9622,7 @@ ListSection.defaultProps = DEFAULT_PROPS$U;
9602
9622
  *
9603
9623
  * Returns null when children is empty so the section header is not rendered as an orphan.
9604
9624
  *
9605
- * When autoFilter is active, the section registers itself with the combobox handle.
9625
+ * When filter="auto" is active, the section registers itself with the combobox handle.
9606
9626
  * The handle monitors registered options within this section and notifies when all
9607
9627
  * are filtered out. When hidden, the core template renders a bare `<li hidden>` wrapper
9608
9628
  * so children (options) stay mounted and registered.