@ahrowe/ui 0.47.3 → 0.48.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/CHANGELOG.md +14 -0
- package/LICENSE +1 -1
- package/README.md +2 -2
- package/dist/cjs-types/common/menu/menu.types.d.ts +5 -0
- package/dist/cjs-types/common/menu/menuHotkeys.d.ts +2 -2
- package/dist/cjs-types/common/menu/menuItem.d.ts +10 -0
- package/dist/esm/common/commandPalette/commandPalette.mjs +1 -1
- package/dist/esm/common/commandPalette/commandPalette.mjs.map +1 -1
- package/dist/esm/common/menu/menu.mjs +1 -1
- package/dist/esm/common/menu/menu.mjs.map +1 -1
- package/dist/esm/common/menu/menu.module.mjs.map +1 -1
- package/dist/esm/common/menu/menu.types.mjs.map +1 -1
- package/dist/esm/common/menu/menuHotkeys.mjs +1 -1
- package/dist/esm/common/menu/menuHotkeys.mjs.map +1 -1
- package/dist/esm/common/menu/menuItem.mjs +2 -0
- package/dist/esm/common/menu/menuItem.mjs.map +1 -0
- package/dist/esm/common/radioGroup/radioGroup.module.mjs.map +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/menu/menu.types.d.ts +5 -0
- package/dist/types/common/menu/menuHotkeys.d.ts +2 -2
- package/dist/types/common/menu/menuItem.d.ts +10 -0
- package/docs/CommandPalette.md +1 -1
- package/docs/InteractableDiv.md +1 -1
- package/docs/Menu.md +6 -5
- package/docs/ProgressBar.md +1 -1
- package/docs/RadioGroup.md +1 -1
- package/docs/ThemeProvider.md +1 -1
- package/package.json +4 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# @ahrowe/ui
|
|
2
2
|
|
|
3
|
+
## 0.48.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 60d1ee6: Menu entries take an `href` and render as real links, so Ctrl/⌘-click and a middle click open them in a new tab; CommandPalette and `menuHotkeys` follow it too
|
|
8
|
+
|
|
9
|
+
### Patch Changes
|
|
10
|
+
|
|
11
|
+
- 60d1ee6: Menu and CommandPalette line up their labels when only some entries have an icon
|
|
12
|
+
- f1eab37: ProgressBar.md gives the track colour's real fallback, `--background-accent-light`
|
|
13
|
+
- f2dd48a: Centred RadioGroup's dot, which rendered a pixel off towards the bottom right
|
|
14
|
+
- 79a9eb7: Moved the repository to github.com/ahrowe-dev/ahrowe-ui, and changed the copyright holder in LICENSE to ahrowe, from 2026
|
|
15
|
+
- f1eab37: ThemeProvider.md points to the full variable list in the installed package instead of a source path npm does not ship
|
|
16
|
+
|
|
3
17
|
## 0.47.3
|
|
4
18
|
|
|
5
19
|
### Patch Changes
|
package/LICENSE
CHANGED
package/README.md
CHANGED
|
@@ -72,7 +72,7 @@ The docs ship inside the package and Claude reads them out of `node_modules`. Th
|
|
|
72
72
|
until Claude actually works on UI.
|
|
73
73
|
|
|
74
74
|
```bash
|
|
75
|
-
/plugin marketplace add
|
|
75
|
+
/plugin marketplace add ahrowe-dev/ahrowe-ui
|
|
76
76
|
/plugin install ahrowe-ui@ahrowe
|
|
77
77
|
```
|
|
78
78
|
|
|
@@ -81,7 +81,7 @@ For a whole team at once, commit this to the project's `.claude/settings.json` i
|
|
|
81
81
|
```json
|
|
82
82
|
{
|
|
83
83
|
"extraKnownMarketplaces": {
|
|
84
|
-
"ahrowe": { "source": { "source": "github", "repo": "
|
|
84
|
+
"ahrowe": { "source": { "source": "github", "repo": "ahrowe-dev/ahrowe-ui" } }
|
|
85
85
|
},
|
|
86
86
|
"enabledPlugins": [{ "name": "ahrowe-ui", "marketplace": "ahrowe" }]
|
|
87
87
|
}
|
|
@@ -25,6 +25,11 @@ export interface MenuAction {
|
|
|
25
25
|
disabled?: boolean;
|
|
26
26
|
/** Renders in the destructive colour. For deleting, discarding, revoking. */
|
|
27
27
|
danger?: boolean;
|
|
28
|
+
/**
|
|
29
|
+
* Followed after `onClick` and `onAction`. In `Menu` a real `<a>`, so Ctrl/⌘-click opens a new tab.
|
|
30
|
+
* Loads the whole page: with a client-side router, navigate in `onClick` instead.
|
|
31
|
+
*/
|
|
32
|
+
href?: string;
|
|
28
33
|
onClick?: () => void;
|
|
29
34
|
}
|
|
30
35
|
/** A line between groups of actions. */
|
|
@@ -14,7 +14,7 @@ import { MenuEntry } from './menu.types.js';
|
|
|
14
14
|
* useHotkeys(menuHotkeys(items, run), { enabled: isOpen }); // only while the menu is open
|
|
15
15
|
* ```
|
|
16
16
|
*
|
|
17
|
-
* Runs each entry the way clicking it does: its own `onClick`, then `onAction
|
|
18
|
-
* entry, and an entry with no `shortcut`, is left out.
|
|
17
|
+
* Runs each entry the way clicking it does: its own `onClick`, then `onAction`, then its `href`.
|
|
18
|
+
* A `disabled` entry, and an entry with no `shortcut`, is left out.
|
|
19
19
|
*/
|
|
20
20
|
export declare function menuHotkeys(items: MenuEntry[], onAction?: (id: string) => void): HotkeyMap;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { CSSProperties, ReactNode } from 'react';
|
|
2
|
+
import { MenuAction, MenuEntry } from './menu.types.js';
|
|
3
|
+
export declare function hasAnyIcon(items: MenuEntry[]): boolean;
|
|
4
|
+
/** The action's icon, or an empty cell as wide when another action has one, so the labels line up. */
|
|
5
|
+
export declare function renderItemIcon(icon: MenuAction['icon'], reserveCell: boolean, props: {
|
|
6
|
+
className: string;
|
|
7
|
+
style?: CSSProperties;
|
|
8
|
+
}): ReactNode;
|
|
9
|
+
/** Runs an action the way clicking it does: its own `onClick`, then `onAction`, then its `href`. */
|
|
10
|
+
export declare function runMenuAction(action: MenuAction, onAction?: (id: string) => void): void;
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
"use client";import{useComponentDefaults as e}from"../configProvider/useComponentDefaults.mjs";import{useLabels as t}from"../configProvider/useLabels.mjs";import{renderIcon as n}from"../utils/renderIcon.mjs";import r from"../modal/modal.mjs";import{formatHotkey as
|
|
1
|
+
"use client";import{useComponentDefaults as e}from"../configProvider/useComponentDefaults.mjs";import{useLabels as t}from"../configProvider/useLabels.mjs";import{renderIcon as n}from"../utils/renderIcon.mjs";import r from"../modal/modal.mjs";import{formatHotkey as i}from"../hooks/useHotkeys.mjs";import{edgeIndex as a,nextIndex as o,scrollOptionIntoView as s}from"../utils/listNavigation.mjs";import{hasAnyIcon as ee,renderItemIcon as te,runMenuAction as c}from"../menu/menuItem.mjs";import{filterCommands as l,groupCommands as u}from"./filterCommands.mjs";import d from"./commandPalette.module.mjs";import{useEffect as f,useId as ne,useRef as p,useState as m}from"react";import h from"classnames";import{faMagnifyingGlass as g}from"@fortawesome/free-solid-svg-icons";import{jsx as _,jsxs as v}from"react/jsx-runtime";var y={label:`Search commands`,placeholder:`Type a command or search…`,empty:`No results`,loading:`Searching…`,results:e=>e===1?`1 result`:`${e} results`};function b(b){let{items:x,isOpen:S,onClose:C,onAction:w,onQueryChange:T,filter:E=!0,isLoading:D=!1,presentation:O,labels:re,className:ie,style:ae,classNames:k,styles:A,...j}=e(`CommandPalette`,b),M=t(`CommandPalette`,re,y),N=ne(),P=p(null),[F,I]=m(``),[L,R]=m(null),[z,B]=m(S);S!==z&&(B(S),S&&(I(``),R(null)));let V=p(T);f(()=>{V.current=T}),f(()=>{S&&V.current?.(``)},[S]);let H=E?l(u(x),F):u(x),U=ee(x),W=H.flatMap(e=>e.actions),G=e=>!!W[e]?.disabled,K=W.findIndex(e=>e.id===L&&!e.disabled),q=K>=0?K:a(`first`,W.length,{isDisabled:G}),J=e=>`${N}-${encodeURIComponent(e.id)}`,Y=q===null?void 0:J(W[q]);f(()=>{Y&&s(P.current,document.getElementById(Y))},[Y]);function X(e){e&&!e.disabled&&(c(e,w),C())}function oe(e){if(!e.nativeEvent.isComposing){if(e.key===`ArrowDown`||e.key===`ArrowUp`){let t=o(q,e.key===`ArrowDown`?1:-1,W.length,{isDisabled:G});R(t===null?null:W[t].id)}else if(e.key===`Enter`)X(q===null?void 0:W[q]);else return;e.preventDefault()}}let Z=F.trim()!==``,Q=D?M.loading:M.empty,$=-1;return v(r,{...j,"aria-label":M.label,isOpen:S,onClose:C,hideHeader:!0,presentation:O,className:h(d.commandPalette,ie,k?.root),style:{...ae,...A?.root},children:[v(`div`,{className:h(d.commandPaletteSearch,k?.search),style:A?.search,children:[n(g,{className:d.commandPaletteSearchIcon}),_(`input`,{type:`text`,role:`combobox`,"aria-label":M.label,"aria-expanded":`true`,"aria-controls":N,"aria-autocomplete":`list`,"aria-activedescendant":Y,autoComplete:`off`,spellCheck:!1,placeholder:M.placeholder,value:F,onChange:e=>{I(e.target.value),R(null),T?.(e.target.value)},onKeyDown:oe,className:h(d.commandPaletteInput,k?.input),style:A?.input})]}),_(`div`,{ref:P,id:N,role:`listbox`,"aria-label":M.label,className:h(d.commandPaletteList,k?.list),style:A?.list,children:H.map((e,t)=>{let n=`${N}-heading-${t}`;return v(`div`,{role:e.heading===void 0?`presentation`:`group`,"aria-labelledby":e.heading===void 0?void 0:n,children:[e.separated&&t>0&&_(`div`,{"aria-hidden":!0,className:h(d.commandPaletteSeparator,k?.separator),style:A?.separator}),e.heading!==void 0&&_(`div`,{id:n,role:`presentation`,className:h(d.commandPaletteHeading,k?.heading),style:A?.heading,children:e.heading}),e.actions.map(e=>{$+=1;let t=$;return v(`div`,{id:J(e),role:`option`,"aria-selected":t===q,"aria-disabled":e.disabled||void 0,className:h(d.commandPaletteItem,t===q&&d.commandPaletteItemActive,e.danger&&d.commandPaletteItemDanger,e.disabled&&d.commandPaletteItemDisabled,k?.item),style:A?.item,onMouseDown:e=>e.preventDefault(),onMouseMove:()=>!e.disabled&&R(e.id),onClick:()=>X(e),children:[te(e.icon,U,{className:h(d.commandPaletteItemIcon,k?.itemIcon),style:A?.itemIcon}),_(`span`,{className:h(d.commandPaletteItemLabel,k?.itemLabel),style:A?.itemLabel,children:e.label}),e.shortcut!=null&&_(`span`,{className:h(d.commandPaletteItemShortcut,k?.itemShortcut),style:A?.itemShortcut,children:i(e.shortcut)})]},e.id)})]},t)})}),Z&&W.length===0&&_(`div`,{className:h(d.commandPaletteEmpty,k?.empty),style:A?.empty,children:Q}),_(`span`,{className:d.commandPaletteStatus,role:`status`,"aria-live":`polite`,children:Z?W.length>0?M.results(W.length):Q:``})]})}export{b as default};
|
|
2
2
|
//# sourceMappingURL=commandPalette.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"commandPalette.mjs","names":[],"sources":["../../../../package/common/commandPalette/commandPalette.tsx"],"sourcesContent":["import { useEffect, useId, useRef, useState, type KeyboardEvent, type ReactElement } from 'react';\nimport cx from 'classnames';\nimport { faMagnifyingGlass } from '@fortawesome/free-solid-svg-icons';\n\nimport Modal from '../modal';\nimport { useComponentDefaults, useLabels } from '../configProvider';\nimport { formatHotkey } from '../hooks/useHotkeys';\nimport { edgeIndex, nextIndex, scrollOptionIntoView } from '../utils/listNavigation';\nimport { renderIcon } from '../utils/renderIcon';\nimport type { MenuAction } from '../menu/menu.types';\nimport { filterCommands, groupCommands } from './filterCommands';\nimport type { CommandPaletteLabels, CommandPaletteProps } from './commandPalette.types';\nimport styles from './commandPalette.module.pcss';\n\nconst DEFAULT_LABELS: Required<CommandPaletteLabels> = {\n label: 'Search commands',\n placeholder: 'Type a command or search…',\n empty: 'No results',\n loading: 'Searching…',\n results: (count) => (count === 1 ? '1 result' : `${count} results`),\n};\n\nfunction CommandPalette(props: CommandPaletteProps): ReactElement {\n const {\n items,\n isOpen,\n onClose,\n onAction,\n onQueryChange,\n filter = true,\n isLoading = false,\n presentation,\n labels,\n className,\n style,\n classNames,\n styles: slotStyles,\n ...rest\n } = useComponentDefaults('CommandPalette', props);\n const text = useLabels('CommandPalette', labels, DEFAULT_LABELS);\n const listId = useId();\n const listRef = useRef<HTMLDivElement>(null);\n\n const [query, setQuery] = useState('');\n // An id rather than an index, so the highlight stays on the same command while server results\n // are replaced around it. Typing clears it instead, so Enter runs the best match.\n const [activeId, setActiveId] = useState<string | null>(null);\n const [wasOpen, setWasOpen] = useState(isOpen);\n if (isOpen !== wasOpen) {\n setWasOpen(isOpen);\n if (isOpen) {\n setQuery('');\n setActiveId(null);\n }\n }\n\n // Read through a ref so an inline handler does not re-fire the effect on every parent render.\n const onQueryChangeRef = useRef(onQueryChange);\n useEffect(() => {\n onQueryChangeRef.current = onQueryChange;\n });\n useEffect(() => {\n if (isOpen) onQueryChangeRef.current?.('');\n }, [isOpen]);\n\n const groups = filter ? filterCommands(groupCommands(items), query) : groupCommands(items);\n const actions = groups.flatMap((group) => group.actions);\n const isDisabled = (index: number) => !!actions[index]?.disabled;\n const found = actions.findIndex((action) => action.id === activeId && !action.disabled);\n const activeIndex = found >= 0 ? found : edgeIndex('first', actions.length, { isDisabled });\n // From the command's id, not its position: an id that stayed the same while the option under it\n // changed would not make a screen reader announce the new highlight.\n const optionId = (action: MenuAction) => `${listId}-${encodeURIComponent(action.id)}`;\n const activeOptionId = activeIndex === null ? undefined : optionId(actions[activeIndex]);\n\n useEffect(() => {\n if (!activeOptionId) return;\n scrollOptionIntoView(listRef.current, document.getElementById(activeOptionId));\n }, [activeOptionId]);\n\n function run(action: MenuAction | undefined) {\n if (!action || action.disabled) return;\n action.onClick?.();\n onAction?.(action.id);\n onClose();\n }\n\n function handleKeyDown(event: KeyboardEvent<HTMLInputElement>) {\n // Enter confirms an IME composition before it means anything here.\n if (event.nativeEvent.isComposing) return;\n if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {\n const next = nextIndex(activeIndex, event.key === 'ArrowDown' ? 1 : -1, actions.length, { isDisabled });\n setActiveId(next === null ? null : actions[next].id);\n } else if (event.key === 'Enter') {\n run(activeIndex === null ? undefined : actions[activeIndex]);\n } else {\n return;\n }\n event.preventDefault();\n }\n\n const hasQuery = query.trim() !== '';\n const emptyText = isLoading ? text.loading : text.empty;\n let index = -1;\n\n return (\n <Modal\n {...rest}\n aria-label={text.label}\n isOpen={isOpen}\n onClose={onClose}\n hideHeader\n presentation={presentation}\n className={cx(styles.commandPalette, className, classNames?.root)}\n style={{ ...style, ...slotStyles?.root }}\n >\n <div className={cx(styles.commandPaletteSearch, classNames?.search)} style={slotStyles?.search}>\n {renderIcon(faMagnifyingGlass, { className: styles.commandPaletteSearchIcon })}\n <input\n type='text'\n role='combobox'\n aria-label={text.label}\n aria-expanded='true'\n aria-controls={listId}\n aria-autocomplete='list'\n aria-activedescendant={activeOptionId}\n autoComplete='off'\n spellCheck={false}\n placeholder={text.placeholder}\n value={query}\n onChange={(event) => {\n setQuery(event.target.value);\n setActiveId(null);\n onQueryChange?.(event.target.value);\n }}\n onKeyDown={handleKeyDown}\n className={cx(styles.commandPaletteInput, classNames?.input)}\n style={slotStyles?.input}\n />\n </div>\n\n <div\n ref={listRef}\n id={listId}\n role='listbox'\n aria-label={text.label}\n className={cx(styles.commandPaletteList, classNames?.list)}\n style={slotStyles?.list}\n >\n {groups.map((group, groupIndex) => {\n const headingId = `${listId}-heading-${groupIndex}`;\n return (\n <div\n // Groups carry no id of their own; a new query re-renders the list wholesale anyway.\n key={groupIndex}\n role={group.heading === undefined ? 'presentation' : 'group'}\n aria-labelledby={group.heading === undefined ? undefined : headingId}\n >\n {group.separated && groupIndex > 0 && (\n <div\n aria-hidden\n className={cx(styles.commandPaletteSeparator, classNames?.separator)}\n style={slotStyles?.separator}\n />\n )}\n {group.heading !== undefined && (\n <div\n id={headingId}\n role='presentation'\n className={cx(styles.commandPaletteHeading, classNames?.heading)}\n style={slotStyles?.heading}\n >\n {group.heading}\n </div>\n )}\n {group.actions.map((action) => {\n index += 1;\n const own = index;\n return (\n <div\n key={action.id}\n id={optionId(action)}\n role='option'\n aria-selected={own === activeIndex}\n aria-disabled={action.disabled || undefined}\n className={cx(\n styles.commandPaletteItem,\n own === activeIndex && styles.commandPaletteItemActive,\n action.danger && styles.commandPaletteItemDanger,\n action.disabled && styles.commandPaletteItemDisabled,\n classNames?.item,\n )}\n style={slotStyles?.item}\n // Keeps the focus, and so the caret, in the search field.\n onMouseDown={(event) => event.preventDefault()}\n // Move, not enter: a list scrolling under a resting pointer must not steal the highlight.\n onMouseMove={() => !action.disabled && setActiveId(action.id)}\n onClick={() => run(action)}\n >\n {action.icon != null &&\n renderIcon(action.icon, {\n className: cx(styles.commandPaletteItemIcon, classNames?.itemIcon),\n style: slotStyles?.itemIcon,\n })}\n <span\n className={cx(styles.commandPaletteItemLabel, classNames?.itemLabel)}\n style={slotStyles?.itemLabel}\n >\n {action.label}\n </span>\n {action.shortcut != null && (\n <span\n className={cx(styles.commandPaletteItemShortcut, classNames?.itemShortcut)}\n style={slotStyles?.itemShortcut}\n >\n {formatHotkey(action.shortcut)}\n </span>\n )}\n </div>\n );\n })}\n </div>\n );\n })}\n </div>\n\n {hasQuery && actions.length === 0 && (\n <div className={cx(styles.commandPaletteEmpty, classNames?.empty)} style={slotStyles?.empty}>\n {emptyText}\n </div>\n )}\n <span className={styles.commandPaletteStatus} role='status' aria-live='polite'>\n {hasQuery ? (actions.length > 0 ? text.results(actions.length) : emptyText) : ''}\n </span>\n </Modal>\n );\n}\n\nexport default CommandPalette;\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"commandPalette.mjs","names":[],"sources":["../../../../package/common/commandPalette/commandPalette.tsx"],"sourcesContent":["import { useEffect, useId, useRef, useState, type KeyboardEvent, type ReactElement } from 'react';\nimport cx from 'classnames';\nimport { faMagnifyingGlass } from '@fortawesome/free-solid-svg-icons';\n\nimport Modal from '../modal';\nimport { useComponentDefaults, useLabels } from '../configProvider';\nimport { formatHotkey } from '../hooks/useHotkeys';\nimport { edgeIndex, nextIndex, scrollOptionIntoView } from '../utils/listNavigation';\nimport { renderIcon } from '../utils/renderIcon';\nimport type { MenuAction } from '../menu/menu.types';\nimport { hasAnyIcon, renderItemIcon, runMenuAction } from '../menu/menuItem';\nimport { filterCommands, groupCommands } from './filterCommands';\nimport type { CommandPaletteLabels, CommandPaletteProps } from './commandPalette.types';\nimport styles from './commandPalette.module.pcss';\n\nconst DEFAULT_LABELS: Required<CommandPaletteLabels> = {\n label: 'Search commands',\n placeholder: 'Type a command or search…',\n empty: 'No results',\n loading: 'Searching…',\n results: (count) => (count === 1 ? '1 result' : `${count} results`),\n};\n\nfunction CommandPalette(props: CommandPaletteProps): ReactElement {\n const {\n items,\n isOpen,\n onClose,\n onAction,\n onQueryChange,\n filter = true,\n isLoading = false,\n presentation,\n labels,\n className,\n style,\n classNames,\n styles: slotStyles,\n ...rest\n } = useComponentDefaults('CommandPalette', props);\n const text = useLabels('CommandPalette', labels, DEFAULT_LABELS);\n const listId = useId();\n const listRef = useRef<HTMLDivElement>(null);\n\n const [query, setQuery] = useState('');\n // An id rather than an index, so the highlight stays on the same command while server results\n // are replaced around it. Typing clears it instead, so Enter runs the best match.\n const [activeId, setActiveId] = useState<string | null>(null);\n const [wasOpen, setWasOpen] = useState(isOpen);\n if (isOpen !== wasOpen) {\n setWasOpen(isOpen);\n if (isOpen) {\n setQuery('');\n setActiveId(null);\n }\n }\n\n // Read through a ref so an inline handler does not re-fire the effect on every parent render.\n const onQueryChangeRef = useRef(onQueryChange);\n useEffect(() => {\n onQueryChangeRef.current = onQueryChange;\n });\n useEffect(() => {\n if (isOpen) onQueryChangeRef.current?.('');\n }, [isOpen]);\n\n const groups = filter ? filterCommands(groupCommands(items), query) : groupCommands(items);\n // From all items, not the matches, so the labels do not jump sideways while the user types.\n const hasIcons = hasAnyIcon(items);\n const actions = groups.flatMap((group) => group.actions);\n const isDisabled = (index: number) => !!actions[index]?.disabled;\n const found = actions.findIndex((action) => action.id === activeId && !action.disabled);\n const activeIndex = found >= 0 ? found : edgeIndex('first', actions.length, { isDisabled });\n // From the command's id, not its position: an id that stayed the same while the option under it\n // changed would not make a screen reader announce the new highlight.\n const optionId = (action: MenuAction) => `${listId}-${encodeURIComponent(action.id)}`;\n const activeOptionId = activeIndex === null ? undefined : optionId(actions[activeIndex]);\n\n useEffect(() => {\n if (!activeOptionId) return;\n scrollOptionIntoView(listRef.current, document.getElementById(activeOptionId));\n }, [activeOptionId]);\n\n function run(action: MenuAction | undefined) {\n if (!action || action.disabled) return;\n runMenuAction(action, onAction);\n onClose();\n }\n\n function handleKeyDown(event: KeyboardEvent<HTMLInputElement>) {\n // Enter confirms an IME composition before it means anything here.\n if (event.nativeEvent.isComposing) return;\n if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {\n const next = nextIndex(activeIndex, event.key === 'ArrowDown' ? 1 : -1, actions.length, { isDisabled });\n setActiveId(next === null ? null : actions[next].id);\n } else if (event.key === 'Enter') {\n run(activeIndex === null ? undefined : actions[activeIndex]);\n } else {\n return;\n }\n event.preventDefault();\n }\n\n const hasQuery = query.trim() !== '';\n const emptyText = isLoading ? text.loading : text.empty;\n let index = -1;\n\n return (\n <Modal\n {...rest}\n aria-label={text.label}\n isOpen={isOpen}\n onClose={onClose}\n hideHeader\n presentation={presentation}\n className={cx(styles.commandPalette, className, classNames?.root)}\n style={{ ...style, ...slotStyles?.root }}\n >\n <div className={cx(styles.commandPaletteSearch, classNames?.search)} style={slotStyles?.search}>\n {renderIcon(faMagnifyingGlass, { className: styles.commandPaletteSearchIcon })}\n <input\n type='text'\n role='combobox'\n aria-label={text.label}\n aria-expanded='true'\n aria-controls={listId}\n aria-autocomplete='list'\n aria-activedescendant={activeOptionId}\n autoComplete='off'\n spellCheck={false}\n placeholder={text.placeholder}\n value={query}\n onChange={(event) => {\n setQuery(event.target.value);\n setActiveId(null);\n onQueryChange?.(event.target.value);\n }}\n onKeyDown={handleKeyDown}\n className={cx(styles.commandPaletteInput, classNames?.input)}\n style={slotStyles?.input}\n />\n </div>\n\n <div\n ref={listRef}\n id={listId}\n role='listbox'\n aria-label={text.label}\n className={cx(styles.commandPaletteList, classNames?.list)}\n style={slotStyles?.list}\n >\n {groups.map((group, groupIndex) => {\n const headingId = `${listId}-heading-${groupIndex}`;\n return (\n <div\n // Groups carry no id of their own; a new query re-renders the list wholesale anyway.\n key={groupIndex}\n role={group.heading === undefined ? 'presentation' : 'group'}\n aria-labelledby={group.heading === undefined ? undefined : headingId}\n >\n {group.separated && groupIndex > 0 && (\n <div\n aria-hidden\n className={cx(styles.commandPaletteSeparator, classNames?.separator)}\n style={slotStyles?.separator}\n />\n )}\n {group.heading !== undefined && (\n <div\n id={headingId}\n role='presentation'\n className={cx(styles.commandPaletteHeading, classNames?.heading)}\n style={slotStyles?.heading}\n >\n {group.heading}\n </div>\n )}\n {group.actions.map((action) => {\n index += 1;\n const own = index;\n return (\n <div\n key={action.id}\n id={optionId(action)}\n role='option'\n aria-selected={own === activeIndex}\n aria-disabled={action.disabled || undefined}\n className={cx(\n styles.commandPaletteItem,\n own === activeIndex && styles.commandPaletteItemActive,\n action.danger && styles.commandPaletteItemDanger,\n action.disabled && styles.commandPaletteItemDisabled,\n classNames?.item,\n )}\n style={slotStyles?.item}\n // Keeps the focus, and so the caret, in the search field.\n onMouseDown={(event) => event.preventDefault()}\n // Move, not enter: a list scrolling under a resting pointer must not steal the highlight.\n onMouseMove={() => !action.disabled && setActiveId(action.id)}\n onClick={() => run(action)}\n >\n {renderItemIcon(action.icon, hasIcons, {\n className: cx(styles.commandPaletteItemIcon, classNames?.itemIcon),\n style: slotStyles?.itemIcon,\n })}\n <span\n className={cx(styles.commandPaletteItemLabel, classNames?.itemLabel)}\n style={slotStyles?.itemLabel}\n >\n {action.label}\n </span>\n {action.shortcut != null && (\n <span\n className={cx(styles.commandPaletteItemShortcut, classNames?.itemShortcut)}\n style={slotStyles?.itemShortcut}\n >\n {formatHotkey(action.shortcut)}\n </span>\n )}\n </div>\n );\n })}\n </div>\n );\n })}\n </div>\n\n {hasQuery && actions.length === 0 && (\n <div className={cx(styles.commandPaletteEmpty, classNames?.empty)} style={slotStyles?.empty}>\n {emptyText}\n </div>\n )}\n <span className={styles.commandPaletteStatus} role='status' aria-live='polite'>\n {hasQuery ? (actions.length > 0 ? text.results(actions.length) : emptyText) : ''}\n </span>\n </Modal>\n );\n}\n\nexport default CommandPalette;\n"],"mappings":"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"}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
"use client";import{
|
|
1
|
+
"use client";import{isSelfActivation as e}from"../utils/activationKey.mjs";import{PopoverAlign as t,PopoverPlacement as n}from"../popover/popover.types.mjs";import{useFocusBoundary as r}from"../hooks/useFocusBoundary.mjs";import i from"../popover/popover.mjs";import{formatHotkey as a}from"../hooks/useHotkeys.mjs";import{isMenuAction as o,isMenuHeading as s,isMenuSeparator as ee}from"./menu.types.mjs";import{hasAnyIcon as c,renderItemIcon as l}from"./menuItem.mjs";import u from"./menu.module.mjs";import{cloneElement as d,isValidElement as te,useEffect as f,useId as ne,useRef as p,useState as m}from"react";import h from"classnames";import{Fragment as g,jsx as _,jsxs as v}from"react/jsx-runtime";var y=600;function b(e,t,n){for(let r=1;r<=e.length;r++){let i=(t+n*r+e.length*r)%e.length,a=e[i];if(o(a)&&!a.disabled)return i}return-1}function x(e){return e.findIndex(e=>o(e)&&!e.disabled)}function S(e){for(let t=e.length-1;t>=0;t--){let n=e[t];if(o(n)&&!n.disabled)return t}return-1}function C({items:C,children:w,onAction:T,isOpen:E,onOpenChange:re,placement:D=n.Bottom,align:O=t.Start,presentation:k,className:A,style:j,classNames:M,styles:N,...P}){let F=E!==void 0,[I,L]=m(!1),R=F?E:I,[z,B]=m(-1),V=p([]),H=p(null),U=p(null),{restoreFocusToTrigger:W,focusAfterTrigger:G}=r({triggerRef:H,contentRef:U}),K=ne(),q=p(`first`),J=p({query:``,timer:void 0}),Y=p(!1);function X(e,t=!1){e&&(Y.current=t),F||L(e),re?.(e)}let Z=p(C);f(()=>{Z.current=C}),f(()=>{if(!R)return;if(!Y.current){B(-1);return}let e=Z.current,t=q.current===`last`?S(e):x(e);B(t);let n=requestAnimationFrame(()=>{V.current[t]?.focus()});return()=>{cancelAnimationFrame(n),q.current=`first`}},[R]),f(()=>()=>clearTimeout(J.current.timer),[]);function Q(e){e<0||(B(e),V.current[e]?.focus())}function $(e){X(!1),e&&W()}function ie(e){if(e.length!==1||!e.trim())return!1;clearTimeout(J.current.timer),J.current.query+=e.toLowerCase(),J.current.timer=setTimeout(()=>{J.current.query=``},y);let t=J.current.query,n=t.length>1&&[...t].every(e=>e===t[0]),r=n?t[0]:t,i=n||t.length===1?z:z-1;for(let e=1;e<=C.length;e++){let t=(i+e+C.length)%C.length,n=C[t];if(o(n)&&!n.disabled&&(V.current[t]?.textContent?.trim().toLowerCase()??``).startsWith(r))return Q(t),!0}return!1}function ae(e){if(e.key===`ArrowDown`||e.key===`ArrowUp`){if(e.preventDefault(),e.stopPropagation(),R){Q(e.key===`ArrowUp`?S(C):x(C));return}q.current=e.key===`ArrowUp`?`last`:`first`,X(!0,!0)}}function oe(e){switch(e.key){case`ArrowDown`:Q(b(C,z,1));break;case`ArrowUp`:Q(z<0?S(C):b(C,z,-1));break;case`Home`:Q(x(C));break;case`End`:Q(S(C));break;case`Escape`:$(!0);break;case`Tab`:$(!1),e.shiftKey?W():G();break;default:if(!ie(e.key))return}e.preventDefault(),e.stopPropagation()}function se(e){let t=C[e];o(t)&&!t.disabled&&(t.onClick?.(),T?.(t.id),$(!0))}let ce=te(w)?d(w,{"aria-haspopup":w.props[`aria-haspopup`]??`menu`,"aria-expanded":w.props[`aria-expanded`]??R,"aria-controls":R?w.props[`aria-controls`]??K:w.props[`aria-controls`]}):w,le=c(C),ue=_(`div`,{ref:U,id:K,role:`menu`,className:h(u.menu,M?.menu),style:N?.menu,onKeyDown:oe,children:C.map((t,n)=>{if(ee(t))return _(`div`,{role:`separator`,className:h(u.menuSeparator,M?.separator),style:N?.separator},`separator-${n}`);if(s(t))return _(`div`,{role:`presentation`,className:h(u.menuHeading,M?.heading),style:N?.heading,children:t.heading},`heading-${n}`);let r={ref:e=>{V.current[n]=e},role:`menuitem`,tabIndex:n===z?0:-1,className:h(u.menuItem,t.danger&&u.menuItemDanger,M?.item),style:N?.item,onClick:()=>se(n),onMouseEnter:()=>B(n)},i=v(g,{children:[l(t.icon,le,{className:h(u.menuItemIcon,M?.itemIcon),style:N?.itemIcon}),_(`span`,{className:h(u.menuItemLabel,M?.itemLabel),style:N?.itemLabel,children:t.label}),t.shortcut!=null&&_(`span`,{className:h(u.menuItemShortcut,M?.itemShortcut),style:N?.itemShortcut,children:a(t.shortcut)})]});return t.href&&!t.disabled?_(`a`,{href:t.href,...r,onKeyDown:t=>{t.key===` `&&e(t)&&(t.preventDefault(),t.currentTarget.click())},children:i},t.id):_(`button`,{type:`button`,disabled:t.disabled,...r,children:i},t.id)})});return _(i,{...P,isOpen:R,onOpenChange:(e,{fromKeyboard:t})=>X(e,t),placement:D,align:O,presentation:k,content:ue,focusOnOpen:!1,constrainWidth:!1,classNames:{panel:u.menuPanel},className:h(A,M?.root),style:{...j,...N?.root},children:_(`span`,{ref:H,className:h(u.menuTrigger,M?.trigger),style:N?.trigger,onKeyDown:ae,children:ce})})}export{C as default};
|
|
2
2
|
//# sourceMappingURL=menu.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"menu.mjs","names":[],"sources":["../../../../package/common/menu/menu.tsx"],"sourcesContent":["import { cloneElement, isValidElement, useEffect, useId, useRef, useState, type ReactElement } from 'react';\nimport cx from 'classnames';\n\nimport styles from './menu.module.pcss';\nimport Popover from '../popover';\nimport { PopoverAlign, PopoverPlacement } from '../popover/popover.types';\nimport { renderIcon } from '../utils/renderIcon';\nimport { useFocusBoundary } from '../hooks/useFocusBoundary';\nimport { formatHotkey } from '../hooks/useHotkeys';\nimport { isMenuAction, isMenuHeading, isMenuSeparator, type MenuEntry, type MenuProps } from './menu.types';\n\n/** How long a typed run stays one search before it starts over. */\nconst TYPEAHEAD_RESET_MS = 600;\n\n/** Index of the next action that can be focused, wrapping, or -1 if there is none. */\nfunction nextEnabled(items: MenuEntry[], from: number, step: 1 | -1): number {\n for (let i = 1; i <= items.length; i++) {\n // The `items.length * i` term only keeps the value positive for a backwards\n // step; JavaScript's `%` would otherwise return a negative index.\n const index = (from + step * i + items.length * i) % items.length;\n const entry = items[index];\n if (isMenuAction(entry) && !entry.disabled) return index;\n }\n return -1;\n}\n\nfunction firstEnabled(items: MenuEntry[]): number {\n return items.findIndex((entry) => isMenuAction(entry) && !entry.disabled);\n}\n\nfunction lastEnabled(items: MenuEntry[]): number {\n for (let i = items.length - 1; i >= 0; i--) {\n const entry = items[i];\n if (isMenuAction(entry) && !entry.disabled) return i;\n }\n return -1;\n}\n\n/**\n * A menu of actions on a trigger of your choosing.\n *\n * `Popover` does the anchoring, portalling and outside-click work but takes\n * arbitrary `content`, which leaves every consumer to write their own `ul`/`li`\n * and, in practice, to leave out the roles and the arrow keys. This is that list,\n * done once: `role=\"menu\"`, a roving focus that skips disabled entries, Escape\n * back to the trigger, and separators and headings that stay out of the keyboard\n * order.\n */\nfunction Menu({\n items,\n children,\n onAction,\n isOpen: controlledOpen,\n onOpenChange,\n placement = PopoverPlacement.Bottom,\n align = PopoverAlign.Start,\n presentation,\n className,\n style,\n classNames,\n styles: slotStyles,\n ...rest\n}: MenuProps): ReactElement {\n const isControlled = controlledOpen !== undefined;\n const [internalOpen, setInternalOpen] = useState(false);\n const isOpen = isControlled ? controlledOpen : internalOpen;\n\n const [activeIndex, setActiveIndex] = useState(-1);\n const itemRefs = useRef<(HTMLButtonElement | null)[]>([]);\n const triggerRef = useRef<HTMLSpanElement>(null);\n const menuRef = useRef<HTMLDivElement>(null);\n // Tab leaves the panel in both directions, so the menu needs the same boundary\n // Popover uses: back to the trigger, or on to whatever follows it.\n const { restoreFocusToTrigger, focusAfterTrigger } = useFocusBoundary({ triggerRef, contentRef: menuRef });\n const menuId = useId();\n // Which end to land on when the menu opens: `ArrowUp` on the trigger opens at\n // the bottom, everything else at the top.\n const openAtRef = useRef<'first' | 'last'>('first');\n const typeahead = useRef<{ query: string; timer: ReturnType<typeof setTimeout> | undefined }>({\n query: '',\n timer: undefined,\n });\n\n // Whether a key opened the menu, which decides between landing the focus on an\n // entry and leaving it on the trigger. Set from Popover, which is what sees the\n // trigger's events, or here when an arrow key did the opening.\n const openedByKeyboardRef = useRef(false);\n\n function setOpen(next: boolean, fromKeyboard = false) {\n if (next) openedByKeyboardRef.current = fromKeyboard;\n if (!isControlled) setInternalOpen(next);\n onOpenChange?.(next);\n }\n\n // The effect below needs the entries, but must not re-run when the array is rebuilt. A consumer\n // writes `items={[…]}` inline, so it is a new identity on every render of *their* component:\n // keyed off it, any parent re-render while the menu was open threw the focus back to the first\n // entry, mid-navigation. Declared above the effect that reads it, so it has already run.\n const itemsRef = useRef(items);\n useEffect(() => {\n itemsRef.current = items;\n });\n\n // Focus follows the active entry rather than the menu keeping focus and\n // pointing at it: the items are real buttons, so Enter and Space are theirs.\n // Popover's own `focusOnOpen` would take the first focusable and stop there;\n // the active index has to move with it, so the menu does it itself.\n useEffect(() => {\n if (!isOpen) return;\n // A pointer open leaves the focus where the pointer put it. A highlight on an\n // entry nobody pointed at reads as a selection, and the mouse is about to land\n // somewhere else anyway; the first arrow key moves in, see the trigger's keys.\n if (!openedByKeyboardRef.current) {\n setActiveIndex(-1);\n return;\n }\n const entries = itemsRef.current;\n const target = openAtRef.current === 'last' ? lastEnabled(entries) : firstEnabled(entries);\n setActiveIndex(target);\n // The panel is portalled and animated in, so its buttons exist a frame later.\n const frame = requestAnimationFrame(() => {\n itemRefs.current[target]?.focus();\n });\n return () => {\n cancelAnimationFrame(frame);\n openAtRef.current = 'first';\n };\n }, [isOpen]);\n\n useEffect(() => () => clearTimeout(typeahead.current.timer), []);\n\n function moveTo(index: number) {\n if (index < 0) return;\n setActiveIndex(index);\n itemRefs.current[index]?.focus();\n }\n\n function close(returnFocus: boolean) {\n setOpen(false);\n if (returnFocus) restoreFocusToTrigger();\n }\n\n /**\n * Jumps to the next entry whose label starts with what has been typed. The\n * text comes off the rendered button rather than the `label` prop, which is a\n * `ReactNode` and may be an icon, a badge or anything else.\n */\n function handleTypeahead(key: string): boolean {\n if (key.length !== 1 || !key.trim()) return false;\n clearTimeout(typeahead.current.timer);\n typeahead.current.query += key.toLowerCase();\n typeahead.current.timer = setTimeout(() => {\n typeahead.current.query = '';\n }, TYPEAHEAD_RESET_MS);\n\n const query = typeahead.current.query;\n // A repeated single character walks through the entries starting with it,\n // rather than sticking to the first one.\n const repeated = query.length > 1 && [...query].every((c) => c === query[0]);\n const needle = repeated ? query[0] : query;\n const from = repeated || query.length === 1 ? activeIndex : activeIndex - 1;\n\n for (let i = 1; i <= items.length; i++) {\n const index = (from + i + items.length) % items.length;\n const entry = items[index];\n if (!isMenuAction(entry) || entry.disabled) continue;\n const text = itemRefs.current[index]?.textContent?.trim().toLowerCase() ?? '';\n if (text.startsWith(needle)) {\n moveTo(index);\n return true;\n }\n }\n return false;\n }\n\n /** The trigger's own keys, which decide where the menu opens. */\n function handleTriggerKeyDown(event: React.KeyboardEvent) {\n if (event.key !== 'ArrowDown' && event.key !== 'ArrowUp') return;\n // Popover opens on these keys too; letting both act would be harmless but\n // this keeps the decision about where to land in one place.\n event.preventDefault();\n event.stopPropagation();\n // Already open means a pointer opened it and the focus never left the trigger,\n // since an entry's own keys never reach here. This is the key that moves in.\n if (isOpen) {\n moveTo(event.key === 'ArrowUp' ? lastEnabled(items) : firstEnabled(items));\n return;\n }\n openAtRef.current = event.key === 'ArrowUp' ? 'last' : 'first';\n setOpen(true, true);\n }\n\n function handleKeyDown(event: React.KeyboardEvent) {\n switch (event.key) {\n case 'ArrowDown':\n moveTo(nextEnabled(items, activeIndex, 1));\n break;\n case 'ArrowUp':\n // Stepping back from \"nothing active\" lands two short of the end, so the end is named.\n moveTo(activeIndex < 0 ? lastEnabled(items) : nextEnabled(items, activeIndex, -1));\n break;\n case 'Home':\n moveTo(firstEnabled(items));\n break;\n case 'End':\n moveTo(lastEnabled(items));\n break;\n // Popover handles both of these too, but only once the focus has actually\n // reached the panel: its Escape restores focus on that condition, and its\n // Tab handling acts at the first and last focusable. A menu focuses its\n // first entry a frame after opening, so a key pressed in between would\n // fall through. Here they are unconditional, which is also what a menu\n // wants from Tab: leave, whatever is focused.\n case 'Escape':\n close(true);\n break;\n case 'Tab':\n // Carry on from the trigger, as if the panel had never been in the tab\n // order. Closing and letting the browser take it from here would move on\n // from an entry that is about to be removed, which lands at the top of\n // the document.\n close(false);\n if (event.shiftKey) restoreFocusToTrigger();\n else focusAfterTrigger();\n break;\n default:\n if (!handleTypeahead(event.key)) return;\n }\n event.preventDefault();\n event.stopPropagation();\n }\n\n function handleAction(index: number) {\n const entry = items[index];\n if (!isMenuAction(entry) || entry.disabled) return;\n entry.onClick?.();\n onAction?.(entry.id);\n close(true);\n }\n\n // A screen reader has no way to tell that this button opens a menu unless the\n // button itself says so. Only filled in where the consumer has not, so a\n // trigger that describes itself keeps doing that.\n const triggerWithMenuSemantics = isValidElement<Record<string, unknown>>(children)\n ? cloneElement(children, {\n 'aria-haspopup': children.props['aria-haspopup'] ?? 'menu',\n 'aria-expanded': children.props['aria-expanded'] ?? isOpen,\n 'aria-controls': isOpen ? (children.props['aria-controls'] ?? menuId) : children.props['aria-controls'],\n })\n : children;\n\n const content = (\n <div\n ref={menuRef}\n id={menuId}\n role='menu'\n className={cx(styles.menu, classNames?.menu)}\n style={slotStyles?.menu}\n onKeyDown={handleKeyDown}\n >\n {items.map((entry, index) => {\n if (isMenuSeparator(entry))\n return (\n <div\n // Separators and headings carry no id of their own, and reordering\n // the list re-renders it wholesale anyway.\n key={`separator-${index}`}\n role='separator'\n className={cx(styles.menuSeparator, classNames?.separator)}\n style={slotStyles?.separator}\n />\n );\n\n if (isMenuHeading(entry))\n return (\n <div\n key={`heading-${index}`}\n role='presentation'\n className={cx(styles.menuHeading, classNames?.heading)}\n style={slotStyles?.heading}\n >\n {entry.heading}\n </div>\n );\n\n return (\n <button\n key={entry.id}\n ref={(el) => {\n itemRefs.current[index] = el;\n }}\n type='button'\n role='menuitem'\n disabled={entry.disabled}\n // One tab stop for the whole menu: the arrow keys move between items.\n tabIndex={index === activeIndex ? 0 : -1}\n className={cx(styles.menuItem, entry.danger && styles.menuItemDanger, classNames?.item)}\n style={slotStyles?.item}\n onClick={() => handleAction(index)}\n onMouseEnter={() => setActiveIndex(index)}\n >\n {entry.icon != null &&\n renderIcon(entry.icon, {\n className: cx(styles.menuItemIcon, classNames?.itemIcon),\n style: slotStyles?.itemIcon,\n })}\n <span className={cx(styles.menuItemLabel, classNames?.itemLabel)} style={slotStyles?.itemLabel}>\n {entry.label}\n </span>\n {entry.shortcut != null && (\n <span className={cx(styles.menuItemShortcut, classNames?.itemShortcut)} style={slotStyles?.itemShortcut}>\n {formatHotkey(entry.shortcut)}\n </span>\n )}\n </button>\n );\n })}\n </div>\n );\n\n return (\n <Popover\n {...rest}\n isOpen={isOpen}\n onOpenChange={(next, { fromKeyboard }) => setOpen(next, fromKeyboard)}\n placement={placement}\n align={align}\n presentation={presentation}\n content={content}\n // The list pads its own rows, so a hover highlight reaches the panel edge,\n // and it sizes itself. Focus is the menu's own business, see above.\n focusOnOpen={false}\n constrainWidth={false}\n classNames={{ panel: styles.menuPanel }}\n className={cx(className, classNames?.root)}\n style={{ ...style, ...slotStyles?.root }}\n >\n <span\n ref={triggerRef}\n className={cx(styles.menuTrigger, classNames?.trigger)}\n style={slotStyles?.trigger}\n onKeyDown={handleTriggerKeyDown}\n >\n {triggerWithMenuSemantics}\n </span>\n </Popover>\n );\n}\n\nexport default Menu;\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"menu.mjs","names":[],"sources":["../../../../package/common/menu/menu.tsx"],"sourcesContent":["import { cloneElement, isValidElement, useEffect, useId, useRef, useState, type ReactElement } from 'react';\nimport cx from 'classnames';\n\nimport styles from './menu.module.pcss';\nimport Popover from '../popover';\nimport { PopoverAlign, PopoverPlacement } from '../popover/popover.types';\nimport { isSelfActivation } from '../utils/activationKey';\nimport { useFocusBoundary } from '../hooks/useFocusBoundary';\nimport { formatHotkey } from '../hooks/useHotkeys';\nimport { isMenuAction, isMenuHeading, isMenuSeparator, type MenuEntry, type MenuProps } from './menu.types';\nimport { hasAnyIcon, renderItemIcon } from './menuItem';\n\n/** How long a typed run stays one search before it starts over. */\nconst TYPEAHEAD_RESET_MS = 600;\n\n/** Index of the next action that can be focused, wrapping, or -1 if there is none. */\nfunction nextEnabled(items: MenuEntry[], from: number, step: 1 | -1): number {\n for (let i = 1; i <= items.length; i++) {\n // The `items.length * i` term only keeps the value positive for a backwards\n // step; JavaScript's `%` would otherwise return a negative index.\n const index = (from + step * i + items.length * i) % items.length;\n const entry = items[index];\n if (isMenuAction(entry) && !entry.disabled) return index;\n }\n return -1;\n}\n\nfunction firstEnabled(items: MenuEntry[]): number {\n return items.findIndex((entry) => isMenuAction(entry) && !entry.disabled);\n}\n\nfunction lastEnabled(items: MenuEntry[]): number {\n for (let i = items.length - 1; i >= 0; i--) {\n const entry = items[i];\n if (isMenuAction(entry) && !entry.disabled) return i;\n }\n return -1;\n}\n\n/**\n * A menu of actions on a trigger of your choosing.\n *\n * `Popover` does the anchoring, portalling and outside-click work but takes\n * arbitrary `content`, which leaves every consumer to write their own `ul`/`li`\n * and, in practice, to leave out the roles and the arrow keys. This is that list,\n * done once: `role=\"menu\"`, a roving focus that skips disabled entries, Escape\n * back to the trigger, and separators and headings that stay out of the keyboard\n * order.\n */\nfunction Menu({\n items,\n children,\n onAction,\n isOpen: controlledOpen,\n onOpenChange,\n placement = PopoverPlacement.Bottom,\n align = PopoverAlign.Start,\n presentation,\n className,\n style,\n classNames,\n styles: slotStyles,\n ...rest\n}: MenuProps): ReactElement {\n const isControlled = controlledOpen !== undefined;\n const [internalOpen, setInternalOpen] = useState(false);\n const isOpen = isControlled ? controlledOpen : internalOpen;\n\n const [activeIndex, setActiveIndex] = useState(-1);\n const itemRefs = useRef<(HTMLElement | null)[]>([]);\n const triggerRef = useRef<HTMLSpanElement>(null);\n const menuRef = useRef<HTMLDivElement>(null);\n // Tab leaves the panel in both directions, so the menu needs the same boundary\n // Popover uses: back to the trigger, or on to whatever follows it.\n const { restoreFocusToTrigger, focusAfterTrigger } = useFocusBoundary({ triggerRef, contentRef: menuRef });\n const menuId = useId();\n // Which end to land on when the menu opens: `ArrowUp` on the trigger opens at\n // the bottom, everything else at the top.\n const openAtRef = useRef<'first' | 'last'>('first');\n const typeahead = useRef<{ query: string; timer: ReturnType<typeof setTimeout> | undefined }>({\n query: '',\n timer: undefined,\n });\n\n // Whether a key opened the menu, which decides between landing the focus on an\n // entry and leaving it on the trigger. Set from Popover, which is what sees the\n // trigger's events, or here when an arrow key did the opening.\n const openedByKeyboardRef = useRef(false);\n\n function setOpen(next: boolean, fromKeyboard = false) {\n if (next) openedByKeyboardRef.current = fromKeyboard;\n if (!isControlled) setInternalOpen(next);\n onOpenChange?.(next);\n }\n\n // The effect below needs the entries, but must not re-run when the array is rebuilt. A consumer\n // writes `items={[…]}` inline, so it is a new identity on every render of *their* component:\n // keyed off it, any parent re-render while the menu was open threw the focus back to the first\n // entry, mid-navigation. Declared above the effect that reads it, so it has already run.\n const itemsRef = useRef(items);\n useEffect(() => {\n itemsRef.current = items;\n });\n\n // Focus follows the active entry rather than the menu keeping focus and\n // pointing at it: the items are real buttons, so Enter and Space are theirs.\n // Popover's own `focusOnOpen` would take the first focusable and stop there;\n // the active index has to move with it, so the menu does it itself.\n useEffect(() => {\n if (!isOpen) return;\n // A pointer open leaves the focus where the pointer put it. A highlight on an\n // entry nobody pointed at reads as a selection, and the mouse is about to land\n // somewhere else anyway; the first arrow key moves in, see the trigger's keys.\n if (!openedByKeyboardRef.current) {\n setActiveIndex(-1);\n return;\n }\n const entries = itemsRef.current;\n const target = openAtRef.current === 'last' ? lastEnabled(entries) : firstEnabled(entries);\n setActiveIndex(target);\n // The panel is portalled and animated in, so its buttons exist a frame later.\n const frame = requestAnimationFrame(() => {\n itemRefs.current[target]?.focus();\n });\n return () => {\n cancelAnimationFrame(frame);\n openAtRef.current = 'first';\n };\n }, [isOpen]);\n\n useEffect(() => () => clearTimeout(typeahead.current.timer), []);\n\n function moveTo(index: number) {\n if (index < 0) return;\n setActiveIndex(index);\n itemRefs.current[index]?.focus();\n }\n\n function close(returnFocus: boolean) {\n setOpen(false);\n if (returnFocus) restoreFocusToTrigger();\n }\n\n /**\n * Jumps to the next entry whose label starts with what has been typed. The\n * text comes off the rendered button rather than the `label` prop, which is a\n * `ReactNode` and may be an icon, a badge or anything else.\n */\n function handleTypeahead(key: string): boolean {\n if (key.length !== 1 || !key.trim()) return false;\n clearTimeout(typeahead.current.timer);\n typeahead.current.query += key.toLowerCase();\n typeahead.current.timer = setTimeout(() => {\n typeahead.current.query = '';\n }, TYPEAHEAD_RESET_MS);\n\n const query = typeahead.current.query;\n // A repeated single character walks through the entries starting with it,\n // rather than sticking to the first one.\n const repeated = query.length > 1 && [...query].every((c) => c === query[0]);\n const needle = repeated ? query[0] : query;\n const from = repeated || query.length === 1 ? activeIndex : activeIndex - 1;\n\n for (let i = 1; i <= items.length; i++) {\n const index = (from + i + items.length) % items.length;\n const entry = items[index];\n if (!isMenuAction(entry) || entry.disabled) continue;\n const text = itemRefs.current[index]?.textContent?.trim().toLowerCase() ?? '';\n if (text.startsWith(needle)) {\n moveTo(index);\n return true;\n }\n }\n return false;\n }\n\n /** The trigger's own keys, which decide where the menu opens. */\n function handleTriggerKeyDown(event: React.KeyboardEvent) {\n if (event.key !== 'ArrowDown' && event.key !== 'ArrowUp') return;\n // Popover opens on these keys too; letting both act would be harmless but\n // this keeps the decision about where to land in one place.\n event.preventDefault();\n event.stopPropagation();\n // Already open means a pointer opened it and the focus never left the trigger,\n // since an entry's own keys never reach here. This is the key that moves in.\n if (isOpen) {\n moveTo(event.key === 'ArrowUp' ? lastEnabled(items) : firstEnabled(items));\n return;\n }\n openAtRef.current = event.key === 'ArrowUp' ? 'last' : 'first';\n setOpen(true, true);\n }\n\n function handleKeyDown(event: React.KeyboardEvent) {\n switch (event.key) {\n case 'ArrowDown':\n moveTo(nextEnabled(items, activeIndex, 1));\n break;\n case 'ArrowUp':\n // Stepping back from \"nothing active\" lands two short of the end, so the end is named.\n moveTo(activeIndex < 0 ? lastEnabled(items) : nextEnabled(items, activeIndex, -1));\n break;\n case 'Home':\n moveTo(firstEnabled(items));\n break;\n case 'End':\n moveTo(lastEnabled(items));\n break;\n // Popover handles both of these too, but only once the focus has actually\n // reached the panel: its Escape restores focus on that condition, and its\n // Tab handling acts at the first and last focusable. A menu focuses its\n // first entry a frame after opening, so a key pressed in between would\n // fall through. Here they are unconditional, which is also what a menu\n // wants from Tab: leave, whatever is focused.\n case 'Escape':\n close(true);\n break;\n case 'Tab':\n // Carry on from the trigger, as if the panel had never been in the tab\n // order. Closing and letting the browser take it from here would move on\n // from an entry that is about to be removed, which lands at the top of\n // the document.\n close(false);\n if (event.shiftKey) restoreFocusToTrigger();\n else focusAfterTrigger();\n break;\n default:\n if (!handleTypeahead(event.key)) return;\n }\n event.preventDefault();\n event.stopPropagation();\n }\n\n function handleAction(index: number) {\n const entry = items[index];\n if (!isMenuAction(entry) || entry.disabled) return;\n // Not runMenuAction: a link entry is an <a>, which the browser follows by itself.\n entry.onClick?.();\n onAction?.(entry.id);\n close(true);\n }\n\n // A screen reader has no way to tell that this button opens a menu unless the\n // button itself says so. Only filled in where the consumer has not, so a\n // trigger that describes itself keeps doing that.\n const triggerWithMenuSemantics = isValidElement<Record<string, unknown>>(children)\n ? cloneElement(children, {\n 'aria-haspopup': children.props['aria-haspopup'] ?? 'menu',\n 'aria-expanded': children.props['aria-expanded'] ?? isOpen,\n 'aria-controls': isOpen ? (children.props['aria-controls'] ?? menuId) : children.props['aria-controls'],\n })\n : children;\n\n const hasIcons = hasAnyIcon(items);\n\n const content = (\n <div\n ref={menuRef}\n id={menuId}\n role='menu'\n className={cx(styles.menu, classNames?.menu)}\n style={slotStyles?.menu}\n onKeyDown={handleKeyDown}\n >\n {items.map((entry, index) => {\n if (isMenuSeparator(entry))\n return (\n <div\n // Separators and headings carry no id of their own, and reordering\n // the list re-renders it wholesale anyway.\n key={`separator-${index}`}\n role='separator'\n className={cx(styles.menuSeparator, classNames?.separator)}\n style={slotStyles?.separator}\n />\n );\n\n if (isMenuHeading(entry))\n return (\n <div\n key={`heading-${index}`}\n role='presentation'\n className={cx(styles.menuHeading, classNames?.heading)}\n style={slotStyles?.heading}\n >\n {entry.heading}\n </div>\n );\n\n const itemProps = {\n ref: (el: HTMLElement | null) => {\n itemRefs.current[index] = el;\n },\n role: 'menuitem',\n // One tab stop for the whole menu: the arrow keys move between items.\n tabIndex: index === activeIndex ? 0 : -1,\n className: cx(styles.menuItem, entry.danger && styles.menuItemDanger, classNames?.item),\n style: slotStyles?.item,\n onClick: () => handleAction(index),\n onMouseEnter: () => setActiveIndex(index),\n };\n const itemContent = (\n <>\n {renderItemIcon(entry.icon, hasIcons, {\n className: cx(styles.menuItemIcon, classNames?.itemIcon),\n style: slotStyles?.itemIcon,\n })}\n <span className={cx(styles.menuItemLabel, classNames?.itemLabel)} style={slotStyles?.itemLabel}>\n {entry.label}\n </span>\n {entry.shortcut != null && (\n <span className={cx(styles.menuItemShortcut, classNames?.itemShortcut)} style={slotStyles?.itemShortcut}>\n {formatHotkey(entry.shortcut)}\n </span>\n )}\n </>\n );\n\n // An <a> cannot be disabled, so a disabled link is a disabled button.\n return entry.href && !entry.disabled ? (\n <a\n key={entry.id}\n href={entry.href}\n {...itemProps}\n // A link takes Enter but not Space, which every other entry answers to.\n onKeyDown={(event) => {\n if (event.key !== ' ' || !isSelfActivation(event)) return;\n event.preventDefault();\n event.currentTarget.click();\n }}\n >\n {itemContent}\n </a>\n ) : (\n <button key={entry.id} type='button' disabled={entry.disabled} {...itemProps}>\n {itemContent}\n </button>\n );\n })}\n </div>\n );\n\n return (\n <Popover\n {...rest}\n isOpen={isOpen}\n onOpenChange={(next, { fromKeyboard }) => setOpen(next, fromKeyboard)}\n placement={placement}\n align={align}\n presentation={presentation}\n content={content}\n // The list pads its own rows, so a hover highlight reaches the panel edge,\n // and it sizes itself. Focus is the menu's own business, see above.\n focusOnOpen={false}\n constrainWidth={false}\n classNames={{ panel: styles.menuPanel }}\n className={cx(className, classNames?.root)}\n style={{ ...style, ...slotStyles?.root }}\n >\n <span\n ref={triggerRef}\n className={cx(styles.menuTrigger, classNames?.trigger)}\n style={slotStyles?.trigger}\n onKeyDown={handleTriggerKeyDown}\n >\n {triggerWithMenuSemantics}\n </span>\n </Popover>\n );\n}\n\nexport default Menu;\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"menu.module.mjs","names":[],"sources":["../../../../package/common/menu/menu.module.pcss"],"sourcesContent":[".menuPanel {\n padding: 0;\n}\n\n.menu {\n display: flex;\n flex-direction: column;\n min-width: 180px;\n padding: 4px 0;\n box-sizing: border-box;\n font-family: var(--default-font);\n font-size: var(--default-font-size);\n color: var(--text-color);\n\n /* Chrome (background, shadow, radius, positioning) comes from the Popover panel\n this renders inside — keep only the list itself here. */\n\n &-trigger {\n /* The trigger is only here to give Escape somewhere to return the focus to;\n it must not become a box of its own in the consumer's layout. */\n display: contents;\n }\n\n &-item {\n display: flex;\n align-items: center;\n gap: 10px;\n width: 100%;\n padding: 8px 12px;\n border: none;\n background: none;\n font: inherit;\n color: inherit;\n text-align: left;\n white-space: nowrap;\n cursor: pointer;\n -webkit-tap-highlight-color: transparent;\n box-sizing: border-box;\n transition: background 0.1s;\n\n /* `:focus`, not `:focus-visible`: the entry with the focus is the one the\n arrow keys are on, and it has to look like it whether the menu was opened\n by keyboard or by mouse. `:focus-visible` would stay dark after a click on\n the trigger, because the browser does not count that as keyboard input.\n\n Not `--background-accent-light`: the panel under this list is\n `--background-modal`, and the default theme gives both the same colour, so\n the highlight was invisible. Dropdown and MultiSelect pick this one too. */\n &:hover:not(:disabled),\n &:focus {\n background: var(--background-accent);\n }\n\n /* The ring would sit on top of the neighbouring rows' backgrounds; the row\n highlight above is what marks the active entry. */\n &:focus {\n outline: none;\n }\n\n &:disabled {\n opacity: 0.45;\n cursor: default;\n }\n\n &-danger {\n color: var(--error-color);\n }\n\n &-icon {\n /* FontAwesome's own rule is unlayered, so it outranks `width` from inside the library's layer and\n reads this instead. `width` still sizes a custom icon. */\n --fa-width: 1em;\n\n flex: none;\n width: 1em;\n text-align: center;\n }\n\n &-label {\n flex: 1;\n overflow: hidden;\n text-overflow: ellipsis;\n }\n\n &-shortcut {\n flex: none;\n margin-left: 16px;\n font-size: 0.85em;\n color: var(--text-light);\n }\n }\n\n &-separator {\n height: 1px;\n margin: 4px 0;\n background: var(--border-color);\n }\n\n &-heading {\n padding: 6px 12px 2px;\n font-size: 0.8em;\n font-weight: 600;\n text-transform: uppercase;\n letter-spacing: 0.04em;\n color: var(--text-light);\n }\n}\n"],"mappings":""}
|
|
1
|
+
{"version":3,"file":"menu.module.mjs","names":[],"sources":["../../../../package/common/menu/menu.module.pcss"],"sourcesContent":[".menuPanel {\n padding: 0;\n}\n\n.menu {\n display: flex;\n flex-direction: column;\n min-width: 180px;\n padding: 4px 0;\n box-sizing: border-box;\n font-family: var(--default-font);\n font-size: var(--default-font-size);\n color: var(--text-color);\n\n /* Chrome (background, shadow, radius, positioning) comes from the Popover panel\n this renders inside — keep only the list itself here. */\n\n &-trigger {\n /* The trigger is only here to give Escape somewhere to return the focus to;\n it must not become a box of its own in the consumer's layout. */\n display: contents;\n }\n\n &-item {\n display: flex;\n align-items: center;\n gap: 10px;\n width: 100%;\n padding: 8px 12px;\n border: none;\n background: none;\n font: inherit;\n color: inherit;\n text-align: left;\n white-space: nowrap;\n cursor: pointer;\n -webkit-tap-highlight-color: transparent;\n box-sizing: border-box;\n transition: background 0.1s;\n\n /* An entry with an `href` is an <a>. */\n text-decoration: none;\n\n /* `:focus`, not `:focus-visible`: the entry with the focus is the one the\n arrow keys are on, and it has to look like it whether the menu was opened\n by keyboard or by mouse. `:focus-visible` would stay dark after a click on\n the trigger, because the browser does not count that as keyboard input.\n\n Not `--background-accent-light`: the panel under this list is\n `--background-modal`, and the default theme gives both the same colour, so\n the highlight was invisible. Dropdown and MultiSelect pick this one too. */\n &:hover:not(:disabled),\n &:focus {\n background: var(--background-accent);\n }\n\n /* The ring would sit on top of the neighbouring rows' backgrounds; the row\n highlight above is what marks the active entry. */\n &:focus {\n outline: none;\n }\n\n &:disabled {\n opacity: 0.45;\n cursor: default;\n }\n\n &-danger {\n color: var(--error-color);\n }\n\n &-icon {\n /* FontAwesome's own rule is unlayered, so it outranks `width` from inside the library's layer and\n reads this instead. `width` still sizes a custom icon. */\n --fa-width: 1em;\n\n flex: none;\n width: 1em;\n text-align: center;\n }\n\n &-label {\n flex: 1;\n overflow: hidden;\n text-overflow: ellipsis;\n }\n\n &-shortcut {\n flex: none;\n margin-left: 16px;\n font-size: 0.85em;\n color: var(--text-light);\n }\n }\n\n &-separator {\n height: 1px;\n margin: 4px 0;\n background: var(--border-color);\n }\n\n &-heading {\n padding: 6px 12px 2px;\n font-size: 0.8em;\n font-weight: 600;\n text-transform: uppercase;\n letter-spacing: 0.04em;\n color: var(--text-light);\n }\n}\n"],"mappings":""}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"menu.types.mjs","names":[],"sources":["../../../../package/common/menu/menu.types.ts"],"sourcesContent":["import type { CSSProperties, ReactNode } from 'react';\nimport type { SlotClassNames, SlotStyles, HtmlProps } from '../types/slots.types';\nimport type { IconContent } from '../types/icon.types';\nimport type { PopoverAlign, PopoverPlacement } from '../popover/popover.types';\nimport type { Presentation } from '../configProvider/presentation.types';\n\n/** One thing the menu can do. */\nexport interface MenuAction {\n /** Stable id, also what `onAction` reports. */\n id: string;\n label: ReactNode;\n icon?: IconContent;\n /**\n * A keyboard shortcut for this action, in `useHotkeys` spelling: `'mod+k'`,\n * `'shift+f2'`, `'delete'`. Shown on the trailing edge written for the\n * platform: `⌘K` on an Apple one, `Ctrl+K` elsewhere.\n *\n * The menu draws it and nothing more. Binding is yours, because only the app\n * knows what else is listening and whether the shortcut should work at all\n * times or only while the menu is open. `menuHotkeys` turns these entries\n * into a `useHotkeys` map, so the one spelling covers both ends.\n */\n shortcut?: string;\n /** Extra search terms for `CommandPalette`, which also matches a string `label`. `Menu` ignores them. */\n keywords?: string[];\n disabled?: boolean;\n /** Renders in the destructive colour. For deleting, discarding, revoking. */\n danger?: boolean;\n onClick?: () => void;\n}\n\n/** A line between groups of actions. */\nexport interface MenuSeparator {\n separator: true;\n}\n\n/** A label over the actions that follow it. */\nexport interface MenuHeading {\n heading: ReactNode;\n}\n\nexport type MenuEntry = MenuAction | MenuSeparator | MenuHeading;\n\nexport function isMenuSeparator(entry: MenuEntry): entry is MenuSeparator {\n return 'separator' in entry;\n}\n\nexport function isMenuHeading(entry: MenuEntry): entry is MenuHeading {\n return 'heading' in entry;\n}\n\nexport function isMenuAction(entry: MenuEntry): entry is MenuAction {\n return !isMenuSeparator(entry) && !isMenuHeading(entry);\n}\n\nexport type MenuSlots =\n | 'root'\n | 'trigger'\n | 'menu'\n | 'item'\n | 'itemIcon'\n | 'itemLabel'\n | 'itemShortcut'\n | 'separator'\n | 'heading';\n\nexport interface MenuProps extends Omit<HtmlProps, 'onSelect'> {\n /** What the menu offers, in the order it is read. */\n items: MenuEntry[];\n /** The element that opens the menu. */\n children?: ReactNode;\n /** Called with the id of the action that was picked, after its own `onClick`. */\n onAction?: (id: string) => void;\n /** Controlled open state. Leave it out to let the menu manage its own. */\n isOpen?: boolean;\n onOpenChange?: (isOpen: boolean) => void;\n /** Which side of the trigger the menu opens on (default `Bottom`). */\n placement?: PopoverPlacement;\n /** Which edge it lines up with on the cross axis (default `Start`). */\n align?: PopoverAlign;\n /** Anchored to the trigger, pinned to the bottom of the screen, or the latter only on touch. */\n presentation?: Presentation;\n className?: string;\n style?: CSSProperties;\n classNames?: SlotClassNames<MenuSlots>;\n styles?: SlotStyles<MenuSlots>;\n}\n"],"mappings":"
|
|
1
|
+
{"version":3,"file":"menu.types.mjs","names":[],"sources":["../../../../package/common/menu/menu.types.ts"],"sourcesContent":["import type { CSSProperties, ReactNode } from 'react';\nimport type { SlotClassNames, SlotStyles, HtmlProps } from '../types/slots.types';\nimport type { IconContent } from '../types/icon.types';\nimport type { PopoverAlign, PopoverPlacement } from '../popover/popover.types';\nimport type { Presentation } from '../configProvider/presentation.types';\n\n/** One thing the menu can do. */\nexport interface MenuAction {\n /** Stable id, also what `onAction` reports. */\n id: string;\n label: ReactNode;\n icon?: IconContent;\n /**\n * A keyboard shortcut for this action, in `useHotkeys` spelling: `'mod+k'`,\n * `'shift+f2'`, `'delete'`. Shown on the trailing edge written for the\n * platform: `⌘K` on an Apple one, `Ctrl+K` elsewhere.\n *\n * The menu draws it and nothing more. Binding is yours, because only the app\n * knows what else is listening and whether the shortcut should work at all\n * times or only while the menu is open. `menuHotkeys` turns these entries\n * into a `useHotkeys` map, so the one spelling covers both ends.\n */\n shortcut?: string;\n /** Extra search terms for `CommandPalette`, which also matches a string `label`. `Menu` ignores them. */\n keywords?: string[];\n disabled?: boolean;\n /** Renders in the destructive colour. For deleting, discarding, revoking. */\n danger?: boolean;\n /**\n * Followed after `onClick` and `onAction`. In `Menu` a real `<a>`, so Ctrl/⌘-click opens a new tab.\n * Loads the whole page: with a client-side router, navigate in `onClick` instead.\n */\n href?: string;\n onClick?: () => void;\n}\n\n/** A line between groups of actions. */\nexport interface MenuSeparator {\n separator: true;\n}\n\n/** A label over the actions that follow it. */\nexport interface MenuHeading {\n heading: ReactNode;\n}\n\nexport type MenuEntry = MenuAction | MenuSeparator | MenuHeading;\n\nexport function isMenuSeparator(entry: MenuEntry): entry is MenuSeparator {\n return 'separator' in entry;\n}\n\nexport function isMenuHeading(entry: MenuEntry): entry is MenuHeading {\n return 'heading' in entry;\n}\n\nexport function isMenuAction(entry: MenuEntry): entry is MenuAction {\n return !isMenuSeparator(entry) && !isMenuHeading(entry);\n}\n\nexport type MenuSlots =\n | 'root'\n | 'trigger'\n | 'menu'\n | 'item'\n | 'itemIcon'\n | 'itemLabel'\n | 'itemShortcut'\n | 'separator'\n | 'heading';\n\nexport interface MenuProps extends Omit<HtmlProps, 'onSelect'> {\n /** What the menu offers, in the order it is read. */\n items: MenuEntry[];\n /** The element that opens the menu. */\n children?: ReactNode;\n /** Called with the id of the action that was picked, after its own `onClick`. */\n onAction?: (id: string) => void;\n /** Controlled open state. Leave it out to let the menu manage its own. */\n isOpen?: boolean;\n onOpenChange?: (isOpen: boolean) => void;\n /** Which side of the trigger the menu opens on (default `Bottom`). */\n placement?: PopoverPlacement;\n /** Which edge it lines up with on the cross axis (default `Start`). */\n align?: PopoverAlign;\n /** Anchored to the trigger, pinned to the bottom of the screen, or the latter only on touch. */\n presentation?: Presentation;\n className?: string;\n style?: CSSProperties;\n classNames?: SlotClassNames<MenuSlots>;\n styles?: SlotStyles<MenuSlots>;\n}\n"],"mappings":"aAgDA,SAAgB,EAAgB,EAA0C,CACxE,MAAO,cAAe,CACxB,CAEA,SAAgB,EAAc,EAAwC,CACpE,MAAO,YAAa,CACtB,CAEA,SAAgB,EAAa,EAAuC,CAClE,MAAO,CAAC,EAAgB,CAAK,GAAK,CAAC,EAAc,CAAK,CACxD"}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
"use client";import{isMenuAction as e}from"./menu.types.mjs";function
|
|
1
|
+
"use client";import{isMenuAction as e}from"./menu.types.mjs";import{runMenuAction as t}from"./menuItem.mjs";function n(n,r){let i={};for(let a of n)e(a)&&!a.disabled&&a.shortcut&&(i[a.shortcut]=()=>t(a,r));return i}export{n as menuHotkeys};
|
|
2
2
|
//# sourceMappingURL=menuHotkeys.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"menuHotkeys.mjs","names":[],"sources":["../../../../package/common/menu/menuHotkeys.ts"],"sourcesContent":["import type { HotkeyMap } from '../hooks/useHotkeys';\nimport { isMenuAction, type MenuEntry } from './menu.types';\n\n/**\n * The bindings for a menu's `shortcut` entries, to hand to `useHotkeys` yourself.\n *\n * The menu draws a shortcut but never listens for one, because only the app knows what else is\n * listening: binding it here as well as in an app's own keymap would run the action twice, and\n * whether a shortcut should work at all times or only while the menu is open is the app's\n * question, not the menu's. Calling this keeps the answer in one spelling, so the hint the menu\n * draws cannot drift from what actually fires.\n *\n * ```tsx\n * useHotkeys(menuHotkeys(items, run)); // whenever the page is up\n * useHotkeys(menuHotkeys(items, run), { enabled: isOpen }); // only while the menu is open\n * ```\n *\n * Runs each entry the way clicking it does: its own `onClick`, then `onAction
|
|
1
|
+
{"version":3,"file":"menuHotkeys.mjs","names":[],"sources":["../../../../package/common/menu/menuHotkeys.ts"],"sourcesContent":["import type { HotkeyMap } from '../hooks/useHotkeys';\nimport { isMenuAction, type MenuEntry } from './menu.types';\nimport { runMenuAction } from './menuItem';\n\n/**\n * The bindings for a menu's `shortcut` entries, to hand to `useHotkeys` yourself.\n *\n * The menu draws a shortcut but never listens for one, because only the app knows what else is\n * listening: binding it here as well as in an app's own keymap would run the action twice, and\n * whether a shortcut should work at all times or only while the menu is open is the app's\n * question, not the menu's. Calling this keeps the answer in one spelling, so the hint the menu\n * draws cannot drift from what actually fires.\n *\n * ```tsx\n * useHotkeys(menuHotkeys(items, run)); // whenever the page is up\n * useHotkeys(menuHotkeys(items, run), { enabled: isOpen }); // only while the menu is open\n * ```\n *\n * Runs each entry the way clicking it does: its own `onClick`, then `onAction`, then its `href`.\n * A `disabled` entry, and an entry with no `shortcut`, is left out.\n */\nexport function menuHotkeys(items: MenuEntry[], onAction?: (id: string) => void): HotkeyMap {\n const bindings: HotkeyMap = {};\n for (const entry of items) {\n if (!isMenuAction(entry) || entry.disabled || !entry.shortcut) continue;\n bindings[entry.shortcut] = () => runMenuAction(entry, onAction);\n }\n return bindings;\n}\n"],"mappings":"4GAqBA,SAAgB,EAAY,EAAoB,EAA4C,CAC1F,IAAM,EAAsB,CAAC,EAC7B,IAAK,IAAM,KAAS,EACb,EAAa,CAAK,GAAK,GAAM,UAAa,EAAM,WACrD,EAAS,EAAM,cAAkB,EAAc,EAAO,CAAQ,GAEhE,OAAO,CACT"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
"use client";import{renderIcon as e}from"../utils/renderIcon.mjs";import{isMenuAction as t}from"./menu.types.mjs";import{jsx as n}from"react/jsx-runtime";function r(e){return e.some(e=>t(e)&&e.icon!=null)}function i(t,r,i){return t==null?r?n(`span`,{"aria-hidden":!0,...i}):null:e(t,i)}function a(e,t){e.onClick?.(),t?.(e.id),e.href&&o(e.href)}function o(e){let t;try{t=new URL(e,document.baseURI)}catch{return}t.protocol!==`javascript:`&&window.location.assign(t)}export{r as hasAnyIcon,i as renderItemIcon,a as runMenuAction};
|
|
2
|
+
//# sourceMappingURL=menuItem.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"menuItem.mjs","names":[],"sources":["../../../../package/common/menu/menuItem.tsx"],"sourcesContent":["import type { CSSProperties, ReactNode } from 'react';\n\nimport { renderIcon } from '../utils/renderIcon';\nimport { isMenuAction, type MenuAction, type MenuEntry } from './menu.types';\n\nexport function hasAnyIcon(items: MenuEntry[]): boolean {\n return items.some((entry) => isMenuAction(entry) && entry.icon != null);\n}\n\n/** The action's icon, or an empty cell as wide when another action has one, so the labels line up. */\nexport function renderItemIcon(\n icon: MenuAction['icon'],\n reserveCell: boolean,\n props: { className: string; style?: CSSProperties },\n): ReactNode {\n if (icon != null) return renderIcon(icon, props);\n return reserveCell ? <span aria-hidden {...props} /> : null;\n}\n\n/** Runs an action the way clicking it does: its own `onClick`, then `onAction`, then its `href`. */\nexport function runMenuAction(action: MenuAction, onAction?: (id: string) => void): void {\n action.onClick?.();\n onAction?.(action.id);\n if (action.href) followHref(action.href);\n}\n\nfunction followHref(href: string) {\n let url: URL;\n try {\n url = new URL(href, document.baseURI);\n } catch {\n return;\n }\n // React refuses a javascript: URL on Menu's <a>, so the same entry must not run one here. Parsed,\n // not matched, because the parser strips the spaces and control characters that can hide one.\n if (url.protocol === 'javascript:') return;\n window.location.assign(url);\n}\n"],"mappings":"0JAKA,SAAgB,EAAW,EAA6B,CACtD,OAAO,EAAM,KAAM,GAAU,EAAa,CAAK,GAAK,EAAM,MAAQ,IAAI,CACxE,CAGA,SAAgB,EACd,EACA,EACA,EACW,CAEX,OADI,GAAQ,KACL,EAAc,EAAC,OAAD,CAAM,cAAA,GAAY,GAAI,CAAQ,CAAA,EAAI,KAD9B,EAAW,EAAM,CAAK,CAEjD,CAGA,SAAgB,EAAc,EAAoB,EAAuC,CACvF,EAAO,UAAU,EACjB,IAAW,EAAO,EAAE,EAChB,EAAO,MAAM,EAAW,EAAO,IAAI,CACzC,CAEA,SAAS,EAAW,EAAc,CAChC,IAAI,EACJ,GAAI,CACF,EAAM,IAAI,IAAI,EAAM,SAAS,OAAO,CACtC,MAAQ,CACN,MACF,CAGI,EAAI,WAAa,eACrB,OAAO,SAAS,OAAO,CAAG,CAC5B"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"radioGroup.module.mjs","names":[],"sources":["../../../../package/common/radioGroup/radioGroup.module.pcss"],"sourcesContent":[".radioGroup {\n /* The base is declared here rather than only on the option, so every em below resolves\n against the theme's text size instead of whatever the group happens to sit in. At a 16px\n base these are the 18px dot, 12px row gap and 10px label gap it has always had. */\n font-size: var(--default-font-size);\n\n --custom-radio-size: var(--radio-size, 1.125em);\n --custom-radio-gap: var(--radio-gap, 0.625em);\n\n user-select: none;\n display: flex;\n flex-direction: column;\n gap: 0.75em;\n width: fit-content;\n\n &-horizontal {\n flex-flow: row wrap;\n gap: 1.25em;\n }\n\n &-disabled {\n filter: brightness(0.85);\n cursor: not-allowed;\n }\n\n &-error &-radio:not(.radioGroup-radio-checked) {\n border-color: var(--error-color);\n }\n\n &-option {\n display: flex;\n align-items: flex-start;\n column-gap: var(--custom-radio-gap);\n cursor: pointer;\n -webkit-tap-highlight-color: transparent;\n border: 1px solid transparent;\n border-radius: var(--default-border-radius);\n margin: -1px;\n color: var(--text-color);\n font-family: var(--default-font);\n\n &:focus-visible {\n outline: none;\n border-color: var(--focus-border-color, var(--primary-color));\n }\n\n &-disabled {\n cursor: not-allowed;\n opacity: 0.55;\n }\n }\n\n &-radio {\n flex-shrink: 0;\n width: var(--custom-radio-size);\n height: var(--custom-radio-size);\n border-radius: 50%;\n border: 0.1125em solid var(--border-color);\n
|
|
1
|
+
{"version":3,"file":"radioGroup.module.mjs","names":[],"sources":["../../../../package/common/radioGroup/radioGroup.module.pcss"],"sourcesContent":[".radioGroup {\n /* The base is declared here rather than only on the option, so every em below resolves\n against the theme's text size instead of whatever the group happens to sit in. At a 16px\n base these are the 18px dot, 12px row gap and 10px label gap it has always had. */\n font-size: var(--default-font-size);\n\n --custom-radio-size: var(--radio-size, 1.125em);\n --custom-radio-gap: var(--radio-gap, 0.625em);\n\n user-select: none;\n display: flex;\n flex-direction: column;\n gap: 0.75em;\n width: fit-content;\n\n &-horizontal {\n flex-flow: row wrap;\n gap: 1.25em;\n }\n\n &-disabled {\n filter: brightness(0.85);\n cursor: not-allowed;\n }\n\n &-error &-radio:not(.radioGroup-radio-checked) {\n border-color: var(--error-color);\n }\n\n &-option {\n display: flex;\n align-items: flex-start;\n column-gap: var(--custom-radio-gap);\n cursor: pointer;\n -webkit-tap-highlight-color: transparent;\n border: 1px solid transparent;\n border-radius: var(--default-border-radius);\n margin: -1px;\n color: var(--text-color);\n font-family: var(--default-font);\n\n &:focus-visible {\n outline: none;\n border-color: var(--focus-border-color, var(--primary-color));\n }\n\n &-disabled {\n cursor: not-allowed;\n opacity: 0.55;\n }\n }\n\n &-radio {\n flex-shrink: 0;\n width: var(--custom-radio-size);\n height: var(--custom-radio-size);\n border-radius: 50%;\n border: 0.1125em solid var(--border-color);\n position: relative;\n margin-top: 0.0625em;\n transition: border-color 0.15s;\n\n &-checked {\n border-color: var(--primary-color);\n }\n }\n\n &-dot {\n /* Inset by a transparent border, not centred by flex: border widths snap to whole device\n pixels, so the gap is equal on every side. A flex-centred 9px dot sat 3.5px into the ring and\n rendered a pixel off. 0.1875 leaves a dot of about half the ring. */\n position: absolute;\n inset: 0;\n border: calc(var(--custom-radio-size) * 0.1875) solid transparent;\n border-radius: 50%;\n background-color: var(--primary-color);\n background-clip: padding-box;\n transform: scale(0);\n transition: transform 0.15s ease;\n\n &-checked {\n transform: scale(1);\n }\n }\n\n &-text {\n display: flex;\n flex-direction: column;\n gap: 0.125em;\n }\n\n &-label {\n line-height: 1.2;\n color: var(--text-color);\n }\n\n &-description {\n font-size: 0.85em;\n line-height: 1.3;\n color: var(--text-dark);\n }\n}\n\n@media (prefers-reduced-motion: reduce) {\n .radioGroup-dot {\n transition: none;\n }\n}\n"],"mappings":""}
|