@lumx/react 4.11.0-next.4 → 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
@@ -1781,6 +1781,8 @@ interface InputLabelProps$1 extends HasClassName, HasTheme {
1781
1781
  children: JSXElement;
1782
1782
  /** Native htmlFor property. */
1783
1783
  htmlFor: string;
1784
+ /** Native id property. */
1785
+ id?: string;
1784
1786
  /** Whether the component is required or not. */
1785
1787
  isRequired?: boolean;
1786
1788
  /** ref to the root element */
@@ -1813,6 +1815,8 @@ interface TextFieldProps$1 extends HasClassName, HasTheme, HasAriaDisabled, HasD
1813
1815
  helperId?: string;
1814
1816
  /** Generated error id for accessibility attributes. */
1815
1817
  errorId?: string;
1818
+ /** Generated label id for accessibility attributes (used to link the clear button to the field label). */
1819
+ labelId?: string;
1816
1820
  /** Whether the component is required or not. */
1817
1821
  isRequired?: boolean;
1818
1822
  /** Whether the text field is displayed with valid style or not. */
@@ -1842,7 +1846,7 @@ interface TextFieldProps$1 extends HasClassName, HasTheme, HasAriaDisabled, HasD
1842
1846
  /** Ref to the component root. */
1843
1847
  ref?: CommonRef;
1844
1848
  }
1845
- type TextFieldPropsToOverride = 'input' | 'IconButton' | 'labelProps' | 'textFieldRef' | 'clearButtonProps' | 'helperId' | 'errorId' | 'isAnyDisabled' | 'isFocus';
1849
+ type TextFieldPropsToOverride = 'input' | 'IconButton' | 'labelProps' | 'textFieldRef' | 'clearButtonProps' | 'helperId' | 'errorId' | 'labelId' | 'isAnyDisabled' | 'isFocus';
1846
1850
 
1847
1851
  /**
1848
1852
  * Defines the props of the component.
@@ -1950,13 +1954,22 @@ interface ComboboxInputProps extends TextFieldProps {
1950
1954
  value: string;
1951
1955
  }) => void;
1952
1956
  /**
1953
- * When true (default), the combobox automatically filters options as the user types.
1954
- * 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.
1958
+ *
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`.
1963
+ */
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.
1955
1969
  *
1956
- * Set to false when you handle filtering yourself (e.g. async search, consumer-side
1957
- * pre-filtering). Options will not be auto-filtered.
1970
+ * @default false (true when filter is 'off')
1958
1971
  */
1959
- autoFilter?: boolean;
1972
+ openOnFocus?: boolean;
1960
1973
  }
1961
1974
 
