@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.
- package/dist/esm/common/actionIcon/actionIcon.module.mjs.map +1 -1
- package/dist/esm/common/breadcrumb/breadcrumb.mjs +1 -1
- package/dist/esm/common/breadcrumb/breadcrumb.mjs.map +1 -1
- package/dist/esm/common/breadcrumb/breadcrumb.module.mjs +1 -1
- package/dist/esm/common/breadcrumb/breadcrumb.module.mjs.map +1 -1
- package/dist/esm/common/colorPicker/colorPicker.mjs +1 -1
- package/dist/esm/common/colorPicker/colorPicker.mjs.map +1 -1
- package/dist/esm/common/colorPicker/colorPicker.module.mjs.map +1 -1
- package/dist/esm/common/configProvider/configProvider.mjs.map +1 -1
- package/dist/esm/common/configProvider/presentation.types.mjs +2 -0
- package/dist/esm/common/configProvider/presentation.types.mjs.map +1 -0
- package/dist/esm/common/configProvider/usePresentationDefault.mjs.map +1 -1
- package/dist/esm/common/datePicker/components/datePickerMonth/datePickerMonth.module.mjs.map +1 -1
- package/dist/esm/common/datePicker/datePicker.mjs +1 -1
- package/dist/esm/common/datePicker/datePicker.mjs.map +1 -1
- package/dist/esm/common/dropdown/dropdown.mjs +1 -1
- package/dist/esm/common/dropdown/dropdown.mjs.map +1 -1
- package/dist/esm/common/dropdown/dropdown.module.mjs +1 -1
- package/dist/esm/common/dropdown/dropdown.module.mjs.map +1 -1
- package/dist/esm/common/hooks/useAnchorTracking.mjs.map +1 -1
- package/dist/esm/common/hooks/useFocusBoundary.mjs +1 -1
- package/dist/esm/common/hooks/useFocusBoundary.mjs.map +1 -1
- package/dist/esm/common/hooks/useSheet.mjs +1 -1
- package/dist/esm/common/hooks/useSheet.mjs.map +1 -1
- package/dist/esm/common/inputDropdown/inputDropdown.mjs +1 -1
- package/dist/esm/common/inputDropdown/inputDropdown.mjs.map +1 -1
- package/dist/esm/common/inputDropdown/inputDropdown.module.mjs +1 -1
- package/dist/esm/common/inputDropdown/inputDropdown.module.mjs.map +1 -1
- package/dist/esm/common/interactableDiv/interactableDiv.mjs +1 -1
- package/dist/esm/common/interactableDiv/interactableDiv.mjs.map +1 -1
- package/dist/esm/common/klipyPicker/klipyPicker.mjs +1 -1
- package/dist/esm/common/klipyPicker/klipyPicker.mjs.map +1 -1
- package/dist/esm/common/menu/menu.mjs +2 -0
- package/dist/esm/common/menu/menu.mjs.map +1 -0
- package/dist/esm/common/menu/menu.module.mjs +2 -0
- package/dist/esm/common/menu/menu.module.mjs.map +1 -0
- package/dist/esm/common/menu/menu.types.mjs +2 -0
- package/dist/esm/common/menu/menu.types.mjs.map +1 -0
- package/dist/esm/common/multiSelect/multiSelect.mjs +1 -1
- package/dist/esm/common/multiSelect/multiSelect.mjs.map +1 -1
- package/dist/esm/common/multiSelect/multiSelect.module.mjs +1 -1
- package/dist/esm/common/multiSelect/multiSelect.module.mjs.map +1 -1
- package/dist/esm/common/popover/popover.mjs +1 -1
- package/dist/esm/common/popover/popover.mjs.map +1 -1
- package/dist/esm/common/popover/popover.module.mjs +1 -1
- package/dist/esm/common/popover/popover.module.mjs.map +1 -1
- package/dist/esm/common/popover/popover.types.mjs.map +1 -1
- package/dist/esm/common/popover/usePopoverPosition.mjs +1 -1
- package/dist/esm/common/popover/usePopoverPosition.mjs.map +1 -1
- package/dist/esm/common/splitButton/splitButton.mjs +1 -1
- package/dist/esm/common/splitButton/splitButton.mjs.map +1 -1
- package/dist/esm/common/splitButton/splitButton.module.mjs +1 -1
- package/dist/esm/common/splitButton/splitButton.module.mjs.map +1 -1
- package/dist/esm/common/timeInput/timeInput.mjs +1 -1
- package/dist/esm/common/timeInput/timeInput.mjs.map +1 -1
- package/dist/esm/common/toast/toast.module.mjs.map +1 -1
- package/dist/esm/common/tooltip/tooltip.mjs +1 -1
- package/dist/esm/common/tooltip/tooltip.mjs.map +1 -1
- package/dist/esm/common/tooltip/tooltip.module.mjs +1 -1
- package/dist/esm/common/tooltip/tooltip.module.mjs.map +1 -1
- package/dist/esm/common/virtualList/virtualList.module.mjs +1 -1
- package/dist/esm/common/virtualList/virtualList.module.mjs.map +1 -1
- package/dist/esm/common/virtualList/virtualListHeader.mjs +1 -1
- package/dist/esm/common/virtualList/virtualListHeader.mjs.map +1 -1
- package/dist/esm/index.mjs +1 -1
- package/dist/index.cjs +3 -3
- package/dist/index.cjs.map +1 -1
- package/dist/style.css +1 -1
- package/dist/types/common/configProvider/configProvider.d.ts +1 -1
- package/dist/types/common/configProvider/configProvider.types.d.ts +2 -3
- package/dist/types/common/configProvider/index.d.ts +1 -0
- package/dist/types/common/configProvider/presentation.types.d.ts +14 -0
- package/dist/types/common/configProvider/usePresentationDefault.d.ts +1 -1
- package/dist/types/common/dropdown/dropdown.types.d.ts +1 -1
- package/dist/types/common/hooks/useAnchorTracking.d.ts +1 -1
- package/dist/types/common/hooks/useFocusBoundary.d.ts +3 -3
- package/dist/types/common/hooks/useSheet.d.ts +1 -1
- package/dist/types/common/menu/index.d.ts +2 -0
- package/dist/types/common/menu/menu.d.ts +14 -0
- package/dist/types/common/menu/menu.types.d.ts +55 -0
- package/dist/types/common/modal/modal.types.d.ts +1 -1
- package/dist/types/common/multiSelect/multiSelect.types.d.ts +1 -1
- package/dist/types/common/popover/popover.types.d.ts +34 -3
- package/dist/types/common/popover/usePopoverPosition.d.ts +20 -1
- package/dist/types/common/splitButton/splitButton.types.d.ts +3 -3
- package/dist/types/index.d.ts +2 -2
- package/docs/Breadcrumb.md +2 -2
- package/docs/ButtonGroup.md +1 -1
- package/docs/CLAUDE.md +1 -1
- package/docs/Card.md +1 -1
- package/docs/ColorPicker.md +2 -0
- package/docs/ConfigProvider.md +2 -2
- package/docs/Dropdown.md +1 -1
- package/docs/InputDropdown.md +1 -1
- package/docs/InteractableDiv.md +2 -0
- package/docs/Menu.md +107 -0
- package/docs/Modal.md +1 -1
- package/docs/Popover.md +11 -5
- package/docs/SplitButton.md +8 -4
- package/docs/ThemeProvider.md +1 -1
- package/docs/VirtualList.md +2 -2
- package/package.json +1 -1
- package/dist/esm/common/floatingMenu/floatingMenu.mjs +0 -2
- package/dist/esm/common/floatingMenu/floatingMenu.mjs.map +0 -1
- package/dist/esm/common/floatingMenu/floatingMenu.module.mjs +0 -2
- package/dist/esm/common/floatingMenu/floatingMenu.module.mjs.map +0 -1
- package/dist/esm/common/floatingMenu/floatingMenu.types.mjs +0 -2
- package/dist/esm/common/floatingMenu/floatingMenu.types.mjs.map +0 -1
- package/dist/esm/common/floatingMenu/useFloatingPosition.mjs +0 -2
- package/dist/esm/common/floatingMenu/useFloatingPosition.mjs.map +0 -1
- package/dist/types/common/floatingMenu/floatingMenu.d.ts +0 -3
- package/dist/types/common/floatingMenu/floatingMenu.types.d.ts +0 -32
- package/dist/types/common/floatingMenu/index.d.ts +0 -2
- package/dist/types/common/floatingMenu/useFloatingPosition.d.ts +0 -27
- package/docs/FloatingMenu.md +0 -90
package/docs/SplitButton.md
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
# SplitButton
|
|
2
2
|
|
|
3
|
-
**When to use:** A primary action button with a small attached dropdown for closely related secondary actions, e.g. "Save ▾" opening "Save as draft" / "Save and close". Built from `Button`, `ButtonGroup
|
|
3
|
+
**When to use:** A primary action button with a small attached dropdown for closely related secondary actions, e.g. "Save ▾" opening "Save as draft" / "Save and close". Built from `Button`, `ButtonGroup` and [Menu](Menu.md) internally, rather than a mechanism of its own. Reach for `Menu` on its own when there is no primary action to weld the list to.
|
|
4
4
|
|
|
5
5
|
**Keywords:** dropdown button, button with menu
|
|
6
6
|
|
|
7
7
|
**Import:** `import { SplitButton } from '@ahrowe/ui'`
|
|
8
|
-
**Types:** `import type { SplitButtonAction, SplitButtonProps } from '@ahrowe/ui'`
|
|
8
|
+
**Types:** `import type { SplitButtonAction, SplitButtonProps } from '@ahrowe/ui'`
|
|
9
|
+
**Enums:** `import { PopoverAlign } from '@ahrowe/ui'` for `align`.
|
|
9
10
|
|
|
10
|
-
**Requires:** `<div id="bodyEnd"></div>` in your HTML (the actions menu
|
|
11
|
+
**Requires:** `<div id="bodyEnd"></div>` in your HTML (the actions menu is a `Menu`, which portals).
|
|
11
12
|
|
|
12
13
|
```tsx
|
|
13
14
|
import { SplitButton } from '@ahrowe/ui';
|
|
@@ -73,11 +74,14 @@ import { ButtonStyleType } from '@ahrowe/ui';
|
|
|
73
74
|
| `size` | `ButtonSize` | Size variant, shared by both buttons |
|
|
74
75
|
| `disabled` | `boolean` | Disables both the primary and trigger button |
|
|
75
76
|
| `isLoading` | `boolean` | Shows a spinner on the primary button (same as `Button`'s own `isLoading`) |
|
|
77
|
+
| `align` | `PopoverAlign` | Which edge of the group the menu lines up with (default `End`, the chevron's side). The menu is a [Menu](Menu.md), hence `Popover`'s enum |
|
|
76
78
|
|
|
77
79
|
**`isLoading` vs. `disabled`:** `isLoading` only affects the primary button — the trigger and its menu stay usable while the primary action is in flight, e.g. to cancel via a menu action. To lock the whole control during the async action instead, pass both: `isLoading={isSaving} disabled={isSaving}`. Setting `disabled` also closes the menu if it happened to be open already, and blocks any in-flight click on a menu action.
|
|
78
80
|
|
|
79
81
|
**Global defaults:** adopts `ConfigProvider`, e.g. `defaultProps={{ SplitButton: { styleType: ButtonStyleType.Default } }}`. See [ConfigProvider.md](ConfigProvider.md).
|
|
80
82
|
|
|
81
|
-
**
|
|
83
|
+
**Keyboard:** the list is a [Menu](Menu.md), so it behaves like one: `↓` `↑` move between actions and skip disabled ones, `Home` and `End` jump to either end, typing a letter jumps to the action that starts with it, `Enter` and `Space` run the focused action, and `Escape` closes and hands the focus back. A click opens the menu without taking the focus, so nothing is marked until you point at an action or press an arrow key; opening with the keyboard focuses the first action straight away. The trigger announces itself as a menu button.
|
|
84
|
+
|
|
85
|
+
**Slots:** `root` `primaryButton` `triggerButton` `menu` `menuItem` (`menu` is Menu's list, `menuItem` one of its rows)
|
|
82
86
|
|
|
83
87
|
**Labels:** `moreActions` — 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: `SplitButtonLabels`.
|
package/docs/ThemeProvider.md
CHANGED
|
@@ -175,5 +175,5 @@ through its own variable (`--slider-range-color`, `--pagination-active-backgroun
|
|
|
175
175
|
|
|
176
176
|
Providers nest: an inner one overrides the variables it declares and inherits the rest, so a differently-themed panel inside a themed app works without restating the whole theme.
|
|
177
177
|
|
|
178
|
-
A portal breaks that by default, because theme variables travel by CSS inheritance and a portal escapes the subtree. Anything rendered through `BodyEnd` — `Tooltip`, `Dropdown`, `InputDropdown`, `ColorPicker`, `
|
|
178
|
+
A portal breaks that by default, because theme variables travel by CSS inheritance and a portal escapes the subtree. Anything rendered through `BodyEnd` — `Tooltip`, `Dropdown`, `InputDropdown`, `ColorPicker`, `Menu`, `Popover`, `Modal`, `Toast` — therefore carries the nearest provider's variables with it, so an open panel matches the panel that opened it rather than whatever wraps `#bodyEnd`.
|
|
179
179
|
|
package/docs/VirtualList.md
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
**Import:** `import { VirtualList } from '@ahrowe/ui'`
|
|
8
8
|
**Types:** `import type { VirtualListProps, VirtualListColumn, VirtualListHandle, VirtualListSort } from '@ahrowe/ui'`
|
|
9
9
|
|
|
10
|
-
**Requires:** `<div id="bodyEnd"></div>` in your app **only if** you use the column-visibility toggle (the gear menu) — its popover is a `
|
|
10
|
+
**Requires:** `<div id="bodyEnd"></div>` in your app **only if** you use the column-visibility toggle (the gear menu) — its popover is a `Popover` that renders via portal. Lists without columns, or with `showColumnToggle={false}`, don't need it.
|
|
11
11
|
|
|
12
12
|
```tsx
|
|
13
13
|
import { VirtualList } from '@ahrowe/ui';
|
|
@@ -367,4 +367,4 @@ This needs the list to have a **bounded height** (the default `height="100%"` in
|
|
|
367
367
|
|
|
368
368
|
**Labels:** `selectRow` `selectAllRows` `toggleColumns` `columnsHidden(count)` `resetColumnWidths` — 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: `VirtualListLabels`.
|
|
369
369
|
|
|
370
|
-
**In-row popovers:** Rows support inline popover content that overflows the row's bottom edge — e.g. a `Dropdown`, `InputDropdown`, `Tooltip`, or `
|
|
370
|
+
**In-row popovers:** Rows support inline popover content that overflows the row's bottom edge — e.g. a `Dropdown`, `InputDropdown`, `Tooltip`, or `Popover` whose list opens below its trigger. All four render their popover through a `BodyEnd` portal and track their trigger's position directly, so they always escape row/cell clipping and paint above the rows below, regardless of focus — no special CSS is needed on the row or cell for this. Cells still clip their own content by default (ellipsis / neighbour-spill guard); that's unaffected. This works in both list mode (`renderRow`) and table mode (`columns`). Stop the row's click handler from firing when interacting with the control (`onClick={(e) => e.stopPropagation()}` on the cell) if the row is also selectable.
|
package/package.json
CHANGED
|
@@ -1,2 +0,0 @@
|
|
|
1
|
-
import{useComponentDefaults as e}from"../configProvider/useComponentDefaults.mjs";import{usePresentationDefault as t}from"../configProvider/usePresentationDefault.mjs";import n from"../bodyEnd/bodyEnd.mjs";import r from"./floatingMenu.module.mjs";import i from"../styles/sheet.module.mjs";import a from"../sheetBackdrop/sheetBackdrop.mjs";import{useFloatingPosition as o}from"./useFloatingPosition.mjs";import{tabbablesIn as s,useFocusBoundary as c}from"../hooks/useFocusBoundary.mjs";import{Align as l,Presentation as u}from"./floatingMenu.types.mjs";import d from"../hooks/useSheet.mjs";import{useOverlay as f}from"../hooks/useOverlay.mjs";import{useEffect as p,useRef as m,useState as h}from"react";import g from"classnames";import{jsx as _,jsxs as v}from"react/jsx-runtime";function y(e){return e.charAt(0).toUpperCase()+e.slice(1)}function b(b){let{align:x=l.Left,children:S=null,content:C=null,isOpen:w=!1,onOpenChange:T=()=>{},dontCloseOnChildClick:E=!1,presentation:D,className:O,style:k,classNames:A,styles:j,...M}=e(`FloatingMenu`,b),N=m(null),P=m(null),F=m(null),I=m(T),[L,R]=h(10),z=m(!1);function B(){z.current=!0}p(()=>{I.current=T});function V(){if(w){E||T(!1);return}let e=F.current?.getBoundingClientRect();R(e?.width??10),T(!0)}let H=t(),U=D??H??u.Default,{isSheet:W,sheetStyle:G}=d(U);o({isOpen:w&&!W,triggerRef:F,anchorRef:P,panelRef:N,flipClassName:r.floatingMenuAnimFlip}),p(()=>{if(!w)return;function e(e){let t=N.current,n=F.current;if(!t||!n)return;let r=e.target;if(n===r||n.contains(r))return;let i=z.current;z.current=!1,(!(i||t===r||t.contains(r))||!E)&&I.current(!1)}return window.addEventListener(`mouseup`,e),()=>window.removeEventListener(`mouseup`,e)},[w,E]);let{restoreFocusToTrigger:K,focusAfterTrigger:q}=c({triggerRef:F,contentRef:N});f({isActive:w,containerRef:N,onEscape:()=>{N.current?.contains(document.activeElement)&&K(),I.current(!1)},trapFocus:!1,lockScroll:W});function J(e){if(e.key!==`Tab`)return;let t=s(N.current);if(t.length===0)return;let n=document.activeElement;!e.shiftKey&&n===t[t.length-1]?(e.preventDefault(),q(),I.current(!1)):e.shiftKey&&n===t[0]&&(e.preventDefault(),K(),I.current(!1))}return v(`div`,{...M,className:g(r.floatingMenu,O,A?.root),style:{...k,...j?.root},children:[_(`div`,{className:g(r.floatingMenuContent,A?.trigger),style:j?.trigger,ref:F,onClick:V,children:S}),w&&v(n,{children:[W&&_(a,{onClose:()=>I.current(!1),className:A?.backdrop,style:j?.backdrop}),v(`div`,{ref:P,className:g(W?i.sheet:r.floatingMenuAnim,A?.menu),style:{...W?G:{width:`${L}px`},...j?.menu},children:[!W&&_(`div`,{className:g(r.floatingMenuContainerFlag,r[`floatingMenuContainerFlagAlign${y(x)}`])}),_(`div`,{ref:N,className:g(r.floatingMenuContainer,W?i.sheetPanel:[r.floatingMenuContainerAnchored,r[`floatingMenuContainerAlign${y(x)}`]],A?.menuContainer),style:j?.menuContainer,onMouseUpCapture:B,onKeyDown:J,children:C})]})]})]})}export{b as default};
|
|
2
|
-
//# sourceMappingURL=floatingMenu.mjs.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"floatingMenu.mjs","names":[],"sources":["../../../../package/common/floatingMenu/floatingMenu.tsx"],"sourcesContent":["import { useRef, useEffect, useState } from 'react';\nimport cx from 'classnames';\nimport BodyEnd from 'components/common/bodyEnd';\n\nimport styles from './floatingMenu.module.pcss';\nimport sheetStyles from '../styles/sheet.module.pcss';\nimport SheetBackdrop from '../sheetBackdrop';\nimport { useFloatingPosition } from './useFloatingPosition';\nimport { useFocusBoundary, tabbablesIn } from '../hooks/useFocusBoundary';\nimport { useSheet } from '../hooks/useSheet';\nimport { useOverlay } from '../hooks/useOverlay';\nimport { useComponentDefaults, usePresentationDefault } from '../configProvider';\nimport { Align, Presentation, FloatingMenuProps } from './floatingMenu.types';\n\nfunction capitalize(s: string) {\n return s.charAt(0).toUpperCase() + s.slice(1);\n}\n\nfunction FloatingMenu(props: FloatingMenuProps) {\n const {\n align = Align.Left,\n children = null,\n content = null,\n isOpen = false,\n onOpenChange = () => {},\n dontCloseOnChildClick = false,\n presentation: presentationProp,\n className,\n style,\n classNames,\n styles: slotStyles,\n ...rest\n } = useComponentDefaults('FloatingMenu', props);\n const menuContainerRef = useRef<HTMLDivElement>(null);\n const menuAnimRef = useRef<HTMLDivElement>(null);\n const triggerRef = useRef<HTMLDivElement>(null);\n const onOpenChangeRef = useRef(onOpenChange);\n const [triggerWidth, setTriggerWidth] = useState(10);\n\n const contentInteractedRef = useRef(false);\n function handleContentMouseUp() {\n contentInteractedRef.current = true;\n }\n\n useEffect(() => {\n onOpenChangeRef.current = onOpenChange;\n });\n\n function handleTriggerClick() {\n if (isOpen) {\n if (!dontCloseOnChildClick) onOpenChange(false);\n return;\n }\n const rect = triggerRef.current?.getBoundingClientRect();\n setTriggerWidth(rect?.width ?? 10);\n onOpenChange(true);\n }\n\n // Explicit prop → this component's global default → the app-wide one → anchored.\n const appPresentation = usePresentationDefault();\n const presentation = presentationProp ?? appPresentation ?? Presentation.Default;\n const { isSheet, sheetStyle } = useSheet(presentation);\n\n // A sheet isn't anchored to anything, so there's nothing to track, flip or clamp.\n useFloatingPosition({\n isOpen: isOpen && !isSheet,\n triggerRef,\n anchorRef: menuAnimRef,\n panelRef: menuContainerRef,\n flipClassName: styles.floatingMenuAnimFlip,\n });\n\n useEffect(() => {\n if (!isOpen) return;\n\n function onMouseUp(event: MouseEvent) {\n const menuElem = menuContainerRef.current;\n const triggerElem = triggerRef.current;\n if (!menuElem || !triggerElem) return;\n\n const target = event.target as Node;\n if (triggerElem === target || triggerElem.contains(target)) return;\n\n const contentInteracted = contentInteractedRef.current;\n contentInteractedRef.current = false;\n const clickedInMenu = contentInteracted || menuElem === target || menuElem.contains(target);\n\n if (!clickedInMenu || !dontCloseOnChildClick) {\n onOpenChangeRef.current(false);\n }\n }\n\n window.addEventListener('mouseup', onMouseUp);\n return () => window.removeEventListener('mouseup', onMouseUp);\n }, [isOpen, dontCloseOnChildClick]);\n\n const { restoreFocusToTrigger, focusAfterTrigger } = useFocusBoundary({\n triggerRef,\n contentRef: menuContainerRef,\n });\n\n // Registered for as long as the menu is open, not just as a sheet: this is what puts it on the\n // overlay stack, so an enclosing Modal knows something sits above it. Without that, the modal\n // stays topmost and its focus trap drags focus out of this menu — which lives outside the\n // modal's panel, in the portal — and Escape closes the modal instead of just this menu.\n // Focus is deliberately not trapped: the menu doesn't take focus on open (see\n // `handleMenuKeyDown`), so there'd be nothing inside to trap.\n useOverlay({\n isActive: isOpen,\n containerRef: menuContainerRef,\n onEscape: () => {\n if (menuContainerRef.current?.contains(document.activeElement)) restoreFocusToTrigger();\n onOpenChangeRef.current(false);\n },\n trapFocus: false,\n lockScroll: isSheet,\n });\n\n // The menu is portaled to the end of the DOM, so Tab never flows into it naturally. When\n // `content` has real focusable elements, close on Tab out of either end and continue focus as\n // if the menu sat right after the trigger. Escape is handled by `useOverlay` above instead, so\n // that it works from the trigger too and closes only the innermost open layer.\n // Unlike Popover, this doesn't also auto-focus the\n // first element on open, content here ranges from menus to live controls like ColorPicker's\n // hue slider, where grabbing focus on open would let a stray arrow-key press nudge a value the\n // user never interacted with. Auto-focus-on-open is still worth doing per-consumer (e.g.\n // SplitButton focuses its first enabled action), just not assumed generically here.\n function handleMenuKeyDown(event: React.KeyboardEvent) {\n if (event.key !== 'Tab') return;\n const focusables = tabbablesIn(menuContainerRef.current);\n if (focusables.length === 0) return;\n const active = document.activeElement;\n if (!event.shiftKey && active === focusables[focusables.length - 1]) {\n event.preventDefault();\n focusAfterTrigger();\n onOpenChangeRef.current(false);\n } else if (event.shiftKey && active === focusables[0]) {\n event.preventDefault();\n restoreFocusToTrigger();\n onOpenChangeRef.current(false);\n }\n }\n\n return (\n <div\n {...rest}\n className={cx(styles.floatingMenu, className, classNames?.root)}\n style={{ ...style, ...slotStyles?.root }}\n >\n <div\n className={cx(styles.floatingMenuContent, classNames?.trigger)}\n style={slotStyles?.trigger}\n ref={triggerRef}\n onClick={handleTriggerClick}\n >\n {children}\n </div>\n {isOpen && (\n <BodyEnd>\n {isSheet && (\n <SheetBackdrop\n onClose={() => onOpenChangeRef.current(false)}\n className={classNames?.backdrop}\n style={slotStyles?.backdrop}\n />\n )}\n <div\n ref={menuAnimRef}\n className={cx(isSheet ? sheetStyles.sheet : styles.floatingMenuAnim, classNames?.menu)}\n style={{\n // A sheet spans the screen; only an anchored menu takes the trigger's width.\n ...(isSheet ? sheetStyle : { width: `${triggerWidth}px` }),\n ...slotStyles?.menu,\n }}\n >\n {!isSheet && (\n <div\n className={cx(\n styles.floatingMenuContainerFlag,\n styles[`floatingMenuContainerFlagAlign${capitalize(align)}`],\n )}\n />\n )}\n <div\n ref={menuContainerRef}\n className={cx(\n styles.floatingMenuContainer,\n isSheet\n ? sheetStyles.sheetPanel\n : [styles.floatingMenuContainerAnchored, styles[`floatingMenuContainerAlign${capitalize(align)}`]],\n classNames?.menuContainer,\n )}\n style={slotStyles?.menuContainer}\n onMouseUpCapture={handleContentMouseUp}\n onKeyDown={handleMenuKeyDown}\n >\n {content}\n </div>\n </div>\n </BodyEnd>\n )}\n </div>\n );\n}\n\nexport default FloatingMenu;\n"],"mappings":"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"}
|
|
@@ -1,2 +0,0 @@
|
|
|
1
|
-
var e=`floatingMenu_E1XCy`,t=`floatingMenu-content_AmxkA`,n=`floatingMenu-container_w7k-Y`,r=`floatingMenu-container-anchored_-8CHY`,i=`floatingMenu-anim-flip_a4-Ri`,a=`floatingMenu-container-align-left_InKnZ`,o=`floatingMenu-container-align-right_a9OoS`,s=`floatingMenu-container-align-center_-8bGT`,c=`floatingMenu-container-flag_VdKHr`,l=`floatingMenu-container-flag-align-right_vxrRN`,u=`floatingMenu-container-flag-align-left_a2Vv-`,d=`floatingMenu-container-flag-align-center_SmaaY`,f=`floatingMenu-anim_5KB-f`,p=`showMenu_AprCh`,m={floatingMenu:e,"floatingMenu-content":`floatingMenu-content_AmxkA`,floatingMenuContent:t,"floatingMenu-container":`floatingMenu-container_w7k-Y`,floatingMenuContainer:n,"floatingMenu-container-anchored":`floatingMenu-container-anchored_-8CHY`,floatingMenuContainerAnchored:r,"floatingMenu-anim-flip":`floatingMenu-anim-flip_a4-Ri`,floatingMenuAnimFlip:i,"floatingMenu-container-align-left":`floatingMenu-container-align-left_InKnZ`,floatingMenuContainerAlignLeft:a,"floatingMenu-container-align-right":`floatingMenu-container-align-right_a9OoS`,floatingMenuContainerAlignRight:o,"floatingMenu-container-align-center":`floatingMenu-container-align-center_-8bGT`,floatingMenuContainerAlignCenter:s,"floatingMenu-container-flag":`floatingMenu-container-flag_VdKHr`,floatingMenuContainerFlag:c,"floatingMenu-container-flag-align-right":`floatingMenu-container-flag-align-right_vxrRN`,floatingMenuContainerFlagAlignRight:l,"floatingMenu-container-flag-align-left":`floatingMenu-container-flag-align-left_a2Vv-`,floatingMenuContainerFlagAlignLeft:u,"floatingMenu-container-flag-align-center":`floatingMenu-container-flag-align-center_SmaaY`,floatingMenuContainerFlagAlignCenter:d,"floatingMenu-anim":`floatingMenu-anim_5KB-f`,floatingMenuAnim:f,showMenu:p};export{m as default,e as floatingMenu,f as floatingMenuAnim,i as floatingMenuAnimFlip,n as floatingMenuContainer,s as floatingMenuContainerAlignCenter,a as floatingMenuContainerAlignLeft,o as floatingMenuContainerAlignRight,r as floatingMenuContainerAnchored,c as floatingMenuContainerFlag,d as floatingMenuContainerFlagAlignCenter,u as floatingMenuContainerFlagAlignLeft,l as floatingMenuContainerFlagAlignRight,t as floatingMenuContent,p as showMenu};
|
|
2
|
-
//# sourceMappingURL=floatingMenu.module.mjs.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"floatingMenu.module.mjs","names":[],"sources":["../../../../package/common/floatingMenu/floatingMenu.module.pcss"],"sourcesContent":[".floatingMenu {\n position: relative;\n\n &-content {\n display: flex;\n align-items: stretch;\n height: 100%;\n cursor: pointer;\n -webkit-tap-highlight-color: transparent;\n }\n\n &-container {\n color: var(--text-color);\n font-family: var(--default-font);\n font-size: var(--default-font-size);\n box-shadow: var(--card-shadow);\n background: var(--background-modal);\n padding: 8px;\n border-radius: calc(var(--default-border-radius) + 4px);\n\n /* Positioning lives here rather than on `&-container` itself, so that sheet presentation\n can leave it off instead of having to override it. */\n &-anchored {\n position: absolute;\n margin-top: 10px;\n\n .floatingMenu-anim-flip & {\n top: auto;\n bottom: 0;\n margin-top: 0;\n margin-bottom: 10px;\n }\n }\n\n &-align {\n &-left {\n left: 0;\n }\n\n &-right {\n right: 0;\n }\n\n &-center {\n left: 50%;\n transform: translateX(-50%);\n }\n }\n\n &-flag {\n box-shadow: var(--card-shadow);\n background: var(--background-modal);\n height: 10px;\n width: 10px;\n position: absolute;\n margin-top: 5px;\n transform: rotate(45deg);\n\n .floatingMenu-anim-flip & {\n top: auto;\n bottom: 0;\n margin-top: 0;\n margin-bottom: 5px;\n }\n\n &-align-right {\n right: 8px;\n }\n\n &-align-left {\n left: 8px;\n }\n\n &-align-center {\n left: 50%;\n transform: translateX(-50%) rotate(45deg);\n }\n }\n }\n\n &-anim {\n transform: scale(0.7);\n opacity: 0.7;\n animation: showMenu 0.1s forwards;\n position: fixed;\n z-index: 1;\n }\n\n @keyframes showMenu {\n to {\n transform: scale(1);\n opacity: 1;\n }\n }\n}\n"],"mappings":""}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"floatingMenu.types.mjs","names":[],"sources":["../../../../package/common/floatingMenu/floatingMenu.types.ts"],"sourcesContent":["import type { CSSProperties, ReactNode } from 'react';\nimport type { SlotClassNames, SlotStyles, HtmlProps } from '../types/slots.types';\n\n/** How a surface is presented on screen. */\nexport enum Presentation {\n /** Wherever the component normally puts it: anchored to its trigger, or centred for a Modal. */\n Default = 'default',\n /** Pinned to the bottom of the screen, full width up to a maximum. */\n Sheet = 'sheet',\n /** A sheet on touch devices with a small screen, anchored everywhere else. */\n Auto = 'auto',\n}\n\nexport enum Align {\n Left = 'left',\n Right = 'right',\n Center = 'center',\n}\n\nexport type FloatingMenuSlots = 'root' | 'trigger' | 'menu' | 'menuContainer' | 'backdrop';\n\nexport interface FloatingMenuProps extends Omit<HtmlProps, 'content'> {\n align?: Align;\n children?: ReactNode;\n content?: ReactNode;\n isOpen?: boolean;\n onOpenChange?: (isOpen: boolean) => void;\n dontCloseOnChildClick?: boolean;\n /** Where the menu opens: anchored to the trigger (`Default`), pinned to the bottom of the\n * screen, or the latter only on a small touch screen. */\n presentation?: Presentation;\n className?: string;\n style?: CSSProperties;\n classNames?: SlotClassNames<FloatingMenuSlots>;\n styles?: SlotStyles<FloatingMenuSlots>;\n}\n"],"mappings":"AAIA,IAAY,EAAL,SAAA,EAAA,OAEL,GAAA,QAAA,UAEA,EAAA,MAAA,QAEA,EAAA,KAAA,QACF,EAAA,CAAA,CAAA,EAEY,EAAL,SAAA,EAAA,OACL,GAAA,KAAA,OACA,EAAA,MAAA,QACA,EAAA,OAAA,UACF,EAAA,CAAA,CAAA"}
|
|
@@ -1,2 +0,0 @@
|
|
|
1
|
-
import{isHiddenByAnyAncestor as e}from"../utils/scrollAncestors.mjs";import{getVisualViewportRect as t}from"../utils/visualViewport.mjs";import{useAnchorTracking as n}from"../hooks/useAnchorTracking.mjs";var r=8;function i({isOpen:i,triggerRef:a,anchorRef:o,panelRef:s,flipClassName:c,placement:l=`bottom`}){n({isOpen:i,triggerRef:a,elementRefs:[o],resizeRefs:[o,s],deps:[o,s,c,l],update:(n,i)=>{let a=o.current;if(!a)return;let u=n.getBoundingClientRect(),d=s.current?.offsetHeight??0,f=t(),p=f.top+f.height-u.bottom,m=u.top-f.top,h=l===`top`?m:p,g=h<d&&(l===`top`?p:m)>h,_=l===`top`?!g:g;a.classList.toggle(c,g),a.style.top=`${_?u.top-a.offsetHeight:u.bottom}px`,a.style.left=`${u.left}px`;let v=s.current;if(v){let e=v===a?0:v.offsetLeft,t=u.left+e,n=t+v.offsetWidth-(f.left+f.width-r),i=f.left+r-t;n>0?a.style.left=`${u.left-n}px`:i>0&&(a.style.left=`${u.left+i}px`)}a.style.visibility=e(u,i)?`hidden`:``},teardown:()=>{let e=o.current;e&&(e.style.top=``,e.style.left=``,e.style.visibility=``,e.classList.remove(c))}})}export{i as useFloatingPosition};
|
|
2
|
-
//# sourceMappingURL=useFloatingPosition.mjs.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"useFloatingPosition.mjs","names":[],"sources":["../../../../package/common/floatingMenu/useFloatingPosition.ts"],"sourcesContent":["import { type RefObject } from 'react';\nimport { isHiddenByAnyAncestor } from 'components/common/utils/scrollAncestors';\nimport { getVisualViewportRect } from 'components/common/utils/visualViewport';\nimport { useAnchorTracking } from '../hooks/useAnchorTracking';\n\nexport interface UseFloatingPositionArgs {\n isOpen: boolean;\n /** The element the popup is anchored to. */\n triggerRef: RefObject<HTMLElement | null>;\n /** The `position: fixed` element that gets `top`/`left`/`visibility` written to it directly. */\n anchorRef: RefObject<HTMLElement | null>;\n /** The visible panel whose height decides whether there's room to render on the preferred side. */\n panelRef: RefObject<HTMLElement | null>;\n /** Class toggled on `anchorRef` when flipped to the non-preferred side. */\n flipClassName: string;\n /** Preferred side relative to the trigger (default `'bottom'`) — flips to the other side when there's no room. */\n placement?: 'top' | 'bottom';\n}\n\n/** Minimum gap kept between the panel and the viewport edge when horizontally clamped. */\nconst EDGE_MARGIN = 8;\n\n/**\n * Keeps a portaled, `position: fixed` popup anchored to a trigger element: flips above the\n * trigger when there's no room below, and hides (not closes) the popup when the trigger itself\n * scrolls behind a clipping ancestor — matching Floating UI's autoUpdate + hide middleware.\n * It also shifts the popup horizontally to stay within the viewport: `align`/RTL-style CSS\n * only positions the panel relative to the anchor's own box and has no awareness of where\n * that box actually sits on screen, so a trigger near a screen edge would otherwise let the\n * panel overflow it with no correction.\n *\n * `useAnchorTracking` supplies the lifecycle around this: mount wait, ancestor resolution, and\n * re-running on scroll, viewport resize and panel size changes.\n */\nexport function useFloatingPosition({\n isOpen,\n triggerRef,\n anchorRef,\n panelRef,\n flipClassName,\n placement = 'bottom',\n}: UseFloatingPositionArgs) {\n useAnchorTracking({\n isOpen,\n triggerRef,\n elementRefs: [anchorRef],\n // Both, since they're the same element for a panel-as-anchor consumer (Dropdown) but not for\n // a zero-height wrapper whose content sits inside it (FloatingMenu).\n resizeRefs: [anchorRef, panelRef],\n deps: [anchorRef, panelRef, flipClassName, placement],\n\n update: (trigger, ancestors) => {\n const anchor = anchorRef.current;\n if (!anchor) return;\n\n const rect = trigger.getBoundingClientRect();\n const panelHeight = panelRef.current?.offsetHeight ?? 0;\n // The visible area, not the full layout viewport — shrinks on mobile when a keyboard is\n // open, so flip/clamp react to what's actually on screen instead of the keyboard hiding\n // a panel that \"fit\" against the unshrunk layout viewport.\n const vv = getVisualViewportRect();\n const spaceBelow = vv.top + vv.height - rect.bottom;\n const spaceAbove = rect.top - vv.top;\n const preferredSpace = placement === 'top' ? spaceAbove : spaceBelow;\n const alternateSpace = placement === 'top' ? spaceBelow : spaceAbove;\n const flip = preferredSpace < panelHeight && alternateSpace > preferredSpace;\n const renderAbove = placement === 'top' ? !flip : flip;\n anchor.classList.toggle(flipClassName, flip);\n // Shift up by the anchor's own height when rendering above, so this works whether\n // the anchor is a zero-height decorative wrapper (FloatingMenu, whose real panel\n // hangs off it via absolute-positioned CSS) or the visible panel itself\n // (Dropdown) — for the former the shift is a no-op (height 0).\n anchor.style.top = `${renderAbove ? rect.top - anchor.offsetHeight : rect.bottom}px`;\n anchor.style.left = `${rect.left}px`;\n\n // Horizontal clamp: shift the anchor so the panel stays within the viewport, since\n // `align` CSS only positions it relative to the anchor's own box. Use offsetWidth/\n // offsetLeft (untransformed layout geometry), not getBoundingClientRect — both this\n // anchor and Dropdown's animate in via a CSS `transform: scale(...)`, which\n // getBoundingClientRect reflects but the box-model offsets don't, so measuring via\n // the transformed rect would under- or over-correct while that entrance animation runs.\n const panelForClamp = panelRef.current;\n if (panelForClamp) {\n const relativeLeft = panelForClamp === anchor ? 0 : panelForClamp.offsetLeft;\n const panelLeft = rect.left + relativeLeft;\n const panelRight = panelLeft + panelForClamp.offsetWidth;\n const overflowRight = panelRight - (vv.left + vv.width - EDGE_MARGIN);\n const overflowLeft = vv.left + EDGE_MARGIN - panelLeft;\n if (overflowRight > 0) {\n anchor.style.left = `${rect.left - overflowRight}px`;\n } else if (overflowLeft > 0) {\n anchor.style.left = `${rect.left + overflowLeft}px`;\n }\n }\n\n anchor.style.visibility = isHiddenByAnyAncestor(rect, ancestors) ? 'hidden' : '';\n },\n\n teardown: () => {\n // Positioning is written as inline styles, which outrank any class the popup might switch\n // to while it stays mounted — a popup turning into a bottom sheet (`useSheet`) would keep\n // the coordinates computed for its anchored self. Hand the element back as we found it.\n const anchor = anchorRef.current;\n if (!anchor) return;\n anchor.style.top = '';\n anchor.style.left = '';\n anchor.style.visibility = '';\n anchor.classList.remove(flipClassName);\n },\n });\n}\n"],"mappings":"4MAoBA,IAAM,EAAc,EAcpB,SAAgB,EAAoB,CAClC,SACA,aACA,YACA,WACA,gBACA,YAAY,UACc,CAC1B,EAAkB,CAChB,SACA,aACA,YAAa,CAAC,CAAS,EAGvB,WAAY,CAAC,EAAW,CAAQ,EAChC,KAAM,CAAC,EAAW,EAAU,EAAe,CAAS,EAEpD,QAAS,EAAS,IAAc,CAC9B,IAAM,EAAS,EAAU,QACzB,GAAI,CAAC,EAAQ,OAEb,IAAM,EAAO,EAAQ,sBAAsB,EACrC,EAAc,EAAS,SAAS,cAAgB,EAIhD,EAAK,EAAsB,EAC3B,EAAa,EAAG,IAAM,EAAG,OAAS,EAAK,OACvC,EAAa,EAAK,IAAM,EAAG,IAC3B,EAAiB,IAAc,MAAQ,EAAa,EAEpD,EAAO,EAAiB,IADP,IAAc,MAAQ,EAAa,GACI,EACxD,EAAc,IAAc,MAAQ,CAAC,EAAO,EAClD,EAAO,UAAU,OAAO,EAAe,CAAI,EAK3C,EAAO,MAAM,IAAM,GAAG,EAAc,EAAK,IAAM,EAAO,aAAe,EAAK,OAAO,IACjF,EAAO,MAAM,KAAO,GAAG,EAAK,KAAK,IAQjC,IAAM,EAAgB,EAAS,QAC/B,GAAI,EAAe,CACjB,IAAM,EAAe,IAAkB,EAAS,EAAI,EAAc,WAC5D,EAAY,EAAK,KAAO,EAExB,EADa,EAAY,EAAc,aACT,EAAG,KAAO,EAAG,MAAQ,GACnD,EAAe,EAAG,KAAO,EAAc,EACzC,EAAgB,EAClB,EAAO,MAAM,KAAO,GAAG,EAAK,KAAO,EAAc,IACxC,EAAe,IACxB,EAAO,MAAM,KAAO,GAAG,EAAK,KAAO,EAAa,IAEpD,CAEA,EAAO,MAAM,WAAa,EAAsB,EAAM,CAAS,EAAI,SAAW,EAChF,EAEA,aAAgB,CAId,IAAM,EAAS,EAAU,QACpB,IACL,EAAO,MAAM,IAAM,GACnB,EAAO,MAAM,KAAO,GACpB,EAAO,MAAM,WAAa,GAC1B,EAAO,UAAU,OAAO,CAAa,EACvC,CACF,CAAC,CACH"}
|
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
import { CSSProperties, ReactNode } from 'react';
|
|
2
|
-
import { SlotClassNames, SlotStyles, HtmlProps } from '../types/slots.types';
|
|
3
|
-
/** How a surface is presented on screen. */
|
|
4
|
-
export declare enum Presentation {
|
|
5
|
-
/** Wherever the component normally puts it: anchored to its trigger, or centred for a Modal. */
|
|
6
|
-
Default = "default",
|
|
7
|
-
/** Pinned to the bottom of the screen, full width up to a maximum. */
|
|
8
|
-
Sheet = "sheet",
|
|
9
|
-
/** A sheet on touch devices with a small screen, anchored everywhere else. */
|
|
10
|
-
Auto = "auto"
|
|
11
|
-
}
|
|
12
|
-
export declare enum Align {
|
|
13
|
-
Left = "left",
|
|
14
|
-
Right = "right",
|
|
15
|
-
Center = "center"
|
|
16
|
-
}
|
|
17
|
-
export type FloatingMenuSlots = 'root' | 'trigger' | 'menu' | 'menuContainer' | 'backdrop';
|
|
18
|
-
export interface FloatingMenuProps extends Omit<HtmlProps, 'content'> {
|
|
19
|
-
align?: Align;
|
|
20
|
-
children?: ReactNode;
|
|
21
|
-
content?: ReactNode;
|
|
22
|
-
isOpen?: boolean;
|
|
23
|
-
onOpenChange?: (isOpen: boolean) => void;
|
|
24
|
-
dontCloseOnChildClick?: boolean;
|
|
25
|
-
/** Where the menu opens: anchored to the trigger (`Default`), pinned to the bottom of the
|
|
26
|
-
* screen, or the latter only on a small touch screen. */
|
|
27
|
-
presentation?: Presentation;
|
|
28
|
-
className?: string;
|
|
29
|
-
style?: CSSProperties;
|
|
30
|
-
classNames?: SlotClassNames<FloatingMenuSlots>;
|
|
31
|
-
styles?: SlotStyles<FloatingMenuSlots>;
|
|
32
|
-
}
|
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
import { RefObject } from 'react';
|
|
2
|
-
export interface UseFloatingPositionArgs {
|
|
3
|
-
isOpen: boolean;
|
|
4
|
-
/** The element the popup is anchored to. */
|
|
5
|
-
triggerRef: RefObject<HTMLElement | null>;
|
|
6
|
-
/** The `position: fixed` element that gets `top`/`left`/`visibility` written to it directly. */
|
|
7
|
-
anchorRef: RefObject<HTMLElement | null>;
|
|
8
|
-
/** The visible panel whose height decides whether there's room to render on the preferred side. */
|
|
9
|
-
panelRef: RefObject<HTMLElement | null>;
|
|
10
|
-
/** Class toggled on `anchorRef` when flipped to the non-preferred side. */
|
|
11
|
-
flipClassName: string;
|
|
12
|
-
/** Preferred side relative to the trigger (default `'bottom'`) — flips to the other side when there's no room. */
|
|
13
|
-
placement?: 'top' | 'bottom';
|
|
14
|
-
}
|
|
15
|
-
/**
|
|
16
|
-
* Keeps a portaled, `position: fixed` popup anchored to a trigger element: flips above the
|
|
17
|
-
* trigger when there's no room below, and hides (not closes) the popup when the trigger itself
|
|
18
|
-
* scrolls behind a clipping ancestor — matching Floating UI's autoUpdate + hide middleware.
|
|
19
|
-
* It also shifts the popup horizontally to stay within the viewport: `align`/RTL-style CSS
|
|
20
|
-
* only positions the panel relative to the anchor's own box and has no awareness of where
|
|
21
|
-
* that box actually sits on screen, so a trigger near a screen edge would otherwise let the
|
|
22
|
-
* panel overflow it with no correction.
|
|
23
|
-
*
|
|
24
|
-
* `useAnchorTracking` supplies the lifecycle around this: mount wait, ancestor resolution, and
|
|
25
|
-
* re-running on scroll, viewport resize and panel size changes.
|
|
26
|
-
*/
|
|
27
|
-
export declare function useFloatingPosition({ isOpen, triggerRef, anchorRef, panelRef, flipClassName, placement, }: UseFloatingPositionArgs): void;
|
package/docs/FloatingMenu.md
DELETED
|
@@ -1,90 +0,0 @@
|
|
|
1
|
-
# FloatingMenu
|
|
2
|
-
|
|
3
|
-
**When to use:** Contextual popover triggered by any element — action menus, dropdowns with custom content, "more options" menus, date pickers, colour swatches.
|
|
4
|
-
|
|
5
|
-
**Keywords:** context menu, dropdown menu, bottom sheet, action sheet, mobile, touch
|
|
6
|
-
|
|
7
|
-
**Import:** `import { FloatingMenu } from '@ahrowe/ui'`
|
|
8
|
-
|
|
9
|
-
```tsx
|
|
10
|
-
import { FloatingMenu, Align } from '@ahrowe/ui';
|
|
11
|
-
|
|
12
|
-
// Basic — children is the trigger, content is the popover body
|
|
13
|
-
<FloatingMenu
|
|
14
|
-
content={
|
|
15
|
-
<ul>
|
|
16
|
-
<li onClick={handleEdit}>Edit</li>
|
|
17
|
-
<li onClick={handleDelete}>Delete</li>
|
|
18
|
-
</ul>
|
|
19
|
-
}
|
|
20
|
-
>
|
|
21
|
-
<button>More ▾</button>
|
|
22
|
-
</FloatingMenu>
|
|
23
|
-
|
|
24
|
-
// Controlled open state
|
|
25
|
-
<FloatingMenu
|
|
26
|
-
isOpen={isMenuOpen}
|
|
27
|
-
onOpenChange={setIsMenuOpen}
|
|
28
|
-
align={Align.Right}
|
|
29
|
-
content={<MenuContent />}
|
|
30
|
-
>
|
|
31
|
-
<ActionIcon icon={faEllipsis} />
|
|
32
|
-
</FloatingMenu>
|
|
33
|
-
|
|
34
|
-
// Keep open through several interactions with content — a column-visibility toggle where
|
|
35
|
-
// checking one box shouldn't close the whole menu — or when re-clicking the trigger itself
|
|
36
|
-
// (e.g. DatePicker re-clicking its own input) shouldn't close it either
|
|
37
|
-
<FloatingMenu dontCloseOnChildClick content={<Form />}>
|
|
38
|
-
<button>Open form</button>
|
|
39
|
-
</FloatingMenu>
|
|
40
|
-
```
|
|
41
|
-
|
|
42
|
-
```tsx
|
|
43
|
-
import { FloatingMenu, Presentation } from '@ahrowe/ui';
|
|
44
|
-
|
|
45
|
-
// On a small touch screen the menu becomes a bottom sheet instead of a popover anchored to
|
|
46
|
-
// the trigger; everywhere else it stays anchored.
|
|
47
|
-
<FloatingMenu presentation={Presentation.Auto} content={<MenuContent />}>
|
|
48
|
-
<button>More ▾</button>
|
|
49
|
-
</FloatingMenu>
|
|
50
|
-
```
|
|
51
|
-
|
|
52
|
-
**Align enum:** `Align.Left` | `Align.Right` | `Align.Center`
|
|
53
|
-
|
|
54
|
-
**Presentation enum:** `Presentation.Default` (anchored to the trigger) | `Presentation.Sheet` | `Presentation.Auto`
|
|
55
|
-
|
|
56
|
-
**Key props:**
|
|
57
|
-
|
|
58
|
-
| Prop | Type | Description |
|
|
59
|
-
|------|------|-------------|
|
|
60
|
-
| `content` | `ReactNode` | Popover body |
|
|
61
|
-
| `children` | `ReactNode` | Trigger element |
|
|
62
|
-
| `align` | `Align` | Popover alignment relative to trigger |
|
|
63
|
-
| `isOpen` | `boolean` | Controlled open state |
|
|
64
|
-
| `onOpenChange` | `(isOpen: boolean) => void` | Open state change callback |
|
|
65
|
-
| `dontCloseOnChildClick` | `boolean` | Keep the menu open when its own content is clicked, and when the trigger itself is re-clicked while already open (default `false`) |
|
|
66
|
-
| `presentation` | `Presentation` | Where the menu opens: anchored to the trigger (`Default`), always a bottom sheet, or a sheet only on a small touch screen |
|
|
67
|
-
|
|
68
|
-
**Slots:** `root` `trigger` `menu` `menuContainer` `backdrop` (`backdrop` only exists while presenting as a sheet)
|
|
69
|
-
|
|
70
|
-
**Bottom sheet:** with `Presentation.Sheet` the menu leaves its trigger entirely: it pins to the bottom of the screen, spans the full width up to 560px, scrolls internally past 70% of the viewport height, and dims the page behind a backdrop. Scrolling on the page behind it is locked while it's open, Escape closes it even when focus never entered it, and it lifts 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 sheet and nothing else: the tap is consumed by the backdrop rather than falling through to whatever sits behind it. `Presentation.Auto` applies that only when the pointer is coarse *and* the screen is under 768px wide: pointer alone would catch touchscreen laptops, width alone would catch a narrow desktop window. Set it once for a whole app via `ConfigProvider`:
|
|
71
|
-
|
|
72
|
-
```tsx
|
|
73
|
-
<ConfigProvider defaultProps={{ FloatingMenu: { presentation: Presentation.Auto } }}>
|
|
74
|
-
```
|
|
75
|
-
|
|
76
|
-
To set it for every sheet-capable component at once, use `ConfigProvider`'s own `presentation` prop instead of the four component-keyed entries — see [ConfigProvider.md](ConfigProvider.md).
|
|
77
|
-
|
|
78
|
-
The default stays `Presentation.Default`, so nothing changes for an existing app until it opts in.
|
|
79
|
-
|
|
80
|
-
**What that one setting covers.** Every component whose popup *is* a `FloatingMenu` inherits the default above, with no prop of its own: `SplitButton`'s action menu, `ColorPicker`'s picker, `Breadcrumb`'s collapsed-crumb menu, `VirtualList`'s column-visibility menu, and `DatePicker`/`TimeInput`/`KlipyPicker` when they're not already using their own `isModal`. To make one instance differ, wrap it in a nested `ConfigProvider`, which overrides only the keys it sets.
|
|
81
|
-
|
|
82
|
-
`Dropdown`, `Popover` and `Modal` place their surfaces themselves rather than through `FloatingMenu`, so each takes its own `presentation` prop and its own `ConfigProvider` key (`Modal`'s covers `DatePicker`/`TimeInput`/`KlipyPicker` in their `isModal` mode). `InputDropdown` and `Tooltip` also position themselves and deliberately have no sheet mode: a typeahead's list belongs next to the text being typed, and a tooltip is a pointer hint, not a screen-owning panel.
|
|
83
|
-
|
|
84
|
-
**Positioning:** the menu is portaled and tracks the trigger across scroll and resize, flipping above it when there's no room below. It re-measures whenever `content` changes size while open, so a menu holding a list that grows or shrinks (filtering, async loading) stays anchored to the trigger and re-evaluates whether it still needs to open upward. It's also shifted horizontally to stay within the viewport — a trigger near the left or right edge of the screen no longer lets the menu overflow off-screen, regardless of `align`.
|
|
85
|
-
|
|
86
|
-
**Closing behaviour:** by default, clicking anywhere in `content` closes the menu — including inside a nested overlay that renders through its own portal (e.g. a `Dropdown` or another `FloatingMenu` used inside `content`), even though that overlay's DOM lives outside `content`'s own subtree. Re-clicking the trigger while open is a clean toggle: it closes the menu (unless `dontCloseOnChildClick` is set, in which case it's a no-op — the trigger owns its own open/close entirely, so it never fights with an outside-click check). Set `dontCloseOnChildClick` when `content` needs several interactions before the user is done (a multi-checkbox toggle, a color picker's slider, a calendar) — the consumer is then responsible for closing explicitly, e.g. calling `onOpenChange(false)` from the handler that reacts to a final selection.
|
|
87
|
-
|
|
88
|
-
**Keyboard:** the menu is portaled to the end of the DOM, so Tab can't reach it in visual order on its own. Escape closes the menu and, when focus is inside it, returns focus to the trigger — it works wherever focus currently is, including on the trigger itself. When `content` has real focusable elements, Tab past the last focusable element (or Shift+Tab past the first) closes the menu and continues focus as if it sat right after the trigger. This doesn't include auto-focusing the first element on open — content ranges from menus to live controls (e.g. `ColorPicker`'s hue slider), where grabbing focus on open would let a stray arrow-key press change a value the user never touched. A consumer that wants that (like `SplitButton` focusing its first enabled action) implements it itself.
|
|
89
|
-
|
|
90
|
-
**Nesting inside a Modal:** an open menu registers itself as the layer above whatever it was opened from, so a menu inside a `Modal` behaves the way it looks: Escape closes the menu and leaves the modal open (a second press closes the modal), and the modal's focus trap leaves focus alone while the menu has it. Both matter because the menu is portaled, so its DOM is a sibling of the modal's panel rather than a descendant, and neither DOM position nor mount order says which one is on top. The same applies to `Dropdown` and `Popover`.
|