@ahrowe/ui 0.33.0 → 0.34.0

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.
Files changed (115) hide show
  1. package/dist/esm/common/actionIcon/actionIcon.module.mjs.map +1 -1
  2. package/dist/esm/common/breadcrumb/breadcrumb.mjs +1 -1
  3. package/dist/esm/common/breadcrumb/breadcrumb.mjs.map +1 -1
  4. package/dist/esm/common/breadcrumb/breadcrumb.module.mjs +1 -1
  5. package/dist/esm/common/breadcrumb/breadcrumb.module.mjs.map +1 -1
  6. package/dist/esm/common/colorPicker/colorPicker.mjs +1 -1
  7. package/dist/esm/common/colorPicker/colorPicker.mjs.map +1 -1
  8. package/dist/esm/common/colorPicker/colorPicker.module.mjs.map +1 -1
  9. package/dist/esm/common/configProvider/configProvider.mjs.map +1 -1
  10. package/dist/esm/common/configProvider/presentation.types.mjs +2 -0
  11. package/dist/esm/common/configProvider/presentation.types.mjs.map +1 -0
  12. package/dist/esm/common/configProvider/usePresentationDefault.mjs.map +1 -1
  13. package/dist/esm/common/datePicker/components/datePickerMonth/datePickerMonth.module.mjs.map +1 -1
  14. package/dist/esm/common/datePicker/datePicker.mjs +1 -1
  15. package/dist/esm/common/datePicker/datePicker.mjs.map +1 -1
  16. package/dist/esm/common/dropdown/dropdown.mjs +1 -1
  17. package/dist/esm/common/dropdown/dropdown.mjs.map +1 -1
  18. package/dist/esm/common/dropdown/dropdown.module.mjs +1 -1
  19. package/dist/esm/common/dropdown/dropdown.module.mjs.map +1 -1
  20. package/dist/esm/common/hooks/useAnchorTracking.mjs.map +1 -1
  21. package/dist/esm/common/hooks/useFocusBoundary.mjs +1 -1
  22. package/dist/esm/common/hooks/useFocusBoundary.mjs.map +1 -1
  23. package/dist/esm/common/hooks/useSheet.mjs +1 -1
  24. package/dist/esm/common/hooks/useSheet.mjs.map +1 -1
  25. package/dist/esm/common/inputDropdown/inputDropdown.mjs +1 -1
  26. package/dist/esm/common/inputDropdown/inputDropdown.mjs.map +1 -1
  27. package/dist/esm/common/inputDropdown/inputDropdown.module.mjs +1 -1
  28. package/dist/esm/common/inputDropdown/inputDropdown.module.mjs.map +1 -1
  29. package/dist/esm/common/interactableDiv/interactableDiv.mjs +1 -1
  30. package/dist/esm/common/interactableDiv/interactableDiv.mjs.map +1 -1
  31. package/dist/esm/common/klipyPicker/klipyPicker.mjs +1 -1
  32. package/dist/esm/common/klipyPicker/klipyPicker.mjs.map +1 -1
  33. package/dist/esm/common/menu/menu.mjs +2 -0
  34. package/dist/esm/common/menu/menu.mjs.map +1 -0
  35. package/dist/esm/common/menu/menu.module.mjs +2 -0
  36. package/dist/esm/common/menu/menu.module.mjs.map +1 -0
  37. package/dist/esm/common/menu/menu.types.mjs +2 -0
  38. package/dist/esm/common/menu/menu.types.mjs.map +1 -0
  39. package/dist/esm/common/multiSelect/multiSelect.mjs +1 -1
  40. package/dist/esm/common/multiSelect/multiSelect.mjs.map +1 -1
  41. package/dist/esm/common/multiSelect/multiSelect.module.mjs +1 -1
  42. package/dist/esm/common/multiSelect/multiSelect.module.mjs.map +1 -1
  43. package/dist/esm/common/popover/popover.mjs +1 -1
  44. package/dist/esm/common/popover/popover.mjs.map +1 -1
  45. package/dist/esm/common/popover/popover.module.mjs +1 -1
  46. package/dist/esm/common/popover/popover.module.mjs.map +1 -1
  47. package/dist/esm/common/popover/popover.types.mjs.map +1 -1
  48. package/dist/esm/common/popover/usePopoverPosition.mjs +1 -1
  49. package/dist/esm/common/popover/usePopoverPosition.mjs.map +1 -1
  50. package/dist/esm/common/splitButton/splitButton.mjs +1 -1
  51. package/dist/esm/common/splitButton/splitButton.mjs.map +1 -1
  52. package/dist/esm/common/splitButton/splitButton.module.mjs +1 -1
  53. package/dist/esm/common/splitButton/splitButton.module.mjs.map +1 -1
  54. package/dist/esm/common/timeInput/timeInput.mjs +1 -1
  55. package/dist/esm/common/timeInput/timeInput.mjs.map +1 -1
  56. package/dist/esm/common/toast/toast.module.mjs.map +1 -1
  57. package/dist/esm/common/tooltip/tooltip.mjs +1 -1
  58. package/dist/esm/common/tooltip/tooltip.mjs.map +1 -1
  59. package/dist/esm/common/tooltip/tooltip.module.mjs +1 -1
  60. package/dist/esm/common/tooltip/tooltip.module.mjs.map +1 -1
  61. package/dist/esm/common/virtualList/virtualList.module.mjs +1 -1
  62. package/dist/esm/common/virtualList/virtualList.module.mjs.map +1 -1
  63. package/dist/esm/common/virtualList/virtualListHeader.mjs +1 -1
  64. package/dist/esm/common/virtualList/virtualListHeader.mjs.map +1 -1
  65. package/dist/esm/index.mjs +1 -1
  66. package/dist/index.cjs +3 -3
  67. package/dist/index.cjs.map +1 -1
  68. package/dist/style.css +1 -1
  69. package/dist/types/common/configProvider/configProvider.d.ts +1 -1
  70. package/dist/types/common/configProvider/configProvider.types.d.ts +2 -3
  71. package/dist/types/common/configProvider/index.d.ts +1 -0
  72. package/dist/types/common/configProvider/presentation.types.d.ts +14 -0
  73. package/dist/types/common/configProvider/usePresentationDefault.d.ts +1 -1
  74. package/dist/types/common/dropdown/dropdown.types.d.ts +1 -1
  75. package/dist/types/common/hooks/useAnchorTracking.d.ts +1 -1
  76. package/dist/types/common/hooks/useFocusBoundary.d.ts +3 -3
  77. package/dist/types/common/hooks/useSheet.d.ts +1 -1
  78. package/dist/types/common/menu/index.d.ts +2 -0
  79. package/dist/types/common/menu/menu.d.ts +14 -0
  80. package/dist/types/common/menu/menu.types.d.ts +55 -0
  81. package/dist/types/common/modal/modal.types.d.ts +1 -1
  82. package/dist/types/common/multiSelect/multiSelect.types.d.ts +1 -1
  83. package/dist/types/common/popover/popover.types.d.ts +34 -3
  84. package/dist/types/common/popover/usePopoverPosition.d.ts +20 -1
  85. package/dist/types/common/splitButton/splitButton.types.d.ts +3 -3
  86. package/dist/types/index.d.ts +2 -2
  87. package/docs/Breadcrumb.md +2 -2
  88. package/docs/ButtonGroup.md +1 -1
  89. package/docs/CLAUDE.md +1 -1
  90. package/docs/Card.md +1 -1
  91. package/docs/ColorPicker.md +2 -0
  92. package/docs/ConfigProvider.md +2 -2
  93. package/docs/Dropdown.md +1 -1
  94. package/docs/InputDropdown.md +1 -1
  95. package/docs/InteractableDiv.md +2 -0
  96. package/docs/Menu.md +107 -0
  97. package/docs/Modal.md +1 -1
  98. package/docs/Popover.md +11 -5
  99. package/docs/SplitButton.md +8 -4
  100. package/docs/ThemeProvider.md +1 -1
  101. package/docs/VirtualList.md +2 -2
  102. package/package.json +1 -1
  103. package/dist/esm/common/floatingMenu/floatingMenu.mjs +0 -2
  104. package/dist/esm/common/floatingMenu/floatingMenu.mjs.map +0 -1
  105. package/dist/esm/common/floatingMenu/floatingMenu.module.mjs +0 -2
  106. package/dist/esm/common/floatingMenu/floatingMenu.module.mjs.map +0 -1
  107. package/dist/esm/common/floatingMenu/floatingMenu.types.mjs +0 -2
  108. package/dist/esm/common/floatingMenu/floatingMenu.types.mjs.map +0 -1
  109. package/dist/esm/common/floatingMenu/useFloatingPosition.mjs +0 -2
  110. package/dist/esm/common/floatingMenu/useFloatingPosition.mjs.map +0 -1
  111. package/dist/types/common/floatingMenu/floatingMenu.d.ts +0 -3
  112. package/dist/types/common/floatingMenu/floatingMenu.types.d.ts +0 -32
  113. package/dist/types/common/floatingMenu/index.d.ts +0 -2
  114. package/dist/types/common/floatingMenu/useFloatingPosition.d.ts +0 -27
  115. package/docs/FloatingMenu.md +0 -90