1962
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",
@@ -7107,15 +7124,17 @@ const {
7107
7124
  * @param existingAriaDescribedBy Existing aria-describedby value to merge
7108
7125
  * @return Object containing helperId, errorId, and combined describedById
7109
7126
  */
7110
- function generateAccessibilityIds(helper, error, generatedId, existingAriaDescribedBy) {
7127
+ function generateAccessibilityIds(helper, error, generatedId, existingAriaDescribedBy, label) {
7111
7128
  const helperId = helper ? `text-field-helper-${generatedId}` : undefined;
7112
7129
  const errorId = error ? `text-field-error-${generatedId}` : undefined;
7130
+ const labelId = label ? `text-field-label-${generatedId}` : undefined;
7113
7131
  const describedByIds = [errorId, helperId, existingAriaDescribedBy].filter(Boolean);
7114
7132
  const describedById = describedByIds.length === 0 ? undefined : describedByIds.join(' ');
7115
7133
  return {
7116
7134
  helperId,
7117
7135
  errorId,
7118
- describedById
7136
+ describedById,
7137
+ labelId
7119
7138
  };
7120
7139
  }
7121
7140
 
@@ -7148,6 +7167,7 @@ const TextField$1 = props => {
7148
7167
  textFieldRef,
7149
7168
  helperId,
7150
7169
  errorId,
7170
+ labelId,
7151
7171
  theme,
7152
7172
  value,
7153
7173
  afterElement,
@@ -7179,6 +7199,7 @@ const TextField$1 = props => {
7179
7199
  className: element$K('header'),
7180
7200
  children: [label && InputLabel$1({
7181
7201
  ...labelProps,
7202
+ id: labelId,
7182
7203
  htmlFor: textFieldId,
7183
7204
  className: element$K('label'),
7184
7205
  isRequired,
@@ -7213,6 +7234,7 @@ const TextField$1 = props => {
7213
7234
  icon: isValid ? mdiCheckCircle : mdiAlertCircle,
7214
7235
  size: Size.xxs
7215
7236
  }), clearButtonProps && isNotEmpty && !isAnyDisabled && /*#__PURE__*/jsx(IconButton, {
7237
+ "aria-describedby": labelId,
7216
7238
  ...clearButtonProps,
7217
7239
  className: element$K('input-clear'),
7218
7240
  icon: mdiCloseCircle,
@@ -7500,8 +7522,9 @@ const TextField = forwardRef((props, ref) => {
7500
7522
  const {
7501
7523
  helperId,
7502
7524
  errorId,
7503
- describedById
7504
- } = generateAccessibilityIds(helper, error, generatedTextFieldId, forwardedProps['aria-describedby']);
7525
+ describedById,
7526
+ labelId
7527
+ } = generateAccessibilityIds(helper, error, generatedTextFieldId, forwardedProps['aria-describedby'], label);
7505
7528
  const [isFocus, setFocus] = useState(false);
7506
7529
 
7507
7530
  /**
@@ -7571,6 +7594,7 @@ const TextField = forwardRef((props, ref) => {
7571
7594
  afterElement,
7572
7595
  hasError,
7573
7596
  helperId,
7597
+ labelId,
7574
7598
  multiline,
7575
7599
  maxLength,
7576
7600
  isRequired,
@@ -7617,7 +7641,8 @@ const ComboboxInput = forwardRef((props, ref) => {
7617
7641
  inputRef: externalInputRef,
7618
7642
  toggleButtonProps,
7619
7643
  onSelect,
7620
- autoFilter,
7644
+ filter,
7645
+ openOnFocus,
7621
7646
  ...otherProps
7622
7647
  } = props;
7623
7648
  const internalInputRef = useRef(null);
@@ -7639,14 +7664,15 @@ const ComboboxInput = forwardRef((props, ref) => {
7639
7664
  onChangeRef.current?.(option.value);
7640
7665
  onSelectRef.current?.(option);
7641
7666
  },
7642
- autoFilter
7667
+ filter,
7668
+ openOnFocus
7643
7669
  });
7644
7670
  setHandle(handle);
7645
7671
  return () => {
7646
7672
  handle.destroy();
7647
7673
  setHandle(null);
7648
7674
  };
7649
- }, [autoFilter, setHandle]);
7675
+ }, [filter, openOnFocus, setHandle]);
7650
7676
  const handleToggle = useCallback(() => {
7651
7677
  setIsOpen(!isOpen);
7652
7678
  internalInputRef.current?.focus();
@@ -7656,6 +7682,7 @@ const ComboboxInput = forwardRef((props, ref) => {
7656
7682
  ref,
7657
7683
  listboxId,
7658
7684
  isOpen,
7685
+ filter,
7659
7686
  inputRef: mergedInputRef,
7660
7687
  textFieldRef: anchorRef,
7661
7688
  toggleButtonProps,
@@ -8149,7 +8176,7 @@ function useComboboxOptionContext() {
8149
8176
  /**
8150
8177
  * Combobox.Option component - wraps ListItem with option role and data-value.
8151
8178
  *
8152
- * 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
8153
8180
  * with the combobox handle (via an internal ref to its root <li>). When the filter changes,
8154
8181
  * the handle calls back with the new match state. When filtered out, the core template renders
8155
8182
  * a bare `<li hidden>` — no ARIA roles, no CSS classes — keeping the element in the DOM so
@@ -9595,7 +9622,7 @@ ListSection.defaultProps = DEFAULT_PROPS$U;
9595
9622
  *
9596
9623
  * Returns null when children is empty so the section header is not rendered as an orphan.
9597
9624
  *
9598
- * 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.
9599
9626
  * The handle monitors registered options within this section and notifies when all
9600
9627
  * are filtered out. When hidden, the core template renders a bare `<li hidden>` wrapper
9601
9628
  * so children (options) stay mounted and registered.