@stackone/malachite 2.53.0 → 2.55.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/COMPONENTS.md +4 -3
- package/dist/components/ActionMenu.js +141 -134
- package/dist/components/ActionMenu.js.map +1 -1
- package/dist/components/FilterSearch.js +572 -0
- package/dist/components/FilterSearch.js.map +1 -0
- package/dist/components/PageLayout/regularLayout.js +58 -61
- package/dist/components/PageLayout/regularLayout.js.map +1 -1
- package/dist/components/Tag.js +22 -21
- package/dist/components/Tag.js.map +1 -1
- package/dist/components/Tooltip.js +97 -93
- package/dist/components/Tooltip.js.map +1 -1
- package/dist/components/actionMenuStyles.js +28 -19
- package/dist/components/actionMenuStyles.js.map +1 -1
- package/dist/components/filterSearchModel.js +76 -0
- package/dist/components/filterSearchModel.js.map +1 -0
- package/dist/components/filterSearchStyles.js +28 -0
- package/dist/components/filterSearchStyles.js.map +1 -0
- package/dist/index.d.ts +70 -0
- package/dist/index.js +407 -405
- package/dist/index.js.map +1 -1
- package/dist/tailwind.css.js +1 -1
- package/package.json +1 -1
package/COMPONENTS.md
CHANGED
|
@@ -38,14 +38,14 @@ The catalog of components exported from `@stackone/malachite`. **Reach for one o
|
|
|
38
38
|
- **`IconIndicator`** — A non-interactive icon that carries a meaning, such as a capability shown on a page that cannot switch it on. Same square as an icon-only `Button` at each `size`, so it lines up where a sibling view shows the button. Renders `role="img"`, named by `aria-label`; with a `tooltip` it takes focus so the tooltip is reachable by keyboard. Reach for it instead of a hand-sized `<span>` around an icon. _Key props:_ `icon`, `aria-label`, `tooltip`, `size`.
|
|
39
39
|
- **`StatusBadge`** — Large status indicator (e.g. success checkmark) for terminal/confirmation screens. _Key props:_ `type`.
|
|
40
40
|
- **`StatBadge`** — Pill showing a numeric value with a label (e.g. "3 accounts selected"). _Key props:_ `count`, `label`.
|
|
41
|
-
- **`Tag`** — Small status/label tag. Types: success, error, warning, default. _Key props:_ `value`, `type`, `paddingSize`, `textTransform`, `WidthSize`, `tooltip`, `
|
|
41
|
+
- **`Tag`** — Small status/label tag. Types: success, error, warning, default. _Key props:_ `value`, `type`, `paddingSize`, `textTransform`, `WidthSize`, `tooltip` (hover-only; wrap in `Tooltip focusableTrigger` for keyboard reach), `tooltipDescribesTag`, `onClick` (makes it a button, unless `onRemove` is set; keep `value`/`icon` non-interactive), `onRemove`.
|
|
42
42
|
- **`NumberTag`** — Compact numeric badge/counter.
|
|
43
43
|
- **`UsageCard`** — A small metered card: a title (optionally a link via `titleLink`, drawn with a trailing arrow), a right-aligned `detail` such as "700/1,000", and a body, usually a `Progress`. Used by `Sidebar.Usage`, and fine on its own, e.g. on a billing page. _Key props:_ `title`, `titleLink`, `detail`, `children`, `aria-label` (required; names the group), `busy` (sets `aria-busy` only).
|
|
44
44
|
- **`Progress`** — Progress bar.
|
|
45
45
|
- **`LoadingBar`** — Indeterminate full-width loading line; the variant colour sweeps across a pale track for work of unknown duration. Toggle `active` rather than unmounting, so the reserved height prevents layout shift. _Key props:_ `active`, `LoadingBarVariant` (`default` | `warning` | `error`), `LoadingBarSize` (`small` | `medium` | `large`), `aria-label`, `aria-labelledby`, `className`, `style`.
|
|
46
46
|
- **`Spinner`** — Loading spinner.
|
|
47
47
|
- **`Skeleton`** — Loading placeholders. Includes `Skeleton.Avatar` and `Skeleton.Input`.
|
|
48
|
-
- **`Tooltip`** — Hover/focus tooltip; also works on disabled controls (see CLAUDE.md). It describes its trigger once shown, so `describesTrigger={false}` where the same sentence is already on screen and referenced — otherwise a screen reader reads it twice. _Key props:_ `text`, `placement`, `delay`, `maxWidth`, `shouldShow`, `triggerAs`, `describesTrigger`, `isShrinkable`, `fill`, `wrapChildren`, `suppressed` (holds the tooltip back, e.g. while the popup its trigger opens is up), `showOnFocus` (false where script can move focus onto the trigger, e.g. an option a panel focuses on open).
|
|
48
|
+
- **`Tooltip`** — Hover/focus tooltip; also works on disabled controls (see CLAUDE.md). It describes its trigger once shown, so `describesTrigger={false}` where the same sentence is already on screen and referenced — otherwise a screen reader reads it twice. _Key props:_ `text`, `placement`, `delay`, `maxWidth`, `shouldShow`, `triggerAs`, `describesTrigger`, `isShrinkable`, `fill`, `wrapChildren`, `suppressed` (holds the tooltip back, e.g. while the popup its trigger opens is up), `showOnFocus` (false where script can move focus onto the trigger, e.g. an option a panel focuses on open), `focusableTrigger` (makes a trigger that cannot take focus — text, a static `Tag` — a tab stop, so the tooltip is not mouse-only; not for focusable children).
|
|
49
49
|
- **`TruncatedTooltip`** — Shows a tooltip only when its text is truncated.
|
|
50
50
|
- **`Confetti`** — Celebratory confetti burst for success moments. _Key props:_ `pieces`.
|
|
51
51
|
- **`ConfirmationDialog`** — Asks the customer to confirm one consequential action — a preset over `Modal`, not a second dialog implementation. `variant` picks a whole coherent set at once (badge fill, tray tint, caption colour, badge icon and confirm-button variant), so `destructive` cannot end up with a non-destructive confirm button; `destructive` also renders as `role="alertdialog"`, and `description` is wired to the dialog through `aria-describedby`, so the consequence is announced and not just the title. `confirmPhrase` gates the confirm action on typing that exact phrase, for a consequence severe enough that one click should not be able to cause it — the typed value is trimmed, since copying a name picks up a trailing space, but stays case-sensitive, and the field clears each time the dialog opens, being a per-decision gate rather than a value. Neither a destructive dialog nor one carrying a `confirmPhrase` dismisses on a scrim click, and `loading` suppresses the pointer routes — the scrim and the close button — as well as disabling the buttons. It never suppresses ESC: that is the only guaranteed keyboard exit from a focus trap, so it always reaches `onClose` and whether to abort is left to the caller. ESC reaches it whenever it is the topmost overlay, so it can be opened over a panel. `callout` renders an `Alert` in a full-width band at the top of the body — it scrolls with the body rather than being pinned under the header — and its `type` is chosen independently of `variant` — the frames pair a green badge with an amber callout. `titleAdornment` names the entity beside the title, `badgeIcon` overrides the variant’s glyph for an action-specific one, and `tip` is the `{ label?, message }` caption on the tray beneath the card, which `Modal` renders and describes. `children` is where extra body content goes. `onConfirm` does not close the dialog, so the caller can hold it open on `loading` while the request is in flight. `confirmPhraseLabel` also takes `{ prefix, suffix }`, which renders the phrase itself as a copyable token between the two — the gate is case-sensitive and space-exact, so a customer who has identified the right entity should not be locked out by a glyph they cannot tell apart. The copy control sits outside the element that names the field, so “Copy…” never lands in the field’s accessible name; `confirmPhraseCopyLabel` names it and `onPhraseCopied` reports a completed write with the phrase it copied. A rejected write says so in the field rather than failing silently, and the control is disabled where the Clipboard API is unavailable. A near miss is named rather than left to a disabled button: typing the phrase with the wrong case or the wrong spacing says which, once the whole phrase is there, and that message is the field’s `aria-describedby`. _Key props:_ `open`, `onClose`, `onConfirm`, `title`, `description`, `titleAdornment`, `badgeIcon`, `variant` (`default` | `warning` | `destructive` | `success`), `callout`, `confirmPhrase`, `confirmPhraseLabel`, `confirmPhraseCopyLabel`, `confirmPhrasePlaceholder`, `onPhraseCopied`, `confirmLabel`, `cancelLabel`, `loading`, `tip`, `size`, `aria-label`, `aria-labelledby`.
|
|
@@ -59,7 +59,7 @@ The catalog of components exported from `@stackone/malachite`. **Reach for one o
|
|
|
59
59
|
- **`PreviewTreeRow`** — A single labelled row within a PreviewTree. _Key props:_ `label`.
|
|
60
60
|
- **`Tabs`** — Tabbed navigation switching between content panels. `trailing` puts content on the same row, settled against its end — a count, a scope picker, the state the tabs are showing — inside the rule this component already draws. Without it a consumer had to redraw that rule to carry it past the tabs, and the tab list, which cannot wrap, would not yield any width to what sat beside it. The tabs hold their width and the trailing content gives ground; the `tablist` role stays on the tabs alone, so the extra content is not announced as one. _Key props:_ `TabItem[]`, `activeKey`, `onChange`, `trailing`, `className`, `style`.
|
|
61
61
|
- **`Breadcrumbs`** — Breadcrumb trail with caret separators, an optional back button (shown only when there is more than one crumb AND the parent — the second-to-last — carries a `url` to target) and a right-aligned `actions` row. A lone crumb marked `isRoot` switches to title mode: plain emphasised text, no trail chrome, but the `actions` row still renders. Renders as a `nav`/`ol` with `aria-current="page"` on the last crumb. Pass `renderLink` to route crumbs through the app's own router instead of a plain `<a>`. Used by `PageLayout`, but standalone too. _Key props:_ `links[]` (`name`, `url`, `isRoot`), `actions[]`, `renderLink`, `className`, `style`.
|
|
62
|
-
- **`ActionMenu`** — Dropdown menu of actions (kebab/overflow style). The trigger is an icon-only `Button` by default, so `size` draws exactly the square a `Button` of that size draws beside it: `xsmall` is Figma's 20px "Button Element - Square (X-Small)", `small` the 28px rung above it. Both squares are fixed rather than read off the `controlHeights` tokens, so re-theming those does not resize the trigger. A supplied `triggerIcon` is held to the tier's glyph size — 11px at `xsmall`, 16px at `small`. At 20px the `xsmall` trigger meets WCAG 2.5.8 through the *Spacing* exception rather than the 24px minimum, so keep at least 4px between adjacent triggers. `showLabel` draws `label` beside the icon instead of leaving it to the accessible name — for a menu that is the point of its corner rather than an overflow for a row that already says what it acts on — and `variant` puts the trigger on any `Button` variant, so a menu handing work to the assistant can wear the assistant ground rather than the overflow-kebab outline. An item's `icon` leads its label: every caller's names the action itself — copy, open, delete — and an icon that is part of an item's identity belongs where the eye starts rather than right-aligned, where it reads as a status column. The menu is Figma's `Dropdown Component - Options List`: a fixed sized to its widest item between a 180px floor and the 280px Figma draws, by default under the trigger's right edge, 14px below it, flipping above or clamping sideways when the viewport has no room, rounding at the library's 12px and padding wider (8px) than the menu a `Dropdown` opens. Items read in the primary text colour, or the secondary one with `tone="muted"` (e.g. a sidebar's menus); destructive ones in red; `danger: 'hover'` keeps an item in its normal colour and turns it destructive (red text on a light red ground) only on hover or keyboard focus, for an action like signing out. A ceiling rather than a flat width: taken literally it gave a menu of four short verbs 130px of air and made every kebab in an app the same width whatever it held. `minWidth` only raises it above that ceiling, so it is the one way to go past 280px, and a label too long for the box truncates rather than wrapping. _Key props:_ `ActionMenuItem[]`, `size`, `label`, `triggerIcon`, `minWidth`, `showLabel`, `variant`, `tone` (`default` | `muted`), `trigger` (your own control in place of the kebab, cloned with the menu's ref and aria state), `placement` (`bottom-end` default | `bottom-start` | `right-start` | `top-start` | `top-center`), `offset` (`small` 8px | `medium` 12px | `large` 14px; applies to every placement; without it the default `bottom-end` keeps the options menu's 14px and the others use 8px). An item's `tag` (e.g. an "Early access" `Tag`) follows its label in a lazily loaded shadow host of its own, since the menu renders into `document.body`, and joins its accessible name; `current: true` marks the item for the page the user is on (`aria-current="page"` and the selected row paint; a `danger` item keeps its red text). An item with an `href` is a link: Cmd/Ctrl-click or a middle click opens it in a new tab and closes the menu, while a plain click runs `onClick` in its place when one is given (so a router can navigate in-app); Enter and Space both activate it. An item with `type: 'text'` is context rather than an action (an account's email above its actions): secondary text the arrow keys skip. Opened from the keyboard, focus lands on the first item; Up/Down/Home/End move through enabled items, Tab closes the menu and moves on as usual; Escape or a picked item returns focus to the trigger.
|
|
62
|
+
- **`ActionMenu`** — Dropdown menu of actions (kebab/overflow style). The trigger is an icon-only `Button` by default, so `size` draws exactly the square a `Button` of that size draws beside it: `xsmall` is Figma's 20px "Button Element - Square (X-Small)", `small` the 28px rung above it. Both squares are fixed rather than read off the `controlHeights` tokens, so re-theming those does not resize the trigger. A supplied `triggerIcon` is held to the tier's glyph size — 11px at `xsmall`, 16px at `small`. At 20px the `xsmall` trigger meets WCAG 2.5.8 through the *Spacing* exception rather than the 24px minimum, so keep at least 4px between adjacent triggers. `showLabel` draws `label` beside the icon instead of leaving it to the accessible name — for a menu that is the point of its corner rather than an overflow for a row that already says what it acts on — and `variant` puts the trigger on any `Button` variant, so a menu handing work to the assistant can wear the assistant ground rather than the overflow-kebab outline. An item's `icon` leads its label: every caller's names the action itself — copy, open, delete — and an icon that is part of an item's identity belongs where the eye starts rather than right-aligned, where it reads as a status column. The menu is Figma's `Dropdown Component - Options List`: a fixed sized to its widest item between a 180px floor and the 280px Figma draws, by default under the trigger's right edge, 14px below it, flipping above or clamping sideways when the viewport has no room, rounding at the library's 12px and padding wider (8px) than the menu a `Dropdown` opens. Items read in the primary text colour, or the secondary one with `tone="muted"` (e.g. a sidebar's menus); destructive ones in red; `danger: 'hover'` keeps an item in its normal colour and turns it destructive (red text on a light red ground) only on hover or keyboard focus, for an action like signing out. A ceiling rather than a flat width: taken literally it gave a menu of four short verbs 130px of air and made every kebab in an app the same width whatever it held. `minWidth` only raises it above that ceiling, so it is the one way to go past 280px, and a label too long for the box truncates rather than wrapping. _Key props:_ `ActionMenuItem[]`, `size`, `label`, `triggerIcon`, `minWidth`, `showLabel`, `variant`, `tone` (`default` | `muted`), `trigger` (your own control in place of the kebab, cloned with the menu's ref and aria state), `placement` (`bottom-end` default | `bottom-start` | `right-start` | `top-start` | `top-center`), `offset` (`small` 8px | `medium` 12px | `large` 14px; applies to every placement; without it the default `bottom-end` keeps the options menu's 14px and the others use 8px). An item's `tag` (e.g. an "Early access" `Tag`) follows its label in a lazily loaded shadow host of its own, since the menu renders into `document.body`, and joins its accessible name; `current: true` marks the item for the page the user is on (`aria-current="page"` and the selected row paint; a `danger` item keeps its red text). An item with an `href` is a link: Cmd/Ctrl-click or a middle click opens it in a new tab and closes the menu, while a plain click runs `onClick` in its place when one is given (so a router can navigate in-app); Enter and Space both activate it. `target: '_blank'` opens it in a new tab, adds `noopener noreferrer` and tells screen readers it opens a new tab. An item with `type: 'text'` is context rather than an action (an account's email above its actions): secondary text the arrow keys skip. Opened from the keyboard, focus lands on the first item; Up/Down/Home/End move through enabled items, Tab closes the menu and moves on as usual; Escape or a picked item returns focus to the trigger.
|
|
63
63
|
- **`NavItem`** — A row in an app's side navigation. Idle rows read secondary and take the list hover; the `active` row wears the accent tint and sets `aria-current="page"` on a link. `collapsed` shrinks it to a 36px icon tile whose label becomes the accessible name and a right-hand tooltip, with any `badge` pinned to the tile's corner. `variant="subitem"` indents a child row behind a dot instead of an icon; `disclosure` makes a group header button with `aria-expanded` and a rotating chevron, and `hasActiveChild` colours its text while a child is current. Renders an `<a>` with `href`, the element passed with `asChild` (a router `Link`), or a `<button>`, and forwards its ref and extra attributes so it can be a `Popover` or `ActionMenu` trigger. _Key props:_ `label`, `icon`, `active`, `hasActiveChild`, `collapsed`, `variant` (`item` | `subitem`), `danger`, `trailing`, `badge`, `disclosure` (`expanded` | `collapsed`), `tooltip`, `accessibleLabel` (overrides the accessible name; collapsed it defaults to the tooltip text plus `badgeLabel`), `href`, `target`, `rel`, `onClick`, `asChild` + `children`, `className`, `size` (`medium` | `small`, the tighter row for lists inside a popover), `tag` (beside the label, e.g. early access), `badgeLabel` (read in place of the badge and added to the collapsed name). A passed `aria-expanded` (e.g. from a `Popover`) wins over `disclosure`, and a button row forwards a passed `aria-current`.
|
|
64
64
|
- **`Sidebar`** — An app's left navigation shell, pinned to the viewport's left edge: 260px wide, 60px `collapsed`, on the sunken page background. Composed from `Sidebar.Header` (a 56px band for `Sidebar.Switcher`, the logo + workspace + parent trigger), `Sidebar.Body` (the part that scrolls), `Sidebar.Notices`, `Sidebar.Search`, `Sidebar.Section` (rows of `NavItem`s at the gutter), `Sidebar.Group` + `Sidebar.GroupItems` (a disclosure header and its sub-items), `Sidebar.Divider`, `Sidebar.Usage` (a `UsageCard` placed at the sidebar's gutter) and `Sidebar.Footer` holding `Sidebar.Account`. Popovers opened from it (with `padding="none"`) wrap their body in `Sidebar.Panel`, sized so the popover comes to `SIDEBAR_POPOVER_WIDTH_PX`, and build it from the `Popover` parts. `onToggleCollapse` adds the edge toggle, shown in CSS while the pointer is over the sidebar or a keyboard focus is inside it, and always on a touch screen, which has no hover to reveal it. A workspace, organization or account name cut off in the expanded sidebar shows in full on hover. Notices, the divider and the usage card hide while collapsed. Render it inside `<MalachiteContext layer="sidebar">`, or the page paints over the edge toggle. _Key props:_ `collapsed`, `onToggleCollapse`, `expandLabel`, `collapseLabel`, `aria-label`; `Sidebar.Switcher`: `logo`, `title`, `subtitle`, `titleTooltip`/`subtitleTooltip` (shown when the name is cut off, the name by default; pass a node to wrap it, e.g. to keep it out of session recordings) + button attributes; `Sidebar.Account`: `avatar` or `initials` (a small chip when there is no photo), `name`, `tooltip` (on the collapsed rail, and expanded when the name is cut off; `name` by default; pass a node to wrap it) + button attributes; `Sidebar.Usage`: the `UsageCard` props; `Sidebar.Notices`: `aria-label` (required). `useSidebarCollapsed()` reads the state from inside; `SIDEBAR_WIDTH_PX` gives the page its offset beside the sidebar; `SIDEBAR_TOOLTIP_DELAY_S` is the 0.2s delay its name and rail tooltips use, for tooltips a consumer adds inside it.
|
|
65
65
|
- **`SearchTrigger`** — A button drawn as a filled search field, for opening a command palette or search dialog from app chrome such as a sidebar. It is not an input — typing happens in what it opens. `shortcut` draws flat `<kbd>` chips at the end (not `Keycap`, whose raised shadow reads as a second button inside a filled field), kept out of the accessible name; `iconOnly` makes a 36px icon tile matching a collapsed `NavItem`, named by `label`. _Key props:_ `label` (default `Search`), `shortcut` (`ReactNode[]`), `iconOnly`, `aria-label`, `className`, plus native button attributes (`onClick`, `aria-haspopup`, `aria-expanded`, `aria-keyshortcuts`).
|
|
@@ -88,6 +88,7 @@ The catalog of components exported from `@stackone/malachite`. **Reach for one o
|
|
|
88
88
|
- **`Toggle`** — On/off switch. Add `label` for a visible caption, and `variant` to give it standalone toolbar-filter chrome — pick the family it sits beside: `pill` matches the filter-bar inputs (`Search`, `MultiSelect`), `button` renders the whole control as an outline `Button` with the switch as its trailing icon. `bordered` is shorthand for `variant="pill"`. `tone="pending"` paints the checked track amber, for a switch whose change has not been written yet — pair it with a `SelectionBar` reporting the same diff. The unchecked track never changes: nothing is pending about a switch that is off. Forwards a ref to its root element, so it can anchor an `InlineConfirm`. _Key props:_ `checked`, `onChange`, `disabled`, `ToggleSize` (`small` | `medium`), `ToggleTone` (`default` | `pending`), `label`, `ToggleVariant` (`pill` | `button`), `bordered`, `aria-label`, `aria-labelledby`, `aria-describedby`, `id`.
|
|
89
89
|
- **`Dropdown`** — Single-select dropdown. `DropdownSize` is `small` (28px) | `medium` (32px) | `large` (42px), matching Button; `fullWidth` fills the container instead of sizing to content, which is what a table cell or a toolbar slot needs — without it the field column sizes to its content, so a consumer has to compute a width; `width` fixes that width outright, the way `MultiSelect` does, for a toolbar lining the two up rather than letting each hug its own label — a narrower container still wins, since the width clamps to it; Until this release a `search` trigger could not go below the intrinsic width of its bare `input`, so a `width` under roughly 200px was ignored; it now shrinks with the rest. The open menu fills the trigger and left-aligns under it, as Figma draws every tier of `Dropdown Component - Standard`, so `width` sizes both — the menu is the width of the control that opened it and does not widen for a long option, which ellipsises instead, so a row of controls and their open menus stay on one set of edges; below 160px the menu keeps that floor rather than shrinking to a chip nobody can read, and `minWidth` raises that floor without widening the trigger — for a picker a column pins narrow whose options carry a subtitle the trigger never shows; `menuAlign="end"` lines a menu wider than its trigger up with the trigger's right edge instead, so a control at the right of a card opens its menu inwards rather than past the card; near a viewport edge it anchors to the trigger's other side before falling back to the viewport, so the two share an edge wherever one fits; `small` is the chip that sits beside a page title and rounds tighter (10px) than the taller sizes. These names each moved up a tier in 0.71 — the old `xsmall`/`small`/`medium` are now `small`/`medium`/`large`. Pass `value` for a controlled selection whose source of truth lives elsewhere (URL or app state): the trigger follows it without the consumer remounting via `key`, which used to discard focus mid-interaction. `defaultValue` keeps the uncontrolled behaviour. `clearable` adds a clear control to the trigger once something is selected and reports it as `onItemSelected(undefined)` — without it a selection cannot be undone, so filter bars had to invent a sentinel "any" item to carry that meaning. `requiredIndicator` switches the label between the library default of naming the optional fields, an `asterisk` on the required ones for designs drawn that way, and `none` for a field whose value is supplied rather than asked for — a locked control is still required, but a marker only sends the reader looking for something to fill in. Visual only: `aria-required` is set from `required` whichever is chosen. `showSubtitleInTrigger` promotes the selected item's `subtitle` into the closed trigger, on one line under the value, so the consequence of a choice stays on screen once the menu shuts — the trigger trades its fixed height for a floor and grows by that line, and either line too long for it ellipsises, with a single tooltip carrying whichever lines are cut off, value first. It does not combine with `search`, whose trigger is a single-line text input with nowhere to put a second line, so the type rejects the pair rather than letting a consumer ask for a subtitle and get none. `subtitleLayout` decides how a subtitle sits in the open menu: the default `inline` keeps it on the label's row after a divider, which suits a short qualifier, while `stacked` gives it its own line beneath the label — one line either way, ellipsised rather than wrapped, so inline a sentence-length subtitle is cut back to a few words. Every option row carries a tooltip for whatever it had to cut, across all three of label, subtitle and `rightSideText`, so nothing the row hides is unreadable. Passing `formControl` is enough to show validation on the same terms as `Input`, though only validation: the trigger still paints `value` or `defaultValue`, never the value the form holds. The resolver's message renders on its own line beneath the field in the destructive colour, politely announced, with `aria-invalid` and `aria-describedby` wired, while an explicit `helperText` keeps its own line above it and stays dimmed, and `error={false}` suppresses the resolver outright. An item's `subtitle` renders inline markdown in the option, trigger and tooltip, with links drawn but inert, since the option is itself the click target; a subtitle with no markdown syntax stays a plain text node. An item's `badge` (e.g. a `Tag`) sits at the right end of its option in the open menu, after any `rightSideText`, and never shows in the closed trigger or the search input; it does not shrink, and the option is still named by its label alone. _Key props:_ `DropdownItem[]` (items take `badge`), `DropdownSize`, `fullWidth`, `width`, `minWidth`, `menuAlign` (`start` | `end`), `value`, `defaultValue`, `label`, `search`, `showSubtitleInTrigger`, `subtitleLayout`, `clearable`, `required`, `requiredIndicator`, `error`, `helperText`, `loading`, `formControl`, `aria-label`, `aria-labelledby`, `id`.
|
|
90
90
|
- **`MultiSelect`** — Multi-select dropdown. `width` fixes the trigger, as `Dropdown` and `Search` take it, so a toolbar can put its controls on one width — and, as with those two, a container narrower than the value still wins; the menu fills the trigger and left-aligns under it, so that one prop sizes both; `minWidth` raises the 160px floor a narrow trigger falls back to. An option label too long for the menu ellipsises, and hovering it shows the full label — above the description in `hover` mode — as `Dropdown` does for its options. `requiredIndicator` follows the same three-way convention the single-select controls do, so a form choosing asterisks does not get "(Optional)" on one of its fields. _Key props:_ `MultiSelectOption[]`, `MultiSelectSize`, `width`, `minWidth`, `label`, `searchable`, `inlineSearch`, `chips`, `chipsRemovable`, `required`, `requiredIndicator`, `aria-label`, `aria-labelledby`, `id`.
|
|
91
|
+
- **`FilterSearch`** — One search field that filters across several categories. Clicking lists the filters, each with its description and an example; picking one lists its values behind a `Filters > Name` breadcrumb, and typing before picking searches the values of every filter at once, tagging each match with the filter it belongs to. Select All / Deselect All act on whatever the query matches. Each filter becomes a chip (`Connector: Notion`, or `Connector: (Notion, Attio)`) that reopens its values when clicked; Backspace in an empty field removes the last one. Controlled: `onChange` gets the next selection and the `changes` that produced it, an empty `items` meaning the filter was removed, so a URL-backed selection writes back only what moved. Chips keep the order filters were added in even when `value` comes back reordered, and keep their labels while `filters` reloads. _Key props:_ `filters`, `value`, `onChange`, `onClear`, `onActiveFilterChange`, `placeholder`, `size`, `width`, `loading`, `disabled`, `clearable`, `maxVisibleChips`, `aria-label`, `aria-labelledby`, `id`.
|
|
91
92
|
- **`QueryInput`** — A textbox with inline syntax colouring and a suggestion menu anchored to the caret. It carries no grammar of its own: the consumer parses the value, hands back `tokens` to colour and `suggestions` to offer, so the vocabulary lives in the consuming app rather than here. _Key props:_ `value`, `onChange`, `tokens`, `suggestions`, `activeRange`, `onAccept`, `onQueryContextChange`, `onSubmit`, `placeholder`, `size`, `disabled`, `loading`, `emptyMessage`, `className`, `style`, `aria-label`, `aria-labelledby`.
|
|
92
93
|
- **`SettingRow`** — One setting: a labelled row with its control on the trailing edge, and the sentence that explains it underneath. `control` may be a render prop receiving the label’s id, so a control that no `<label htmlFor>` can reach — `Toggle` renders a `<button role="switch">` — still names itself via `aria-labelledby`. _Key props:_ `label`, `badge`, `description`, `control`, `className`.
|
|
93
94
|
|
|
@@ -1,193 +1,200 @@
|
|
|
1
|
-
import { jsxs as
|
|
2
|
-
import
|
|
3
|
-
import { createPortal as
|
|
4
|
-
import { Button as
|
|
5
|
-
import
|
|
6
|
-
import { focusEdge as x, edgeForTriggerKey as
|
|
7
|
-
import { TRIGGER_SIZES as
|
|
8
|
-
import { getDeepActiveElement as
|
|
9
|
-
import { elementRef as ce, composeRefs as
|
|
10
|
-
import { pushEscapeLayer as
|
|
11
|
-
import { isPlainClick as
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
if (
|
|
19
|
-
return /* @__PURE__ */ o("div", {
|
|
20
|
-
|
|
1
|
+
import { jsxs as E, jsx as o, Fragment as N } from "react/jsx-runtime";
|
|
2
|
+
import _, { useState as z, useRef as v, useEffect as G, useMemo as U } from "react";
|
|
3
|
+
import { createPortal as $ } from "react-dom";
|
|
4
|
+
import { Button as H } from "./Button.js";
|
|
5
|
+
import V from "./CustomIcons/MoreHorizontalIcon.js";
|
|
6
|
+
import { focusEdge as x, edgeForTriggerKey as X, itemForMenuKey as Z, enabledItems as q } from "./actionMenuKeyboard.js";
|
|
7
|
+
import { TRIGGER_SIZES as J, EMPTY_ITEM_STYLE as Q, menuStyle as W, itemStyle as A, TEXT_ITEM_STYLE as ee, ITEM_ICON_LAYOUT as ne, ITEM_ICON_STYLE as re, ITEM_ICON_CLASS as oe, LABEL_STYLE as te, VISUALLY_HIDDEN_STYLE as ae, ITEM_CLASS as le } from "./actionMenuStyles.js";
|
|
8
|
+
import { getDeepActiveElement as se } from "./activeElement.js";
|
|
9
|
+
import { elementRef as ce, composeRefs as ie, chainHandler as S } from "./composeRefs.js";
|
|
10
|
+
import { pushEscapeLayer as ue } from "./escapeLayers.js";
|
|
11
|
+
import { isPlainClick as de } from "./linkClick.js";
|
|
12
|
+
import { resolveRel as fe } from "./linkRel.js";
|
|
13
|
+
import { usePopupLayer as pe } from "./overlayLayers.js";
|
|
14
|
+
import { safeHref as ye } from "./safeHref.js";
|
|
15
|
+
import { useActionMenuPosition as me } from "./useActionMenuPosition.js";
|
|
16
|
+
const he = _.lazy(() => import("./ActionMenuTagHost.js")), ge = (e) => e != null && e !== !1, I = (e) => e.stopPropagation(), Ee = ({ item: e, tone: d, onSelect: b, onFollow: M }) => {
|
|
17
|
+
var h;
|
|
18
|
+
if (e.type === "divider")
|
|
19
|
+
return /* @__PURE__ */ o("div", { style: A(e) });
|
|
20
|
+
if (e.type === "text")
|
|
21
|
+
return /* @__PURE__ */ o("div", { role: "menuitem", "aria-disabled": "true", style: ee, children: e.label });
|
|
22
|
+
const m = {
|
|
21
23
|
role: "menuitem",
|
|
22
|
-
title:
|
|
23
|
-
"aria-current":
|
|
24
|
-
className:
|
|
25
|
-
"data-danger":
|
|
26
|
-
"data-tone":
|
|
27
|
-
style:
|
|
28
|
-
},
|
|
29
|
-
|
|
24
|
+
title: e.tooltip,
|
|
25
|
+
"aria-current": e.current ? "page" : void 0,
|
|
26
|
+
className: le,
|
|
27
|
+
"data-danger": e.danger === "hover" ? "hover" : void 0,
|
|
28
|
+
"data-tone": d,
|
|
29
|
+
style: A(e, d)
|
|
30
|
+
}, c = e.disabled ? void 0 : ye(e.href), T = c !== void 0 && ((h = e.target) == null ? void 0 : h.toLowerCase()) === "_blank", t = /* @__PURE__ */ E(N, { children: [
|
|
31
|
+
e.icon && /* @__PURE__ */ o(
|
|
30
32
|
"span",
|
|
31
33
|
{
|
|
32
34
|
className: oe,
|
|
33
|
-
style:
|
|
34
|
-
children:
|
|
35
|
+
style: e.danger === "hover" ? ne : re,
|
|
36
|
+
children: e.icon
|
|
35
37
|
}
|
|
36
38
|
),
|
|
37
|
-
/* @__PURE__ */
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
39
|
+
/* @__PURE__ */ E("span", { style: te, children: [
|
|
40
|
+
e.label,
|
|
41
|
+
T && /* @__PURE__ */ o("span", { style: ae, children: " (opens in a new tab)" })
|
|
42
|
+
] }),
|
|
43
|
+
ge(e.tag) ? /* @__PURE__ */ o(_.Suspense, { fallback: null, children: /* @__PURE__ */ o(he, { children: e.tag }) }) : null
|
|
44
|
+
] });
|
|
45
|
+
return c !== void 0 ? /* @__PURE__ */ o(
|
|
41
46
|
"a",
|
|
42
47
|
{
|
|
43
|
-
...
|
|
44
|
-
href:
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
+
...m,
|
|
49
|
+
href: c,
|
|
50
|
+
target: e.target,
|
|
51
|
+
rel: fe(void 0, e.target),
|
|
52
|
+
onClick: (f) => {
|
|
53
|
+
if (de(f) && e.onClick) {
|
|
54
|
+
f.preventDefault(), b(e);
|
|
48
55
|
return;
|
|
49
56
|
}
|
|
50
|
-
|
|
57
|
+
M();
|
|
51
58
|
},
|
|
52
|
-
onAuxClick: (
|
|
53
|
-
|
|
59
|
+
onAuxClick: (f) => {
|
|
60
|
+
f.button === 1 && M();
|
|
54
61
|
},
|
|
55
|
-
children:
|
|
62
|
+
children: t
|
|
56
63
|
}
|
|
57
|
-
) : /* @__PURE__ */ o("button", { ...
|
|
58
|
-
},
|
|
59
|
-
items:
|
|
60
|
-
label:
|
|
61
|
-
triggerIcon:
|
|
62
|
-
minWidth:
|
|
63
|
-
size:
|
|
64
|
-
showLabel:
|
|
65
|
-
variant:
|
|
64
|
+
) : /* @__PURE__ */ o("button", { ...m, type: "button", disabled: e.disabled, onClick: () => b(e), children: t });
|
|
65
|
+
}, Oe = ({
|
|
66
|
+
items: e,
|
|
67
|
+
label: d = "More actions",
|
|
68
|
+
triggerIcon: b,
|
|
69
|
+
minWidth: M,
|
|
70
|
+
size: m = "small",
|
|
71
|
+
showLabel: c = !1,
|
|
72
|
+
variant: T = "outline",
|
|
66
73
|
trigger: t,
|
|
67
|
-
placement:
|
|
68
|
-
offset:
|
|
69
|
-
tone:
|
|
74
|
+
placement: h = "bottom-end",
|
|
75
|
+
offset: f,
|
|
76
|
+
tone: O = "default"
|
|
70
77
|
}) => {
|
|
71
|
-
const
|
|
72
|
-
minWidth:
|
|
73
|
-
placement:
|
|
74
|
-
offset:
|
|
78
|
+
const D = J[m], [a, i] = z(!1), p = v(null), u = v(null), g = v(null), y = v(!1), P = pe("actionmenu", a, u), K = me(a, p, u, {
|
|
79
|
+
minWidth: M,
|
|
80
|
+
placement: h,
|
|
81
|
+
offset: f
|
|
75
82
|
});
|
|
76
83
|
G(() => {
|
|
77
84
|
if (!a)
|
|
78
85
|
return;
|
|
79
|
-
|
|
80
|
-
const
|
|
81
|
-
const
|
|
82
|
-
[
|
|
83
|
-
(
|
|
84
|
-
) || (
|
|
86
|
+
g.current && (x(u.current, g.current), g.current = null);
|
|
87
|
+
const n = (s) => {
|
|
88
|
+
const k = s.composedPath();
|
|
89
|
+
[p.current, u.current].some(
|
|
90
|
+
(R) => R && k.includes(R)
|
|
91
|
+
) || (y.current = !1, i(!1));
|
|
85
92
|
};
|
|
86
|
-
document.addEventListener("mousedown",
|
|
87
|
-
const r =
|
|
93
|
+
document.addEventListener("mousedown", n, !0);
|
|
94
|
+
const r = ue(() => i(!1));
|
|
88
95
|
return () => {
|
|
89
|
-
var
|
|
90
|
-
document.removeEventListener("mousedown",
|
|
91
|
-
const
|
|
92
|
-
|
|
96
|
+
var k;
|
|
97
|
+
document.removeEventListener("mousedown", n, !0), r();
|
|
98
|
+
const s = se();
|
|
99
|
+
y.current && (!s || s === document.body) && ((k = p.current) == null || k.focus({ preventScroll: !0 })), y.current = !1;
|
|
93
100
|
};
|
|
94
101
|
}, [a]);
|
|
95
|
-
const
|
|
96
|
-
|
|
97
|
-
const r = !a &&
|
|
98
|
-
|
|
99
|
-
},
|
|
100
|
-
if (
|
|
101
|
-
|
|
102
|
+
const C = (n) => {
|
|
103
|
+
n.stopPropagation();
|
|
104
|
+
const r = !a && n.detail === 0;
|
|
105
|
+
g.current = r ? "first" : null, i((s) => !s);
|
|
106
|
+
}, L = (n) => {
|
|
107
|
+
if (n.key === "Tab" && a) {
|
|
108
|
+
i(!1);
|
|
102
109
|
return;
|
|
103
110
|
}
|
|
104
|
-
const r =
|
|
111
|
+
const r = X(n.key, h);
|
|
105
112
|
if (r) {
|
|
106
|
-
if (
|
|
107
|
-
x(
|
|
113
|
+
if (n.preventDefault(), a) {
|
|
114
|
+
x(u.current, r);
|
|
108
115
|
return;
|
|
109
116
|
}
|
|
110
|
-
|
|
117
|
+
g.current = r, i(!0);
|
|
111
118
|
}
|
|
112
|
-
},
|
|
113
|
-
var
|
|
114
|
-
if (
|
|
115
|
-
|
|
119
|
+
}, Y = (n) => {
|
|
120
|
+
var s;
|
|
121
|
+
if (n.key === "Tab") {
|
|
122
|
+
y.current = !1, (s = p.current) == null || s.focus({ preventScroll: !0 }), i(!1);
|
|
116
123
|
return;
|
|
117
124
|
}
|
|
118
|
-
if (
|
|
119
|
-
|
|
125
|
+
if (n.key === " " && n.target instanceof HTMLAnchorElement) {
|
|
126
|
+
n.preventDefault(), n.target.click();
|
|
120
127
|
return;
|
|
121
128
|
}
|
|
122
|
-
const r =
|
|
123
|
-
r && (
|
|
124
|
-
},
|
|
129
|
+
const r = Z(q(u.current), n.target, n.key);
|
|
130
|
+
r && (n.preventDefault(), r.focus());
|
|
131
|
+
}, F = (n) => {
|
|
125
132
|
var r;
|
|
126
|
-
|
|
127
|
-
},
|
|
128
|
-
() =>
|
|
129
|
-
[
|
|
130
|
-
), l = t == null ? void 0 : t.props, j = t ?
|
|
133
|
+
n.type === "divider" || n.type === "text" || n.disabled || ((r = n.onClick) == null || r.call(n), i(!1));
|
|
134
|
+
}, w = t ? ce(t) : void 0, B = U(
|
|
135
|
+
() => ie(w, p),
|
|
136
|
+
[w]
|
|
137
|
+
), l = t == null ? void 0 : t.props, j = t ? _.cloneElement(t, {
|
|
131
138
|
ref: B,
|
|
132
139
|
"aria-haspopup": "menu",
|
|
133
140
|
"aria-expanded": a,
|
|
134
|
-
onClick:
|
|
135
|
-
onMouseDown:
|
|
136
|
-
onKeyDown:
|
|
137
|
-
}) : /* @__PURE__ */
|
|
138
|
-
|
|
141
|
+
onClick: S(l == null ? void 0 : l.onClick, C),
|
|
142
|
+
onMouseDown: S(l == null ? void 0 : l.onMouseDown, I),
|
|
143
|
+
onKeyDown: S(l == null ? void 0 : l.onKeyDown, L)
|
|
144
|
+
}) : /* @__PURE__ */ E(
|
|
145
|
+
H,
|
|
139
146
|
{
|
|
140
|
-
ref:
|
|
147
|
+
ref: p,
|
|
141
148
|
type: "button",
|
|
142
|
-
variant:
|
|
143
|
-
size:
|
|
144
|
-
iconOnly: !
|
|
145
|
-
"aria-label":
|
|
149
|
+
variant: T,
|
|
150
|
+
size: m,
|
|
151
|
+
iconOnly: !c,
|
|
152
|
+
"aria-label": c ? void 0 : d,
|
|
146
153
|
"aria-haspopup": "menu",
|
|
147
154
|
"aria-expanded": a,
|
|
148
|
-
className: `shrink-0${
|
|
149
|
-
onClick:
|
|
150
|
-
onMouseDown:
|
|
151
|
-
onKeyDown:
|
|
155
|
+
className: `shrink-0${c ? " gap-2" : ""}`,
|
|
156
|
+
onClick: C,
|
|
157
|
+
onMouseDown: I,
|
|
158
|
+
onKeyDown: L,
|
|
152
159
|
children: [
|
|
153
|
-
/* @__PURE__ */ o("span", { className: `inline-flex items-center justify-center ${
|
|
154
|
-
|
|
160
|
+
/* @__PURE__ */ o("span", { className: `inline-flex items-center justify-center ${D.iconBox}`, children: b ?? /* @__PURE__ */ o(V, { size: D.icon }) }),
|
|
161
|
+
c && /* @__PURE__ */ o("span", { children: d })
|
|
155
162
|
]
|
|
156
163
|
}
|
|
157
164
|
);
|
|
158
|
-
return /* @__PURE__ */
|
|
165
|
+
return /* @__PURE__ */ E(N, { children: [
|
|
159
166
|
j,
|
|
160
|
-
a &&
|
|
161
|
-
/* @__PURE__ */
|
|
167
|
+
a && $(
|
|
168
|
+
/* @__PURE__ */ E(
|
|
162
169
|
"div",
|
|
163
170
|
{
|
|
164
|
-
ref:
|
|
171
|
+
ref: u,
|
|
165
172
|
role: "menu",
|
|
166
173
|
tabIndex: -1,
|
|
167
|
-
"aria-label":
|
|
168
|
-
...
|
|
169
|
-
style: W(
|
|
170
|
-
onMouseDown:
|
|
171
|
-
onClick:
|
|
172
|
-
onKeyDown:
|
|
174
|
+
"aria-label": d,
|
|
175
|
+
...P,
|
|
176
|
+
style: W(K),
|
|
177
|
+
onMouseDown: I,
|
|
178
|
+
onClick: I,
|
|
179
|
+
onKeyDown: Y,
|
|
173
180
|
onFocus: () => {
|
|
174
|
-
|
|
181
|
+
y.current = !0;
|
|
175
182
|
},
|
|
176
|
-
onBlur: (
|
|
183
|
+
onBlur: (n) => {
|
|
177
184
|
var r;
|
|
178
|
-
(r =
|
|
185
|
+
(r = u.current) != null && r.contains(n.relatedTarget) || (y.current = !1);
|
|
179
186
|
},
|
|
180
187
|
children: [
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
188
|
+
e.length === 0 && /* @__PURE__ */ o("div", { role: "menuitem", "aria-disabled": "true", style: Q, children: "No actions available" }),
|
|
189
|
+
e.map((n) => /* @__PURE__ */ o(
|
|
190
|
+
Ee,
|
|
184
191
|
{
|
|
185
|
-
item:
|
|
186
|
-
tone:
|
|
187
|
-
onSelect:
|
|
188
|
-
onFollow: () =>
|
|
192
|
+
item: n,
|
|
193
|
+
tone: O,
|
|
194
|
+
onSelect: F,
|
|
195
|
+
onFollow: () => i(!1)
|
|
189
196
|
},
|
|
190
|
-
|
|
197
|
+
n.key
|
|
191
198
|
))
|
|
192
199
|
]
|
|
193
200
|
}
|
|
@@ -197,6 +204,6 @@ const ye = v.lazy(() => import("./ActionMenuTagHost.js")), me = (n) => n != null
|
|
|
197
204
|
] });
|
|
198
205
|
};
|
|
199
206
|
export {
|
|
200
|
-
|
|
207
|
+
Oe as default
|
|
201
208
|
};
|
|
202
209
|
//# sourceMappingURL=ActionMenu.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ActionMenu.js","sources":["../../src/components/ActionMenu.tsx"],"sourcesContent":["import React, { useEffect, useMemo, useRef, useState } from 'react';\nimport { createPortal } from 'react-dom';\nimport Button from './Button';\nimport type { ButtonVariant } from './Button';\nimport MoreHorizontalIcon from './CustomIcons/MoreHorizontalIcon';\nimport {\n type MenuEdge,\n edgeForTriggerKey,\n enabledItems,\n focusEdge,\n itemForMenuKey,\n} from './actionMenuKeyboard';\nimport {\n type ActionMenuTone,\n EMPTY_ITEM_STYLE,\n ITEM_CLASS,\n ITEM_ICON_CLASS,\n ITEM_ICON_LAYOUT,\n ITEM_ICON_STYLE,\n LABEL_STYLE,\n TEXT_ITEM_STYLE,\n TRIGGER_SIZES,\n itemStyle,\n menuStyle,\n} from './actionMenuStyles';\nimport type { ActionMenuItem, ActionMenuPlacement } from './actionMenuTypes';\nimport { getDeepActiveElement } from './activeElement';\nimport { chainHandler, composeRefs, elementRef } from './composeRefs';\nimport { pushEscapeLayer } from './escapeLayers';\nimport type { FloatingOffset } from './floatingMenuGeometry';\nimport { isPlainClick } from './linkClick';\nimport { usePopupLayer } from './overlayLayers';\nimport { safeHref } from './safeHref';\nimport { useActionMenuPosition } from './useActionMenuPosition';\n\nexport type { ActionMenuItem, ActionMenuPlacement, ActionMenuTone };\n\nexport interface ActionMenuProps {\n items: ActionMenuItem[];\n label?: string;\n triggerIcon?: React.ReactNode;\n minWidth?: number;\n size?: 'small' | 'xsmall';\n /**\n * Shows `label` beside the icon instead of leaving it to the accessible name. For a menu\n * that is the point of its corner rather than an overflow for a row that already reads —\n * a kebab nobody can name is only safe when the row it belongs to says what it acts on.\n */\n showLabel?: boolean;\n /** Trigger's Button variant. Defaults to `outline`, the overflow-kebab look. */\n variant?: ButtonVariant;\n trigger?: React.ReactElement;\n placement?: ActionMenuPlacement;\n offset?: FloatingOffset;\n /** `muted` reads items in the secondary text colour, as in a sidebar's menus. */\n tone?: ActionMenuTone;\n}\n\ntype TriggerHandlers = {\n onClick?: (event: React.MouseEvent) => void;\n onMouseDown?: (event: React.MouseEvent) => void;\n onKeyDown?: (event: React.KeyboardEvent) => void;\n};\n\n// Loaded only when an item has a tag: its shadow host pulls in the Tailwind sheet, which a menu\n// without tags should not pay for.\nconst ActionMenuTagHost = React.lazy(() => import('./ActionMenuTagHost'));\n\nconst hasTag = (tag: React.ReactNode) => tag !== undefined && tag !== null && tag !== false;\n\nconst stopPropagation = (event: React.SyntheticEvent) => event.stopPropagation();\n\nconst MenuItemRow: React.FC<{\n item: ActionMenuItem;\n tone: ActionMenuTone;\n onSelect: (item: ActionMenuItem) => void;\n onFollow: () => void;\n}> = ({ item, tone, onSelect, onFollow }) => {\n if (item.type === 'divider') {\n return <div style={itemStyle(item)} />;\n }\n if (item.type === 'text') {\n return (\n <div role=\"menuitem\" aria-disabled=\"true\" style={TEXT_ITEM_STYLE}>\n {item.label}\n </div>\n );\n }\n const shared = {\n role: 'menuitem',\n title: item.tooltip,\n 'aria-current': item.current ? ('page' as const) : undefined,\n className: ITEM_CLASS,\n 'data-danger': item.danger === 'hover' ? 'hover' : undefined,\n 'data-tone': tone,\n style: itemStyle(item, tone),\n };\n const content = (\n <>\n {item.icon && (\n <span\n className={ITEM_ICON_CLASS}\n style={item.danger === 'hover' ? ITEM_ICON_LAYOUT : ITEM_ICON_STYLE}\n >\n {item.icon}\n </span>\n )}\n <span style={LABEL_STYLE}>{item.label}</span>\n {hasTag(item.tag) ? (\n <React.Suspense fallback={null}>\n <ActionMenuTagHost>{item.tag}</ActionMenuTagHost>\n </React.Suspense>\n ) : null}\n </>\n );\n const href = item.disabled ? undefined : safeHref(item.href);\n if (href !== undefined) {\n return (\n <a\n {...shared}\n href={href}\n onClick={(event) => {\n if (isPlainClick(event) && item.onClick) {\n event.preventDefault();\n onSelect(item);\n return;\n }\n onFollow();\n }}\n onAuxClick={(event) => {\n if (event.button === 1) {\n onFollow();\n }\n }}\n >\n {content}\n </a>\n );\n }\n return (\n <button {...shared} type=\"button\" disabled={item.disabled} onClick={() => onSelect(item)}>\n {content}\n </button>\n );\n};\n\n/**\n * ActionMenu is an overflow action menu. The trigger is a three-dot `Button` by default, or\n * your own control via `trigger`. The menu is portaled to document.body, so it escapes any\n * Shadow DOM or overflow:hidden ancestor, which is also why it is styled inline.\n */\nconst ActionMenu: React.FC<ActionMenuProps> = ({\n items,\n label = 'More actions',\n triggerIcon,\n minWidth,\n size = 'small',\n showLabel = false,\n variant = 'outline',\n trigger: customTrigger,\n placement = 'bottom-end',\n offset,\n tone = 'default',\n}) => {\n const triggerSize = TRIGGER_SIZES[size];\n const [open, setOpen] = useState(false);\n const triggerRef = useRef<HTMLElement | null>(null);\n const menuRef = useRef<HTMLDivElement>(null);\n const focusOnOpen = useRef<MenuEdge | null>(null);\n const focusInMenu = useRef(false);\n const popupIdentity = usePopupLayer('actionmenu', open, menuRef);\n\n const position = useActionMenuPosition(open, triggerRef, menuRef, {\n minWidth,\n placement,\n offset,\n });\n\n useEffect(() => {\n if (!open) {\n return;\n }\n if (focusOnOpen.current) {\n focusEdge(menuRef.current, focusOnOpen.current);\n focusOnOpen.current = null;\n }\n const closeOnOutsidePress = (event: MouseEvent) => {\n const path = event.composedPath();\n const inside = [triggerRef.current, menuRef.current].some(\n (node) => node && path.includes(node),\n );\n if (!inside) {\n focusInMenu.current = false;\n setOpen(false);\n }\n };\n document.addEventListener('mousedown', closeOnOutsidePress, true);\n const popEscapeLayer = pushEscapeLayer(() => setOpen(false));\n return () => {\n document.removeEventListener('mousedown', closeOnOutsidePress, true);\n popEscapeLayer();\n const active = getDeepActiveElement();\n if (focusInMenu.current && (!active || active === document.body)) {\n triggerRef.current?.focus({ preventScroll: true });\n }\n focusInMenu.current = false;\n };\n }, [open]);\n\n const toggleFromTrigger = (event: React.MouseEvent) => {\n event.stopPropagation();\n const openedFromKeyboard = !open && event.detail === 0;\n focusOnOpen.current = openedFromKeyboard ? 'first' : null;\n setOpen((v) => !v);\n };\n\n const handleTriggerKeyDown = (event: React.KeyboardEvent) => {\n if (event.key === 'Tab' && open) {\n setOpen(false);\n return;\n }\n const edge = edgeForTriggerKey(event.key, placement);\n if (!edge) {\n return;\n }\n event.preventDefault();\n if (open) {\n focusEdge(menuRef.current, edge);\n return;\n }\n focusOnOpen.current = edge;\n setOpen(true);\n };\n\n const handleMenuKeyDown = (event: React.KeyboardEvent<HTMLDivElement>) => {\n if (event.key === 'Tab') {\n focusInMenu.current = false;\n triggerRef.current?.focus({ preventScroll: true });\n setOpen(false);\n return;\n }\n if (event.key === ' ' && event.target instanceof HTMLAnchorElement) {\n event.preventDefault();\n event.target.click();\n return;\n }\n const target = itemForMenuKey(enabledItems(menuRef.current), event.target, event.key);\n if (target) {\n event.preventDefault();\n target.focus();\n }\n };\n\n const selectItem = (item: ActionMenuItem) => {\n if (item.type === 'divider' || item.type === 'text' || item.disabled) {\n return;\n }\n item.onClick?.();\n setOpen(false);\n };\n\n const ownTriggerRef = customTrigger ? elementRef<HTMLElement>(customTrigger) : undefined;\n const composedTriggerRef = useMemo(\n () => composeRefs(ownTriggerRef, triggerRef),\n [ownTriggerRef],\n );\n const customHandlers = customTrigger?.props as TriggerHandlers | undefined;\n\n const triggerElement = customTrigger ? (\n React.cloneElement(customTrigger, {\n ref: composedTriggerRef,\n 'aria-haspopup': 'menu',\n 'aria-expanded': open,\n onClick: chainHandler(customHandlers?.onClick, toggleFromTrigger),\n onMouseDown: chainHandler(customHandlers?.onMouseDown, stopPropagation),\n onKeyDown: chainHandler(customHandlers?.onKeyDown, handleTriggerKeyDown),\n } as Record<string, unknown>)\n ) : (\n <Button\n ref={triggerRef as React.RefObject<HTMLButtonElement>}\n type=\"button\"\n variant={variant}\n size={size}\n iconOnly={!showLabel}\n aria-label={showLabel ? undefined : label}\n aria-haspopup=\"menu\"\n aria-expanded={open}\n className={`shrink-0${showLabel ? ' gap-2' : ''}`}\n onClick={toggleFromTrigger}\n onMouseDown={stopPropagation}\n onKeyDown={handleTriggerKeyDown}\n >\n <span className={`inline-flex items-center justify-center ${triggerSize.iconBox}`}>\n {triggerIcon ?? <MoreHorizontalIcon size={triggerSize.icon} />}\n </span>\n {showLabel && <span>{label}</span>}\n </Button>\n );\n\n return (\n <>\n {triggerElement}\n {open &&\n createPortal(\n <div\n ref={menuRef}\n role=\"menu\"\n tabIndex={-1}\n aria-label={label}\n {...popupIdentity}\n style={menuStyle(position)}\n onMouseDown={stopPropagation}\n onClick={stopPropagation}\n onKeyDown={handleMenuKeyDown}\n onFocus={() => {\n focusInMenu.current = true;\n }}\n onBlur={(e) => {\n if (!menuRef.current?.contains(e.relatedTarget as Node | null)) {\n focusInMenu.current = false;\n }\n }}\n >\n {items.length === 0 && (\n <div role=\"menuitem\" aria-disabled=\"true\" style={EMPTY_ITEM_STYLE}>\n No actions available\n </div>\n )}\n {items.map((item) => (\n <MenuItemRow\n key={item.key}\n item={item}\n tone={tone}\n onSelect={selectItem}\n onFollow={() => setOpen(false)}\n />\n ))}\n </div>,\n document.body,\n )}\n </>\n );\n};\n\nexport default ActionMenu;\n"],"names":["ActionMenuTagHost","React","hasTag","tag","stopPropagation","event","MenuItemRow","item","tone","onSelect","onFollow","jsx","itemStyle","TEXT_ITEM_STYLE","shared","ITEM_CLASS","content","jsxs","Fragment","ITEM_ICON_CLASS","ITEM_ICON_LAYOUT","ITEM_ICON_STYLE","LABEL_STYLE","href","safeHref","isPlainClick","ActionMenu","items","label","triggerIcon","minWidth","size","showLabel","variant","customTrigger","placement","offset","triggerSize","TRIGGER_SIZES","open","setOpen","useState","triggerRef","useRef","menuRef","focusOnOpen","focusInMenu","popupIdentity","usePopupLayer","position","useActionMenuPosition","useEffect","focusEdge","closeOnOutsidePress","path","node","popEscapeLayer","pushEscapeLayer","active","getDeepActiveElement","_a","toggleFromTrigger","openedFromKeyboard","v","handleTriggerKeyDown","edge","edgeForTriggerKey","handleMenuKeyDown","target","itemForMenuKey","enabledItems","selectItem","ownTriggerRef","elementRef","composedTriggerRef","useMemo","composeRefs","customHandlers","triggerElement","chainHandler","Button","MoreHorizontalIcon","createPortal","menuStyle","EMPTY_ITEM_STYLE"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"ActionMenu.js","sources":["../../src/components/ActionMenu.tsx"],"sourcesContent":["import React, { useEffect, useMemo, useRef, useState } from 'react';\nimport { createPortal } from 'react-dom';\nimport Button from './Button';\nimport type { ButtonVariant } from './Button';\nimport MoreHorizontalIcon from './CustomIcons/MoreHorizontalIcon';\nimport {\n type MenuEdge,\n edgeForTriggerKey,\n enabledItems,\n focusEdge,\n itemForMenuKey,\n} from './actionMenuKeyboard';\nimport {\n type ActionMenuTone,\n EMPTY_ITEM_STYLE,\n ITEM_CLASS,\n ITEM_ICON_CLASS,\n ITEM_ICON_LAYOUT,\n ITEM_ICON_STYLE,\n LABEL_STYLE,\n TEXT_ITEM_STYLE,\n TRIGGER_SIZES,\n VISUALLY_HIDDEN_STYLE,\n itemStyle,\n menuStyle,\n} from './actionMenuStyles';\nimport type { ActionMenuItem, ActionMenuPlacement } from './actionMenuTypes';\nimport { getDeepActiveElement } from './activeElement';\nimport { chainHandler, composeRefs, elementRef } from './composeRefs';\nimport { pushEscapeLayer } from './escapeLayers';\nimport type { FloatingOffset } from './floatingMenuGeometry';\nimport { isPlainClick } from './linkClick';\nimport { resolveRel } from './linkRel';\nimport { usePopupLayer } from './overlayLayers';\nimport { safeHref } from './safeHref';\nimport { useActionMenuPosition } from './useActionMenuPosition';\n\nexport type { ActionMenuItem, ActionMenuPlacement, ActionMenuTone };\n\nexport interface ActionMenuProps {\n items: ActionMenuItem[];\n label?: string;\n triggerIcon?: React.ReactNode;\n minWidth?: number;\n size?: 'small' | 'xsmall';\n /**\n * Shows `label` beside the icon instead of leaving it to the accessible name. For a menu\n * that is the point of its corner rather than an overflow for a row that already reads —\n * a kebab nobody can name is only safe when the row it belongs to says what it acts on.\n */\n showLabel?: boolean;\n /** Trigger's Button variant. Defaults to `outline`, the overflow-kebab look. */\n variant?: ButtonVariant;\n trigger?: React.ReactElement;\n placement?: ActionMenuPlacement;\n offset?: FloatingOffset;\n /** `muted` reads items in the secondary text colour, as in a sidebar's menus. */\n tone?: ActionMenuTone;\n}\n\ntype TriggerHandlers = {\n onClick?: (event: React.MouseEvent) => void;\n onMouseDown?: (event: React.MouseEvent) => void;\n onKeyDown?: (event: React.KeyboardEvent) => void;\n};\n\n// Loaded only when an item has a tag: its shadow host pulls in the Tailwind sheet, which a menu\n// without tags should not pay for.\nconst ActionMenuTagHost = React.lazy(() => import('./ActionMenuTagHost'));\n\nconst hasTag = (tag: React.ReactNode) => tag !== undefined && tag !== null && tag !== false;\n\nconst stopPropagation = (event: React.SyntheticEvent) => event.stopPropagation();\n\nconst MenuItemRow: React.FC<{\n item: ActionMenuItem;\n tone: ActionMenuTone;\n onSelect: (item: ActionMenuItem) => void;\n onFollow: () => void;\n}> = ({ item, tone, onSelect, onFollow }) => {\n if (item.type === 'divider') {\n return <div style={itemStyle(item)} />;\n }\n if (item.type === 'text') {\n return (\n <div role=\"menuitem\" aria-disabled=\"true\" style={TEXT_ITEM_STYLE}>\n {item.label}\n </div>\n );\n }\n const shared = {\n role: 'menuitem',\n title: item.tooltip,\n 'aria-current': item.current ? ('page' as const) : undefined,\n className: ITEM_CLASS,\n 'data-danger': item.danger === 'hover' ? 'hover' : undefined,\n 'data-tone': tone,\n style: itemStyle(item, tone),\n };\n const href = item.disabled ? undefined : safeHref(item.href);\n const opensNewTab = href !== undefined && item.target?.toLowerCase() === '_blank';\n const content = (\n <>\n {item.icon && (\n <span\n className={ITEM_ICON_CLASS}\n style={item.danger === 'hover' ? ITEM_ICON_LAYOUT : ITEM_ICON_STYLE}\n >\n {item.icon}\n </span>\n )}\n <span style={LABEL_STYLE}>\n {item.label}\n {opensNewTab && <span style={VISUALLY_HIDDEN_STYLE}> (opens in a new tab)</span>}\n </span>\n {hasTag(item.tag) ? (\n <React.Suspense fallback={null}>\n <ActionMenuTagHost>{item.tag}</ActionMenuTagHost>\n </React.Suspense>\n ) : null}\n </>\n );\n if (href !== undefined) {\n return (\n <a\n {...shared}\n href={href}\n target={item.target}\n rel={resolveRel(undefined, item.target)}\n onClick={(event) => {\n if (isPlainClick(event) && item.onClick) {\n event.preventDefault();\n onSelect(item);\n return;\n }\n onFollow();\n }}\n onAuxClick={(event) => {\n if (event.button === 1) {\n onFollow();\n }\n }}\n >\n {content}\n </a>\n );\n }\n return (\n <button {...shared} type=\"button\" disabled={item.disabled} onClick={() => onSelect(item)}>\n {content}\n </button>\n );\n};\n\n/**\n * ActionMenu is an overflow action menu. The trigger is a three-dot `Button` by default, or\n * your own control via `trigger`. The menu is portaled to document.body, so it escapes any\n * Shadow DOM or overflow:hidden ancestor, which is also why it is styled inline.\n */\nconst ActionMenu: React.FC<ActionMenuProps> = ({\n items,\n label = 'More actions',\n triggerIcon,\n minWidth,\n size = 'small',\n showLabel = false,\n variant = 'outline',\n trigger: customTrigger,\n placement = 'bottom-end',\n offset,\n tone = 'default',\n}) => {\n const triggerSize = TRIGGER_SIZES[size];\n const [open, setOpen] = useState(false);\n const triggerRef = useRef<HTMLElement | null>(null);\n const menuRef = useRef<HTMLDivElement>(null);\n const focusOnOpen = useRef<MenuEdge | null>(null);\n const focusInMenu = useRef(false);\n const popupIdentity = usePopupLayer('actionmenu', open, menuRef);\n\n const position = useActionMenuPosition(open, triggerRef, menuRef, {\n minWidth,\n placement,\n offset,\n });\n\n useEffect(() => {\n if (!open) {\n return;\n }\n if (focusOnOpen.current) {\n focusEdge(menuRef.current, focusOnOpen.current);\n focusOnOpen.current = null;\n }\n const closeOnOutsidePress = (event: MouseEvent) => {\n const path = event.composedPath();\n const inside = [triggerRef.current, menuRef.current].some(\n (node) => node && path.includes(node),\n );\n if (!inside) {\n focusInMenu.current = false;\n setOpen(false);\n }\n };\n document.addEventListener('mousedown', closeOnOutsidePress, true);\n const popEscapeLayer = pushEscapeLayer(() => setOpen(false));\n return () => {\n document.removeEventListener('mousedown', closeOnOutsidePress, true);\n popEscapeLayer();\n const active = getDeepActiveElement();\n if (focusInMenu.current && (!active || active === document.body)) {\n triggerRef.current?.focus({ preventScroll: true });\n }\n focusInMenu.current = false;\n };\n }, [open]);\n\n const toggleFromTrigger = (event: React.MouseEvent) => {\n event.stopPropagation();\n const openedFromKeyboard = !open && event.detail === 0;\n focusOnOpen.current = openedFromKeyboard ? 'first' : null;\n setOpen((v) => !v);\n };\n\n const handleTriggerKeyDown = (event: React.KeyboardEvent) => {\n if (event.key === 'Tab' && open) {\n setOpen(false);\n return;\n }\n const edge = edgeForTriggerKey(event.key, placement);\n if (!edge) {\n return;\n }\n event.preventDefault();\n if (open) {\n focusEdge(menuRef.current, edge);\n return;\n }\n focusOnOpen.current = edge;\n setOpen(true);\n };\n\n const handleMenuKeyDown = (event: React.KeyboardEvent<HTMLDivElement>) => {\n if (event.key === 'Tab') {\n focusInMenu.current = false;\n triggerRef.current?.focus({ preventScroll: true });\n setOpen(false);\n return;\n }\n if (event.key === ' ' && event.target instanceof HTMLAnchorElement) {\n event.preventDefault();\n event.target.click();\n return;\n }\n const target = itemForMenuKey(enabledItems(menuRef.current), event.target, event.key);\n if (target) {\n event.preventDefault();\n target.focus();\n }\n };\n\n const selectItem = (item: ActionMenuItem) => {\n if (item.type === 'divider' || item.type === 'text' || item.disabled) {\n return;\n }\n item.onClick?.();\n setOpen(false);\n };\n\n const ownTriggerRef = customTrigger ? elementRef<HTMLElement>(customTrigger) : undefined;\n const composedTriggerRef = useMemo(\n () => composeRefs(ownTriggerRef, triggerRef),\n [ownTriggerRef],\n );\n const customHandlers = customTrigger?.props as TriggerHandlers | undefined;\n\n const triggerElement = customTrigger ? (\n React.cloneElement(customTrigger, {\n ref: composedTriggerRef,\n 'aria-haspopup': 'menu',\n 'aria-expanded': open,\n onClick: chainHandler(customHandlers?.onClick, toggleFromTrigger),\n onMouseDown: chainHandler(customHandlers?.onMouseDown, stopPropagation),\n onKeyDown: chainHandler(customHandlers?.onKeyDown, handleTriggerKeyDown),\n } as Record<string, unknown>)\n ) : (\n <Button\n ref={triggerRef as React.RefObject<HTMLButtonElement>}\n type=\"button\"\n variant={variant}\n size={size}\n iconOnly={!showLabel}\n aria-label={showLabel ? undefined : label}\n aria-haspopup=\"menu\"\n aria-expanded={open}\n className={`shrink-0${showLabel ? ' gap-2' : ''}`}\n onClick={toggleFromTrigger}\n onMouseDown={stopPropagation}\n onKeyDown={handleTriggerKeyDown}\n >\n <span className={`inline-flex items-center justify-center ${triggerSize.iconBox}`}>\n {triggerIcon ?? <MoreHorizontalIcon size={triggerSize.icon} />}\n </span>\n {showLabel && <span>{label}</span>}\n </Button>\n );\n\n return (\n <>\n {triggerElement}\n {open &&\n createPortal(\n <div\n ref={menuRef}\n role=\"menu\"\n tabIndex={-1}\n aria-label={label}\n {...popupIdentity}\n style={menuStyle(position)}\n onMouseDown={stopPropagation}\n onClick={stopPropagation}\n onKeyDown={handleMenuKeyDown}\n onFocus={() => {\n focusInMenu.current = true;\n }}\n onBlur={(e) => {\n if (!menuRef.current?.contains(e.relatedTarget as Node | null)) {\n focusInMenu.current = false;\n }\n }}\n >\n {items.length === 0 && (\n <div role=\"menuitem\" aria-disabled=\"true\" style={EMPTY_ITEM_STYLE}>\n No actions available\n </div>\n )}\n {items.map((item) => (\n <MenuItemRow\n key={item.key}\n item={item}\n tone={tone}\n onSelect={selectItem}\n onFollow={() => setOpen(false)}\n />\n ))}\n </div>,\n document.body,\n )}\n </>\n );\n};\n\nexport default ActionMenu;\n"],"names":["ActionMenuTagHost","React","hasTag","tag","stopPropagation","event","MenuItemRow","item","tone","onSelect","onFollow","jsx","itemStyle","TEXT_ITEM_STYLE","shared","ITEM_CLASS","href","safeHref","opensNewTab","_a","content","jsxs","Fragment","ITEM_ICON_CLASS","ITEM_ICON_LAYOUT","ITEM_ICON_STYLE","LABEL_STYLE","VISUALLY_HIDDEN_STYLE","resolveRel","isPlainClick","ActionMenu","items","label","triggerIcon","minWidth","size","showLabel","variant","customTrigger","placement","offset","triggerSize","TRIGGER_SIZES","open","setOpen","useState","triggerRef","useRef","menuRef","focusOnOpen","focusInMenu","popupIdentity","usePopupLayer","position","useActionMenuPosition","useEffect","focusEdge","closeOnOutsidePress","path","node","popEscapeLayer","pushEscapeLayer","active","getDeepActiveElement","toggleFromTrigger","openedFromKeyboard","v","handleTriggerKeyDown","edge","edgeForTriggerKey","handleMenuKeyDown","target","itemForMenuKey","enabledItems","selectItem","ownTriggerRef","elementRef","composedTriggerRef","useMemo","composeRefs","customHandlers","triggerElement","chainHandler","Button","MoreHorizontalIcon","createPortal","menuStyle","e","EMPTY_ITEM_STYLE"],"mappings":"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"}
|