@blakesteve/roster 5.8.0 → 5.9.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/README.md +86 -2
- package/dist/components/molecules/Menu/Menu.cjs +1 -0
- package/dist/components/molecules/Menu/Menu.d.ts +169 -0
- package/dist/components/molecules/Menu/Menu.js +577 -0
- package/dist/components/molecules/Menu/menu-typeahead.cjs +1 -0
- package/dist/components/molecules/Menu/menu-typeahead.d.ts +7 -0
- package/dist/components/molecules/Menu/menu-typeahead.js +13 -0
- package/dist/components/organisms/Navbar/Navbar.cjs +1 -1
- package/dist/components/organisms/Navbar/Navbar.d.ts +1 -1
- package/dist/components/organisms/Navbar/Navbar.js +275 -302
- package/dist/index.d.ts +1 -0
- package/dist/meta.json +3 -3
- package/dist/node_modules/@floating-ui/core/dist/floating-ui.core.js +7 -7
- package/dist/node_modules/@floating-ui/dom/dist/floating-ui.dom.js +2 -2
- package/dist/node_modules/@floating-ui/react-dom/dist/floating-ui.react-dom.js +5 -5
- package/dist/node_modules/@headlessui/react/dist/components/portal/portal.js +6 -6
- package/dist/node_modules/@headlessui/react/dist/utils/focus-management.cjs +1 -1
- package/dist/node_modules/@headlessui/react/dist/utils/focus-management.js +52 -61
- package/dist/node_modules/@radix-ui/react-popper/dist/index.js +6 -6
- package/dist/roster.cjs +1 -1
- package/dist/roster.css +1 -1
- package/dist/roster.js +127 -116
- package/package.json +2 -1
- package/dist/node_modules/@headlessui/react/dist/components/menu/menu-machine-glue.cjs +0 -1
- package/dist/node_modules/@headlessui/react/dist/components/menu/menu-machine-glue.js +0 -21
- package/dist/node_modules/@headlessui/react/dist/components/menu/menu-machine.cjs +0 -1
- package/dist/node_modules/@headlessui/react/dist/components/menu/menu-machine.js +0 -120
- package/dist/node_modules/@headlessui/react/dist/components/menu/menu.cjs +0 -1
- package/dist/node_modules/@headlessui/react/dist/components/menu/menu.js +0 -197
package/README.md
CHANGED
|
@@ -924,6 +924,10 @@ look like oversights until you know why:
|
|
|
924
924
|
page does not reach it. All three components copy the nearest `.dark` onto
|
|
925
925
|
the panel to fix that. Custom properties are not carried the same way — set
|
|
926
926
|
`--roster-popover-*` at `:root`.
|
|
927
|
+
- **`Menu` shares the panel, not the anchoring.** Its panel takes the same
|
|
928
|
+
surface, tokens and dark carry, but `Menu` places itself (see
|
|
929
|
+
[Menu](#menu)), writing its own inline `max-height`. It reads the same
|
|
930
|
+
`--anchor-max-height`, so the hatch above works on `MenuItems` too.
|
|
927
931
|
- **`variant="slate"` opens on the dark palette**, in light mode too. That
|
|
928
932
|
variant paints a dark trigger, and its panel was rendering white: a dark
|
|
929
933
|
control opening a white sheet, which is the mismatch `--roster-popover-*`
|
|
@@ -1499,6 +1503,84 @@ shown beside it.
|
|
|
1499
1503
|
|
|
1500
1504
|
Every string in all five is a prop with an English default.
|
|
1501
1505
|
|
|
1506
|
+
### Menu
|
|
1507
|
+
|
|
1508
|
+
A button that opens a panel of actions: the menu button pattern. Compose it:
|
|
1509
|
+
|
|
1510
|
+
```tsx
|
|
1511
|
+
import { Menu, MenuButton, MenuItems, MenuItem, MenuSeparator } from "@blakesteve/roster";
|
|
1512
|
+
|
|
1513
|
+
<Menu>
|
|
1514
|
+
<MenuButton variant="ghost" size="icon" aria-label="More for Banded agate">⋯</MenuButton>
|
|
1515
|
+
<MenuItems align="end">
|
|
1516
|
+
<MenuItem icon={<EditIcon />} shortcut="⌘E" onSelect={edit}>Edit</MenuItem>
|
|
1517
|
+
<MenuItem href="/rocks/agate/share">Share</MenuItem>
|
|
1518
|
+
<MenuSeparator />
|
|
1519
|
+
<MenuItem destructive onSelect={confirmDelete}>Delete</MenuItem>
|
|
1520
|
+
</MenuItems>
|
|
1521
|
+
</Menu>;
|
|
1522
|
+
```
|
|
1523
|
+
|
|
1524
|
+
- **Items.** `MenuItem` is an action, or a link with `href` (rendered through
|
|
1525
|
+
the menu's `linkComponent`; pass `next/link` from a client component, as with
|
|
1526
|
+
Breadcrumbs). Each takes a leading `icon`, a `shortcut` to show (display
|
|
1527
|
+
only: your app handles the keys; `keyshortcuts` tells assistive technology),
|
|
1528
|
+
a `description` line, a trailing `badge`, `destructive` and `disabled`.
|
|
1529
|
+
`MenuCheckboxItem` is an on/off choice, `MenuRadioGroup` with
|
|
1530
|
+
`MenuRadioItem` one of several, `MenuGroup` puts items under an optional
|
|
1531
|
+
label, `MenuSeparator` draws a rule, and `MenuSubmenu` opens a menu beside
|
|
1532
|
+
its item. Submenus nest.
|
|
1533
|
+
- **Keyboard.** Enter, Space or Down open it on the first item and Up on the
|
|
1534
|
+
last; a click opens it on the panel. The arrows move, Home and End jump, and
|
|
1535
|
+
typing jumps to the next item starting with what's typed, a space included
|
|
1536
|
+
("save a"). Escape closes it and returns focus to the button; Tab closes it
|
|
1537
|
+
and moves on from the button, to what the browser's own Tab would reach. A
|
|
1538
|
+
submenu opens with the arrow away from its parent, Enter, Space, a click or
|
|
1539
|
+
the pointer resting on it, and closes with the arrow back or Escape. A click
|
|
1540
|
+
only ever opens it.
|
|
1541
|
+
- **Choosing.** A click or Enter chooses and closes, with focus back on the
|
|
1542
|
+
button, so a choice that opens a Dialog hands focus to it and gets it back.
|
|
1543
|
+
Space on a checkbox or radio item chooses and keeps the menu open, so
|
|
1544
|
+
several can be set in one visit. `closeOnSelect={false}` keeps it open on
|
|
1545
|
+
any choice. A disabled item can be reached, so it can be read, but not
|
|
1546
|
+
chosen. A press outside closes it without moving the page, and focus stays
|
|
1547
|
+
where the press put it.
|
|
1548
|
+
- **Events.** Keys inside the panel stay there: the panel is portaled, and
|
|
1549
|
+
React would otherwise carry them to the components around the menu, so
|
|
1550
|
+
Enter on Delete would also open the row whose overflow menu it is. A key
|
|
1551
|
+
with Ctrl, Meta or Alt goes on, for the app's shortcuts. Clicks go on to the
|
|
1552
|
+
page, where analytics listen, and so React carries an item's click to that
|
|
1553
|
+
row too: a row with its own `onClick` checks that the click landed inside
|
|
1554
|
+
it (`event.currentTarget.contains(event.target as Node)`). The button is
|
|
1555
|
+
in your markup, and its keys and clicks reach the row like any other.
|
|
1556
|
+
- **Placement.** Below the button, lined up with its start (`align="end"` for
|
|
1557
|
+
the other edge); flipped above when there's more room there, shifted to stay
|
|
1558
|
+
on screen, and capped at the room there is. A taller menu scrolls with
|
|
1559
|
+
Roster's scrollbar and a shadow at whichever edge has more. Under
|
|
1560
|
+
`dir="rtl"` it lines up with the right edge and submenus open to the left.
|
|
1561
|
+
It takes its button's direction, not the page's.
|
|
1562
|
+
- **Inside other layers.** The panel opens in a portal, so nothing it sits in
|
|
1563
|
+
clips it, a card with `overflow: hidden` included. A `Dialog`, a `Sheet` or
|
|
1564
|
+
Navbar's mobile panel counts it as inside: choosing from it leaves them
|
|
1565
|
+
open.
|
|
1566
|
+
- **Styling.** `className` on `MenuItems` and on each item merges last, so a
|
|
1567
|
+
fill such as `bg-gray-100` takes; the scroll shadows are drawn over it. Its
|
|
1568
|
+
ring and separators follow `--roster-border-width`. Items show the pointer
|
|
1569
|
+
cursor, a disabled one `not-allowed`. In forced colors the focused item gets
|
|
1570
|
+
an outline, since its fill is dropped.
|
|
1571
|
+
- **Targets.** Every item is at least 44px tall, and `MenuButton` at least 44
|
|
1572
|
+
by 44 whatever its size. `MenuButton plain` renders a bare `<button>` for a
|
|
1573
|
+
trigger drawn its own way; keep its target at least 44 by 44 with a hit
|
|
1574
|
+
area, as Navbar's avatar does.
|
|
1575
|
+
|
|
1576
|
+
Built on Roster's own popup panel and on Floating UI for placement, which
|
|
1577
|
+
Roster already ships through its other popups, rather than on Headless UI's
|
|
1578
|
+
`Menu`: that has no submenus, and no checkbox or radio items. Navbar's user
|
|
1579
|
+
menu is built on it since 5.9.0. It looks as it did, in every state, except
|
|
1580
|
+
that its rows are 44px tall rather than 36, and that with neither an inbox nor
|
|
1581
|
+
a theme row it no longer draws an empty band and rule at the top. It keeps its
|
|
1582
|
+
own white fill, whatever `--roster-popover-bg` says.
|
|
1583
|
+
|
|
1502
1584
|
### Pagination
|
|
1503
1585
|
|
|
1504
1586
|
Moves through pages of a list, built for a long list browsed on a phone.
|
|
@@ -1778,6 +1860,7 @@ function App() {
|
|
|
1778
1860
|
| `LiquidTabs` | Tab strip with a sliding pill; arrow keys, roving focus, 44px `lg` size |
|
|
1779
1861
|
| `MatchupCard` | Head-to-head comparison card |
|
|
1780
1862
|
| `MediaPreview` | Thumbnail that falls back to "Preview unavailable, open original" when it can't render |
|
|
1863
|
+
| `Menu` | A button's menu of actions: icons, shortcuts, checkbox and radio items, links, groups, submenus; the full keyboard pattern, 44px targets (see [Menu](#menu)) |
|
|
1781
1864
|
| `FileUpload` | Picks files and uploads each through the app's `upload`, with per-file progress, retry and rules |
|
|
1782
1865
|
| `Pagination` | Page numbers with gaps, or a compact page picker on narrow screens; buttons or real links, 44px targets |
|
|
1783
1866
|
| `SkeletonCard`, `SkeletonTableRow`, `SkeletonAvatar`, `SkeletonStat` | Skeleton presets in the real component's box |
|
|
@@ -1799,7 +1882,7 @@ function App() {
|
|
|
1799
1882
|
| `ModerationQueue` | Items waiting for a decision: busy per action, confirm, reason and edit steps, focus after an item leaves, a live region. Never fetches (see [ModerationQueue](#moderationqueue)) |
|
|
1800
1883
|
| `QueueEditDialog` | A ModerationQueue edit step in a Dialog: pass it as an edit's `surface`. A separate export, so a queue that never uses it doesn't ship it |
|
|
1801
1884
|
| `QueueEditSheet` | A ModerationQueue edit step in a Sheet, for a long form: pass it as an edit's `surface`. A separate export, like `QueueEditDialog` |
|
|
1802
|
-
| `Navbar` | Responsive navigation bar with mobile slide-out
|
|
1885
|
+
| `Navbar` | Responsive navigation bar with mobile slide-out; its user menu is a `Menu`. A wide logo keeps its own proportions through `logoHeight` and `logoWidth`; its built-in words are in `labels` |
|
|
1803
1886
|
| `Sheet` | Bottom sheet: drag, Escape or backdrop to dismiss; only its content scrolls; returns focus, even from a deep link; the title may be a node |
|
|
1804
1887
|
| `Table` | Static data table |
|
|
1805
1888
|
|
|
@@ -1953,7 +2036,8 @@ reason and the build goes green on the state it inherited while still failing on
|
|
|
1953
2036
|
anything new. It also fails when a listed class starts emitting, so a fix cannot
|
|
1954
2037
|
leave a stale entry behind. Run it with `--verbose` to print the list.
|
|
1955
2038
|
|
|
1956
|
-
Story tests that need the user's own input, a wheel
|
|
2039
|
+
Story tests that need the user's own input, a wheel, a mouse drag or a pointer
|
|
2040
|
+
resting on something, get it
|
|
1957
2041
|
from `.storybook/real-input.ts`: Vitest browser commands that drive
|
|
1958
2042
|
Playwright's mouse, whose events are trusted. A synthetic wheel or pointer
|
|
1959
2043
|
event never scrolls anything, so "a vertical wheel scrolls the page, not the
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use client";"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const c=require("react/jsx-runtime"),ge=require("react"),W=require("../../../node_modules/@headlessui/react/dist/components/portal/portal.cjs"),A=require("../../../node_modules/@floating-ui/react-dom/dist/floating-ui.react-dom.cjs"),E=require("../../../lib/utils.cjs"),he=require("../../../internal/popup.cjs"),be=require("../../atoms/Button/Button.cjs"),xe=require("./menu-typeahead.cjs"),ye=require("../../../node_modules/@floating-ui/dom/dist/floating-ui.dom.cjs");function ve(e){const r=Object.create(null,{[Symbol.toStringTag]:{value:"Module"}});if(e){for(const n in e)if(n!=="default"){const o=Object.getOwnPropertyDescriptor(e,n);Object.defineProperty(r,n,o.get?o:{enumerable:!0,get:()=>e[n]})}}return r.default=e,Object.freeze(r)}const i=ve(ge);function we({href:e,to:r,children:n,...o}){return c.jsx("a",{href:e,...o,children:n})}const G=i.createContext(null),_=i.createContext(null),V=i.createContext(null);function R(e){const r=i.useContext(G);if(!r)throw new Error(`<${e}> must be inside a <Menu>.`);return r}function K(e){const r=i.useContext(_);if(!r)throw new Error(`<${e}> must be inside <MenuItems> or a <MenuSubmenu>.`);return r}const ke="[data-menu-item]",Y=100,Ie=75,Ee='a[href], button:not([disabled]), input:not([disabled]):not([type="hidden"]), select:not([disabled]), textarea:not([disabled]), summary, [tabindex]:not([tabindex="-1"]), [contenteditable]:not([contenteditable="false"])';function L(e){return e?[...e.querySelectorAll(ke)].filter(r=>r.closest("[data-menu-panel]")===e):[]}function S(e){e&&(e.focus({preventScroll:!0}),e.scrollIntoView?.({block:"nearest"}))}function $(e){return!!e&&getComputedStyle(e).direction==="rtl"}function Se(e,r){const n=[...document.querySelectorAll(Ee)].filter(t=>t===e||t.tabIndex>=0&&!t.matches(":disabled")&&!t.closest("[data-menu-panel], [inert]")&&t.getClientRects().length>0&&getComputedStyle(t).visibility!=="hidden"&&!Me(t,r)),o=n.indexOf(e);((r?n[o-1]:n[o+1])??e).focus()}function Me(e,r){if(!(e instanceof HTMLInputElement)||e.type!=="radio"||!e.name)return!1;const n=[...e.ownerDocument.querySelectorAll(`input[type="radio"][name="${CSS.escape(e.name)}"]`)].filter(a=>a.form===e.form&&!a.disabled);return(n.find(a=>a.checked)??(r?n[n.length-1]:n[0]))!==e}const D={x:Number.NaN,y:Number.NaN};let H=!1;function je(){H||typeof window>"u"||(H=!0,window.addEventListener("pointermove",e=>{D.x=e.clientX,D.y=e.clientY},{passive:!0}))}function X(e){return!Number.isNaN(D.x)&&(e.clientX!==D.x||e.clientY!==D.y)}const U=e=>`radial-gradient(farthest-side at 50% ${e==="top"?"0":"100%"}, var(--menu-edge), transparent)`,J={backgroundImage:"var(--menu-edge-top, none), var(--menu-edge-bottom, none)",backgroundPosition:"top, bottom",backgroundSize:"100% 0.5rem",backgroundRepeat:"no-repeat"};function Pe(e){i.useLayoutEffect(()=>{if(!e)return;const r=()=>{const o=e.scrollTop>8,a=e.scrollTop+e.clientHeight<e.scrollHeight-8;e.toggleAttribute("data-more-above",o),e.toggleAttribute("data-more-below",a),e.style.setProperty("--menu-edge-top",o?U("top"):"none"),e.style.setProperty("--menu-edge-bottom",a?U("bottom"):"none")};r(),e.addEventListener("scroll",r,{passive:!0});const n=typeof ResizeObserver>"u"?null:new ResizeObserver(r);return n?.observe(e),()=>{e.removeEventListener("scroll",r),n?.disconnect()}},[e])}function Q(e,r){const[n,o]=e.split("-"),a=r?"right":"left",t=r?"left":"right";return n==="top"||n==="bottom"?`${n==="bottom"?"top":"bottom"} ${o==="end"?t:o==="start"?a:"center"}`:`${o==="end"?"bottom":"top"} ${n==="right"?"left":"right"}`}const Z=E.cn("rst:font-ui",he.POPUP_PANEL,"rst:w-max rst:min-w-48 rst:max-w-[min(20rem,calc(100vw-1rem))] rst:overflow-y-auto rst:overscroll-contain rst:custom-scrollbar","rst:[--menu-edge:rgb(0_0_0/0.18)] rst:dark:[--menu-edge:rgb(0_0_0/0.6)]","rst:transition-[opacity,scale] rst:duration-100 rst:ease-out rst:data-closed:opacity-0 rst:data-closed:scale-95 rst:data-leaving:duration-75 rst:data-leaving:ease-in rst:data-closed:pointer-events-none rst:motion-reduce:transition-none");function ee(e){const[r,n]=i.useState(e),[o,a]=i.useState(!0);return i.useLayoutEffect(()=>{if(e){n(!0);const s=requestAnimationFrame(()=>a(!1));return()=>cancelAnimationFrame(s)}a(!0);const t=window.matchMedia?.("(prefers-reduced-motion: reduce)").matches,d=window.setTimeout(()=>n(!1),t?0:Ie);return()=>window.clearTimeout(d)},[e]),{mounted:r,closed:o,leaving:r&&!e}}function te(e,r,n,o,a,t=!1){const[d,s]=i.useState(null),u=A.useFloating({open:r,strategy:"fixed",transform:!1,placement:n,elements:{reference:e},whileElementsMounted:ye.autoUpdate,middleware:[A.offset(o),A.flip({padding:8}),A.shift({padding:8,crossAxis:t}),A.size({padding:8,apply({availableHeight:f,elements:l}){l.floating.style.maxHeight=`min(var(--anchor-max-height, 100vh), ${Math.max(f,96)}px)`}})]}),{setFloating:p}=u.refs,m=i.useCallback(f=>{a.current=f,s(f),p(f)},[a,p]);return Pe(d),{...u,setPanel:m,panelEl:d}}const re=500;function ne(){const e=i.useRef({text:"",at:0}),r=i.useCallback(()=>e.current.text!==""&&performance.now()-e.current.at<=re,[]);return{typedRef:e,typingAhead:r}}function oe({panelRef:e,typedRef:r,onEscape:n,onBack:o}){const a=R("MenuItems");return t=>{const d=e.current;if(!d||!d.contains(t.target)||(!t.ctrlKey&&!t.metaKey&&!t.altKey&&t.stopPropagation(),t.defaultPrevented))return;const s=L(d),u=s.indexOf(document.activeElement),p=$(d);switch(t.key){case"ArrowDown":t.preventDefault(),t.stopPropagation(),S(s[(u+1)%s.length]);return;case"ArrowUp":t.preventDefault(),t.stopPropagation(),S(s[u<=0?s.length-1:u-1]);return;case"Home":case"PageUp":t.preventDefault(),t.stopPropagation(),S(s[0]);return;case"End":case"PageDown":t.preventDefault(),t.stopPropagation(),S(s[s.length-1]);return;case"Escape":t.preventDefault(),t.stopPropagation(),n();return;case"Tab":{t.preventDefault(),t.stopPropagation();const m=a.triggerRef.current;a.close(!1),m&&Se(m,t.shiftKey);return}case"ArrowLeft":case"ArrowRight":o&&t.key===(p?"ArrowRight":"ArrowLeft")&&(t.preventDefault(),t.stopPropagation(),o());return}if(t.key.length===1&&!t.ctrlKey&&!t.metaKey&&!t.altKey){const m=performance.now(),f=r.current.text!==""&&m-r.current.at<=re;if(t.key===" "&&!f)return;t.preventDefault(),r.current={text:f?r.current.text+t.key:t.key,at:m};const l=s.map(w=>w.dataset.menuText??w.textContent??""),b=xe.nextByTypeahead(l,u,r.current.text);b!==-1&&S(s[b])}}}function Te({children:e,open:r,onOpenChange:n,linkComponent:o=we}){const[a,t]=i.useState(!1),d=r??a,s=i.useId(),u=i.useRef(null),[p,m]=i.useState(null),f=i.useCallback(g=>{u.current=g,m(g)},[]),l=i.useRef("panel"),b=i.useCallback(g=>{r===void 0&&t(g),n?.(g)},[r,n]),w=i.useMemo(()=>({treeId:s,open:d,openWith:g=>{l.current=g,b(!0)},close:g=>{b(!1),g&&u.current?.focus()},triggerRef:u,triggerEl:p,registerTrigger:f,triggerId:`${s}-button`,panelId:`${s}-menu`,focusOnOpen:l,linkComponent:o}),[s,d,b,o,p,f]);return i.useEffect(()=>{if(!d)return;je();const g=I=>{I.target?.closest?.(`[data-menu-tree="${s}"]`)||(b(!1),requestAnimationFrame(()=>{(document.activeElement===document.body||!document.activeElement)&&u.current?.focus({preventScroll:!0})}))};return document.addEventListener("pointerdown",g,!0),()=>document.removeEventListener("pointerdown",g,!0)},[d,s,b]),c.jsx(G.Provider,{value:w,children:e})}const Re=i.forwardRef(function({plain:r=!1,className:n,onClick:o,onKeyDown:a,children:t,...d},s){const{variant:u,colorScheme:p,size:m,isLoading:f,loadingLabel:l,startIcon:b,endIcon:w,surface:g,...I}=d,h=R("MenuButton"),{registerTrigger:M}=h,j=i.useCallback(x=>{M(x),typeof s=="function"?s(x):s&&(s.current=x)},[M,s]),P={id:h.triggerId,"aria-haspopup":"menu","aria-expanded":h.open,"aria-controls":h.open?h.panelId:void 0,"data-menu-tree":h.treeId,onClick:x=>{o?.(x),!x.defaultPrevented&&(h.open?h.close(!1):h.openWith("panel"))},onKeyDown:x=>{if(a?.(x),x.defaultPrevented)return;const y=x.key==="Enter"||x.key===" "||x.key==="ArrowDown"?"first":x.key==="ArrowUp"?"last":null;if(!y)return;x.preventDefault();const k=h.open?document.getElementById(h.panelId):null;if(k){const C=L(k);S(y==="last"?C[C.length-1]:C[0]);return}h.openWith(y)}};return r?c.jsx("button",{ref:j,type:"button",className:n,...I,...P,children:t}):c.jsx(be.Button,{ref:j,className:E.cn("rst:min-h-11 rst:min-w-11",n),variant:u,colorScheme:p,size:m,isLoading:f,loadingLabel:l,startIcon:b,endIcon:w,surface:g,...I,...P,children:t})});function Ce({children:e,align:r="start",offset:n=4,className:o,...a}){const t=R("MenuItems"),{mounted:d,closed:s,leaving:u}=ee(t.open),p=i.useRef(null),m=$(t.triggerEl),{setPanel:f,panelEl:l,floatingStyles:b,placement:w,isPositioned:g}=te(t.triggerEl,d,`bottom-${r}`,n,p),I=se(),{typedRef:h,typingAhead:M}=ne(),j=oe({panelRef:p,typedRef:h,onEscape:()=>t.close(!0)});i.useLayoutEffect(()=>{if(!t.open||!l)return;const y=L(l),k=t.focusOnOpen.current;k==="first"&&y[0]?S(y[0]):k==="last"&&y.length?S(y[y.length-1]):l.focus({preventScroll:!0})},[t.open,l,t.focusOnOpen]),i.useLayoutEffect(()=>{if(!g||!l)return;const y=document.activeElement;y&&y!==l&&l.contains(y)&&y.scrollIntoView?.({block:"nearest"})},[g,l]),i.useLayoutEffect(()=>{!t.open&&l?.contains(document.activeElement)&&t.triggerRef.current?.focus({preventScroll:!0})},[t.open,l,t.triggerRef]);const P=y=>{const k=y.relatedTarget;k&&!k.closest(`[data-menu-tree="${t.treeId}"]`)&&t.close(!1)};if(!d)return null;const x=!!t.triggerEl?.closest(".dark");return c.jsx(W.Portal,{children:c.jsx(_.Provider,{value:{...I,panelRef:p,typingAhead:M},children:c.jsx("div",{ref:f,id:t.panelId,dir:m?"rtl":"ltr",role:"menu","aria-labelledby":a["aria-label"]?void 0:t.triggerId,"aria-label":a["aria-label"],tabIndex:-1,"data-menu-panel":"","data-menu-tree":t.treeId,"data-closed":s?"":void 0,"data-leaving":u?"":void 0,onKeyDown:j,onBlur:P,style:{...b,...J,transformOrigin:Q(w,m)},className:E.cn(x&&"dark",Z,o),children:e})})})}function se(){const[e,r]=i.useState(null),n=i.useRef(!1),o=i.useRef(void 0);return i.useEffect(()=>()=>window.clearTimeout(o.current),[]),{openSubmenu:e,focusFirstInSubmenu:n,setOpenSubmenu:(a,t=!1)=>{window.clearTimeout(o.current),n.current=t,r(a)},hoverItem:a=>{window.clearTimeout(o.current),a===null&&(o.current=window.setTimeout(()=>r(null),Y))}}}const O=E.cn("rst:relative rst:flex rst:w-full rst:min-h-11 rst:items-center rst:gap-2.5 rst:border-0 rst:bg-transparent rst:px-3 rst:py-2 rst:text-start rst:text-sm rst:text-inherit rst:no-underline rst:[font-family:inherit] rst:cursor-pointer rst:select-none rst:outline-none rst:transition-colors","rst:focus:bg-primary-100 rst:focus:text-primary-900 rst:dark:focus:bg-primary-900/30 rst:dark:focus:text-primary-100 rst:forced-colors:focus:outline-solid rst:forced-colors:focus:outline-2 rst:forced-colors:focus:-outline-offset-2","rst:aria-disabled:opacity-50 rst:aria-disabled:cursor-not-allowed"),q="rst:text-error-600 rst:dark:text-error-400 rst:focus:bg-error-50 rst:focus:text-error-700 rst:dark:focus:bg-error-900/30 rst:dark:focus:text-error-300",Ae=()=>c.jsx("svg",{viewBox:"0 0 16 16",width:"16",height:"16",fill:"none",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",strokeLinejoin:"round","aria-hidden":"true",children:c.jsx("path",{d:"M3.5 8.5 6.5 11.5 12.5 4.5"})}),De=()=>c.jsx("svg",{viewBox:"0 0 16 16",width:"16",height:"16","aria-hidden":"true",children:c.jsx("circle",{cx:"8",cy:"8",r:"3.5",fill:"currentColor"})}),Le=()=>c.jsx("svg",{viewBox:"0 0 16 16",width:"16",height:"16",fill:"none",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",strokeLinejoin:"round","aria-hidden":"true",className:"rst:rtl:rotate-180",children:c.jsx("path",{d:"M6 3.5 10.5 8 6 12.5"})});function T({icon:e,shortcut:r,description:n,badge:o,children:a},t,d,s){const u=t??e;return c.jsxs(c.Fragment,{children:[u!=null&&c.jsx("span",{"aria-hidden":"true",className:E.cn("rst:flex rst:size-4 rst:shrink-0 rst:items-center rst:justify-center",n!=null&&"rst:self-start rst:mt-0.5"),"data-menu-lead":"",children:u}),c.jsxs("span",{className:"rst:min-w-0 rst:flex-1 rst:break-words","data-menu-label":"",children:[a,n!=null&&c.jsx("span",{id:s,"aria-hidden":"true",className:"rst:block rst:text-xs rst:opacity-75","data-menu-description":"",children:n})]}),o!=null&&c.jsx("span",{className:"rst:flex rst:shrink-0 rst:items-center","data-menu-badge":"",children:o}),r&&c.jsx("span",{"aria-hidden":"true",className:"rst:ms-auto rst:shrink-0 rst:ps-4 rst:text-xs rst:tracking-wide rst:opacity-70","data-menu-shortcut":"",children:r}),d]})}function z({disabled:e,closeOnSelect:r,onChoose:n,spaceKeepsOpen:o}){const a=R("MenuItem"),t=K("MenuItem"),d=(s,u)=>{if(e){s.preventDefault();return}n(s),u&&a.close(!0)};return{onClick:s=>d(s,r),onKeyDown:s=>{if(s.key===" "){if(t.typingAhead())return;s.preventDefault(),d(s,r&&!o)}},onPointerMove:s=>{X(s)&&(t.hoverItem(null),document.activeElement!==s.currentTarget&&s.currentTarget.focus({preventScroll:!0}))}}}function Oe({onSelect:e,href:r,closeOnSelect:n=!0,disabled:o,destructive:a,keyshortcuts:t,textValue:d,className:s,...u}){const p=R("MenuItem"),m=K("MenuItem"),f=i.useId(),l=z({disabled:o,closeOnSelect:n,onChoose:g=>e?.(g)}),b=E.cn(O,a&&q,s),w={role:"menuitem",tabIndex:-1,"aria-disabled":o?!0:void 0,"aria-keyshortcuts":t,"aria-describedby":u.description!=null?f:void 0,"data-menu-item":"","data-menu-text":d,"data-destructive":a?"":void 0};if(r!==void 0){const g=p.linkComponent;return c.jsx(g,{href:r,to:r,className:b,...w,onClick:l.onClick,onPointerMove:l.onPointerMove,onKeyDown:I=>{if(I.key===" "){if(m.typingAhead())return;I.preventDefault(),o||I.currentTarget.click()}},children:T(u,void 0,void 0,f)})}return c.jsx("button",{type:"button",className:b,...w,...l,children:T(u,void 0,void 0,f)})}function Ne({checked:e,onCheckedChange:r,indicator:n,closeOnSelect:o=!0,disabled:a,destructive:t,keyshortcuts:d,textValue:s,className:u,...p}){const m=z({disabled:a,closeOnSelect:o,spaceKeepsOpen:!0,onChoose:()=>r(!e)}),f=i.useId();return c.jsx("button",{type:"button",role:"menuitemcheckbox","aria-checked":e,"aria-describedby":p.description!=null?f:void 0,tabIndex:-1,"aria-disabled":a||void 0,"aria-keyshortcuts":d,"data-menu-item":"","data-menu-text":s,className:E.cn(O,t&&q,u),...m,children:n===void 0?T(p,c.jsx("span",{className:e?void 0:"rst:invisible","data-menu-check":"",children:c.jsx(Ae,{})}),void 0,f):T(p,void 0,c.jsx("span",{"aria-hidden":"true",className:"rst:ms-auto rst:flex rst:shrink-0 rst:items-center",children:n}),f)})}function Be({value:e,onValueChange:r,label:n,children:o,className:a}){return c.jsx(V.Provider,{value:{value:e,onValueChange:r},children:c.jsx(ae,{label:n,className:a,children:o})})}function _e({value:e,closeOnSelect:r=!0,disabled:n,destructive:o,keyshortcuts:a,textValue:t,className:d,...s}){const u=i.useContext(V);if(!u)throw new Error("<MenuRadioItem> must be inside a <MenuRadioGroup>.");const p=u.value===e,m=z({disabled:n,closeOnSelect:r,spaceKeepsOpen:!0,onChoose:()=>u.onValueChange?.(e)}),f=i.useId();return c.jsx("button",{type:"button",role:"menuitemradio","aria-checked":p,"aria-describedby":s.description!=null?f:void 0,tabIndex:-1,"aria-disabled":n||void 0,"aria-keyshortcuts":a,"data-menu-item":"","data-menu-text":t,className:E.cn(O,o&&q,d),...m,children:T(s,c.jsx("span",{className:p?void 0:"rst:invisible","data-menu-check":"",children:c.jsx(De,{})}),void 0,f)})}function ae({label:e,children:r,className:n}){const o=i.useId();return c.jsxs("div",{role:"group","aria-labelledby":e?o:void 0,className:n,children:[e&&c.jsx("div",{id:o,className:"rst:px-3 rst:pt-2 rst:pb-1 rst:text-xs rst:font-semibold rst:text-gray-600 rst:dark:text-gray-400","data-menu-heading":"",children:e}),r]})}function Ke({className:e}){return c.jsx("div",{role:"separator",className:E.cn("rst:my-1 rst:h-[var(--roster-border-width,1px)] rst:bg-[var(--roster-popover-border)]",e)})}function $e({label:e,children:r,icon:n,disabled:o,textValue:a,className:t,panelClassName:d}){const s=R("MenuSubmenu"),u=K("MenuSubmenu"),p=i.useId(),m=`${p}-item`,f=`${p}-menu`,l=u.openSubmenu===m,b=i.useRef(null),[w,g]=i.useState(null),I=i.useCallback(v=>{b.current=v,g(v)},[]),h=i.useRef(null),M=i.useRef(void 0),j=i.useRef("mouse"),{mounted:P,closed:x,leaving:y}=ee(l),k=$(w),{setPanel:C,panelEl:N,floatingStyles:ie,placement:ue}=te(w,P,k?"left-start":"right-start",{mainAxis:0,alignmentAxis:-4},h,!0),ce=se();i.useEffect(()=>()=>window.clearTimeout(M.current),[]);const F=()=>{u.setOpenSubmenu(null),b.current?.focus({preventScroll:!0})},{typedRef:le,typingAhead:de}=ne(),fe=oe({panelRef:h,typedRef:le,onEscape:F,onBack:F});i.useLayoutEffect(()=>{!l||!N||u.focusFirstInSubmenu.current&&S(L(N)[0])},[l,N,u.focusFirstInSubmenu]);const B=v=>{if(!o){if(l&&v&&h.current){S(L(h.current)[0]);return}u.setOpenSubmenu(m,v)}},me=!!s.triggerEl?.closest(".dark");return c.jsxs(c.Fragment,{children:[c.jsx("button",{ref:I,id:m,type:"button",role:"menuitem",tabIndex:-1,"aria-haspopup":"menu","aria-expanded":l,"aria-controls":l?f:void 0,"aria-disabled":o||void 0,"data-menu-item":"","data-menu-text":a,"data-menu-tree":s.treeId,className:E.cn(O,l&&"rst:bg-gray-100 rst:dark:bg-gray-700/50",t),onPointerDown:v=>{j.current=v.pointerType},onClick:()=>l&&j.current!=="mouse"?u.setOpenSubmenu(null):B(!1),onKeyDown:v=>{const pe=k?"ArrowLeft":"ArrowRight";v.key===" "&&u.typingAhead()||(v.key===pe||v.key==="Enter"||v.key===" ")&&(v.preventDefault(),B(!0))},onPointerMove:v=>{X(v)&&(u.hoverItem(m),document.activeElement!==v.currentTarget&&v.currentTarget.focus({preventScroll:!0}),!l&&!o&&(window.clearTimeout(M.current),M.current=window.setTimeout(()=>B(!1),Y)))},onPointerLeave:()=>window.clearTimeout(M.current),children:T({icon:n,children:e},void 0,c.jsx("span",{"aria-hidden":"true",className:"rst:ms-auto rst:flex rst:shrink-0 rst:items-center rst:opacity-70","data-menu-chevron":"",children:c.jsx(Le,{})}))}),P&&c.jsx(W.Portal,{children:c.jsx(_.Provider,{value:{...ce,panelRef:h,typingAhead:de},children:c.jsx("div",{ref:C,id:f,dir:k?"rtl":"ltr",role:"menu","aria-labelledby":m,tabIndex:-1,"data-menu-panel":"","data-menu-tree":s.treeId,"data-closed":x?"":void 0,"data-leaving":y?"":void 0,onKeyDown:fe,onPointerEnter:()=>u.hoverItem(m),style:{...ie,...J,transformOrigin:Q(ue,k)},className:E.cn(me&&"dark",Z,d),children:r})})})]})}exports.Menu=Te;exports.MenuButton=Re;exports.MenuCheckboxItem=Ne;exports.MenuGroup=ae;exports.MenuItem=Oe;exports.MenuItems=Ce;exports.MenuRadioGroup=Be;exports.MenuRadioItem=_e;exports.MenuSeparator=Ke;exports.MenuSubmenu=$e;
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { ButtonProps } from '../../atoms/Button/Button';
|
|
2
|
+
import * as React from "react";
|
|
3
|
+
/**
|
|
4
|
+
* The props every link item is rendered with. `href` and `to` carry the same
|
|
5
|
+
* value, so `next/link`, React Router's `Link` and a plain `<a>` all work
|
|
6
|
+
* without a wrapper. The rest must reach the DOM: the role, the tab index and
|
|
7
|
+
* the handlers are what make the link an item of the menu.
|
|
8
|
+
*/
|
|
9
|
+
export interface MenuLinkProps {
|
|
10
|
+
href: string;
|
|
11
|
+
to: string;
|
|
12
|
+
className: string;
|
|
13
|
+
children: React.ReactNode;
|
|
14
|
+
role: "menuitem";
|
|
15
|
+
tabIndex: -1;
|
|
16
|
+
"aria-disabled"?: true;
|
|
17
|
+
"aria-keyshortcuts"?: string;
|
|
18
|
+
"aria-describedby"?: string;
|
|
19
|
+
"data-menu-item": "";
|
|
20
|
+
onClick: (event: React.MouseEvent<HTMLAnchorElement>) => void;
|
|
21
|
+
onKeyDown: (event: React.KeyboardEvent<HTMLAnchorElement>) => void;
|
|
22
|
+
onPointerMove: (event: React.PointerEvent<HTMLAnchorElement>) => void;
|
|
23
|
+
}
|
|
24
|
+
export interface MenuProps {
|
|
25
|
+
/** The trigger (`MenuButton`) and the panel (`MenuItems`). */
|
|
26
|
+
children: React.ReactNode;
|
|
27
|
+
/**
|
|
28
|
+
* Controlled open state. Leave both off to let the menu keep its own. Pass
|
|
29
|
+
* `onOpenChange` with `open`: every way the menu closes itself (Escape,
|
|
30
|
+
* Tab, a press outside, a choice) is a call to it.
|
|
31
|
+
*/
|
|
32
|
+
open?: boolean;
|
|
33
|
+
onOpenChange?: (open: boolean) => void;
|
|
34
|
+
/**
|
|
35
|
+
* Renders every link item (a `MenuItem` with an `href`). Defaults to a plain
|
|
36
|
+
* `<a>`, which inside a router turns each choice into a full page load: pass
|
|
37
|
+
* `next/link` or your router's link, from a client component.
|
|
38
|
+
*/
|
|
39
|
+
linkComponent?: React.ComponentType<MenuLinkProps>;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* A button that opens a panel of actions: the menu button pattern. Compose it:
|
|
43
|
+
*
|
|
44
|
+
* ```tsx
|
|
45
|
+
* <Menu>
|
|
46
|
+
* <MenuButton>Actions</MenuButton>
|
|
47
|
+
* <MenuItems>
|
|
48
|
+
* <MenuItem icon={<EditIcon />} shortcut="⌘E" onSelect={edit}>Edit</MenuItem>
|
|
49
|
+
* <MenuSeparator />
|
|
50
|
+
* <MenuItem destructive onSelect={remove}>Delete</MenuItem>
|
|
51
|
+
* </MenuItems>
|
|
52
|
+
* </Menu>
|
|
53
|
+
* ```
|
|
54
|
+
*
|
|
55
|
+
* The keyboard follows the pattern: Enter, Space or Down open it on the first
|
|
56
|
+
* item and Up on the last; arrows move, Home and End jump, a typed letter
|
|
57
|
+
* jumps to the next item starting with it; Escape closes it and puts focus
|
|
58
|
+
* back on the button; Tab closes it and moves on. A submenu opens with the
|
|
59
|
+
* arrow away from the parent and closes with the arrow back.
|
|
60
|
+
*/
|
|
61
|
+
declare function Menu({ children, open: openProp, onOpenChange, linkComponent }: MenuProps): import("react/jsx-runtime").JSX.Element;
|
|
62
|
+
export interface MenuButtonProps extends ButtonProps {
|
|
63
|
+
/**
|
|
64
|
+
* A plain `<button>` with only your `className`, for a trigger drawn its own
|
|
65
|
+
* way: an avatar, an icon. Keep its target at least 44 by 44, with
|
|
66
|
+
* padding or a hit area, if you draw it smaller.
|
|
67
|
+
*/
|
|
68
|
+
plain?: boolean;
|
|
69
|
+
}
|
|
70
|
+
declare const MenuButton: React.ForwardRefExoticComponent<MenuButtonProps & React.RefAttributes<HTMLButtonElement>>;
|
|
71
|
+
export interface MenuItemsProps {
|
|
72
|
+
children: React.ReactNode;
|
|
73
|
+
/** Which edge of the button the panel lines up with. */
|
|
74
|
+
align?: "start" | "end";
|
|
75
|
+
/** The gap between the button and the panel, in px. */
|
|
76
|
+
offset?: number;
|
|
77
|
+
/** Names the menu when the button's own name shouldn't. By default the button names it. */
|
|
78
|
+
"aria-label"?: string;
|
|
79
|
+
/** Applied to the panel. Its surface reads `--roster-popover-*`; set those to repaint it. */
|
|
80
|
+
className?: string;
|
|
81
|
+
}
|
|
82
|
+
declare function MenuItems({ children, align, offset, className, ...aria }: MenuItemsProps): import("react/jsx-runtime").JSX.Element | null;
|
|
83
|
+
interface ItemContent {
|
|
84
|
+
/** A leading icon. Decorative: the item's words name it. */
|
|
85
|
+
icon?: React.ReactNode;
|
|
86
|
+
/** A second, quieter line under the label: what the item does, or to what. Read as the item's description. */
|
|
87
|
+
description?: React.ReactNode;
|
|
88
|
+
/** A count or a status at the end of the row, read with the item's name. */
|
|
89
|
+
badge?: React.ReactNode;
|
|
90
|
+
/** A keyboard shortcut to show, right-aligned: display only. The app handles the keys. */
|
|
91
|
+
shortcut?: string;
|
|
92
|
+
/** The same shortcut in `aria-keyshortcuts` form ("Control+E"), for assistive technology. */
|
|
93
|
+
keyshortcuts?: string;
|
|
94
|
+
/** Reads as a consequence: red. The words still have to say what it does. */
|
|
95
|
+
destructive?: boolean;
|
|
96
|
+
/** Can be reached by the arrows but not chosen. */
|
|
97
|
+
disabled?: boolean;
|
|
98
|
+
/** What typeahead matches, when the item's text isn't the right thing to match. */
|
|
99
|
+
textValue?: string;
|
|
100
|
+
className?: string;
|
|
101
|
+
children: React.ReactNode;
|
|
102
|
+
}
|
|
103
|
+
export interface MenuItemProps extends ItemContent {
|
|
104
|
+
/** Called when the item is chosen: a click, Enter or Space. */
|
|
105
|
+
onSelect?: (event: React.SyntheticEvent) => void;
|
|
106
|
+
/** Makes it a link item. It navigates through the menu's `linkComponent`. */
|
|
107
|
+
href?: string;
|
|
108
|
+
/** Close the menu once chosen. On by default. */
|
|
109
|
+
closeOnSelect?: boolean;
|
|
110
|
+
}
|
|
111
|
+
/** One action. With `href`, a link. */
|
|
112
|
+
declare function MenuItem({ onSelect, href, closeOnSelect, disabled, destructive, keyshortcuts, textValue, className, ...content }: MenuItemProps): import("react/jsx-runtime").JSX.Element;
|
|
113
|
+
export interface MenuCheckboxItemProps extends ItemContent {
|
|
114
|
+
checked: boolean;
|
|
115
|
+
onCheckedChange: (checked: boolean) => void;
|
|
116
|
+
/**
|
|
117
|
+
* Replaces the leading check mark with an indicator of your own at the end
|
|
118
|
+
* of the row: a switch's track, say. Its state still reaches assistive
|
|
119
|
+
* technology through `aria-checked`.
|
|
120
|
+
*/
|
|
121
|
+
indicator?: React.ReactNode;
|
|
122
|
+
/** Close the menu once chosen by click or Enter. Space always keeps it open. */
|
|
123
|
+
closeOnSelect?: boolean;
|
|
124
|
+
}
|
|
125
|
+
/** An on/off choice: `menuitemcheckbox`. */
|
|
126
|
+
declare function MenuCheckboxItem({ checked, onCheckedChange, indicator, closeOnSelect, disabled, destructive, keyshortcuts, textValue, className, ...content }: MenuCheckboxItemProps): import("react/jsx-runtime").JSX.Element;
|
|
127
|
+
export interface MenuRadioGroupProps {
|
|
128
|
+
value: string | undefined;
|
|
129
|
+
onValueChange: (value: string) => void;
|
|
130
|
+
/** The choice's name, shown above its items and read with them. */
|
|
131
|
+
label?: React.ReactNode;
|
|
132
|
+
children: React.ReactNode;
|
|
133
|
+
className?: string;
|
|
134
|
+
}
|
|
135
|
+
/** One choice among several: its items are `menuitemradio`. */
|
|
136
|
+
declare function MenuRadioGroup({ value, onValueChange, label, children, className }: MenuRadioGroupProps): import("react/jsx-runtime").JSX.Element;
|
|
137
|
+
export interface MenuRadioItemProps extends ItemContent {
|
|
138
|
+
value: string;
|
|
139
|
+
/** Close the menu once chosen by click or Enter. Space always keeps it open. */
|
|
140
|
+
closeOnSelect?: boolean;
|
|
141
|
+
}
|
|
142
|
+
declare function MenuRadioItem({ value, closeOnSelect, disabled, destructive, keyshortcuts, textValue, className, ...content }: MenuRadioItemProps): import("react/jsx-runtime").JSX.Element;
|
|
143
|
+
export interface MenuGroupProps {
|
|
144
|
+
/** Shown above the group's items, and names the group. */
|
|
145
|
+
label?: React.ReactNode;
|
|
146
|
+
children: React.ReactNode;
|
|
147
|
+
className?: string;
|
|
148
|
+
}
|
|
149
|
+
/** Items that belong together, under an optional label. */
|
|
150
|
+
declare function MenuGroup({ label, children, className }: MenuGroupProps): import("react/jsx-runtime").JSX.Element;
|
|
151
|
+
/** A rule between groups of items. */
|
|
152
|
+
declare function MenuSeparator({ className }: {
|
|
153
|
+
className?: string;
|
|
154
|
+
}): import("react/jsx-runtime").JSX.Element;
|
|
155
|
+
export interface MenuSubmenuProps extends Omit<ItemContent, "shortcut" | "keyshortcuts" | "children" | "destructive" | "description" | "badge"> {
|
|
156
|
+
/** The item that opens it. */
|
|
157
|
+
label: React.ReactNode;
|
|
158
|
+
/** Its items. */
|
|
159
|
+
children: React.ReactNode;
|
|
160
|
+
/** Applied to the submenu's panel. */
|
|
161
|
+
panelClassName?: string;
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* An item that opens a menu of its own beside it. It opens with the arrow away
|
|
165
|
+
* from its parent, Enter, Space, a click, or the pointer resting on it; the
|
|
166
|
+
* arrow back, or Escape, closes it and returns to its item.
|
|
167
|
+
*/
|
|
168
|
+
declare function MenuSubmenu({ label, children, icon, disabled, textValue, className, panelClassName }: MenuSubmenuProps): import("react/jsx-runtime").JSX.Element;
|
|
169
|
+
export { Menu, MenuButton, MenuItems, MenuItem, MenuCheckboxItem, MenuRadioGroup, MenuRadioItem, MenuGroup, MenuSeparator, MenuSubmenu, };
|