@@ -1,6 +1,6 @@
1
1
  import { default as React } from 'react';
2
2
  import { ComponentDefaults, ConfigProviderProps } from './configProvider.types';
3
- import { Presentation } from '../floatingMenu/floatingMenu.types';
3
+ import { Presentation } from '../configProvider/presentation.types';
4
4
  export declare const ConfigContext: React.Context<ComponentDefaults | null>;
5
5
  /**
6
6
  * One setting for every component that can present as a bottom sheet, kept separate from
@@ -39,9 +39,9 @@ import { PaginationProps } from '../pagination/pagination.types';
39
39
  import { TreeProps } from '../tree/tree.types';
40
40
  import { RoomDrawerProps } from '../roomDrawer/roomDrawer.types';
41
41
  import { RoomViewerProps } from '../roomViewer/roomViewer.types';
42
- import { FloatingMenuProps, Presentation } from '../floatingMenu/floatingMenu.types';
43
42
  import { PopoverProps } from '../popover/popover.types';
44
43
  import { ModalProps } from '../modal/modal.types';
44
+ import { Presentation } from '../configProvider/presentation.types';
45
45
  /**
46
46
  * Global default props keyed by component name.
47
47
  *
@@ -112,7 +112,6 @@ export interface ComponentDefaults {
112
112
  Tree?: Partial<TreeProps>;
113
113
  RoomDrawer?: Partial<RoomDrawerProps>;
114
114
  RoomViewer?: Partial<RoomViewerProps>;
115
- FloatingMenu?: Partial<FloatingMenuProps>;
116
115
  Popover?: Partial<PopoverProps>;
117
116
  Modal?: Partial<ModalProps>;
118
117
  }
@@ -122,7 +121,7 @@ export interface ConfigProviderProps {
122
121
  defaultProps?: ComponentDefaults;
123
122
  /**
124
123
  * How every sheet-capable component presents by default: anchored to its trigger, always a
125
- * bottom sheet, or a sheet only on a small touch screen. One switch for `FloatingMenu`,
124
+ * bottom sheet, or a sheet only on a small touch screen. One switch for `Popover`,
126
125
  * `Dropdown`, `Popover` and `Modal` at once (and so for everything built on them). A
127
126
  * component-keyed entry in `defaultProps` overrides it, and an explicit prop overrides both.
128
127
  */
@@ -3,3 +3,4 @@ export { useComponentDefaults } from './useComponentDefaults';
3
3
  export { useLabels } from './useLabels';
4
4
  export { usePresentationDefault } from './usePresentationDefault';
5
5
  export type { ComponentDefaults, ConfigProviderProps } from './configProvider.types';
6
+ export { Presentation } from './presentation.types';
@@ -0,0 +1,14 @@
1
+ /**
2
+ * How a surface is presented on screen. Lives here rather than with any one of
3
+ * the components that read it: `Modal`, `Popover`, `Dropdown` and `MultiSelect`
4
+ * all take it, and `ConfigProvider` owns the app-wide switch that sets it for
5
+ * every one of them at once.
6
+ */
7
+ export declare enum Presentation {
8
+ /** Wherever the component normally puts it: anchored to its trigger, or centred for a Modal. */
9
+ Default = "default",
10
+ /** Pinned to the bottom of the screen, full width up to a maximum. */
11
+ Sheet = "sheet",
12
+ /** A sheet on touch devices with a small screen, anchored everywhere else. */
13
+ Auto = "auto"
14
+ }
@@ -1,4 +1,4 @@
1
- import { Presentation } from '../floatingMenu/floatingMenu.types';
1
+ import { Presentation } from '../configProvider/presentation.types';
2
2
  /**
3
3
  * The nearest `ConfigProvider`'s app-wide `presentation`, or `undefined` when none is set.
4
4
  *
@@ -1,7 +1,7 @@
1
1
  import { SlotClassNames, SlotStyles, HtmlProps } from '../types/slots.types';
2
2
  import { ReactNode, Ref } from 'react';
3
3
  import { FormValidator } from '../../services/formValidation';
4
- import { Presentation } from '../floatingMenu/floatingMenu.types';
4
+ import { Presentation } from '../configProvider/presentation.types';
5
5
  export interface DropdownItem {
6
6
  key: string | number;
7
7
  label: ReactNode;
@@ -31,7 +31,7 @@ export interface UseAnchorTrackingArgs {
31
31
  * positioning on every scroll, viewport resize and tracked size change until it closes.
32
32
  *
33
33
  * Callers supply only the geometry (`update`) and the inline styles to clear again (`teardown`);
34
- * `useFloatingPosition` and `usePopoverPosition` are both built on this.
34
+ * `usePopoverPosition` is built on this.
35
35
  *
36
36
  * Position is deliberately written straight to the DOM by the caller rather than through React
37
37
  * state: going through setState -> re-render -> commit adds a round-trip that lags behind the
@@ -9,9 +9,9 @@ export interface UseFocusBoundaryArgs {
9
9
  }
10
10
  /**
11
11
  * Focus helpers for content portaled to the end of the DOM (e.g. via BodyEnd), where Tab can't
12
- * reach it in visual document order on its own. Shared by FloatingMenu and Popover, which both
13
- * need to hand focus back to the trigger on Escape / Shift+Tab out of the top, and continue
14
- * focus naturally past the trigger when Tab exits the bottom.
12
+ * reach it in visual document order on its own. Used by Popover and by Menu, which both need to
13
+ * hand focus back to the trigger on Escape / Shift+Tab out of the top, and continue focus
14
+ * naturally past the trigger when Tab exits the bottom.
15
15
  */
