@lumx/react 4.17.0-next.0 → 4.17.0-next.2

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
@@ -1,7 +1,7 @@
1
1
  import { HorizontalAlignment as HorizontalAlignment$1, Orientation as Orientation$1, Alignment as Alignment$1, Size as Size$1, AspectRatio as AspectRatio$1, ColorPalette as ColorPalette$1, Kind as Kind$1, Emphasis as Emphasis$1, Theme as Theme$1 } from '@lumx/core/js/constants';
2
2
  export * from '@lumx/core/js/constants';
3
3
  import * as _lumx_core_js_types from '@lumx/core/js/types';
4
- import { ValueOf as ValueOf$1, GenericProps as GenericProps$1, HasTheme as HasTheme$1, PropsToOverride, HasAriaDisabled as HasAriaDisabled$1, HasRequiredLinkHref as HasRequiredLinkHref$1, HasClassName as HasClassName$1, HasCloseMode as HasCloseMode$1, JSXElement as JSXElement$1, CommonRef as CommonRef$1, Falsy, HeadingElement as HeadingElement$1, NamedProps, HasAriaLabelOrLabelledBy as HasAriaLabelOrLabelledBy$1 } from '@lumx/core/js/types';
4
+ import { ValueOf as ValueOf$1, GenericProps as GenericProps$1, HasTheme as HasTheme$1, PropsToOverride, HasAriaDisabled as HasAriaDisabled$1, HasRequiredLinkHref as HasRequiredLinkHref$1, HasClassName as HasClassName$1, HasCloseMode as HasCloseMode$1, JSXElement as JSXElement$1, CommonRef as CommonRef$1, Falsy, HeadingElement as HeadingElement$1, HasAriaLabelOrLabelledBy as HasAriaLabelOrLabelledBy$1, NamedProps } from '@lumx/core/js/types';
5
5
  export * from '@lumx/core/js/types';
6
6
  import * as React$1 from 'react';
7
7
  import React__default, { Ref, ReactElement, ReactNode, SyntheticEvent, MouseEventHandler, KeyboardEventHandler, RefObject, SetStateAction, Key, CSSProperties, ElementType as ElementType$1, HTMLInputTypeAttribute, ComponentProps, ImgHTMLAttributes } from 'react';
@@ -3472,7 +3472,26 @@ interface LinkPreviewProps extends GenericProps$1, ReactToJSX<LinkPreviewProps$1
3472
3472
  */
3473
3473
  declare const LinkPreview: Comp<LinkPreviewProps, HTMLDivElement>;
3474
3474
 
