@charcuterie/ui 2.11.0 → 2.12.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/Toolbar/Toolbar.d.ts +156 -0
- package/dist/Toolbar/Toolbar.d.ts.map +1 -0
- package/dist/Toolbar/Toolbar.js +169 -0
- package/dist/Toolbar/Toolbar.js.map +1 -0
- package/dist/Toolbar/ToolbarSlot.d.ts +44 -0
- package/dist/Toolbar/ToolbarSlot.d.ts.map +1 -0
- package/dist/Toolbar/ToolbarSlot.js +69 -0
- package/dist/Toolbar/ToolbarSlot.js.map +1 -0
- package/dist/Toolbar/chooseVisibleCount.d.ts +38 -0
- package/dist/Toolbar/chooseVisibleCount.d.ts.map +1 -0
- package/dist/Toolbar/chooseVisibleCount.js +69 -0
- package/dist/Toolbar/chooseVisibleCount.js.map +1 -0
- package/dist/Toolbar/useToolbarOverflow.d.ts +49 -0
- package/dist/Toolbar/useToolbarOverflow.d.ts.map +1 -0
- package/dist/Toolbar/useToolbarOverflow.js +157 -0
- package/dist/Toolbar/useToolbarOverflow.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/testing/expectAgentDrivable.d.ts.map +1 -1
- package/dist/testing/expectAgentDrivable.js +32 -7
- package/dist/testing/expectAgentDrivable.js.map +1 -1
- package/package.json +2 -2
- package/src/Toolbar/Toolbar.mdx +126 -0
- package/src/Toolbar/Toolbar.stories.tsx +347 -0
- package/src/Toolbar/Toolbar.test.tsx +286 -0
- package/src/Toolbar/Toolbar.tsx +461 -0
- package/src/Toolbar/ToolbarSlot.tsx +122 -0
- package/src/Toolbar/chooseVisibleCount.test.ts +92 -0
- package/src/Toolbar/chooseVisibleCount.ts +92 -0
- package/src/Toolbar/useToolbarOverflow.ts +229 -0
- package/src/icons.storyHelpers.tsx +23 -0
- package/src/index.ts +8 -0
- package/src/sourceRules.test.ts +18 -1
- package/src/testing/expectAgentDrivable.ts +50 -20
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import type { ControlSize } from "@charcuterie/tokens";
|
|
2
|
+
import type { ReactElement, ReactNode } from "react";
|
|
3
|
+
/**
|
|
4
|
+
* Something the toolbar renders itself: a button that **does**
|
|
5
|
+
* something. The same shape as `MenuItem`, because a collapsed one
|
|
6
|
+
* becomes exactly that.
|
|
7
|
+
*/
|
|
8
|
+
export type ToolbarAction = {
|
|
9
|
+
icon?: ReactNode;
|
|
10
|
+
isDisabled?: boolean;
|
|
11
|
+
key: string;
|
|
12
|
+
/** The accessible name, and the label when it collapses. */
|
|
13
|
+
label: string;
|
|
14
|
+
onSelect: () => void;
|
|
15
|
+
/**
|
|
16
|
+
* The default kind, and optional so the common
|
|
17
|
+
* `{ key, label, onSelect }` still type-checks. Present only to
|
|
18
|
+
* discriminate this from a `control`.
|
|
19
|
+
*/
|
|
20
|
+
type?: "action";
|
|
21
|
+
};
|
|
22
|
+
/**
|
|
23
|
+
* Something the **app** renders: a `Switch`, a `SegmentedControl`,
|
|
24
|
+
* a `ColorSchemeSwitcher` — a control with state, not an action.
|
|
25
|
+
*
|
|
26
|
+
* Exactly one instance of `element` is mounted at any width. It
|
|
27
|
+
* moves between the bar and the overflow panel; it is never
|
|
28
|
+
* rendered in both and hidden in one.
|
|
29
|
+
*/
|
|
30
|
+
export type ToolbarControl = {
|
|
31
|
+
element: ReactElement;
|
|
32
|
+
key: string;
|
|
33
|
+
/**
|
|
34
|
+
* Optional row label, used **only** in the overflow panel and
|
|
35
|
+
* only when the control does not already show its own text. A
|
|
36
|
+
* `Switch` renders its label; an icon-only toggle does not.
|
|
37
|
+
*/
|
|
38
|
+
label?: string;
|
|
39
|
+
type: "control";
|
|
40
|
+
};
|
|
41
|
+
export type ToolbarItem = ToolbarAction | ToolbarControl;
|
|
42
|
+
type ToolbarSharedProps = {
|
|
43
|
+
className?: string;
|
|
44
|
+
/**
|
|
45
|
+
* **Initial** only, and the toolbar owns it from then on — the
|
|
46
|
+
* same uncontrolled contract as every other component here. It
|
|
47
|
+
* exists so a story (and a screenshot) can show the panel open;
|
|
48
|
+
* an app almost never passes it.
|
|
49
|
+
*/
|
|
50
|
+
isOverflowVisible?: boolean;
|
|
51
|
+
/**
|
|
52
|
+
* Required, and it becomes `aria-label`. A `role="toolbar"` takes
|
|
53
|
+
* **no accessible name from its content** — the same trap
|
|
54
|
+
* `Popover`'s `heading` and `Spinner`'s label exist for. mux-magic
|
|
55
|
+
* gets this one right and then gives the toolbar nothing else.
|
|
56
|
+
*/
|
|
57
|
+
label: string;
|
|
58
|
+
/**
|
|
59
|
+
* The overflow trigger's glyph. **No default** — the library
|
|
60
|
+
* ships no icons, and `⋮` renders as nothing where the font lacks
|
|
61
|
+
* it. With no icon the trigger is a text button reading
|
|
62
|
+
* `overflowLabel`, which works everywhere.
|
|
63
|
+
*/
|
|
64
|
+
overflowIcon?: ReactNode;
|
|
65
|
+
/**
|
|
66
|
+
* Names the overflow trigger, and through it the panel it opens —
|
|
67
|
+
* a menu is named by its trigger, and `Popover`'s `aria-label`
|
|
68
|
+
* takes the same string.
|
|
69
|
+
*/
|
|
70
|
+
overflowLabel?: string;
|
|
71
|
+
size?: ControlSize;
|
|
72
|
+
};
|
|
73
|
+
/**
|
|
74
|
+
* **The overflow's role is a type, not a flag.**
|
|
75
|
+
*
|
|
76
|
+
* `role="menu"` permits only `menuitem`, `menuitemradio`,
|
|
77
|
+
* `menuitemcheckbox`, `group` and `separator` — so a header's
|
|
78
|
+
* scheme switcher inside one is invalid, which is what
|
|
79
|
+
* plex-channels ships today. The fix is not a runtime warning and
|
|
80
|
+
* not a boolean: with `overflow="menu"` the item type narrows to
|
|
81
|
+
* `ToolbarAction`, and a `control` in the array is a **compile
|
|
82
|
+
* error**. Mixed content declares `overflow="panel"` and gets a
|
|
83
|
+
* `Popover` — `role="dialog"`, `aria-haspopup="dialog"` — where a
|
|
84
|
+
* switch is perfectly legal content.
|
|
85
|
+
*
|
|
86
|
+
* Same instinct as
|
|
87
|
+
* [`Menu`'s items being a discriminated union](../../../../docs/decisions/2026-08-05-menu-items-is-a-discriminated-union.md):
|
|
88
|
+
* the kinds are told apart by the type system, in the data.
|
|
89
|
+
*/
|
|
90
|
+
export type ToolbarProps = ToolbarSharedProps & ({
|
|
91
|
+
items: readonly ToolbarAction[];
|
|
92
|
+
overflow: "menu";
|
|
93
|
+
} | {
|
|
94
|
+
items: readonly ToolbarItem[];
|
|
95
|
+
overflow: "panel";
|
|
96
|
+
});
|
|
97
|
+
/**
|
|
98
|
+
* A row of controls that collapses into an overflow when it runs
|
|
99
|
+
* out of room — the fleet's fourth-largest duplication, and the one
|
|
100
|
+
* where the copies are worst.
|
|
101
|
+
*
|
|
102
|
+
* Four repos have a toolbar-with-overflow; mux-magic's `PageHeader`
|
|
103
|
+
* is the original and plex-channels' `Header` says in its own
|
|
104
|
+
* comments that it copied it. What they copied:
|
|
105
|
+
*
|
|
106
|
+
* - **`role="toolbar"` with no toolbar keyboard behaviour.** Six
|
|
107
|
+
* sequential tab stops where the APG requires one, with the
|
|
108
|
+
* arrow keys moving between members. Here that is `RovingFocus`,
|
|
109
|
+
* the same kind `Menu`, `Tabs` and `SegmentedControl` use, and
|
|
110
|
+
* the tab stop is read from `selectTabIndex` rather than
|
|
111
|
+
* restated.
|
|
112
|
+
* - **Triggers that advertise nothing.** No `aria-expanded`, no
|
|
113
|
+
* `aria-haspopup`, no `aria-controls` anywhere in the file. Here
|
|
114
|
+
* they come from `useRole` inside `Menu`/`Popover`, which cannot
|
|
115
|
+
* be forgotten because nothing writes them by hand.
|
|
116
|
+
* - **No focus management.** `Menu` moves focus to the first item
|
|
117
|
+
* on opening and `FloatingFocusManager` returns it to the
|
|
118
|
+
* trigger on close.
|
|
119
|
+
* - **Collapse by DOM duplication.** Both repos render every
|
|
120
|
+
* action twice and hide one set with a media query — mux-magic's
|
|
121
|
+
* is a documented CSS specificity coin-flip ("source order
|
|
122
|
+
* wins"). Every action here is mounted **exactly once** and
|
|
123
|
+
* moves.
|
|
124
|
+
* - **Nothing decides what collapses.** Two hardcoded media
|
|
125
|
+
* queries at 480/481px, and the ⋮ shows at every width, so it is
|
|
126
|
+
* not progressive collapse at all. Here the row is measured, the
|
|
127
|
+
* items are in priority order, and the trigger exists only when
|
|
128
|
+
* something actually overflowed.
|
|
129
|
+
*
|
|
130
|
+
* The toggles' missing `aria-pressed` is the one defect this
|
|
131
|
+
* component does not fix, because it cannot: a `control`'s state is
|
|
132
|
+
* the control's own, and `Switch` has carried `role="switch"` since
|
|
133
|
+
* M6a. The bar's job is to hold it once.
|
|
134
|
+
*
|
|
135
|
+
* ## Priority is the array order
|
|
136
|
+
*
|
|
137
|
+
* `items[0]` is the last thing to collapse. One axis, and it is
|
|
138
|
+
* also paint order — see `chooseVisibleCount`.
|
|
139
|
+
*
|
|
140
|
+
* ## Measured, not breakpointed
|
|
141
|
+
*
|
|
142
|
+
* There is no `collapseAt` prop and no media query. The bar reads
|
|
143
|
+
* the widths its items actually have, so a longer label or a
|
|
144
|
+
* `data-density="kiosk"` collapses at the width it should rather
|
|
145
|
+
* than at a number someone typed in 2024. `useMediaQuery` is still
|
|
146
|
+
* the right tool one level up — for **relocating** a whole toolbar
|
|
147
|
+
* out of a header on a phone, which is a layout decision and not a
|
|
148
|
+
* fit — and `Toolbar.stories.tsx` shows that composition.
|
|
149
|
+
*
|
|
150
|
+
* ⚠️ The root is `flex-1 min-w-0`: it must be sized by its
|
|
151
|
+
* container, never by its own contents, or collapsing an item
|
|
152
|
+
* narrows the box that decides whether to collapse.
|
|
153
|
+
*/
|
|
154
|
+
export declare const Toolbar: ({ className, isOverflowVisible, items, label, overflow, overflowIcon, overflowLabel, size, }: ToolbarProps) => ReactNode;
|
|
155
|
+
export {};
|
|
156
|
+
//# sourceMappingURL=Toolbar.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Toolbar.d.ts","sourceRoot":"","sources":["../../src/Toolbar/Toolbar.tsx"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAA;AACtD,OAAO,KAAK,EAAE,YAAY,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AAWpD;;;;GAIG;AACH,MAAM,MAAM,aAAa,GAAG;IAC1B,IAAI,CAAC,EAAE,SAAS,CAAA;IAChB,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB,GAAG,EAAE,MAAM,CAAA;IACX,4DAA4D;IAC5D,KAAK,EAAE,MAAM,CAAA;IACb,QAAQ,EAAE,MAAM,IAAI,CAAA;IACpB;;;;OAIG;IACH,IAAI,CAAC,EAAE,QAAQ,CAAA;CAChB,CAAA;AAED;;;;;;;GAOG;AACH,MAAM,MAAM,cAAc,GAAG;IAC3B,OAAO,EAAE,YAAY,CAAA;IACrB,GAAG,EAAE,MAAM,CAAA;IACX;;;;OAIG;IACH,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,IAAI,EAAE,SAAS,CAAA;CAChB,CAAA;AAED,MAAM,MAAM,WAAW,GAAG,aAAa,GAAG,cAAc,CAAA;AAExD,KAAK,kBAAkB,GAAG;IACxB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB;;;;;OAKG;IACH,iBAAiB,CAAC,EAAE,OAAO,CAAA;IAC3B;;;;;OAKG;IACH,KAAK,EAAE,MAAM,CAAA;IACb;;;;;OAKG;IACH,YAAY,CAAC,EAAE,SAAS,CAAA;IACxB;;;;OAIG;IACH,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,IAAI,CAAC,EAAE,WAAW,CAAA;CACnB,CAAA;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,MAAM,YAAY,GAAG,kBAAkB,GAC3C,CACI;IACE,KAAK,EAAE,SAAS,aAAa,EAAE,CAAA;IAC/B,QAAQ,EAAE,MAAM,CAAA;CACjB,GACD;IACE,KAAK,EAAE,SAAS,WAAW,EAAE,CAAA;IAC7B,QAAQ,EAAE,OAAO,CAAA;CAClB,CACJ,CAAA;AAaH;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwDG;AACH,eAAO,MAAM,OAAO,GAAI,8FASrB,YAAY,KAAG,SAuQjB,CAAA"}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { selectTabIndex, useRovingFocus, useVisibility, } from "@charcuterie/logic";
|
|
3
|
+
import { useCallback, useEffect, useRef } from "react";
|
|
4
|
+
import { Button } from "../Button/Button.js";
|
|
5
|
+
import { IconButton } from "../IconButton/IconButton.js";
|
|
6
|
+
import { Menu } from "../Menu/Menu.js";
|
|
7
|
+
import { Popover } from "../Popover/Popover.js";
|
|
8
|
+
import { toClassName } from "../toClassName.js";
|
|
9
|
+
import { ToolbarSlot } from "./ToolbarSlot.js";
|
|
10
|
+
import { useToolbarOverflow } from "./useToolbarOverflow.js";
|
|
11
|
+
/**
|
|
12
|
+
* The roving group's last member. Not a key any item can collide
|
|
13
|
+
* with, because a caller's keys are its own strings and this one
|
|
14
|
+
* is namespaced.
|
|
15
|
+
*/
|
|
16
|
+
const OVERFLOW_KEY = "charcuterie-toolbar-overflow";
|
|
17
|
+
const isToolbarAction = (item) => item.type !== "control";
|
|
18
|
+
/**
|
|
19
|
+
* A row of controls that collapses into an overflow when it runs
|
|
20
|
+
* out of room — the fleet's fourth-largest duplication, and the one
|
|
21
|
+
* where the copies are worst.
|
|
22
|
+
*
|
|
23
|
+
* Four repos have a toolbar-with-overflow; mux-magic's `PageHeader`
|
|
24
|
+
* is the original and plex-channels' `Header` says in its own
|
|
25
|
+
* comments that it copied it. What they copied:
|
|
26
|
+
*
|
|
27
|
+
* - **`role="toolbar"` with no toolbar keyboard behaviour.** Six
|
|
28
|
+
* sequential tab stops where the APG requires one, with the
|
|
29
|
+
* arrow keys moving between members. Here that is `RovingFocus`,
|
|
30
|
+
* the same kind `Menu`, `Tabs` and `SegmentedControl` use, and
|
|
31
|
+
* the tab stop is read from `selectTabIndex` rather than
|
|
32
|
+
* restated.
|
|
33
|
+
* - **Triggers that advertise nothing.** No `aria-expanded`, no
|
|
34
|
+
* `aria-haspopup`, no `aria-controls` anywhere in the file. Here
|
|
35
|
+
* they come from `useRole` inside `Menu`/`Popover`, which cannot
|
|
36
|
+
* be forgotten because nothing writes them by hand.
|
|
37
|
+
* - **No focus management.** `Menu` moves focus to the first item
|
|
38
|
+
* on opening and `FloatingFocusManager` returns it to the
|
|
39
|
+
* trigger on close.
|
|
40
|
+
* - **Collapse by DOM duplication.** Both repos render every
|
|
41
|
+
* action twice and hide one set with a media query — mux-magic's
|
|
42
|
+
* is a documented CSS specificity coin-flip ("source order
|
|
43
|
+
* wins"). Every action here is mounted **exactly once** and
|
|
44
|
+
* moves.
|
|
45
|
+
* - **Nothing decides what collapses.** Two hardcoded media
|
|
46
|
+
* queries at 480/481px, and the ⋮ shows at every width, so it is
|
|
47
|
+
* not progressive collapse at all. Here the row is measured, the
|
|
48
|
+
* items are in priority order, and the trigger exists only when
|
|
49
|
+
* something actually overflowed.
|
|
50
|
+
*
|
|
51
|
+
* The toggles' missing `aria-pressed` is the one defect this
|
|
52
|
+
* component does not fix, because it cannot: a `control`'s state is
|
|
53
|
+
* the control's own, and `Switch` has carried `role="switch"` since
|
|
54
|
+
* M6a. The bar's job is to hold it once.
|
|
55
|
+
*
|
|
56
|
+
* ## Priority is the array order
|
|
57
|
+
*
|
|
58
|
+
* `items[0]` is the last thing to collapse. One axis, and it is
|
|
59
|
+
* also paint order — see `chooseVisibleCount`.
|
|
60
|
+
*
|
|
61
|
+
* ## Measured, not breakpointed
|
|
62
|
+
*
|
|
63
|
+
* There is no `collapseAt` prop and no media query. The bar reads
|
|
64
|
+
* the widths its items actually have, so a longer label or a
|
|
65
|
+
* `data-density="kiosk"` collapses at the width it should rather
|
|
66
|
+
* than at a number someone typed in 2024. `useMediaQuery` is still
|
|
67
|
+
* the right tool one level up — for **relocating** a whole toolbar
|
|
68
|
+
* out of a header on a phone, which is a layout decision and not a
|
|
69
|
+
* fit — and `Toolbar.stories.tsx` shows that composition.
|
|
70
|
+
*
|
|
71
|
+
* ⚠️ The root is `flex-1 min-w-0`: it must be sized by its
|
|
72
|
+
* container, never by its own contents, or collapsing an item
|
|
73
|
+
* narrows the box that decides whether to collapse.
|
|
74
|
+
*/
|
|
75
|
+
export const Toolbar = ({ className, isOverflowVisible = false, items, label, overflow, overflowIcon, overflowLabel = "More actions", size = "sm", }) => {
|
|
76
|
+
const focusableElements = useRef(new Map());
|
|
77
|
+
const focus = useRovingFocus();
|
|
78
|
+
const { activeValue, register, setActiveValue } = focus;
|
|
79
|
+
const itemKeys = items.map((item) => item.key);
|
|
80
|
+
const { containerRef, trackItem, trackTrigger, visibleCount, } = useToolbarOverflow({ itemKeys });
|
|
81
|
+
const visibleItems = items.slice(0, visibleCount);
|
|
82
|
+
const collapsedItems = items.slice(visibleCount);
|
|
83
|
+
const hasOverflow = collapsedItems.length > 0;
|
|
84
|
+
const { hide, isVisible, toggle } = useVisibility({
|
|
85
|
+
isVisible: isOverflowVisible,
|
|
86
|
+
});
|
|
87
|
+
const trackFocusable = useCallback((key, element) => {
|
|
88
|
+
if (element) {
|
|
89
|
+
focusableElements.current.set(key, element);
|
|
90
|
+
}
|
|
91
|
+
else {
|
|
92
|
+
focusableElements.current.delete(key);
|
|
93
|
+
}
|
|
94
|
+
}, []);
|
|
95
|
+
const trackOverflowElement = useCallback((_key, element) => {
|
|
96
|
+
trackTrigger(element);
|
|
97
|
+
}, [trackTrigger]);
|
|
98
|
+
/**
|
|
99
|
+
* Move the caret only when the toolbar already has it.
|
|
100
|
+
*
|
|
101
|
+
* Same guard as `SegmentedControl`: the active member changes
|
|
102
|
+
* whenever the row re-measures, and a bar that stole focus every
|
|
103
|
+
* time the window resized would be worse than one with no
|
|
104
|
+
* keyboard model at all.
|
|
105
|
+
*/
|
|
106
|
+
useEffect(() => {
|
|
107
|
+
const container = containerRef.current;
|
|
108
|
+
if (!container ||
|
|
109
|
+
activeValue === null ||
|
|
110
|
+
!container.contains(document.activeElement)) {
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
focusableElements.current.get(activeValue)?.focus();
|
|
114
|
+
}, [activeValue, containerRef]);
|
|
115
|
+
const renderAction = (action) => action.icon === undefined ? (_jsx(Button, { appearance: "ghost", intent: "neutral", isDisabled: action.isDisabled, onClick: action.onSelect, size: size, children: action.label })) : (_jsx(IconButton, { appearance: "ghost", intent: "neutral", isDisabled: action.isDisabled, label: action.label, onClick: action.onSelect, size: size, children: action.icon }));
|
|
116
|
+
const trigger = overflowIcon === undefined ? (_jsx(Button, { appearance: "ghost", intent: "neutral", onClick: toggle, size: size, children: overflowLabel })) : (_jsx(IconButton, { appearance: "ghost", intent: "neutral", label: overflowLabel, onClick: toggle, size: size, children: overflowIcon }));
|
|
117
|
+
const overflowElement = overflow === "menu" ? (_jsx(Menu, { isVisible: isVisible, items: collapsedItems
|
|
118
|
+
.filter(isToolbarAction)
|
|
119
|
+
.map((action) => ({
|
|
120
|
+
icon: action.icon,
|
|
121
|
+
isDisabled: action.isDisabled,
|
|
122
|
+
key: action.key,
|
|
123
|
+
label: action.label,
|
|
124
|
+
onSelect: action.onSelect,
|
|
125
|
+
})), onDismiss: hide, placement: "bottom-end", trigger: trigger })) : (_jsx(Popover, { heading: overflowLabel, isVisible: isVisible, onDismiss: hide, placement: "bottom-end", trigger: trigger, children: _jsx("div", { className: "flex min-w-48 flex-col gap-1", children: collapsedItems.map((item) => isToolbarAction(item) ? (_jsx(Button, { appearance: "ghost", className: "justify-start", iconStart: item.icon, intent: "neutral", isDisabled: item.isDisabled, isFullWidth: true, onClick: () => {
|
|
126
|
+
item.onSelect();
|
|
127
|
+
// Choosing dismisses, exactly as it does in a
|
|
128
|
+
// `Menu`. A panel that stays open after an action
|
|
129
|
+
// has fired is the one users read as "it didn't
|
|
130
|
+
// work".
|
|
131
|
+
hide();
|
|
132
|
+
}, size: size, children: item.label }, item.key)) : (_jsxs("div", { className: "flex items-center justify-between gap-3", children: [item.label === undefined ? null : (_jsx("span", { className: "text-content-secondary text-sm", children: item.label })), item.element] }, item.key))) }) }));
|
|
133
|
+
return (_jsxs("div", { "aria-label": label, className: toClassName("flex min-w-0 flex-1 items-center gap-1", className),
|
|
134
|
+
/**
|
|
135
|
+
* Focus arriving from **outside** the keyboard model — a
|
|
136
|
+
* click, or Tab landing on the one member in the tab order —
|
|
137
|
+
* becomes the active member, so the next arrow key continues
|
|
138
|
+
* from where the user actually is rather than from wherever
|
|
139
|
+
* the group was last left. React's `onFocus` is `focusin`, so
|
|
140
|
+
* this hears a descendant.
|
|
141
|
+
*/
|
|
142
|
+
onFocus: (focusEvent) => {
|
|
143
|
+
for (const [key, element,] of focusableElements.current) {
|
|
144
|
+
if (element.contains(focusEvent.target)) {
|
|
145
|
+
setActiveValue(key);
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}, onKeyDown: (keyEvent) => {
|
|
150
|
+
const commands = {
|
|
151
|
+
ArrowLeft: focus.previous,
|
|
152
|
+
ArrowRight: focus.next,
|
|
153
|
+
End: focus.last,
|
|
154
|
+
Home: focus.first,
|
|
155
|
+
};
|
|
156
|
+
const command = commands[keyEvent.key];
|
|
157
|
+
if (command) {
|
|
158
|
+
// Arrow keys scroll the page by default, and a toolbar
|
|
159
|
+
// that scrolls the document while moving focus is the
|
|
160
|
+
// hand-rolled version of this component.
|
|
161
|
+
keyEvent.preventDefault();
|
|
162
|
+
command();
|
|
163
|
+
}
|
|
164
|
+
}, ref: containerRef, role: "toolbar", children: [visibleItems.map((item) => (_jsx(ToolbarSlot, { isDisabled: isToolbarAction(item) &&
|
|
165
|
+
item.isDisabled === true, itemKey: item.key, register: register, tabIndex: selectTabIndex(focus, item.key), trackElement: trackItem, trackFocusable: trackFocusable, children: isToolbarAction(item)
|
|
166
|
+
? renderAction(item)
|
|
167
|
+
: item.element }, item.key))), hasOverflow ? (_jsx(ToolbarSlot, { itemKey: OVERFLOW_KEY, register: register, tabIndex: selectTabIndex(focus, OVERFLOW_KEY), trackElement: trackOverflowElement, trackFocusable: trackFocusable, children: overflowElement })) : null] }));
|
|
168
|
+
};
|
|
169
|
+
//# sourceMappingURL=Toolbar.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Toolbar.js","sourceRoot":"","sources":["../../src/Toolbar/Toolbar.tsx"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
export type ToolbarSlotProps = {
|
|
3
|
+
children: ReactNode;
|
|
4
|
+
/** A disabled member never registers, so the arrows skip it. */
|
|
5
|
+
isDisabled?: boolean;
|
|
6
|
+
itemKey: string;
|
|
7
|
+
/** `RovingFocus.register` — membership of the arrow-key group. */
|
|
8
|
+
register: (value: string) => () => void;
|
|
9
|
+
tabIndex: number;
|
|
10
|
+
/** Hands the slot's box to the measuring hook. */
|
|
11
|
+
trackElement: (key: string, element: HTMLElement | null) => void;
|
|
12
|
+
/** Hands the slot's focusable to the toolbar, for `.focus()`. */
|
|
13
|
+
trackFocusable: (key: string, element: HTMLElement | null) => void;
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* One member of the toolbar's roving group: an action the toolbar
|
|
17
|
+
* rendered, or a control the caller handed in.
|
|
18
|
+
*
|
|
19
|
+
* Its own file because **registration is an effect**, and an effect
|
|
20
|
+
* cannot run in a loop — the same reason `MenuAction`, `TabTrigger`
|
|
21
|
+
* and `AccordionSection` are separate files.
|
|
22
|
+
*
|
|
23
|
+
* ### Why the tab stop is written imperatively
|
|
24
|
+
*
|
|
25
|
+
* Every other roving group in this library passes `tabIndex` as a
|
|
26
|
+
* prop, because it renders its own members. A toolbar does not: a
|
|
27
|
+
* `control` item is whatever the app already has — a `Switch`, a
|
|
28
|
+
* `ColorSchemeSwitcher`, a `SegmentedControl` — and most components
|
|
29
|
+
* in this library do not forward a `tabIndex` prop at all. Cloning
|
|
30
|
+
* one onto them would compile, do nothing, and leave the bar with
|
|
31
|
+
* one tab stop per control, which is precisely the APG violation
|
|
32
|
+
* this component exists to fix (mux-magic's `role="toolbar"` is six
|
|
33
|
+
* sequential tab stops).
|
|
34
|
+
*
|
|
35
|
+
* So the slot finds the one focusable in its subtree and writes
|
|
36
|
+
* `tabIndex` onto it. React never sets that attribute on these
|
|
37
|
+
* elements, so there is no second writer to fight with.
|
|
38
|
+
*
|
|
39
|
+
* A control holding **more than one** focusable is a nested
|
|
40
|
+
* composite the toolbar does not manage; only the first is given
|
|
41
|
+
* the tab stop, and `Toolbar.mdx` says so.
|
|
42
|
+
*/
|
|
43
|
+
export declare const ToolbarSlot: ({ children, isDisabled, itemKey, register, tabIndex, trackElement, trackFocusable, }: ToolbarSlotProps) => ReactNode;
|
|
44
|
+
//# sourceMappingURL=ToolbarSlot.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ToolbarSlot.d.ts","sourceRoot":"","sources":["../../src/Toolbar/ToolbarSlot.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AAetC,MAAM,MAAM,gBAAgB,GAAG;IAC7B,QAAQ,EAAE,SAAS,CAAA;IACnB,gEAAgE;IAChE,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB,OAAO,EAAE,MAAM,CAAA;IACf,kEAAkE;IAClE,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,MAAM,IAAI,CAAA;IACvC,QAAQ,EAAE,MAAM,CAAA;IAChB,kDAAkD;IAClD,YAAY,EAAE,CACZ,GAAG,EAAE,MAAM,EACX,OAAO,EAAE,WAAW,GAAG,IAAI,KACxB,IAAI,CAAA;IACT,iEAAiE;IACjE,cAAc,EAAE,CACd,GAAG,EAAE,MAAM,EACX,OAAO,EAAE,WAAW,GAAG,IAAI,KACxB,IAAI,CAAA;CACV,CAAA;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,eAAO,MAAM,WAAW,GAAI,sFAQzB,gBAAgB,KAAG,SAkDrB,CAAA"}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useRef } from "react";
|
|
3
|
+
/**
|
|
4
|
+
* Every focusable a toolbar knows how to hand a tab stop to.
|
|
5
|
+
*
|
|
6
|
+
* `[tabindex]` carries **no** `:not([tabindex="-1"])` clause, unlike
|
|
7
|
+
* the usual copy of this selector: the whole job here is to move a
|
|
8
|
+
* tab stop between elements, so an element this slot has already
|
|
9
|
+
* pushed out of the tab order still has to be findable to be let
|
|
10
|
+
* back in.
|
|
11
|
+
*/
|
|
12
|
+
const FOCUSABLE_SELECTOR = "a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]";
|
|
13
|
+
/**
|
|
14
|
+
* One member of the toolbar's roving group: an action the toolbar
|
|
15
|
+
* rendered, or a control the caller handed in.
|
|
16
|
+
*
|
|
17
|
+
* Its own file because **registration is an effect**, and an effect
|
|
18
|
+
* cannot run in a loop — the same reason `MenuAction`, `TabTrigger`
|
|
19
|
+
* and `AccordionSection` are separate files.
|
|
20
|
+
*
|
|
21
|
+
* ### Why the tab stop is written imperatively
|
|
22
|
+
*
|
|
23
|
+
* Every other roving group in this library passes `tabIndex` as a
|
|
24
|
+
* prop, because it renders its own members. A toolbar does not: a
|
|
25
|
+
* `control` item is whatever the app already has — a `Switch`, a
|
|
26
|
+
* `ColorSchemeSwitcher`, a `SegmentedControl` — and most components
|
|
27
|
+
* in this library do not forward a `tabIndex` prop at all. Cloning
|
|
28
|
+
* one onto them would compile, do nothing, and leave the bar with
|
|
29
|
+
* one tab stop per control, which is precisely the APG violation
|
|
30
|
+
* this component exists to fix (mux-magic's `role="toolbar"` is six
|
|
31
|
+
* sequential tab stops).
|
|
32
|
+
*
|
|
33
|
+
* So the slot finds the one focusable in its subtree and writes
|
|
34
|
+
* `tabIndex` onto it. React never sets that attribute on these
|
|
35
|
+
* elements, so there is no second writer to fight with.
|
|
36
|
+
*
|
|
37
|
+
* A control holding **more than one** focusable is a nested
|
|
38
|
+
* composite the toolbar does not manage; only the first is given
|
|
39
|
+
* the tab stop, and `Toolbar.mdx` says so.
|
|
40
|
+
*/
|
|
41
|
+
export const ToolbarSlot = ({ children, isDisabled = false, itemKey, register, tabIndex, trackElement, trackFocusable, }) => {
|
|
42
|
+
const slotRef = useRef(null);
|
|
43
|
+
useEffect(() => {
|
|
44
|
+
if (isDisabled) {
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
return register(itemKey);
|
|
48
|
+
}, [isDisabled, itemKey, register]);
|
|
49
|
+
useEffect(() => {
|
|
50
|
+
// The first focusable that is not an `aria-hidden` prop —
|
|
51
|
+
// floating-ui plants focus guards (`<span tabindex="0">`) either
|
|
52
|
+
// side of an overlay's reference element, and the overflow
|
|
53
|
+
// trigger *is* one, so a bare `querySelector` can hand back a
|
|
54
|
+
// guard and give the tab stop to something no user can see.
|
|
55
|
+
const focusable = Array.from(slotRef.current?.querySelectorAll(FOCUSABLE_SELECTOR) ?? []).find((candidate) => candidate.getAttribute("aria-hidden") !== "true") ?? null;
|
|
56
|
+
trackFocusable(itemKey, focusable);
|
|
57
|
+
if (focusable && !isDisabled) {
|
|
58
|
+
focusable.tabIndex = tabIndex;
|
|
59
|
+
}
|
|
60
|
+
return () => {
|
|
61
|
+
trackFocusable(itemKey, null);
|
|
62
|
+
};
|
|
63
|
+
}, [isDisabled, itemKey, tabIndex, trackFocusable]);
|
|
64
|
+
return (_jsx("div", { className: "flex shrink-0 items-center", ref: (element) => {
|
|
65
|
+
slotRef.current = element;
|
|
66
|
+
trackElement(itemKey, element);
|
|
67
|
+
}, children: children }));
|
|
68
|
+
};
|
|
69
|
+
//# sourceMappingURL=ToolbarSlot.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ToolbarSlot.js","sourceRoot":"","sources":["../../src/Toolbar/ToolbarSlot.tsx"],"names":[],"mappings":";AACA,OAAO,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,OAAO,CAAA;AAEzC;;;;;;;;GAQG;AACH,MAAM,kBAAkB,GACtB,sHAAsH,CAAA;AAsBxH;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,MAAM,CAAC,MAAM,WAAW,GAAG,CAAC,EAC1B,QAAQ,EACR,UAAU,GAAG,KAAK,EAClB,OAAO,EACP,QAAQ,EACR,QAAQ,EACR,YAAY,EACZ,cAAc,GACG,EAAa,EAAE;IAChC,MAAM,OAAO,GAAG,MAAM,CAAiB,IAAI,CAAC,CAAA;IAE5C,SAAS,CAAC,GAAG,EAAE;QACb,IAAI,UAAU,EAAE,CAAC;YACf,OAAM;QACR,CAAC;QAED,OAAO,QAAQ,CAAC,OAAO,CAAC,CAAA;IAC1B,CAAC,EAAE,CAAC,UAAU,EAAE,OAAO,EAAE,QAAQ,CAAC,CAAC,CAAA;IAEnC,SAAS,CAAC,GAAG,EAAE;QACb,0DAA0D;QAC1D,iEAAiE;QACjE,2DAA2D;QAC3D,8DAA8D;QAC9D,4DAA4D;QAC5D,MAAM,SAAS,GACb,KAAK,CAAC,IAAI,CACR,OAAO,CAAC,OAAO,EAAE,gBAAgB,CAC/B,kBAAkB,CACnB,IAAI,EAAE,CACR,CAAC,IAAI,CACJ,CAAC,SAAS,EAAE,EAAE,CACZ,SAAS,CAAC,YAAY,CAAC,aAAa,CAAC,KAAK,MAAM,CACnD,IAAI,IAAI,CAAA;QAEX,cAAc,CAAC,OAAO,EAAE,SAAS,CAAC,CAAA;QAElC,IAAI,SAAS,IAAI,CAAC,UAAU,EAAE,CAAC;YAC7B,SAAS,CAAC,QAAQ,GAAG,QAAQ,CAAA;QAC/B,CAAC;QAED,OAAO,GAAG,EAAE;YACV,cAAc,CAAC,OAAO,EAAE,IAAI,CAAC,CAAA;QAC/B,CAAC,CAAA;IACH,CAAC,EAAE,CAAC,UAAU,EAAE,OAAO,EAAE,QAAQ,EAAE,cAAc,CAAC,CAAC,CAAA;IAEnD,OAAO,CACL,cACE,SAAS,EAAC,4BAA4B,EACtC,GAAG,EAAE,CAAC,OAAO,EAAE,EAAE;YACf,OAAO,CAAC,OAAO,GAAG,OAAO,CAAA;YAEzB,YAAY,CAAC,OAAO,EAAE,OAAO,CAAC,CAAA;QAChC,CAAC,YAEA,QAAQ,GACL,CACP,CAAA;AACH,CAAC,CAAA"}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The collapse rule: how many of a toolbar's items fit, given what
|
|
3
|
+
* they measured.
|
|
4
|
+
*
|
|
5
|
+
* Pure arithmetic, in its own file for the same reason
|
|
6
|
+
* `chooseColumns` is — the rule is a table of numbers anyone can
|
|
7
|
+
* check in Node, and everything that needs a browser is quarantined
|
|
8
|
+
* in `useToolbarOverflow`.
|
|
9
|
+
*
|
|
10
|
+
* ## Priority is the array order
|
|
11
|
+
*
|
|
12
|
+
* `itemInlineSizes` is in **priority order, highest first**, which
|
|
13
|
+
* is also paint order: the toolbar collapses from the end. One axis
|
|
14
|
+
* rather than two, because a separate `priority` number and a
|
|
15
|
+
* separate display order is a pair a caller can put out of step,
|
|
16
|
+
* and neither mux-magic nor plex-channels has *any* ordering — they
|
|
17
|
+
* hardcode which controls live in the bar and which live in the
|
|
18
|
+
* popover, at every width.
|
|
19
|
+
*
|
|
20
|
+
* ## Why the trigger's width is an input
|
|
21
|
+
*
|
|
22
|
+
* The overflow trigger only exists once something has collapsed, so
|
|
23
|
+
* the question is not "how many fit" but "how many fit **beside a
|
|
24
|
+
* trigger** — unless all of them fit without one". Ignoring that is
|
|
25
|
+
* how an overflow control ends up wrapping to a second line at
|
|
26
|
+
* exactly the width where it first appears.
|
|
27
|
+
*/
|
|
28
|
+
export declare const chooseVisibleCount: ({ availableInlineSize, gapInlineSize, itemInlineSizes, triggerInlineSize, }: {
|
|
29
|
+
/** The room the bar has, in CSS px. */
|
|
30
|
+
availableInlineSize: number;
|
|
31
|
+
/** The row's `column-gap`, in CSS px. */
|
|
32
|
+
gapInlineSize: number;
|
|
33
|
+
/** Each item's measured inline size, priority order, in CSS px. */
|
|
34
|
+
itemInlineSizes: readonly number[];
|
|
35
|
+
/** The overflow trigger's inline size, in CSS px. */
|
|
36
|
+
triggerInlineSize: number;
|
|
37
|
+
}) => number;
|
|
38
|
+
//# sourceMappingURL=chooseVisibleCount.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"chooseVisibleCount.d.ts","sourceRoot":"","sources":["../../src/Toolbar/chooseVisibleCount.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAEH,eAAO,MAAM,kBAAkB,GAAI,6EAKhC;IACD,uCAAuC;IACvC,mBAAmB,EAAE,MAAM,CAAA;IAC3B,yCAAyC;IACzC,aAAa,EAAE,MAAM,CAAA;IACrB,mEAAmE;IACnE,eAAe,EAAE,SAAS,MAAM,EAAE,CAAA;IAClC,qDAAqD;IACrD,iBAAiB,EAAE,MAAM,CAAA;CAC1B,KAAG,MAiDH,CAAA"}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The collapse rule: how many of a toolbar's items fit, given what
|
|
3
|
+
* they measured.
|
|
4
|
+
*
|
|
5
|
+
* Pure arithmetic, in its own file for the same reason
|
|
6
|
+
* `chooseColumns` is — the rule is a table of numbers anyone can
|
|
7
|
+
* check in Node, and everything that needs a browser is quarantined
|
|
8
|
+
* in `useToolbarOverflow`.
|
|
9
|
+
*
|
|
10
|
+
* ## Priority is the array order
|
|
11
|
+
*
|
|
12
|
+
* `itemInlineSizes` is in **priority order, highest first**, which
|
|
13
|
+
* is also paint order: the toolbar collapses from the end. One axis
|
|
14
|
+
* rather than two, because a separate `priority` number and a
|
|
15
|
+
* separate display order is a pair a caller can put out of step,
|
|
16
|
+
* and neither mux-magic nor plex-channels has *any* ordering — they
|
|
17
|
+
* hardcode which controls live in the bar and which live in the
|
|
18
|
+
* popover, at every width.
|
|
19
|
+
*
|
|
20
|
+
* ## Why the trigger's width is an input
|
|
21
|
+
*
|
|
22
|
+
* The overflow trigger only exists once something has collapsed, so
|
|
23
|
+
* the question is not "how many fit" but "how many fit **beside a
|
|
24
|
+
* trigger** — unless all of them fit without one". Ignoring that is
|
|
25
|
+
* how an overflow control ends up wrapping to a second line at
|
|
26
|
+
* exactly the width where it first appears.
|
|
27
|
+
*/
|
|
28
|
+
export const chooseVisibleCount = ({ availableInlineSize, gapInlineSize, itemInlineSizes, triggerInlineSize, }) => {
|
|
29
|
+
const itemCount = itemInlineSizes.length;
|
|
30
|
+
if (itemCount === 0) {
|
|
31
|
+
return 0;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Nothing measured yet — show everything.
|
|
35
|
+
*
|
|
36
|
+
* The safe direction to be wrong in for one frame, and the same
|
|
37
|
+
* one `useAdaptiveColumns` argues from the other end: a bar that
|
|
38
|
+
* starts whole and collapses over-draws, where a bar that starts
|
|
39
|
+
* collapsed hides controls that were there. It is also what makes
|
|
40
|
+
* the first measurement possible at all, because an item that was
|
|
41
|
+
* never mounted has no width to read.
|
|
42
|
+
*/
|
|
43
|
+
if (availableInlineSize <= 0) {
|
|
44
|
+
return itemCount;
|
|
45
|
+
}
|
|
46
|
+
const measure = (count, extra) => itemInlineSizes
|
|
47
|
+
.slice(0, count)
|
|
48
|
+
.reduce((total, size) => total + size, 0) +
|
|
49
|
+
gapInlineSize *
|
|
50
|
+
Math.max(count + (extra > 0 ? 1 : 0) - 1, 0) +
|
|
51
|
+
extra;
|
|
52
|
+
if (measure(itemCount, 0) <= availableInlineSize) {
|
|
53
|
+
return itemCount;
|
|
54
|
+
}
|
|
55
|
+
// Everything below here has an overflow trigger in the row, so
|
|
56
|
+
// every candidate pays for it. Counting down rather than up
|
|
57
|
+
// because the answer is usually near the top.
|
|
58
|
+
for (let count = itemCount - 1; count > 0; count -= 1) {
|
|
59
|
+
if (measure(count, triggerInlineSize) <=
|
|
60
|
+
availableInlineSize) {
|
|
61
|
+
return count;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
// Not even one item and the trigger fit. Everything collapses;
|
|
65
|
+
// the trigger alone may still overflow, and a bar narrower than
|
|
66
|
+
// one control is a layout problem no collapse rule can solve.
|
|
67
|
+
return 0;
|
|
68
|
+
};
|
|
69
|
+
//# sourceMappingURL=chooseVisibleCount.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"chooseVisibleCount.js","sourceRoot":"","sources":["../../src/Toolbar/chooseVisibleCount.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAEH,MAAM,CAAC,MAAM,kBAAkB,GAAG,CAAC,EACjC,mBAAmB,EACnB,aAAa,EACb,eAAe,EACf,iBAAiB,GAUlB,EAAU,EAAE;IACX,MAAM,SAAS,GAAG,eAAe,CAAC,MAAM,CAAA;IAExC,IAAI,SAAS,KAAK,CAAC,EAAE,CAAC;QACpB,OAAO,CAAC,CAAA;IACV,CAAC;IAED;;;;;;;;;OASG;IACH,IAAI,mBAAmB,IAAI,CAAC,EAAE,CAAC;QAC7B,OAAO,SAAS,CAAA;IAClB,CAAC;IAED,MAAM,OAAO,GAAG,CAAC,KAAa,EAAE,KAAa,EAAE,EAAE,CAC/C,eAAe;SACZ,KAAK,CAAC,CAAC,EAAE,KAAK,CAAC;SACf,MAAM,CAAC,CAAC,KAAK,EAAE,IAAI,EAAE,EAAE,CAAC,KAAK,GAAG,IAAI,EAAE,CAAC,CAAC;QAC3C,aAAa;YACX,IAAI,CAAC,GAAG,CAAC,KAAK,GAAG,CAAC,KAAK,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;QAC9C,KAAK,CAAA;IAEP,IAAI,OAAO,CAAC,SAAS,EAAE,CAAC,CAAC,IAAI,mBAAmB,EAAE,CAAC;QACjD,OAAO,SAAS,CAAA;IAClB,CAAC;IAED,+DAA+D;IAC/D,4DAA4D;IAC5D,8CAA8C;IAC9C,KAAK,IAAI,KAAK,GAAG,SAAS,GAAG,CAAC,EAAE,KAAK,GAAG,CAAC,EAAE,KAAK,IAAI,CAAC,EAAE,CAAC;QACtD,IACE,OAAO,CAAC,KAAK,EAAE,iBAAiB,CAAC;YACjC,mBAAmB,EACnB,CAAC;YACD,OAAO,KAAK,CAAA;QACd,CAAC;IACH,CAAC;IAED,+DAA+D;IAC/D,gEAAgE;IAChE,8DAA8D;IAC9D,OAAO,CAAC,CAAA;AACV,CAAC,CAAA"}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { RefObject } from "react";
|
|
2
|
+
/**
|
|
3
|
+
* The measuring half of progressive overflow — `chooseVisibleCount`
|
|
4
|
+
* is the decision, this is the tape measure.
|
|
5
|
+
*
|
|
6
|
+
* ## Measured, not breakpointed
|
|
7
|
+
*
|
|
8
|
+
* mux-magic collapses at a hardcoded `480px` and plex-channels at a
|
|
9
|
+
* hardcoded `760px`, and both numbers are wrong the moment a label
|
|
10
|
+
* gets longer, the density axis changes, or a rail takes half the
|
|
11
|
+
* row. A measured bar has no number to be wrong: it asks the
|
|
12
|
+
* elements how wide they actually are, in the font and density they
|
|
13
|
+
* are actually rendered in.
|
|
14
|
+
*
|
|
15
|
+
* ## One instance, and cached widths
|
|
16
|
+
*
|
|
17
|
+
* An item that has collapsed is **not in the DOM** — that is the
|
|
18
|
+
* whole point, against a mechanism that renders every action twice
|
|
19
|
+
* and hides one with a CSS specificity coin-flip. So its width
|
|
20
|
+
* cannot be re-read while it is away, and it is remembered from
|
|
21
|
+
* when it was last in the bar instead.
|
|
22
|
+
*
|
|
23
|
+
* The first paint is what seeds the cache: every item starts in the
|
|
24
|
+
* bar (`chooseVisibleCount` returns all of them for an unmeasured
|
|
25
|
+
* container), gets measured, and the row collapses **before paint**
|
|
26
|
+
* because the measuring runs in a layout effect. Any item whose
|
|
27
|
+
* width is still unknown forces one more all-visible pass rather
|
|
28
|
+
* than being guessed at — one extra layout pass, and it converges,
|
|
29
|
+
* because a mounted item always measures.
|
|
30
|
+
*
|
|
31
|
+
* ⚠️ **The measured element must not be sized by its own contents.**
|
|
32
|
+
* If the bar shrink-wraps its items, collapsing one narrows the box,
|
|
33
|
+
* which collapses another — a loop with no error anywhere.
|
|
34
|
+
* `Toolbar`'s root is `flex-1 min-w-0` for exactly this reason, the
|
|
35
|
+
* same trap `useAdaptiveColumns` documents about a container capped
|
|
36
|
+
* by its own answer.
|
|
37
|
+
*/
|
|
38
|
+
export declare const useToolbarOverflow: ({ itemKeys, }: {
|
|
39
|
+
/** Priority order, highest first. */
|
|
40
|
+
itemKeys: readonly string[];
|
|
41
|
+
}) => {
|
|
42
|
+
containerRef: RefObject<HTMLDivElement | null>;
|
|
43
|
+
/** Callback ref for each item's box. `null` on unmount. */
|
|
44
|
+
trackItem: (key: string, element: HTMLElement | null) => void;
|
|
45
|
+
/** Callback ref for the overflow trigger's box. */
|
|
46
|
+
trackTrigger: (element: HTMLElement | null) => void;
|
|
47
|
+
visibleCount: number;
|
|
48
|
+
};
|
|
49
|
+
//# sourceMappingURL=useToolbarOverflow.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useToolbarOverflow.d.ts","sourceRoot":"","sources":["../../src/Toolbar/useToolbarOverflow.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AAUtC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AACH,eAAO,MAAM,kBAAkB,GAAI,eAEhC;IACD,qCAAqC;IACrC,QAAQ,EAAE,SAAS,MAAM,EAAE,CAAA;CAC5B,KAAG;IACF,YAAY,EAAE,SAAS,CAAC,cAAc,GAAG,IAAI,CAAC,CAAA;IAC9C,2DAA2D;IAC3D,SAAS,EAAE,CACT,GAAG,EAAE,MAAM,EACX,OAAO,EAAE,WAAW,GAAG,IAAI,KACxB,IAAI,CAAA;IACT,mDAAmD;IACnD,YAAY,EAAE,CAAC,OAAO,EAAE,WAAW,GAAG,IAAI,KAAK,IAAI,CAAA;IACnD,YAAY,EAAE,MAAM,CAAA;CAwKrB,CAAA"}
|