16
16
  export declare function useFocusBoundary({ triggerRef, contentRef }: UseFocusBoundaryArgs): {
17
17
  firstTriggerFocusable: () => HTMLElement | null;
@@ -1,5 +1,5 @@
1
1
  import { CSSProperties } from 'react';
2
- import { Presentation } from '../floatingMenu/floatingMenu.types';
2
+ import { Presentation } from '../configProvider/presentation.types';
3
3
  export interface Sheet {
4
4
  /** Whether the popup should render as a bottom sheet instead of anchored to its trigger. */
5
5
  isSheet: boolean;
@@ -0,0 +1,2 @@
1
+ export { default } from './menu';
2
+ export * from './menu.types';
@@ -0,0 +1,14 @@
1
+ import { ReactElement } from 'react';
2
+ import { MenuProps } from './menu.types';
3
+ /**
4
+ * A menu of actions on a trigger of your choosing.
5
+ *
6
+ * `Popover` does the anchoring, portalling and outside-click work but takes
7
+ * arbitrary `content`, which leaves every consumer to write their own `ul`/`li`
8
+ * and, in practice, to leave out the roles and the arrow keys. This is that list,
9
+ * done once: `role="menu"`, a roving focus that skips disabled entries, Escape
10
+ * back to the trigger, and separators and headings that stay out of the keyboard
11
+ * order.
12
+ */
13
+ declare function Menu({ items, children, onAction, isOpen: controlledOpen, onOpenChange, placement, align, presentation, className, style, classNames, styles: slotStyles, ...rest }: MenuProps): ReactElement;
14
+ export default Menu;
@@ -0,0 +1,55 @@
1
+ import { CSSProperties, ReactNode } from 'react';
2
+ import { SlotClassNames, SlotStyles, HtmlProps } from '../types/slots.types';
3
+ import { IconContent } from '../types/icon.types';
4
+ import { PopoverAlign, PopoverPlacement } from '../popover/popover.types';
5
+ import { Presentation } from '../configProvider/presentation.types';
6
+ /** One thing the menu can do. */
7
+ export interface MenuAction {
8
+ /** Stable id, also what `onAction` reports. */
9
+ id: string;
10
+ label: ReactNode;
11
+ icon?: IconContent;
12
+ /**
13
+ * Keyboard hint on the trailing edge, e.g. `⌘K`. Display only: binding the
14
+ * shortcut is yours, since only you know what else is listening.
15
+ */
16
+ shortcut?: ReactNode;
17
+ disabled?: boolean;
18
+ /** Renders in the destructive colour. For deleting, discarding, revoking. */
19
+ danger?: boolean;
20
+ onClick?: () => void;
21
+ }
22
+ /** A line between groups of actions. */
23
+ export interface MenuSeparator {
24
+ separator: true;
25
+ }
26
+ /** A label over the actions that follow it. */
27
+ export interface MenuHeading {
28
+ heading: ReactNode;
29
+ }
30
+ export type MenuEntry = MenuAction | MenuSeparator | MenuHeading;
31
+ export declare function isMenuSeparator(entry: MenuEntry): entry is MenuSeparator;
32
+ export declare function isMenuHeading(entry: MenuEntry): entry is MenuHeading;
33
+ export declare function isMenuAction(entry: MenuEntry): entry is MenuAction;
34
+ export type MenuSlots = 'root' | 'trigger' | 'menu' | 'item' | 'itemIcon' | 'itemLabel' | 'itemShortcut' | 'separator' | 'heading';
35
+ export interface MenuProps extends Omit<HtmlProps, 'onSelect'> {
36
+ /** What the menu offers, in the order it is read. */
37
+ items: MenuEntry[];
38
+ /** The element that opens the menu. */
39
+ children?: ReactNode;
40
+ /** Called with the id of the action that was picked, after its own `onClick`. */
41
+ onAction?: (id: string) => void;
42
+ /** Controlled open state. Leave it out to let the menu manage its own. */
43
+ isOpen?: boolean;
44
+ onOpenChange?: (isOpen: boolean) => void;
45
+ /** Which side of the trigger the menu opens on (default `Bottom`). */
46
+ placement?: PopoverPlacement;
47
+ /** Which edge it lines up with on the cross axis (default `Start`). */
48
+ align?: PopoverAlign;
49
+ /** Anchored to the trigger, pinned to the bottom of the screen, or the latter only on touch. */
50
+ presentation?: Presentation;
51
+ className?: string;
52
+ style?: CSSProperties;
53
+ classNames?: SlotClassNames<MenuSlots>;
54
+ styles?: SlotStyles<MenuSlots>;
55
+ }
@@ -1,6 +1,6 @@
1
1
  import { CSSProperties } from 'react';
2
2
  import { SlotClassNames, SlotStyles, HtmlProps } from '../types/slots.types';
3
- import { Presentation } from '../floatingMenu/floatingMenu.types';
3
+ import { Presentation } from '../configProvider/presentation.types';
4
4
  export type ModalSlots = 'container' | 'backdrop' | 'modal' | 'header' | 'title' | 'closeButton' | 'body';
5
5
  /** Text Modal renders of its own, for translating it. */
6
6
  export interface ModalLabels {
@@ -1,7 +1,7 @@
1
1
  import { CSSProperties } from 'react';
2
2
  import { FormValidator } from '../../services/formValidation';
3
3
  import { SlotClassNames, SlotStyles, HtmlProps } from '../types/slots.types';
4
- import { Presentation } from '../floatingMenu/floatingMenu.types';
4
+ import { Presentation } from '../configProvider/presentation.types';
5
5
  export type MultiSelectValue = string | number;
6
6
  export interface MultiSelectItem {
7
7
  key: MultiSelectValue;
@@ -1,6 +1,6 @@
1
1
  import { CSSProperties, ReactNode } from 'react';
2
2
  import { SlotClassNames, SlotStyles, HtmlProps } from '../types/slots.types';
3
- import { Presentation } from '../floatingMenu/floatingMenu.types';
3
+ import { Presentation } from '../configProvider/presentation.types';
4
4
  /** Preferred side of the trigger the panel opens on. Flips to the opposite side when there's no room. */
5
5
  export declare enum PopoverPlacement {
6
6
  Top = "top",
@@ -36,8 +36,14 @@ export interface PopoverProps extends Omit<HtmlProps, 'content'> {
36
36
  isOpen?: boolean;
37
37
  /** Initial open state when uncontrolled (default `false`). */
38
38
  defaultOpen?: boolean;
39
- /** Fires whenever the popover wants to open or close. */
40
- onOpenChange?: (isOpen: boolean) => void;
39
+ /**
40
+ * Fires whenever the popover wants to open or close. `fromKeyboard` says
41
+ * whether a key caused it, which is what a menu needs in order to put the
42
+ * focus on an entry only when the pointer was not what opened it.
43
+ */
44
+ onOpenChange?: (isOpen: boolean, meta: {
45
+ fromKeyboard: boolean;
46
+ }) => void;
41
47
  /** Gap in px between trigger and panel (default `10`). */
42
48
  offset?: number;
43
49
  /** Render the arrow pointing at the trigger (default `true`). */
@@ -52,6 +58,31 @@ export interface PopoverProps extends Omit<HtmlProps, 'content'> {
52
58
  closeDelay?: number;
53
59
  /** Prevent the popover from opening. */
54
60
  disabled?: boolean;
61
+ /**
62
+ * Move focus into the panel when it opens, and claim `role="dialog"` with it.
63
+ * `'keyboard'` (the default) does it only when the open came from the
64
+ * keyboard: the panel is portaled to the end of the document, so Tab never
65
+ * reaches it on its own, while a click leaves the focus where the pointer
66
+ * already put it. `true` always, `false` never.
67
+ */
68
+ focusOnOpen?: boolean | 'keyboard';
69
+ /**
70
+ * Whether clicking the trigger while the panel is open closes it (default
71
+ * `true`). A menu button should toggle; a text field that opens a picker
72
+ * should not, or clicking into it to place the caret would close the panel.
73
+ */
74
+ closeOnTriggerClick?: boolean;
75
+ /**
76
+ * `'trigger'` gives the panel at least the trigger's width, for a list that
77
+ * should line up with the field it belongs to. `'auto'` (the default) lets it
78
+ * size to its own content.
79
+ */
80
+ width?: 'auto' | 'trigger';
81
+ /**
82
+ * Cap the panel at a readable measure (default `true`). Off for a panel with
83
+ * its own width, a calendar or a media grid.
84
+ */
85
+ constrainWidth?: boolean;
55
86
  /** Where the panel opens: anchored to the trigger (`Default`), pinned to the bottom of the
56
87
  * screen, or the latter only on a small touch screen. Only a `Click` trigger presents as a
57
88
  * sheet. */
@@ -12,6 +12,25 @@ export interface UsePopoverPositionArgs {
12
12
  panelRef: RefObject<HTMLElement | null>;
13
13
  /** Arrow element; gets `--arrow-x` / `--arrow-y` written to it. Pass a null ref when `withArrow` is false. */
14
14
  arrowRef: RefObject<HTMLElement | null>;
15
+ /**
16
+ * Give the panel at least the trigger's width, for a list that lines up with
17
+ * the field it belongs to. A minimum rather than a fixed width: a menu on a
18
+ * narrow icon button still sizes to its own content.
19
+ */
20
+ matchTriggerWidth?: boolean;
21
+ /**
22
+ * When the panel does not fit on the side its alignment puts it, narrow it
23
+ * rather than sliding it away from the trigger. Sliding keeps the panel
24
+ * on-screen but detaches it: an end-aligned bubble stops ending where its
25
+ * trigger ends, and its arrow stops pointing at anything. Off by default, since
26
+ * a panel whose width is its content, a menu or a list, would rather overhang
27
+ * its trigger than wrap.
28
+ *
29
+ * Only down to `MIN_SHRINK_WIDTH`. Past that the panel is narrower than it is
30
+ * readable, and a trigger that close to the screen edge is better served by a
31
+ * panel that overhangs it.
32
+ */
33
+ shrinkToFit?: boolean;
15
34
  }
16
35
  /**
17
36
  * Positions a portaled, `position: fixed` popover on any of the four sides of its trigger with
@@ -22,4 +41,4 @@ export interface UsePopoverPositionArgs {
22
41
  * `useAnchorTracking` supplies the lifecycle around this: mount wait, ancestor resolution, and
23
42
  * re-running on scroll, viewport resize and panel size changes.
24
43
  */
25
- export declare function usePopoverPosition({ isOpen, placement, align, offset, triggerRef, floatingRef, panelRef, arrowRef, }: UsePopoverPositionArgs): void;
44
+ export declare function usePopoverPosition({ isOpen, placement, align, offset, triggerRef, floatingRef, panelRef, arrowRef, matchTriggerWidth, shrinkToFit, }: UsePopoverPositionArgs): void;
@@ -1,7 +1,7 @@
1
+ import { PopoverAlign } from '../popover/popover.types';
1
2
  import { CSSProperties, ReactNode } from 'react';
2
3
  import { IconContent } from '../types/icon.types';
3
4
  import { ButtonStyleType, ButtonSize } from '../button/button.types';
4
- import { Align } from '../floatingMenu/floatingMenu.types';
5
5
  import { SlotClassNames, SlotStyles, HtmlProps } from '../types/slots.types';
6
6
  export interface SplitButtonAction {
7
7
  id: string;
@@ -24,8 +24,8 @@ export interface SplitButtonProps extends HtmlProps {
24
24
  size?: ButtonSize;
25
25
  disabled?: boolean;
26
26
  isLoading?: boolean;
27
- /** Alignment of the actions menu relative to the trigger button (default `Align.Right`) */
28
- align?: Align;
27
+ /** Alignment of the actions menu relative to the trigger button (default `PopoverAlign.End`) */
28
+ align?: PopoverAlign;
29
29
  className?: string;
30
30
  style?: CSSProperties;
31
31
  classNames?: SlotClassNames<SplitButtonSlots>;
@@ -53,8 +53,6 @@ export { default as Fab } from './common/fab';
53
53
  export * from './common/fab';
54
54
  export { default as Flip } from './common/flip';
55
55
  export * from './common/flip';
56
- export { default as FloatingMenu } from './common/floatingMenu';
57
- export * from './common/floatingMenu';
58
56
  export * from './common/floorPlan';
59
57
  export { default as IconPicker } from './common/iconPicker';
60
58
  export * from './common/iconPicker';
@@ -73,6 +71,8 @@ export * from './common/kanbanBoard';
73
71
  export { default as KlipyPicker } from './common/klipyPicker';
74
72
  export * from './common/klipyPicker';
75
73
  export * from './common/loading';
74
+ export { default as Menu } from './common/menu';
75
+ export * from './common/menu';
76
76
  export { default as Modal } from './common/modal';
77
77
  export * from './common/modal';
78
78
  export { default as MultiSelect } from './common/multiSelect';
@@ -68,7 +68,7 @@ function Example() {
68
68
  // Long trails: collapse the middle behind a clickable ellipsis once items.length
69
69
  // exceeds maxItems. The ellipsis counts as one of the visible slots, so maxItems={3}
70
70
  // on a 4-item trail shows: Home / ... / Settings. Clicking the ellipsis opens a menu
71
- // (via FloatingMenu) listing the hidden items.
71
+ // (via Popover) listing the hidden items.
72
72
  <Breadcrumb
73
73
  items={[
74
74
  { label: 'Home', href: '/' },
@@ -80,7 +80,7 @@ function Example() {
80
80
  />
81
81
  ```
82
82
 
83
- **Requires:** `<div id="bodyEnd"></div>` in your HTML when `maxItems` is used and the trail actually collapses (the hidden-items menu is a `FloatingMenu`, which renders via portal).
83
+ **Requires:** `<div id="bodyEnd"></div>` in your HTML when `maxItems` is used and the trail actually collapses (the hidden-items menu is a `Popover`, which renders via portal).
84
84
 
85
85
  **BreadcrumbItem:**
86
86
 
@@ -81,6 +81,6 @@ Flattening reaches a child's visible border even when that border doesn't live o
81
81
 
82
82
  **Separators:** any child with `role="separator"` (what `Divider` sets) is excluded from that border-collapsing margin, on both sides — it stays fully visible between its neighbors instead of being partially hidden under one of them. Use a vertical `Divider` (see [Divider.md](Divider.md)) between two buttons when their style has no visible border of its own to merge (`Primary`, `Delete`), so there's still a clear seam between them.
83
83
 
84
- **Note:** the corner-flattening rules key off DOM position (`:first-child`/`:last-child`), so they work for direct `Button`/`Input`/`Dropdown`-family children out of the box. A child that renders as a wrapper around its own button (e.g. a component that portals its real content elsewhere) won't get the flattening automatically. `SplitButton` is exactly that case (its second child is a `FloatingMenu`-wrapped button), which is why it sets its own corner overrides directly rather than relying on `ButtonGroup`'s automatic ones. See [SplitButton.md](SplitButton.md). A fully custom component whose CSS doesn't reference the `--group-radius-*` variables can still be flattened manually the same way, via its own `classNames`/`styles` slots if it exposes the right one.
84
+ **Note:** the corner-flattening rules key off DOM position (`:first-child`/`:last-child`), so they work for direct `Button`/`Input`/`Dropdown`-family children out of the box. A child that renders as a wrapper around its own button (e.g. a component that portals its real content elsewhere) won't get the flattening automatically. `SplitButton` is exactly that case (its second child is a `Popover`-wrapped button), which is why it sets its own corner overrides directly rather than relying on `ButtonGroup`'s automatic ones. See [SplitButton.md](SplitButton.md). A fully custom component whose CSS doesn't reference the `--group-radius-*` variables can still be flattened manually the same way, via its own `classNames`/`styles` slots if it exposes the right one.
85
85
 
86
86
  **Slots:** none — `ButtonGroup` has no named inner elements, only `className`/`style` on the root.
package/docs/CLAUDE.md CHANGED
@@ -142,7 +142,6 @@ FontAwesome internals, so they still take an `IconDefinition` only.
142
142
  @ErrorBoundary.md
143
143
  @Fab.md
144
144
  @Flip.md
145
- @FloatingMenu.md
146
145
  @FloorPlan.md
147
146
  @FormValidator.md
148
147
  @FormValidatorGroup.md
@@ -155,6 +154,7 @@ FontAwesome internals, so they still take an `IconDefinition` only.
155
154
  @KanbanBoard.md
156
155
  @KlipyPicker.md
157
156
  @Loading.md
157
+ @Menu.md
158
158
  @Modal.md
159
159
  @MultiSelect.md
160
160
  @NumberInput.md
package/docs/Card.md CHANGED
@@ -60,7 +60,7 @@ All sub-components are optional — use only what you need. Stacked sub-componen
60
60
  | `hoverEffect` | `boolean` | Adds hover elevation |
61
61
  | `tabIndex` | `number \| null` | For keyboard navigation |
62
62
  | `autoGap` | `boolean` | Space stacked sub-components apart with a shared gap (default `true`); set `false` to space them yourself |
63
- | `clip` | `boolean` | Clip content to the rounded corners (`overflow: hidden`, default `false`). Set `true` for full-bleed `CardMedia` or a clipped hover/ripple effect. Safe to enable even when the card holds a `Dropdown`/`FloatingMenu`/`Tooltip` — those popovers are portaled and always escape the card regardless of this setting |
63
+ | `clip` | `boolean` | Clip content to the rounded corners (`overflow: hidden`, default `false`). Set `true` for full-bleed `CardMedia` or a clipped hover/ripple effect. Safe to enable even when the card holds a `Dropdown`/`Popover`/`Tooltip` — those popovers are portaled and always escape the card regardless of this setting |
64
64
 
65
65
  **CardHeaderProps:**
66
66
 
@@ -53,4 +53,6 @@ The picker panel deliberately does not. It is a floating control surface with it
53
53
 
54
54
  **Labels:** `pickFromScreen` — the text this component renders of its own. Pass `labels` to override any of them, on the component or app-wide through `ConfigProvider`; they merge per key. Type: `ColorPickerLabels`.
55
55
 
56
+ **Keyboard:** the swatch is a button, so `Enter` or `Space` opens the panel and puts the focus on the hue slider, the arrow keys move it, `Tab` walks the rest of the panel, and `Escape` closes and hands the focus back. The saturation and value square is a pointer control with no keyboard equivalent; the hex field covers the same ground for anyone not using a mouse.
57
+
56
58
  **Slots:** `root` `swatch` `panel` `gradient` `hueSlider` `alphaSlider` `hexInput` `eyeDropper`
@@ -59,7 +59,7 @@ So a global default only fills in props a given instance left out — any instan
59
59
 
60
60
  ## App-wide `presentation`
61
61
 
62
- `FloatingMenu`, `Dropdown`, `Popover` and `Modal` can each present as a bottom sheet instead of anchored to their trigger (centred, for `Modal`). They're unrelated components, so setting that through `defaultProps` would mean four entries, and missing one would leave sheets in half an app. `presentation` is the single switch:
62
+ `Popover`, `Menu`, `Dropdown`, `MultiSelect` and `Modal` can each present as a bottom sheet instead of anchored to their trigger (centred, for `Modal`). They're unrelated components, so setting that through `defaultProps` would mean five entries, and missing one would leave sheets in half an app. `presentation` is the single switch:
63
63
 
64
64
  ```tsx
65
65
  // Bottom sheets on small touch screens, everywhere, in one line
@@ -93,7 +93,7 @@ Each entry is a `Partial<...Props>`, so any of that component's props can be def
93
93
 
94
94
  - **Form inputs:** `Input` · `Textarea` · `NumberInput` · `Dropdown` · `InputDropdown` · `Checkbox` · `Switch` · `RadioGroup` · `DatePicker` · `OptionPicker` · `OtpInput`
95
95
  - **Display:** `Button` · `ActionButtons` · `Badge` · `Chip` · `Card` · `SectionHeader` · `Skeleton` · `Accordion` · `Divider` · `Timer`
96
- - **Overlays:** `ConfirmModal` · `Modal` · `FloatingMenu` · `Popover`
96
+ - **Overlays:** `ConfirmModal` · `Modal` · `Popover` · `Popover`
97
97
  - **Editors:** `RoomDrawer`, `RoomViewer`
98
98
 
99
99
  ```tsx
package/docs/Dropdown.md CHANGED
@@ -62,7 +62,7 @@ const statusValidator = new FormValidator('', [Validators.required()]);
62
62
  | `presentation` | `Presentation` | Where the list opens: anchored to the input (`Default`), always a bottom sheet, or a sheet only on a small touch screen. Imported from the same package: `import { Presentation } from '@ahrowe/ui'` |
63
63
  | `alwaysFloatLabel` | `boolean` | Keep the material label floated in the border notch even when nothing is selected, so an empty dropdown reads as blank instead of showing the label as if it were the selected value. Only applies with `useMatLabelStyle` (default) |
64
64
 
65
- The list is rendered through a portal and tracks the trigger across scroll, so it always escapes clipping ancestors (cards, scroll containers, virtualized lists). It flips to open upward automatically when there's no room below, and shifts horizontally to stay within the viewport when the trigger sits near the left or right edge of the screen. It hides (without closing) if the trigger itself scrolls behind a clipping ancestor, reappearing once it's back in view. It also re-measures when its own content changes size while open, so a list that shrinks (e.g. filtered down to fewer entries) stays anchored to the trigger instead of hanging above it.
65
+ The list is rendered through a portal and tracks the trigger across scroll, so it always escapes clipping ancestors (cards, scroll containers, virtualized lists). The panel itself is a [Popover](Popover.md), so it flips, clamps and presents as a bottom sheet the same way every other anchored surface in the library does. It flips to open upward automatically when there's no room below, and shifts horizontally to stay within the viewport when the trigger sits near the left or right edge of the screen. It hides (without closing) if the trigger itself scrolls behind a clipping ancestor, reappearing once it's back in view. It also re-measures when its own content changes size while open, so a list that shrinks (e.g. filtered down to fewer entries) stays anchored to the trigger instead of hanging above it.
66
66
 
67
67
  **Bottom sheet:** `presentation={Presentation.Sheet}` drops the anchoring entirely and pins the list to the bottom of the screen instead, full width up to 560px, behind a backdrop, scrolling internally past 70% of the viewport height and lifting itself above an open on-screen keyboard, shrinking to fit the space that leaves rather than running off the top of the screen. Tapping the backdrop closes the list without also pressing whatever sits behind it. It is the same list with the same keyboard handling, so selection, highlighting and infinite scroll are unchanged. `Presentation.Auto` does that only when the pointer is coarse *and* the screen is under 768px wide. The default is `Presentation.Default`. A whole app can opt in with `<ConfigProvider defaultProps={{ Dropdown: { presentation: Presentation.Auto } }}>`, or for every sheet-capable component at once with `<ConfigProvider presentation={Presentation.Auto}>` — see [ConfigProvider.md](ConfigProvider.md).
68
68
 
@@ -71,4 +71,4 @@ interface InputDropdownItem {
71
71
 
72
72
  Long lists are handled by capping how many matches are rendered (`maxItems`, default `100`) rather than by virtualizing: only about six rows fit in the scroll box, so anything past the cap is DOM the user never sees. Raise `maxItems` (or set it to `0`) if a list is genuinely meant to be scrolled end to end. For lists too large to pass as `items` at all, fetch on the server, keep the results in your own state and feed them in as `items`.
73
73
 
74
- The list is rendered through a portal and tracks the trigger across scroll, so it always escapes clipping ancestors (cards, scroll containers, virtualized lists). It flips to open upward automatically when there's no room below, and shifts horizontally to stay within the viewport when the trigger sits near the left or right edge of the screen. It hides (without closing) if the trigger itself scrolls behind a clipping ancestor, reappearing once it's back in view. It also re-measures when its own content changes size while open, so a list that shrinks (e.g. filtered down to fewer entries) stays anchored to the trigger instead of hanging above it.
74
+ The list is rendered through a portal and tracks the trigger across scroll, so it always escapes clipping ancestors (cards, scroll containers, virtualized lists). The panel itself is a [Popover](Popover.md), so it flips, clamps and presents as a bottom sheet the same way every other anchored surface in the library does. It flips to open upward automatically when there's no room below, and shifts horizontally to stay within the viewport when the trigger sits near the left or right edge of the screen. It hides (without closing) if the trigger itself scrolls behind a clipping ancestor, reappearing once it's back in view. It also re-measures when its own content changes size while open, so a list that shrinks (e.g. filtered down to fewer entries) stays anchored to the trigger instead of hanging above it.
@@ -2,6 +2,8 @@
2
2
 
3
3
  **When to use:** Accessible `<div>` that behaves like a button — click and keyboard (Enter/Space) events, with proper ARIA semantics. Use when you need a custom-styled clickable container that isn't a `<button>`.
4
4
 
5
+ Enter and Space dispatch a real click, the way a native button does, so `onClick` always receives a `MouseEvent` and a handler on an element above this one sees the activation too.
6
+
5
7
  **Keywords:** custom button div
6
8
 
7
9
  **Import:** `import { InteractableDiv } from '@ahrowe/ui'`
package/docs/Menu.md ADDED
@@ -0,0 +1,107 @@
1
+ # Menu
2
+
3
+ **When to use:** A list of actions hanging off a trigger — a row's "more" button, a toolbar overflow, a context menu on a card. Use [Popover](Popover.md) directly when the panel holds something other than a list of actions, and [Dropdown](Dropdown.md) when the user is picking a value rather than doing something.
4
+
5
+ **Keywords:** context menu, action menu, overflow menu, kebab menu, three dots, popup menu, right click, anchored list
6
+
7
+ **Import:** `import { Menu } from '@ahrowe/ui'`
8
+ **Types:** `import type { MenuProps, MenuEntry, MenuAction } from '@ahrowe/ui'`
9
+
10
+ **Requires:** `<div id="bodyEnd"></div>` in your app: the menu is portalled there, like every other anchored panel in the library.
11
+
12
+ ```tsx
13
+ import { Menu } from '@ahrowe/ui';
14
+ import type { MenuEntry } from '@ahrowe/ui';
15
+ import { faPen, faCopy, faTrash } from '@fortawesome/free-solid-svg-icons';
16
+
17
+ const items: MenuEntry[] = [
18
+ { id: 'edit', label: 'Edit', icon: faPen, shortcut: '⌘E' },
19
+ { id: 'duplicate', label: 'Duplicate', icon: faCopy },
20
+ { separator: true },
21
+ { id: 'delete', label: 'Delete', icon: faTrash, danger: true },
22
+ ];
23
+
24
+ <Menu items={items} onAction={(id) => run(id)}>
25
+ <Button>Actions</Button>
26
+ </Menu>;
27
+ ```
28
+
29
+ **Entries** are one of three shapes, and the list is read in the order you give it:
30
+
31
+ ```ts
32
+ { id: 'edit', label: 'Edit', icon, shortcut, disabled, danger, onClick } // an action
33
+ { separator: true } // a line
34
+ { heading: 'This document' } // a label over what follows
35
+ ```
36
+
37
+ An action runs its own `onClick` first, then `onAction` on the menu, then the menu closes. Use whichever of the two fits: a handler per entry when they are unrelated, `onAction` when they funnel into one place.
38
+
39
+ ```tsx
40
+ // Grouped, with a heading over each group
41
+ <Menu
42
+ items={[
43
+ { heading: 'This document' },
44
+ { id: 'rename', label: 'Rename' },
45
+ { separator: true },
46
+ { heading: 'Elsewhere' },
47
+ { id: 'open', label: 'Open in a new tab' },
48
+ ]}
49
+ >
50
+ <ActionIcon icon={faEllipsisVertical} aria-label='More actions' />
51
+ </Menu>
52
+
53
+ // A sheet on a small touch screen, anchored everywhere else
54
+ <Menu items={items} presentation={Presentation.Auto}>
55
+ <Button>Open</Button>
56
+ </Menu>
57
+ ```
58
+
59
+ **Keyboard:** the menu takes one tab stop, and follows the menu-button pattern.
60
+
61
+ | Key | Does |
62
+ |-----|------|
63
+ | `↓` on the closed trigger | Open, focus the first entry |
64
+ | `↑` on the closed trigger | Open, focus the last entry |
65
+ | `↓` `↑` on the trigger of a menu opened by mouse | Move into the list, at the first or last entry |
66
+ | `↑` `↓` | Move between entries, wrapping, skipping disabled ones |
67
+ | `Home` `End` | First or last entry |
68
+ | a letter | Jump to the next entry that starts with it; keep typing to narrow, or repeat one letter to walk the matches |
69
+ | `Enter` `Space` | Run the focused entry |
70
+ | `Escape` | Close and hand focus back to the trigger |
71
+ | `Tab` | Close, and carry on to whatever follows the trigger, from any entry |
72
+ | `Shift+Tab` | Close, and hand focus back to the trigger |
73
+
74
+ A click opens the menu without moving the focus, so no entry is marked until you point at one or press an arrow key: a highlight that does not follow the mouse reads as a selection. Opening with the keyboard, including `Enter` and `Space`, puts the focus on an entry straight away.
75
+
76
+ Separators and headings are never focused. The trigger is given `aria-haspopup`, `aria-expanded` and `aria-controls` unless it sets them itself, so a screen reader announces it as a menu button.
77
+
78
+ **Key props:**
79
+
80
+ | Prop | Type | Description |
81
+ |------|------|-------------|
82
+ | `items` | `MenuEntry[]` | What the menu offers (required) |
83
+ | `children` | `ReactNode` | The trigger |
84
+ | `onAction` | `(id: string) => void` | Called with the id of the entry that was picked |
85
+ | `isOpen` | `boolean` | Controlled open state; leave it out to let the menu manage its own |
86
+ | `onOpenChange` | `(isOpen: boolean) => void` | Called when the menu wants to open or close |
87
+ | `placement` | `PopoverPlacement` | Which side of the trigger it opens on (default `Bottom`) |
88
+ | `align` | `PopoverAlign` | Which edge it lines up with (default `Start`) |
89
+ | `presentation` | `Presentation` | Anchored, a bottom sheet, or a sheet only on touch |
90
+
91
+ **Entry fields (`MenuAction`):**
92
+
93
+ | Field | Type | Description |
94
+ |-------|------|-------------|
95
+ | `id` | `string` | Stable id, also what `onAction` reports |
96
+ | `label` | `ReactNode` | What the entry says |
97
+ | `icon` | `IconContent` | Leading icon |
98
+ | `shortcut` | `ReactNode` | Hint on the trailing edge, e.g. `⌘K`. Display only: binding it is yours |
99
+ | `disabled` | `boolean` | Not clickable, and skipped by the arrow keys |
100
+ | `danger` | `boolean` | Renders in the destructive colour |
101
+ | `onClick` | `() => void` | Runs before `onAction` |
102
+
103
+ **Slots:** `root` `trigger` `menu` `item` `itemIcon` `itemLabel` `itemShortcut` `separator` `heading`
104
+
105
+ `root` and `trigger` generate no box, so your trigger keeps the place it would have had without a Menu around it. A `className` or `style` on those two cannot paint; style the trigger you pass in instead.
106
+
107
+ **No submenus yet.** An entry is a single action. Nesting brings its own interaction model — hover intent, a second layer of focus management, and a different shape again as a sheet on touch — so it is a separate piece of work rather than a field on `MenuAction`.
package/docs/Modal.md CHANGED
@@ -10,7 +10,7 @@
10
10
 
11
11
  The panel itself never scrolls: the header is fixed and the children scroll inside the `body` slot, so the title and close button stay visible no matter how long the content is.
12
12
 
13
- While open, the modal traps focus (focus moves into the panel and returns to whatever was focused before on close), locks scrolling on the page behind it, and closes on Escape. Modals nest correctly: Escape closes only the topmost one, and the page stays locked until the last one closes. The same applies to anything opened from inside a modal — a `FloatingMenu`, `Dropdown` or `Popover` counts as the layer above it, so Escape closes that first, and the modal's focus trap leaves focus alone while it's open (all three render through the same portal, so their DOM sits beside the modal's panel rather than inside it).
13
+ While open, the modal traps focus (focus moves into the panel and returns to whatever was focused before on close), locks scrolling on the page behind it, and closes on Escape. Modals nest correctly: Escape closes only the topmost one, and the page stays locked until the last one closes. The same applies to anything opened from inside a modal — a `Menu`, `Dropdown` or `Popover` counts as the layer above it, so Escape closes that first, and the modal's focus trap leaves focus alone while it's open (all three render through the same portal, so their DOM sits beside the modal's panel rather than inside it).
14
14
 
15
15
  ```tsx
16
16
  import { Modal, ActionButtons } from '@ahrowe/ui';
package/docs/Popover.md CHANGED
@@ -1,8 +1,8 @@
1
1
  # Popover
2
2
 
3
- **When to use:** A floating panel anchored to a trigger — profile cards, quick forms, action menus, help panels, filter dropdowns. Reach for `Tooltip` for plain text hints, `FloatingMenu` for a menu that mirrors the trigger width; use `Popover` when you need free content on any side with an arrow.
3
+ **When to use:** Any panel anchored to a trigger: profile cards, quick forms, action menus, pickers, filter dropdowns. It is the one anchored surface in the library, and it absorbed `FloatingMenu`, which used to be the other one. Reach for `Tooltip` instead when all you need is a plain text hint on hover.
4
4
 
5
- **Keywords:** hover card, info panel, bottom sheet, action sheet, mobile, touch
5
+ **Keywords:** hover card, info panel, bottom sheet, action sheet, mobile, touch, anchored panel, dropdown surface
6
6
 
7
7
  **Import:** `import { Popover } from '@ahrowe/ui'`
8
8
 
@@ -49,7 +49,7 @@ import { Popover, PopoverPlacement, PopoverAlign, PopoverTrigger } from '@ahrowe
49
49
  | `trigger` | `PopoverTrigger` | `Click` | What opens the popover |
50
50
  | `isOpen` | `boolean` | — | Controlled open state (omit for uncontrolled) |
51
51
  | `defaultOpen` | `boolean` | `false` | Initial open state when uncontrolled |
52
- | `onOpenChange` | `(isOpen: boolean) => void` | — | Open-state change callback |
52
+ | `onOpenChange` | `(isOpen, { fromKeyboard }) => void` | — | Open-state change callback. `fromKeyboard` says whether a key caused it, which is what a menu uses to decide between putting the focus on an entry and leaving it on the trigger |
53
53
  | `offset` | `number` | `10` | Gap in px between trigger and panel |
54
54
  | `withArrow` | `boolean` | `true` | Render the arrow pointing at the trigger |
55
55
  | `closeOnOutsideClick` | `boolean` | `true` | Close on click outside (ignored for hover trigger) |
@@ -58,15 +58,21 @@ import { Popover, PopoverPlacement, PopoverAlign, PopoverTrigger } from '@ahrowe
58
58
  | `closeDelay` | `number` | `120` | Hover close delay in ms |
59
59
  | `disabled` | `boolean` | `false` | Prevent opening |
60
60
  | `presentation` | `Presentation` | `Default` | Where the panel opens: anchored to the trigger, always a bottom sheet, or a sheet only on a small touch screen |
61
+ | `focusOnOpen` | `boolean \| 'keyboard'` | `'keyboard'` | Move focus into the panel on open, and claim `role="dialog"` with it. `'keyboard'` does it only when the open came from the keyboard, which is the only way into a panel portaled to the end of the document, while a click leaves the focus where the pointer already put it. `true` always, `false` never |
62
+ | `closeOnTriggerClick` | `boolean` | `true` | Whether clicking the trigger while open closes it. Off for a field that opens a picker, where a click to place the caret must not take the panel away |
63
+ | `width` | `'auto' \| 'trigger'` | `'auto'` | `'trigger'` gives the panel at least the trigger's width, for a list that lines up with its field |
64
+ | `constrainWidth` | `boolean` | `true` | Cap the panel at a readable measure. Off for a panel with its own width, a calendar or a media grid |
61
65
 
62
66
  **Slots:** `root` `trigger` `floating` `panel` `arrow` `backdrop` (`backdrop` only exists while presenting as a sheet)
63
67
 
68
+ `root` and `trigger` generate no box of their own, so your trigger sits in your layout exactly as it would without Popover around it: it is the flex item, the grid item, the block-level child, and a row that stretches its children reaches it. The trade is that a `className` or `style` on those two cannot paint a background, padding or a border. Style the trigger you pass in instead.
69
+
64
70
  **Bottom sheet:** `presentation={Presentation.Sheet}` drops the anchoring and the arrow, and pins the panel to the bottom of the screen instead — full width up to 560px, behind a backdrop, scrolling internally past 70% of the viewport height, lifted above an open on-screen keyboard, and shrunk to fit the space that leaves rather than running off the top of the screen. Scrolling on the page behind it is locked while it's open, and tapping the backdrop closes it without pressing whatever sits behind it (`closeOnOutsideClick={false}` leaves the backdrop inert but still blocking). `Presentation.Auto` applies that only when the pointer is coarse *and* the screen is under 768px wide.
65
71
 
66
72
  Only a `Click` trigger presents as a sheet. `Hover` and `Focus` open something small next to whatever caused them, which is the opposite of a screen-owning sheet, so they stay anchored regardless of `presentation`.
67
73
 
68
74
  **Global defaults:** adopts `ConfigProvider`, e.g. `defaultProps={{ Popover: { presentation: Presentation.Auto } }}`, or `presentation={Presentation.Auto}` on the provider itself to set every sheet-capable component at once. See [ConfigProvider.md](ConfigProvider.md).
69
75
 
70
- Requires a `<div id="bodyEnd"></div>` at the app root — the panel is portaled there, same as `Tooltip` and `FloatingMenu`.
76
+ Requires a `<div id="bodyEnd"></div>` at the app root — the panel is portaled there, same as `Tooltip` and `Menu`.
71
77
 
72
- **Keyboard & focus (click trigger):** the panel is portaled to the end of the DOM, so Tab can't reach it naturally. When the panel has focusable content, opening it (click trigger only) moves focus to the first focusable element and the panel gets `role="dialog"`. Tabbing past the last element closes the popover and moves focus to the element after the trigger; Shift+Tab past the first element, and Escape, close it and return focus to the trigger. This is non-modal — focus is not trapped, and hover/focus triggers don't steal focus. Escape closes only the innermost open layer, so a popover inside a `Modal` closes on the first press and leaves the modal open. Popovers with only static content don't manage focus or claim the dialog role.
78
+ **Keyboard & focus (click trigger):** the panel is portaled to the end of the DOM, so Tab can't reach it naturally. When the panel has focusable content and the keyboard opened it (Enter or Space on the trigger, or ArrowDown/ArrowUp), focus moves to the first focusable element and the panel gets `role="dialog"`; a pointer click leaves the focus where it already is. `focusOnOpen` forces either. Tabbing past the last element closes the popover and moves focus to the element after the trigger; Shift+Tab past the first element, and Escape, close it and return focus to the trigger. This is non-modal: focus is not trapped, and hover/focus triggers don't steal focus. Escape closes only the innermost open layer, so a popover inside a `Modal` closes on the first press and leaves the modal open. Popovers with only static content don't manage focus or claim the dialog role.