3475
+ type MenuButtonVariant = 'button' | 'icon-button' | 'chip' | 'link';
3476
+ /** ARIA keys set by MenuButton on the trigger — omitted from variant component props. */
3477
+ type MenuButtonAriaKeys = 'aria-haspopup' | 'aria-controls' | 'aria-expanded';
3478
+ /** Per-variant keys internally managed by MenuButton — omitted from variant component props. */
3479
+ type MenuButtonVariantsInternalKeys = {
3480
+ button: MenuButtonAriaKeys;
3481
+ 'icon-button': MenuButtonAriaKeys;
3482
+ chip: MenuButtonAriaKeys | 'isClickable';
3483
+ link: MenuButtonAriaKeys | 'href' | 'linkAs';
3484
+ };
3485
+ /** Discriminated union of MenuButton props across all trigger variants. */
3486
+ type MenuButtonVariantsProps<TBase, TVariantProps extends Record<MenuButtonVariant, any>> = {
3487
+ [V in MenuButtonVariant]: TBase & (V extends 'button' ? {
3488
+ variant?: V;
3489
+ } : {
3490
+ variant: V;
3491
+ }) & Omit<TVariantProps[V], MenuButtonVariantsInternalKeys[V] | keyof TBase>;
3492
+ }[MenuButtonVariant];
3475
3493
  interface MenuButtonProps$1 {
3494
+ variant?: MenuButtonVariant;
3476
3495
  label?: JSXElement;
3477
3496
  children?: JSXElement;
3478
3497
  triggerProps?: Record<string, any>;
@@ -3481,25 +3500,9 @@ interface MenuButtonProps$1 {
3481
3500
  }
3482
3501
 
3483
3502
  /** MenuPopover props. */
3484
- interface MenuPopoverProps$1 extends HasClassName {
3503
+ interface MenuPopoverProps$1 extends HasClassName, Pick<PopoverProps$1, 'placement' | 'anchorRef' | 'isOpen' | 'handleClose'> {
3485
3504
  /** Popover content (a `Menu`). */
3486
3505
  children?: JSXElement;
3487
- /** Whether the popover is open. */
3488
- isOpen?: boolean;
3489
- /** Placement relative to the anchor. Defaults to `'bottom-start'`. */
3490
- placement?: Placement;
3491
- /** Reference to the anchor element. */
3492
- anchorRef?: CommonRef;
3493
- /** Callback invoked when the popover requests to close (click away, escape). */
3494
- handleClose?(): void;
3495
- /** Whether the popover should close when clicking outside. Default: true. */
3496
- closeOnClickAway?: boolean;
3497
- /** Whether the popover should close on Escape. Default: true. */
3498
- closeOnEscape?: boolean;
3499
- /** Whether to render in a portal. Default: false (avoid stacking-context surprises). */
3500
- usePortal?: boolean;
3501
- /** Whether to focus the anchor on close. Default: true. */
3502
- focusAnchorOnClose?: boolean;
3503
3506
  }
3504
3507
 
3505
3508
  /** Popover props forwarded to the inner Popover (minus managed props). */
@@ -3509,35 +3512,27 @@ interface MenuPopoverProps extends ReactToJSX<MenuPopoverProps$1, 'isOpen' | 'an
3509
3512
  children: ReactNode;
3510
3513
  }
3511
3514
 
3512
- /** Keys managed by MenuButton omitted from the polymorphic trigger props. */
3513
- type OmittedTriggerKeys = 'aria-haspopup' | 'aria-controls' | 'aria-expanded' | 'label' | 'children' | 'ref';
3514
- /** Polymorphic trigger props with index signature stripped and managed keys removed. */
3515
- type TriggerProps$1<E extends ElementType$1> = Omit<NamedProps<React__default.ComponentProps<E>>, OmittedTriggerKeys>;
3516
- /** Menu button props */
3517
- type MenuButtonProps<E extends ElementType$1 = typeof Button> = TriggerProps$1<E> & ReactToJSX<MenuButtonProps$1, 'triggerProps'> & {
3518
- /** Customize the rendered trigger component. */
3519
- as?: E;
3515
+ /** Props that MenuButton explicitly declares. */
3516
+ type MenuButtonBase = ReactToJSX<MenuButtonProps$1, 'triggerProps' | 'variant'> & {
3520
3517
  children?: React__default.ReactNode;
3521
3518
  popoverProps?: MenuPopoverProps;
3522
3519
  onOpen?: (isOpen: boolean) => void;
3520
+ label: string;
3523
3521
  };
3522
+ /** MenuButton props — discriminated union over the variant to inherit the target component's props. */
3523
+ type MenuButtonProps = MenuButtonVariantsProps<MenuButtonBase, {
3524
+ button: ButtonProps;
3525
+ 'icon-button': IconButtonProps;
3526
+ chip: ChipProps;
3527
+ link: LinkProps;
3528
+ }>;
3524
3529
  /**
3525
3530
  * MenuButton component.
3526
3531
  *
3527
3532
  * @param props Component props.
3528
3533
  * @return React element.
3529
3534
  */
3530
- declare const MenuButton: (<E extends ElementType$1 = Comp<ButtonProps, HTMLButtonElement | HTMLAnchorElement>>(props: TriggerProps$1<E> & ReactToJSX<MenuButtonProps$1, "triggerProps"> & {
3531
- /** Customize the rendered trigger component. */
3532
- as?: E | undefined;
3533
- children?: React__default.ReactNode;
3534
- popoverProps?: MenuPopoverProps;
3535
- onOpen?: (isOpen: boolean) => void;
3536
- } & React__default.ComponentProps<E> & {
3537
- ref?: ComponentRef<E> | undefined;
3538
- }) => React__default.JSX.Element) & {
3539
- displayName: string;
3540
- };
3535
+ declare const MenuButton: Comp<MenuButtonProps, HTMLElement>;
3541
3536
 
3542
3537
  /** MenuItem props. */
3543
3538
  interface MenuItemProps$1 extends HasClassName {
@@ -3573,6 +3568,8 @@ interface MenuItemProps<E extends ElementType$1 = 'button'> extends GenericProps
3573
3568
  onClick?(event: SyntheticEvent): void;
3574
3569
  /** MDI icon rendered as `<Icon size="xs" />` prepended to the `before` slot. */
3575
3570
  icon?: string;
3571
+ /** MDI icon rendered as `<Icon size="xs" />` appended to the `after` slot. */
3572
+ afterIcon?: string;
3576
3573
  /** Foreground color applied to the icon and label text. */
3577
3574
  color?: ColorPalette$1;
3578
3575
  /** Content rendered before the label (rendered AFTER `icon` if both are provided). */
package/index.js CHANGED
@@ -46,6 +46,7 @@ import range from 'lodash/range.js';
46
46
  import { mdiPlayCircleOutline } from '@lumx/icons/esm/play-circle-outline.js';
47
47
  import { mdiPauseCircleOutline } from '@lumx/icons/esm/pause-circle-outline.js';
48
48
  import chunk from 'lodash/chunk.js';
49
+ import { mdiDotsVertical } from '@lumx/icons/esm/dots-vertical.js';
49
50
  import take from 'lodash/take.js';
50
51
  import { mdiRadioboxBlank } from '@lumx/icons/esm/radiobox-blank.js';
51
52
  import { mdiRadioboxMarked } from '@lumx/icons/esm/radiobox-marked.js';
@@ -6377,7 +6378,7 @@ function setupListbox(handle, signal, notify, options) {
6377
6378
  wrap: options?.wrapNavigation,
6378
6379
  getActiveItem: () => {
6379
6380
  const id = trigger.getAttribute('aria-activedescendant');
6380
- return id ? document.getElementById(id) : null;
6381
+ return id && listbox.querySelector(`#${CSS.escape(id)}`) || null;
6381
6382
  }
6382
6383
  }, focusCallbacks, signal);
6383
6384
  }
@@ -6399,11 +6400,6 @@ function setupListbox(handle, signal, notify, options) {
6399
6400
  }
6400
6401
  handle.select(cell);
6401
6402
  trigger.focus();
6402
-
6403
- // In multi-select mode, keep visual focus on the selected option
6404
- if (!handle.isMultiSelect) {
6405
- handle.setIsOpen(false);
6406
- }
6407
6403
  }, {
6408
6404
  signal
6409
6405
  });
@@ -6593,10 +6589,6 @@ function setupCombobox(callbacks, options, onTriggerAttach) {
6593
6589
  if (!isOptionDisabled(activeItem)) {
6594
6590
  activeItem.click();
6595
6591
  }
6596
- // Only close when not in multi select and not in action cell
6597
- if (!handle.isMultiSelect && !isActionCell(activeItem)) {
6598
- handle.setIsOpen(false);
6599
- }
6600
6592
  flag = true;
6601
6593
  } else if (handle.isOpen && !handle.isMultiSelect) {
6602
6594
  // Open with no active item (single select) => close the popup,
@@ -6751,6 +6743,15 @@ function setupCombobox(callbacks, options, onTriggerAttach) {
6751
6743
  callbacks.onSelect?.({
6752
6744
  value: option ? getOptionValue(option) : ''
6753
6745
  });
6746
+
6747
+ // Close on selection (when not multiselectable)
6748
+ if (option && !handle.isMultiSelect) {
6749
+ handle.focusNav?.clear();
6750
+ // Defer the close to the next frame (to make sure all other click handler resolve).
6751
+ requestAnimationFrame(() => {
6752
+ handle.setIsOpen(false);
6753
+ });
6754
+ }
6754
6755
  },
6755
6756
  flushPendingNavigation() {
6756
6757
  // Do navigations actions we could not do because the combobox items were not mounted yet
@@ -7349,7 +7350,8 @@ function setupComboboxInput(input, options) {
7349
7350
  let handle;
7350
7351
  const {
7351
7352
  filter = 'auto',
7352
- onSelect: optionOnSelect
7353
+ onSelect: optionOnSelect,
7354
+ onInput: onInputCallback
7353
7355
  } = options;
7354
7356
  const openOnFocus = options.openOnFocus ?? filter === 'off';
7355
7357
  const autoFilter = filter === 'auto';
@@ -7366,18 +7368,11 @@ function setupComboboxInput(input, options) {
7366
7368
 
7367
7369
  /**
7368
7370
  * Wraps the consumer's onSelect to perform input-mode side effects after selection:
7369
- * clears the active descendant, resets the filter, and re-opens the popup.
7371
+ * resets the filter and typing state.
7370
7372
  */
7371
7373
  const onSelect = option => {
7372
7374
  optionOnSelect?.(option);
7373
-
7374
- // Clear the active item. In multi-select, keep visual focus so the
7375
- // user can continue navigating after selection.
7376
- if (!handle.isMultiSelect) {
7377
- handle.focusNav?.clear();
7378
- }
7379
7375
  userHasTyped = false;
7380
- handle.setIsOpen(true);
7381
7376
  if (autoFilter) {
7382
7377
  handle.setFilter('');
7383
7378
  }
@@ -7397,6 +7392,9 @@ function setupComboboxInput(input, options) {
7397
7392
  if (isDisabled()) return;
7398
7393
  combobox.focusNav?.clear();
7399
7394
  userHasTyped = true;
7395
+
7396
+ // Notify the framework wrapper of the new input value synchronously.
7397
+ onInputCallback?.(input.value);
7400
7398
  combobox.setIsOpen(true);
7401
7399
  if (autoFilter) {
7402
7400
  combobox.setFilter(input.value);
@@ -8141,6 +8139,10 @@ const ComboboxInput = forwardRef((props, ref) => {
8141
8139
  onChangeRef.current?.(option.value);
8142
8140
  onSelectRef.current?.(option);
8143
8141
  },
8142
+ onInput(value) {
8143
+ // Keep controlled value in sync.
8144
+ onChangeRef.current?.(value);
8145
+ },
8144
8146
  filter,
8145
8147
  openOnFocus
8146
8148
  });
@@ -14708,23 +14710,45 @@ const COMPONENT_NAME$J = 'MenuButton';
14708
14710
 
14709
14711
  /** Menu button core template (composition of menu provider, trigger, popover and list) */
14710
14712
  const MenuButton$1 = (props, {
14711
- Menu
14713
+ MenuProvider,
14714
+ MenuTrigger,
14715
+ MenuPopover,
14716
+ MenuList
14712
14717
  }) => {
14713
14718
  const {
14714
14719
  label,
14715
14720
  children,
14716
14721
  triggerProps,
14717
14722
  popoverProps,
14718
- onOpen
14723
+ onOpen,
14724
+ variant
14719
14725
  } = props;
14720
- return /*#__PURE__*/jsxs(Menu.Provider, {
14726
+ const extraTriggerProps = {};
14727
+ if (variant === 'chip') {
14728
+ // Chip render as clickable (since the component cannot detect this automatically)
14729
+ extraTriggerProps.isClickable = true;
14730
+ }
14731
+ let triggerChildren;
14732
+ if (variant === 'icon-button') {
14733
+ // Label as prop (renders as tooltip label)
14734
+ extraTriggerProps.label = label;
14735
+ // Default icon
14736
+ if (!triggerProps?.icon) {
14737
+ extraTriggerProps.icon = mdiDotsVertical;
14738
+ }
14739
+ } else {
14740
+ // Only set the label as children when we are not displaying an icon-button
14741
+ triggerChildren = label;
14742
+ }
14743
+ return /*#__PURE__*/jsxs(MenuProvider, {
14721
14744
  onOpen: onOpen,
14722
- children: [/*#__PURE__*/jsx(Menu.Trigger, {
14745
+ children: [/*#__PURE__*/jsx(MenuTrigger, {
14723
14746
  ...triggerProps,
14724
- children: label
14725
- }), /*#__PURE__*/jsx(Menu.Popover, {
14747
+ ...extraTriggerProps,
14748
+ children: triggerChildren
14749
+ }), /*#__PURE__*/jsx(MenuPopover, {
14726
14750
  ...popoverProps,
14727
- children: /*#__PURE__*/jsx(Menu.List, {
14751
+ children: /*#__PURE__*/jsx(MenuList, {
14728
14752
  children: children
14729
14753
  })
14730
14754
  })]
@@ -15059,11 +15083,11 @@ function setupMenu(options = {}) {
15059
15083
 
15060
15084
  const MenuContext = /*#__PURE__*/createContext(undefined);
15061
15085
 
15062
- /** Use Menu context. @throws if used outside `Menu.Provider`. */
15086
+ /** Use Menu context. @throws if used outside `MenuProvider`. */
15063
15087
  function useMenuContext() {
15064
15088
  const context = useContext(MenuContext);
15065
15089
  if (!context) {
15066
- throw new Error('Menu sub-components must be used within a Menu.Provider');
15090
+ throw new Error('Menu sub-components must be used within a MenuProvider');
15067
15091
  }
15068
15092
  return context;
15069
15093
  }
@@ -15234,10 +15258,6 @@ const MenuPopover$1 = (props, {
15234
15258
  placement = 'bottom-start',
15235
15259
  anchorRef,
15236
15260
  handleClose,
15237
- closeOnClickAway = true,
15238
- closeOnEscape = true,
15239
- usePortal = false,
15240
- focusAnchorOnClose = true,
15241
15261
  ...forwardedProps
15242
15262
  } = props;
15243
15263
  return /*#__PURE__*/jsx(Popover, {
@@ -15246,10 +15266,10 @@ const MenuPopover$1 = (props, {
15246
15266
  anchorRef: anchorRef,
15247
15267
  isOpen: isOpen,
15248
15268
  onClose: handleClose,
15249
- closeOnClickAway: closeOnClickAway,
15250
- closeOnEscape: closeOnEscape,
15251
- usePortal: usePortal,
15252
- focusAnchorOnClose: focusAnchorOnClose,
15269
+ closeOnClickAway: true,
15270
+ closeOnEscape: true,
15271
+ usePortal: false,
15272
+ focusAnchorOnClose: true,
15253
15273
  withFocusTrap: false,
15254
15274
  closeMode: "hide",
15255
15275
  fitToAnchorWidth: false,
@@ -15363,18 +15383,17 @@ const MenuList = forwardRef((props, ref) => {
15363
15383
  MenuList.displayName = COMPONENT_NAME$G;
15364
15384
  MenuList.className = CLASSNAME$H;
15365
15385
 
15366
- const Menu = {
15367
- Provider: MenuProvider,
15368
- Trigger: MenuTrigger,
15369
- Popover: MenuPopover,
15370
- List: MenuList
15371
- };
15372
-
15373
- /** Keys managed by MenuButton — omitted from the polymorphic trigger props. */
15386
+ /** Props that MenuButton explicitly declares. */
15374
15387
 
15375
- /** Polymorphic trigger props with index signature stripped and managed keys removed. */
15388
+ /** MenuButton props discriminated union over the variant to inherit the target component's props. */
15376
15389
 
15377
- /** Menu button props */
15390
+ /** Menu trigger button component by variant */
15391
+ const TRIGGER_COMPONENTS = {
15392
+ button: Button,
15393
+ 'icon-button': IconButton,
15394
+ chip: Chip,
15395
+ link: Link
15396
+ };
15378
15397
 
15379
15398
  /**
15380
15399
  * MenuButton component.
@@ -15382,37 +15401,34 @@ const Menu = {
15382
15401
  * @param props Component props.
15383
15402
  * @return React element.
15384
15403
  */
15385
- const MenuButton = Object.assign(forwardRefPolymorphic((props, ref) => {
15404
+ const MenuButton = forwardRef((props, ref) => {
15386
15405
  const {
15387
15406
  label,
15388
15407
  children,
15389
15408
  popoverProps,
15390
15409
  onOpen,
15391
- as,
15410
+ variant = 'button',
15392
15411
  ...triggerProps
15393
15412
  } = props;
15394
- const isIconButton = as === IconButton;
15395
- const triggerLabel = isIconButton ? undefined : label;
15396
- const extraTriggerProps = isIconButton ? {
15397
- label
15398
- } : {};
15399
15413
  return MenuButton$1({
15400
- label: triggerLabel,
15414
+ label,
15401
15415
  children,
15402
15416
  popoverProps,
15403
15417
  onOpen,
15418
+ variant,
15404
15419
  triggerProps: {
15405
15420
  ...triggerProps,
15406
- ...extraTriggerProps,
15407
- as,
15421
+ as: TRIGGER_COMPONENTS[variant],
15408
15422
  ref
15409
15423
  }
15410
15424
  }, {
15411
- Menu
15425
+ MenuProvider,
15426
+ MenuTrigger,
15427
+ MenuPopover,
15428
+ MenuList
15412
15429
  });
15413
- }), {
15414
- displayName: COMPONENT_NAME$J
15415
15430
  });
15431
+ MenuButton.displayName = COMPONENT_NAME$J;
15416
15432
 
15417
15433
  /** MenuItem props. */
15418
15434
 
@@ -15474,6 +15490,7 @@ const MenuItem = forwardRef((props, ref) => {
15474
15490
  const {
15475
15491
  children,
15476
15492
  icon,
15493
+ afterIcon,
15477
15494
  color,
15478
15495
  before,
15479
15496
  after,
@@ -15508,12 +15525,21 @@ const MenuItem = forwardRef((props, ref) => {
15508
15525
  color: color
15509
15526
  }), before]
15510
15527
  }) : before;
15528
+
15529
+ // Append the optional `afterIcon` to the `after` slot.
15530
+ const mergedAfter = afterIcon ? /*#__PURE__*/jsxs(Fragment, {
15531
+ children: [after, /*#__PURE__*/jsx(Icon, {
15532
+ icon: afterIcon,
15533
+ size: "xs",
15534
+ color: color
15535
+ })]
15536
+ }) : after;
15511
15537
  return MenuItem$1({
15512
15538
  ...rest,
15513
15539
  ref: mergedRef,
15514
15540
  actionRef,
15515
15541
  before: mergedBefore,
15516
- after,
15542
+ after: mergedAfter,
15517
15543
  children: /*#__PURE__*/jsx(Text, {
15518
15544
  as: "span",
15519
15545
  color: color,