@charcuterie/ui 0.1.1 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -0
- package/dist/Accordion/Accordion.d.ts +82 -0
- package/dist/Accordion/Accordion.d.ts.map +1 -0
- package/dist/Accordion/Accordion.js +108 -0
- package/dist/Accordion/Accordion.js.map +1 -0
- package/dist/Accordion/AccordionSection.d.ts +28 -0
- package/dist/Accordion/AccordionSection.d.ts.map +1 -0
- package/dist/Accordion/AccordionSection.js +53 -0
- package/dist/Accordion/AccordionSection.js.map +1 -0
- package/dist/Field/Field.d.ts +104 -0
- package/dist/Field/Field.d.ts.map +1 -0
- package/dist/Field/Field.js +115 -0
- package/dist/Field/Field.js.map +1 -0
- package/dist/Field/FieldGroup.d.ts +58 -0
- package/dist/Field/FieldGroup.d.ts.map +1 -0
- package/dist/Field/FieldGroup.js +65 -0
- package/dist/Field/FieldGroup.js.map +1 -0
- package/dist/FileDropZone/FileDropZone.d.ts +63 -0
- package/dist/FileDropZone/FileDropZone.d.ts.map +1 -0
- package/dist/FileDropZone/FileDropZone.js +122 -0
- package/dist/FileDropZone/FileDropZone.js.map +1 -0
- package/dist/LogViewer/LogViewer.d.ts +90 -0
- package/dist/LogViewer/LogViewer.d.ts.map +1 -0
- package/dist/LogViewer/LogViewer.js +165 -0
- package/dist/LogViewer/LogViewer.js.map +1 -0
- package/dist/Menu/Menu.d.ts +81 -0
- package/dist/Menu/Menu.d.ts.map +1 -0
- package/dist/Menu/Menu.js +199 -0
- package/dist/Menu/Menu.js.map +1 -0
- package/dist/Menu/MenuAction.d.ts +24 -0
- package/dist/Menu/MenuAction.d.ts.map +1 -0
- package/dist/Menu/MenuAction.js +37 -0
- package/dist/Menu/MenuAction.js.map +1 -0
- package/dist/Select/Select.d.ts +87 -0
- package/dist/Select/Select.d.ts.map +1 -0
- package/dist/Select/Select.js +51 -0
- package/dist/Select/Select.js.map +1 -0
- package/dist/SortableTableHeader/SortableTableHeader.d.ts +61 -0
- package/dist/SortableTableHeader/SortableTableHeader.d.ts.map +1 -0
- package/dist/SortableTableHeader/SortableTableHeader.js +56 -0
- package/dist/SortableTableHeader/SortableTableHeader.js.map +1 -0
- package/dist/Toast/Toast.d.ts +54 -0
- package/dist/Toast/Toast.d.ts.map +1 -0
- package/dist/Toast/Toast.js +121 -0
- package/dist/Toast/Toast.js.map +1 -0
- package/dist/Toast/ToastRegion.d.ts +57 -0
- package/dist/Toast/ToastRegion.d.ts.map +1 -0
- package/dist/Toast/ToastRegion.js +41 -0
- package/dist/Toast/ToastRegion.js.map +1 -0
- package/dist/Toast/toastLifecycle.d.ts +34 -0
- package/dist/Toast/toastLifecycle.d.ts.map +1 -0
- package/dist/Toast/toastLifecycle.js +25 -0
- package/dist/Toast/toastLifecycle.js.map +1 -0
- package/dist/Tooltip/Tooltip.d.ts +82 -0
- package/dist/Tooltip/Tooltip.d.ts.map +1 -0
- package/dist/Tooltip/Tooltip.js +124 -0
- package/dist/Tooltip/Tooltip.js.map +1 -0
- package/dist/index.d.ts +26 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +13 -0
- package/dist/index.js.map +1 -1
- package/dist/mountStory.testHelpers.d.ts +0 -11
- package/dist/mountStory.testHelpers.d.ts.map +1 -1
- package/dist/mountStory.testHelpers.js +27 -0
- package/dist/mountStory.testHelpers.js.map +1 -1
- package/dist/slotProps.d.ts +110 -0
- package/dist/slotProps.d.ts.map +1 -0
- package/dist/slotProps.js +39 -0
- package/dist/slotProps.js.map +1 -0
- package/package.json +3 -3
- package/src/Accordion/Accordion.mdx +83 -0
- package/src/Accordion/Accordion.stories.tsx +146 -0
- package/src/Accordion/Accordion.test.tsx +163 -0
- package/src/Accordion/Accordion.tsx +182 -0
- package/src/Accordion/AccordionSection.tsx +163 -0
- package/src/Field/Field.mdx +144 -0
- package/src/Field/Field.stories.tsx +280 -0
- package/src/Field/Field.test.tsx +344 -0
- package/src/Field/Field.tsx +212 -0
- package/src/Field/FieldGroup.tsx +138 -0
- package/src/FileDropZone/FileDropZone.mdx +78 -0
- package/src/FileDropZone/FileDropZone.stories.tsx +136 -0
- package/src/FileDropZone/FileDropZone.test.tsx +230 -0
- package/src/FileDropZone/FileDropZone.tsx +234 -0
- package/src/LogViewer/LogViewer.mdx +123 -0
- package/src/LogViewer/LogViewer.stories.tsx +169 -0
- package/src/LogViewer/LogViewer.test.tsx +236 -0
- package/src/LogViewer/LogViewer.tsx +261 -0
- package/src/Menu/Menu.mdx +103 -0
- package/src/Menu/Menu.stories.tsx +161 -0
- package/src/Menu/Menu.test.tsx +202 -0
- package/src/Menu/Menu.tsx +300 -0
- package/src/Menu/MenuAction.tsx +95 -0
- package/src/Select/Select.mdx +74 -0
- package/src/Select/Select.stories.tsx +126 -0
- package/src/Select/Select.test.tsx +129 -0
- package/src/Select/Select.tsx +200 -0
- package/src/SortableTableHeader/SortableTableHeader.mdx +67 -0
- package/src/SortableTableHeader/SortableTableHeader.stories.tsx +207 -0
- package/src/SortableTableHeader/SortableTableHeader.test.tsx +180 -0
- package/src/SortableTableHeader/SortableTableHeader.tsx +131 -0
- package/src/Toast/Toast.mdx +84 -0
- package/src/Toast/Toast.stories.tsx +192 -0
- package/src/Toast/Toast.test.tsx +217 -0
- package/src/Toast/Toast.tsx +224 -0
- package/src/Toast/ToastRegion.tsx +98 -0
- package/src/Toast/toastLifecycle.ts +45 -0
- package/src/Tooltip/Tooltip.mdx +69 -0
- package/src/Tooltip/Tooltip.stories.tsx +104 -0
- package/src/Tooltip/Tooltip.test.tsx +138 -0
- package/src/Tooltip/Tooltip.tsx +193 -0
- package/src/index.ts +47 -0
- package/src/mountStory.testHelpers.ts +30 -0
- package/src/slotProps.ts +143 -0
- package/src/sourceRules.test.ts +221 -30
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { selectTabIndex, useClonedChild, useRovingFocus, } from "@charcuterie/logic";
|
|
3
|
+
import { autoUpdate, FloatingFocusManager, flip, offset, shift, useDismiss, useFloating, useInteractions, useRole, } from "@floating-ui/react";
|
|
4
|
+
import { useEffect, useRef } from "react";
|
|
5
|
+
import { toClassName } from "../toClassName.js";
|
|
6
|
+
import { MenuAction } from "./MenuAction.js";
|
|
7
|
+
/**
|
|
8
|
+
* A menu is **not** a listbox, and mux-magic's `TypePicker` is the
|
|
9
|
+
* fleet's one attempt at telling them apart — it renders
|
|
10
|
+
* `role="menu"` over items that set a value, which is a listbox
|
|
11
|
+
* wearing the wrong role. The distinction is what the items *are*:
|
|
12
|
+
*
|
|
13
|
+
* - `menuitem` **does** something. Rename, Delete, Copy path.
|
|
14
|
+
* - `option` **is** something. It is a value you are choosing.
|
|
15
|
+
*
|
|
16
|
+
* A screen reader announces "menu, 4 items" for one and "listbox,
|
|
17
|
+
* selected, 2 of 4" for the other, and an agent driving the app
|
|
18
|
+
* needs the second to know what state it left behind. Getting it
|
|
19
|
+
* wrong is invisible in a browser and invisible to axe.
|
|
20
|
+
*
|
|
21
|
+
* ### There is no `label` prop, and that corrects a lesson
|
|
22
|
+
*
|
|
23
|
+
* `Spinner` (M3), `Popover` and `Modal` (M4) all take a **required**
|
|
24
|
+
* name, because a `role="status"` and a `role="dialog"` take none
|
|
25
|
+
* from their content. The obvious move here was a `label` prop doing
|
|
26
|
+
* the same job, and it was written that way first — then measured
|
|
27
|
+
* as doing nothing.
|
|
28
|
+
*
|
|
29
|
+
* `useRole(context, { role: "menu" })` puts **`aria-labelledby` on
|
|
30
|
+
* the panel pointing at the trigger**, and `aria-labelledby` beats
|
|
31
|
+
* `aria-label`. So the menu was already named — "Bay 3", from the
|
|
32
|
+
* button — and the prop was silently discarded. That is also the ARIA
|
|
33
|
+
* Authoring Practices menu-button pattern, where a menu is named by
|
|
34
|
+
* the control that opens it.
|
|
35
|
+
*
|
|
36
|
+
* The rule the other three teach is therefore narrower than it
|
|
37
|
+
* looked: **an overlay with no trigger relationship needs its own
|
|
38
|
+
* name.** A menu has one, so the requirement moves to the trigger —
|
|
39
|
+
* which `Button` and `IconButton` already enforce.
|
|
40
|
+
*
|
|
41
|
+
* ### Roving focus, and the composition `Tabs` already proved
|
|
42
|
+
*
|
|
43
|
+
* `RovingFocus` alone this time — there is no selection, because a
|
|
44
|
+
* menu item is not selected, it is invoked. That is the same
|
|
45
|
+
* two-kinds argument from the other side: `Tabs` needed both kinds
|
|
46
|
+
* because a tab is chosen *and* focused, and a menu needs one
|
|
47
|
+
* because a menu item is only ever focused.
|
|
48
|
+
*
|
|
49
|
+
* ### Focus moves in, once, and only on opening
|
|
50
|
+
*
|
|
51
|
+
* A menu is one of the few patterns where focus must jump on open —
|
|
52
|
+
* otherwise the keyboard user who pressed Enter on the trigger is
|
|
53
|
+
* still on the trigger with a menu they cannot reach. It happens in
|
|
54
|
+
* the panel's own effect rather than through
|
|
55
|
+
* `FloatingFocusManager`'s `initialFocus`, because the element to
|
|
56
|
+
* focus is whichever one `RovingFocus` says is active, and that is
|
|
57
|
+
* not known until the items have registered.
|
|
58
|
+
*
|
|
59
|
+
* ### No type-ahead, deliberately
|
|
60
|
+
*
|
|
61
|
+
* The APG lists type-ahead as optional for a menu and required for a
|
|
62
|
+
* listbox, and none of the fleet's menus are long enough to need it
|
|
63
|
+
* — the largest has six items. Adding it would mean a keystroke
|
|
64
|
+
* buffer and a timer in a component that currently has neither, for
|
|
65
|
+
* a menu you can cross in three arrow presses.
|
|
66
|
+
*/
|
|
67
|
+
export const Menu = ({ className, isVisible, items, onDismiss, placement = "bottom-start", trigger, }) => {
|
|
68
|
+
const itemElements = useRef(new Map());
|
|
69
|
+
const panelElement = useRef(null);
|
|
70
|
+
const focus = useRovingFocus();
|
|
71
|
+
const { activeValue, registeredValues, setActiveValue } = focus;
|
|
72
|
+
const [firstValue] = registeredValues;
|
|
73
|
+
const { context, floatingStyles, refs } = useFloating({
|
|
74
|
+
open: isVisible,
|
|
75
|
+
onOpenChange: (isNextVisible) => {
|
|
76
|
+
if (!isNextVisible) {
|
|
77
|
+
onDismiss();
|
|
78
|
+
}
|
|
79
|
+
},
|
|
80
|
+
placement,
|
|
81
|
+
middleware: [offset(4), flip(), shift({ padding: 8 })],
|
|
82
|
+
strategy: "fixed",
|
|
83
|
+
whileElementsMounted: autoUpdate,
|
|
84
|
+
});
|
|
85
|
+
const { getFloatingProps, getReferenceProps } = useInteractions([
|
|
86
|
+
useDismiss(context),
|
|
87
|
+
useRole(context, { role: "menu" }),
|
|
88
|
+
]);
|
|
89
|
+
const clonedTrigger = useClonedChild(trigger, {
|
|
90
|
+
...getReferenceProps(),
|
|
91
|
+
ref: refs.setReference,
|
|
92
|
+
});
|
|
93
|
+
/**
|
|
94
|
+
* The other half of the `isConnected` guard below. If the ref
|
|
95
|
+
* fired while the node was still detached, nothing has shown the
|
|
96
|
+
* popover — and a `[popover]` that was never shown is
|
|
97
|
+
* `display: none`, so the menu renders, passes typecheck, and is
|
|
98
|
+
* invisible to `getByRole` and to a user alike.
|
|
99
|
+
*
|
|
100
|
+
* Effects run after the tree is connected, so this is the
|
|
101
|
+
* backstop. It is idempotent: `:popover-open` is the same question
|
|
102
|
+
* the ref asks.
|
|
103
|
+
*/
|
|
104
|
+
useEffect(() => {
|
|
105
|
+
const panel = panelElement.current;
|
|
106
|
+
if (isVisible &&
|
|
107
|
+
panel?.isConnected &&
|
|
108
|
+
!panel.matches(":popover-open")) {
|
|
109
|
+
panel.showPopover();
|
|
110
|
+
}
|
|
111
|
+
}, [isVisible]);
|
|
112
|
+
/**
|
|
113
|
+
* Opening **makes** a member active, and this is the step the
|
|
114
|
+
* first version was missing.
|
|
115
|
+
*
|
|
116
|
+
* `RovingFocus` leaves `activeValue` at `null` until something
|
|
117
|
+
* sets it — `selectTabIndex` covers the first paint by giving the
|
|
118
|
+
* tab stop to `registeredValues[0]` while it is still null, which
|
|
119
|
+
* is exactly why nothing looked wrong: the menu had a correct tab
|
|
120
|
+
* stop, correct roles, and no focus. `Tabs` never hit it because a
|
|
121
|
+
* tab bar seeds itself from its selected key.
|
|
122
|
+
*
|
|
123
|
+
* Re-running on every open is deliberate. The APG says a menu
|
|
124
|
+
* opens on its first item, so a menu reopened after arrowing to
|
|
125
|
+
* the third one starts at the top again rather than resuming a
|
|
126
|
+
* position the user has forgotten.
|
|
127
|
+
*/
|
|
128
|
+
useEffect(() => {
|
|
129
|
+
if (!isVisible || firstValue === undefined) {
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
setActiveValue(firstValue);
|
|
133
|
+
}, [firstValue, isVisible, setActiveValue]);
|
|
134
|
+
useEffect(() => {
|
|
135
|
+
if (!isVisible || activeValue === null) {
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
itemElements.current.get(activeValue)?.focus();
|
|
139
|
+
}, [activeValue, isVisible]);
|
|
140
|
+
return (_jsxs(_Fragment, { children: [clonedTrigger, isVisible ? (_jsx(FloatingFocusManager, { context: context,
|
|
141
|
+
// `false`, so Tab leaves the menu and lands on the next
|
|
142
|
+
// control in the page — which is what closes it, through
|
|
143
|
+
// `useDismiss`. A trapped menu is a dialog.
|
|
144
|
+
modal: false,
|
|
145
|
+
// Focus is moved by the effect above, to whichever item
|
|
146
|
+
// `RovingFocus` made active. Letting the manager also aim
|
|
147
|
+
// it is two owners for one caret.
|
|
148
|
+
initialFocus: -1, children: _jsx("div", { ...getFloatingProps(), className: toClassName("inset-auto m-0 flex min-w-48 flex-col gap-0.5 rounded-md border border-border-default bg-surface-overlay p-1 shadow-medium", className), onKeyDown: (keyEvent) => {
|
|
149
|
+
const commands = {
|
|
150
|
+
ArrowDown: focus.next,
|
|
151
|
+
ArrowUp: focus.previous,
|
|
152
|
+
End: focus.last,
|
|
153
|
+
Home: focus.first,
|
|
154
|
+
};
|
|
155
|
+
const command = commands[keyEvent.key];
|
|
156
|
+
if (command) {
|
|
157
|
+
// Arrow keys scroll the page by default, and a menu
|
|
158
|
+
// that scrolls the document behind it while moving
|
|
159
|
+
// focus is the hand-rolled version of this
|
|
160
|
+
// component.
|
|
161
|
+
keyEvent.preventDefault();
|
|
162
|
+
command();
|
|
163
|
+
}
|
|
164
|
+
}, popover: "manual", ref: (node) => {
|
|
165
|
+
refs.setFloating(node);
|
|
166
|
+
panelElement.current = node;
|
|
167
|
+
// A ref callback rather than an effect, for the same
|
|
168
|
+
// reason as `Popover`: a `[popover]` is
|
|
169
|
+
// `display: none` until shown, and every child effect
|
|
170
|
+
// — including the focus move above — would otherwise
|
|
171
|
+
// be aiming at a hidden element.
|
|
172
|
+
//
|
|
173
|
+
// `isConnected` is the guard `Popover` does not need
|
|
174
|
+
// and this does. `FloatingFocusManager` renders a pair
|
|
175
|
+
// of focus guards around its child, so React commits
|
|
176
|
+
// this subtree in an order where the ref can fire
|
|
177
|
+
// while the node is still detached — and
|
|
178
|
+
// `showPopover()` on a disconnected element throws
|
|
179
|
+
// `InvalidStateError`, which surfaces as an unhandled
|
|
180
|
+
// rejection rather than as a failing assertion.
|
|
181
|
+
if (node?.isConnected &&
|
|
182
|
+
!node.matches(":popover-open")) {
|
|
183
|
+
node.showPopover();
|
|
184
|
+
}
|
|
185
|
+
},
|
|
186
|
+
// Duplicated from `getFloatingProps()` on purpose, and
|
|
187
|
+
// the two cannot differ — `useRole(context, { role:
|
|
188
|
+
// "menu" })` above is where it comes from. Stated here
|
|
189
|
+
// because a linter cannot see a role through a spread.
|
|
190
|
+
role: "menu", style: floatingStyles, children: items.map((item) => (_jsx(MenuAction, { item: item, onDismiss: onDismiss, register: focus.register, tabIndex: selectTabIndex(focus, item.key), trackElement: (key, element) => {
|
|
191
|
+
if (element) {
|
|
192
|
+
itemElements.current.set(key, element);
|
|
193
|
+
}
|
|
194
|
+
else {
|
|
195
|
+
itemElements.current.delete(key);
|
|
196
|
+
}
|
|
197
|
+
} }, item.key))) }) })) : null] }));
|
|
198
|
+
};
|
|
199
|
+
//# sourceMappingURL=Menu.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Menu.js","sourceRoot":"","sources":["../../src/Menu/Menu.tsx"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
import type { MenuItem } from "./Menu.tsx";
|
|
3
|
+
export type MenuActionProps = {
|
|
4
|
+
item: MenuItem;
|
|
5
|
+
onDismiss: () => void;
|
|
6
|
+
/** `RovingFocus.register` — membership of the arrow-key group. */
|
|
7
|
+
register: (value: string) => () => void;
|
|
8
|
+
tabIndex: number;
|
|
9
|
+
trackElement: (key: string, element: HTMLButtonElement | null) => void;
|
|
10
|
+
};
|
|
11
|
+
/**
|
|
12
|
+
* Its own file because **registration is an effect**, and an effect
|
|
13
|
+
* cannot run in a loop — the same reason `TabTrigger` and
|
|
14
|
+
* `AccordionSection` are separate. Any member of a registering group
|
|
15
|
+
* ends up shaped like this.
|
|
16
|
+
*
|
|
17
|
+
* A disabled item is simply not registered, so the arrow keys skip
|
|
18
|
+
* it without any command in `RovingFocus` knowing what "disabled"
|
|
19
|
+
* means. It stays in the DOM and stays announced, which is the
|
|
20
|
+
* difference between "you cannot do this right now" and "this does
|
|
21
|
+
* not exist".
|
|
22
|
+
*/
|
|
23
|
+
export declare const MenuAction: ({ item, onDismiss, register, tabIndex, trackElement, }: MenuActionProps) => ReactNode;
|
|
24
|
+
//# sourceMappingURL=MenuAction.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"MenuAction.d.ts","sourceRoot":"","sources":["../../src/Menu/MenuAction.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AAKtC,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAA;AAE1C,MAAM,MAAM,eAAe,GAAG;IAC5B,IAAI,EAAE,QAAQ,CAAA;IACd,SAAS,EAAE,MAAM,IAAI,CAAA;IACrB,kEAAkE;IAClE,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,MAAM,IAAI,CAAA;IACvC,QAAQ,EAAE,MAAM,CAAA;IAChB,YAAY,EAAE,CACZ,GAAG,EAAE,MAAM,EACX,OAAO,EAAE,iBAAiB,GAAG,IAAI,KAC9B,IAAI,CAAA;CACV,CAAA;AAED;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,UAAU,GAAI,wDAMxB,eAAe,KAAG,SAyDpB,CAAA"}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect } from "react";
|
|
3
|
+
import { FOCUS_RING_CLASS } from "../intentStyles.js";
|
|
4
|
+
import { toClassName } from "../toClassName.js";
|
|
5
|
+
/**
|
|
6
|
+
* Its own file because **registration is an effect**, and an effect
|
|
7
|
+
* cannot run in a loop — the same reason `TabTrigger` and
|
|
8
|
+
* `AccordionSection` are separate. Any member of a registering group
|
|
9
|
+
* ends up shaped like this.
|
|
10
|
+
*
|
|
11
|
+
* A disabled item is simply not registered, so the arrow keys skip
|
|
12
|
+
* it without any command in `RovingFocus` knowing what "disabled"
|
|
13
|
+
* means. It stays in the DOM and stays announced, which is the
|
|
14
|
+
* difference between "you cannot do this right now" and "this does
|
|
15
|
+
* not exist".
|
|
16
|
+
*/
|
|
17
|
+
export const MenuAction = ({ item, onDismiss, register, tabIndex, trackElement, }) => {
|
|
18
|
+
const { icon, isDisabled = false, key, label, onSelect, } = item;
|
|
19
|
+
useEffect(() => {
|
|
20
|
+
if (isDisabled) {
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
return register(key);
|
|
24
|
+
}, [isDisabled, key, register]);
|
|
25
|
+
return (_jsxs("button", { className: toClassName("flex w-full cursor-pointer items-center gap-2 rounded-sm bg-transparent px-2 py-1.5 text-start text-content-primary text-sm transition-colors duration-(--duration-fast) ease-standard", "hover:bg-intent-neutral-surface", isDisabled &&
|
|
26
|
+
"cursor-not-allowed text-content-disabled hover:bg-transparent", FOCUS_RING_CLASS), disabled: isDisabled, onClick: () => {
|
|
27
|
+
onSelect();
|
|
28
|
+
// Choosing dismisses. A menu that stays open after an action
|
|
29
|
+
// has fired is the one interaction users read as "it didn't
|
|
30
|
+
// work", and every one of the fleet's menus closes here
|
|
31
|
+
// today by unmounting its whole parent.
|
|
32
|
+
onDismiss();
|
|
33
|
+
}, ref: (element) => {
|
|
34
|
+
trackElement(key, element);
|
|
35
|
+
}, role: "menuitem", tabIndex: tabIndex, type: "button", children: [icon === undefined ? null : (_jsx("span", { "aria-hidden": "true", className: "shrink-0 text-content-secondary", children: icon })), label] }));
|
|
36
|
+
};
|
|
37
|
+
//# sourceMappingURL=MenuAction.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"MenuAction.js","sourceRoot":"","sources":["../../src/Menu/MenuAction.tsx"],"names":[],"mappings":";AACA,OAAO,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AAEjC,OAAO,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAA;AACrD,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAA;AAe/C;;;;;;;;;;;GAWG;AACH,MAAM,CAAC,MAAM,UAAU,GAAG,CAAC,EACzB,IAAI,EACJ,SAAS,EACT,QAAQ,EACR,QAAQ,EACR,YAAY,GACI,EAAa,EAAE;IAC/B,MAAM,EACJ,IAAI,EACJ,UAAU,GAAG,KAAK,EAClB,GAAG,EACH,KAAK,EACL,QAAQ,GACT,GAAG,IAAI,CAAA;IAER,SAAS,CAAC,GAAG,EAAE;QACb,IAAI,UAAU,EAAE,CAAC;YACf,OAAM;QACR,CAAC;QAED,OAAO,QAAQ,CAAC,GAAG,CAAC,CAAA;IACtB,CAAC,EAAE,CAAC,UAAU,EAAE,GAAG,EAAE,QAAQ,CAAC,CAAC,CAAA;IAE/B,OAAO,CACL,kBACE,SAAS,EAAE,WAAW,CACpB,wLAAwL,EACxL,iCAAiC,EACjC,UAAU;YACR,+DAA+D,EACjE,gBAAgB,CACjB,EACD,QAAQ,EAAE,UAAU,EACpB,OAAO,EAAE,GAAG,EAAE;YACZ,QAAQ,EAAE,CAAA;YAEV,6DAA6D;YAC7D,4DAA4D;YAC5D,wDAAwD;YACxD,wCAAwC;YACxC,SAAS,EAAE,CAAA;QACb,CAAC,EACD,GAAG,EAAE,CAAC,OAAO,EAAE,EAAE;YACf,YAAY,CAAC,GAAG,EAAE,OAAO,CAAC,CAAA;QAC5B,CAAC,EACD,IAAI,EAAC,UAAU,EACf,QAAQ,EAAE,QAAQ,EAClB,IAAI,EAAC,QAAQ,aAEZ,IAAI,KAAK,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAC3B,8BAGc,MAAM,EAClB,SAAS,EAAC,iCAAiC,YAE1C,IAAI,GACA,CACR,EAEA,KAAK,IACC,CACV,CAAA;AACH,CAAC,CAAA"}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import type { ControlSize } from "@charcuterie/tokens";
|
|
2
|
+
import type { ComponentPropsWithRef, ReactNode } from "react";
|
|
3
|
+
export type SelectOption = {
|
|
4
|
+
isDisabled?: boolean;
|
|
5
|
+
label: string;
|
|
6
|
+
value: string;
|
|
7
|
+
};
|
|
8
|
+
export type SelectOptionGroup = {
|
|
9
|
+
label: string;
|
|
10
|
+
options: SelectOption[];
|
|
11
|
+
};
|
|
12
|
+
export type SelectItem = SelectOption | SelectOptionGroup;
|
|
13
|
+
/**
|
|
14
|
+
* The HTML attributes are passed through, and that is not
|
|
15
|
+
* convenience — it is what makes the `Field` slot work at all.
|
|
16
|
+
*
|
|
17
|
+
* `Field` clones `id`, `aria-describedby`, `aria-invalid` and
|
|
18
|
+
* `required` onto whatever control it is given. A component with a
|
|
19
|
+
* closed prop list **silently drops all four**: the field renders,
|
|
20
|
+
* the label still points at nothing, and every assertion about the
|
|
21
|
+
* control's own props passes because they never arrived. Found by
|
|
22
|
+
* the one test that asked the control what it had been handed.
|
|
23
|
+
*
|
|
24
|
+
* Same arrangement as `Button`, for the same reason.
|
|
25
|
+
*/
|
|
26
|
+
export type SelectProps = Omit<ComponentPropsWithRef<"select">, "disabled" | "onChange" | "size" | "value"> & {
|
|
27
|
+
className?: string;
|
|
28
|
+
isDisabled?: boolean;
|
|
29
|
+
/**
|
|
30
|
+
* The accessible name, when this is **not** inside a `Field`. A
|
|
31
|
+
* `<select>` with neither a `<label for>` nor an `aria-label` is
|
|
32
|
+
* announced as "combobox" and is unfindable by
|
|
33
|
+
* `getByRole("combobox", { name })` — twelve of the fleet's
|
|
34
|
+
* fourteen native selects are in exactly that state.
|
|
35
|
+
*/
|
|
36
|
+
label?: string;
|
|
37
|
+
onChange?: (value: string) => void;
|
|
38
|
+
options: SelectItem[];
|
|
39
|
+
/**
|
|
40
|
+
* A disabled first entry, not a real option. Reads as a prompt to
|
|
41
|
+
* a sighted user and cannot be submitted, which is what a
|
|
42
|
+
* placeholder should mean — unlike an empty-valued option, which
|
|
43
|
+
* is selectable and silently posts "".
|
|
44
|
+
*/
|
|
45
|
+
placeholder?: string;
|
|
46
|
+
size?: ControlSize;
|
|
47
|
+
/** **Initial** only, like every other value prop in this library. */
|
|
48
|
+
value?: string;
|
|
49
|
+
};
|
|
50
|
+
/**
|
|
51
|
+
* A styled native `<select>`, and the fleet's fourteen hand-rolled
|
|
52
|
+
* ones collapse onto it.
|
|
53
|
+
*
|
|
54
|
+
* ### There is no state kind here, and that is the finding
|
|
55
|
+
*
|
|
56
|
+
* Every other interactive component in this package owns its state
|
|
57
|
+
* through `@charcuterie/logic`. This one owns none, because the
|
|
58
|
+
* platform already does: a `<select>` holds the chosen option in the
|
|
59
|
+
* DOM, and putting a `useSinglePicker` beside it would create the
|
|
60
|
+
* exact conflict the state layer exists to prevent — two things
|
|
61
|
+
* believing they hold `value`, which is the Radix argument from
|
|
62
|
+
* `Popover` pointed at ourselves. So `value` seeds `defaultValue`
|
|
63
|
+
* and changes are reported outward.
|
|
64
|
+
*
|
|
65
|
+
* The consequence worth knowing: **`Select` is uncontrolled and
|
|
66
|
+
* `Tabs` is not**, and a reader should be able to tell why without
|
|
67
|
+
* asking. A tab bar's selection has no DOM representation, so
|
|
68
|
+
* something has to hold it; a select's does.
|
|
69
|
+
*
|
|
70
|
+
* ### Native, and what that rules out
|
|
71
|
+
*
|
|
72
|
+
* A `<select>` cannot render a rich option — an icon, two lines, a
|
|
73
|
+
* badge — and cannot filter. mux-magic has six controls that need
|
|
74
|
+
* one of those (`CommandPicker`, `PathPicker`, `LinkPicker`,
|
|
75
|
+
* `EnumPicker`, `AssFieldPicker`, `RenameTargetPicker`), and every
|
|
76
|
+
* one of them is a **`Combobox`**: a text input that filters a
|
|
77
|
+
* listbox. That is a different pattern with a different ARIA
|
|
78
|
+
* contract, it is P2 in the plan, and building a bare `Listbox`
|
|
79
|
+
* here that none of the six could adopt would have shipped a
|
|
80
|
+
* component with no caller.
|
|
81
|
+
*
|
|
82
|
+
* What native buys, and what a hand-rolled listbox has to rebuild
|
|
83
|
+
* from nothing: type-ahead, Home/End, PageUp/PageDown, the mobile
|
|
84
|
+
* wheel picker, form submission, `:invalid`, and autofill.
|
|
85
|
+
*/
|
|
86
|
+
export declare const Select: ({ className, isDisabled, label, onChange, options, placeholder, size, value, ...selectProps }: SelectProps) => ReactNode;
|
|
87
|
+
//# sourceMappingURL=Select.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Select.d.ts","sourceRoot":"","sources":["../../src/Select/Select.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAA;AACtD,OAAO,KAAK,EAEV,qBAAqB,EACrB,SAAS,EACV,MAAM,OAAO,CAAA;AAYd,MAAM,MAAM,YAAY,GAAG;IACzB,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,MAAM,CAAA;CACd,CAAA;AAED,MAAM,MAAM,iBAAiB,GAAG;IAC9B,KAAK,EAAE,MAAM,CAAA;IACb,OAAO,EAAE,YAAY,EAAE,CAAA;CACxB,CAAA;AAED,MAAM,MAAM,UAAU,GAAG,YAAY,GAAG,iBAAiB,CAAA;AAEzD;;;;;;;;;;;;GAYG;AACH,MAAM,MAAM,WAAW,GAAG,IAAI,CAC5B,qBAAqB,CAAC,QAAQ,CAAC,EAC/B,UAAU,GAAG,UAAU,GAAG,MAAM,GAAG,OAAO,CAC3C,GAAG;IACF,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB;;;;;;OAMG;IACH,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAA;IAClC,OAAO,EAAE,UAAU,EAAE,CAAA;IACrB;;;;;OAKG;IACH,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,IAAI,CAAC,EAAE,WAAW,CAAA;IAClB,qEAAqE;IACrE,KAAK,CAAC,EAAE,MAAM,CAAA;CACf,CAAA;AAMD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AACH,eAAO,MAAM,MAAM,GAAI,+FAUpB,WAAW,KAAG,SA8EhB,CAAA"}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { CONTROL_SIZE_CLASS, MIN_TOUCH_TARGET_CLASS, } from "../controlStyles.js";
|
|
3
|
+
import { DISABLED_CLASS, FOCUS_RING_CLASS, } from "../intentStyles.js";
|
|
4
|
+
import { toClassName } from "../toClassName.js";
|
|
5
|
+
const getIsGroup = (item) => "options" in item;
|
|
6
|
+
/**
|
|
7
|
+
* A styled native `<select>`, and the fleet's fourteen hand-rolled
|
|
8
|
+
* ones collapse onto it.
|
|
9
|
+
*
|
|
10
|
+
* ### There is no state kind here, and that is the finding
|
|
11
|
+
*
|
|
12
|
+
* Every other interactive component in this package owns its state
|
|
13
|
+
* through `@charcuterie/logic`. This one owns none, because the
|
|
14
|
+
* platform already does: a `<select>` holds the chosen option in the
|
|
15
|
+
* DOM, and putting a `useSinglePicker` beside it would create the
|
|
16
|
+
* exact conflict the state layer exists to prevent — two things
|
|
17
|
+
* believing they hold `value`, which is the Radix argument from
|
|
18
|
+
* `Popover` pointed at ourselves. So `value` seeds `defaultValue`
|
|
19
|
+
* and changes are reported outward.
|
|
20
|
+
*
|
|
21
|
+
* The consequence worth knowing: **`Select` is uncontrolled and
|
|
22
|
+
* `Tabs` is not**, and a reader should be able to tell why without
|
|
23
|
+
* asking. A tab bar's selection has no DOM representation, so
|
|
24
|
+
* something has to hold it; a select's does.
|
|
25
|
+
*
|
|
26
|
+
* ### Native, and what that rules out
|
|
27
|
+
*
|
|
28
|
+
* A `<select>` cannot render a rich option — an icon, two lines, a
|
|
29
|
+
* badge — and cannot filter. mux-magic has six controls that need
|
|
30
|
+
* one of those (`CommandPicker`, `PathPicker`, `LinkPicker`,
|
|
31
|
+
* `EnumPicker`, `AssFieldPicker`, `RenameTargetPicker`), and every
|
|
32
|
+
* one of them is a **`Combobox`**: a text input that filters a
|
|
33
|
+
* listbox. That is a different pattern with a different ARIA
|
|
34
|
+
* contract, it is P2 in the plan, and building a bare `Listbox`
|
|
35
|
+
* here that none of the six could adopt would have shipped a
|
|
36
|
+
* component with no caller.
|
|
37
|
+
*
|
|
38
|
+
* What native buys, and what a hand-rolled listbox has to rebuild
|
|
39
|
+
* from nothing: type-ahead, Home/End, PageUp/PageDown, the mobile
|
|
40
|
+
* wheel picker, form submission, `:invalid`, and autofill.
|
|
41
|
+
*/
|
|
42
|
+
export const Select = ({ className, isDisabled = false, label, onChange, options, placeholder, size = "md", value, ...selectProps }) => (_jsxs("div", { className: "relative inline-grid w-full items-center", children: [_jsxs("select", { ...selectProps, "aria-label": label, className: toClassName("w-full cursor-pointer appearance-none rounded-md border border-border-default bg-surface-raised text-content-primary transition-colors duration-(--duration-fast) ease-standard",
|
|
43
|
+
// Room for the chevron below, which sits in the same grid
|
|
44
|
+
// cell. A `<select>`'s own arrow is what `appearance-none`
|
|
45
|
+
// removes, and forgetting to put one back leaves a control
|
|
46
|
+
// that looks exactly like a text input.
|
|
47
|
+
"pe-9", CONTROL_SIZE_CLASS[size], MIN_TOUCH_TARGET_CLASS, "hover:border-border-strong", FOCUS_RING_CLASS, DISABLED_CLASS, className), defaultValue: value ??
|
|
48
|
+
(placeholder === undefined ? undefined : ""), disabled: isDisabled, onChange: (changeEvent) => {
|
|
49
|
+
onChange?.(changeEvent.currentTarget.value);
|
|
50
|
+
}, children: [placeholder === undefined ? null : (_jsx("option", { disabled: true, value: "", children: placeholder })), options.map((item) => getIsGroup(item) ? (_jsx("optgroup", { label: item.label, children: item.options.map((option) => (_jsx("option", { disabled: option.isDisabled, value: option.value, children: option.label }, option.value))) }, item.label)) : (_jsx("option", { disabled: item.isDisabled, value: item.value, children: item.label }, item.value)))] }), _jsx("svg", { "aria-hidden": "true", className: "pointer-events-none absolute end-3 size-4 text-content-secondary", fill: "none", focusable: false, stroke: "currentColor", strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 1.75, viewBox: "0 0 24 24", xmlns: "http://www.w3.org/2000/svg", children: _jsx("path", { d: "m6 9 6 6 6-6" }) })] }));
|
|
51
|
+
//# sourceMappingURL=Select.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Select.js","sourceRoot":"","sources":["../../src/Select/Select.tsx"],"names":[],"mappings":";AAOA,OAAO,EACL,kBAAkB,EAClB,sBAAsB,GACvB,MAAM,qBAAqB,CAAA;AAC5B,OAAO,EACL,cAAc,EACd,gBAAgB,GACjB,MAAM,oBAAoB,CAAA;AAC3B,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAA;AAwD/C,MAAM,UAAU,GAAG,CACjB,IAAgB,EACW,EAAE,CAAC,SAAS,IAAI,IAAI,CAAA;AAEjD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AACH,MAAM,CAAC,MAAM,MAAM,GAAG,CAAC,EACrB,SAAS,EACT,UAAU,GAAG,KAAK,EAClB,KAAK,EACL,QAAQ,EACR,OAAO,EACP,WAAW,EACX,IAAI,GAAG,IAAI,EACX,KAAK,EACL,GAAG,WAAW,EACF,EAAa,EAAE,CAAC,CAC5B,eAAK,SAAS,EAAC,0CAA0C,aACvD,qBACM,WAAW,gBACH,KAAK,EACjB,SAAS,EAAE,WAAW,CACpB,iLAAiL;YACjL,0DAA0D;YAC1D,2DAA2D;YAC3D,2DAA2D;YAC3D,wCAAwC;YACxC,MAAM,EACN,kBAAkB,CAAC,IAAI,CAAC,EACxB,sBAAsB,EACtB,4BAA4B,EAC5B,gBAAgB,EAChB,cAAc,EACd,SAAS,CACV,EACD,YAAY,EACV,KAAK;gBACL,CAAC,WAAW,KAAK,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,EAE9C,QAAQ,EAAE,UAAU,EACpB,QAAQ,EAAE,CACR,WAA2C,EAC3C,EAAE;gBACF,QAAQ,EAAE,CAAC,WAAW,CAAC,aAAa,CAAC,KAAK,CAAC,CAAA;YAC7C,CAAC,aAEA,WAAW,KAAK,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAClC,iBAAQ,QAAQ,QAAC,KAAK,EAAC,EAAE,YACtB,WAAW,GACL,CACV,EAEA,OAAO,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CACpB,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CACjB,mBAA2B,KAAK,EAAE,IAAI,CAAC,KAAK,YACzC,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,CAC5B,iBACE,QAAQ,EAAE,MAAM,CAAC,UAAU,EAE3B,KAAK,EAAE,MAAM,CAAC,KAAK,YAElB,MAAM,CAAC,KAAK,IAHR,MAAM,CAAC,KAAK,CAIV,CACV,CAAC,IATW,IAAI,CAAC,KAAK,CAUd,CACZ,CAAC,CAAC,CAAC,CACF,iBACE,QAAQ,EAAE,IAAI,CAAC,UAAU,EAEzB,KAAK,EAAE,IAAI,CAAC,KAAK,YAEhB,IAAI,CAAC,KAAK,IAHN,IAAI,CAAC,KAAK,CAIR,CACV,CACF,IACM,EAET,6BAGc,MAAM,EAClB,SAAS,EAAC,kEAAkE,EAC5E,IAAI,EAAC,MAAM,EACX,SAAS,EAAE,KAAK,EAChB,MAAM,EAAC,cAAc,EACrB,aAAa,EAAC,OAAO,EACrB,cAAc,EAAC,OAAO,EACtB,WAAW,EAAE,IAAI,EACjB,OAAO,EAAC,WAAW,EACnB,KAAK,EAAC,4BAA4B,YAElC,eAAM,CAAC,EAAC,cAAc,GAAG,GACrB,IACF,CACP,CAAA"}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
export type SortDirection = "ascending" | "descending";
|
|
3
|
+
export type SortableTableHeaderProps = {
|
|
4
|
+
children: ReactNode;
|
|
5
|
+
className?: string;
|
|
6
|
+
/**
|
|
7
|
+
* The direction **this column** is sorted in, or `undefined` when
|
|
8
|
+
* it is not the sorted one. Undefined is not "ascending by
|
|
9
|
+
* default": a table has exactly one sorted column and every other
|
|
10
|
+
* header has to say `aria-sort="none"`, which is what tells a
|
|
11
|
+
* screen reader which one is live.
|
|
12
|
+
*/
|
|
13
|
+
direction?: SortDirection;
|
|
14
|
+
/** Called with the direction the header is asking to move to. */
|
|
15
|
+
onSort: (direction: SortDirection) => void;
|
|
16
|
+
};
|
|
17
|
+
/**
|
|
18
|
+
* **The header cell, and nothing else.** No `DataTable`, no rows, no
|
|
19
|
+
* comparator — the plan is explicit about that, and the reason is
|
|
20
|
+
* that sorting *data* is app logic that differs per column type,
|
|
21
|
+
* while announcing *that* a column is sorted is markup every table
|
|
22
|
+
* gets wrong the same way.
|
|
23
|
+
*
|
|
24
|
+
* ### `aria-sort` does not exist anywhere in the fleet
|
|
25
|
+
*
|
|
26
|
+
* Not in mux-magic, not in rip-deck, not in castkit. mux-magic's
|
|
27
|
+
* `FileExplorerModal` — the one sortable table there is — renders
|
|
28
|
+
* the state as a character:
|
|
29
|
+
*
|
|
30
|
+
* ```tsx
|
|
31
|
+
* {sortDirection === "asc" ? "▲" : "▼"}
|
|
32
|
+
* ```
|
|
33
|
+
*
|
|
34
|
+
* A screen reader announces that as "black up-pointing triangle", if
|
|
35
|
+
* the font has it at all, and the sandbox's headless Chromium does
|
|
36
|
+
* not have it — so the same glyph measures blank in a screenshot.
|
|
37
|
+
* Sighted users see a direction, everyone else gets nothing, and no
|
|
38
|
+
* automated check anywhere reports it: axe has no rule for a missing
|
|
39
|
+
* `aria-sort`, because a table with none is a table that is simply
|
|
40
|
+
* not sorted as far as the accessibility tree knows.
|
|
41
|
+
*
|
|
42
|
+
* This is therefore the one component in M6 whose payoff is not
|
|
43
|
+
* duplication — there is a single site — but a class of failure
|
|
44
|
+
* that is invisible to every gate the fleet has.
|
|
45
|
+
*
|
|
46
|
+
* ### The cell announces, the button acts
|
|
47
|
+
*
|
|
48
|
+
* `aria-sort` belongs on the `<th>`; the click target is a `<button>`
|
|
49
|
+
* inside it. Putting the role on the button instead is the common
|
|
50
|
+
* mistake and it breaks the grid semantics — a `<th>` that is a
|
|
51
|
+
* button is no longer a column header to anything reading the table
|
|
52
|
+
* structure.
|
|
53
|
+
*
|
|
54
|
+
* The name stays the column's name. It is tempting to append the
|
|
55
|
+
* state — "Name, sorted ascending" — and that duplicates what
|
|
56
|
+
* `aria-sort` already says, so the column is announced twice and
|
|
57
|
+
* `getByRole("columnheader", { name: "Name" })` stops matching the
|
|
58
|
+
* moment somebody sorts it.
|
|
59
|
+
*/
|
|
60
|
+
export declare const SortableTableHeader: ({ children, className, direction, onSort, }: SortableTableHeaderProps) => ReactNode;
|
|
61
|
+
//# sourceMappingURL=SortableTableHeader.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"SortableTableHeader.d.ts","sourceRoot":"","sources":["../../src/SortableTableHeader/SortableTableHeader.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AAKtC,MAAM,MAAM,aAAa,GAAG,WAAW,GAAG,YAAY,CAAA;AAEtD,MAAM,MAAM,wBAAwB,GAAG;IACrC,QAAQ,EAAE,SAAS,CAAA;IACnB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB;;;;;;OAMG;IACH,SAAS,CAAC,EAAE,aAAa,CAAA;IACzB,iEAAiE;IACjE,MAAM,EAAE,CAAC,SAAS,EAAE,aAAa,KAAK,IAAI,CAAA;CAC3C,CAAA;AAQD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA0CG;AACH,eAAO,MAAM,mBAAmB,GAAI,6CAKjC,wBAAwB,KAAG,SAsD7B,CAAA"}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { FOCUS_RING_CLASS } from "../intentStyles.js";
|
|
3
|
+
import { toClassName } from "../toClassName.js";
|
|
4
|
+
const NEXT_DIRECTION = {
|
|
5
|
+
ascending: "descending",
|
|
6
|
+
descending: "ascending",
|
|
7
|
+
};
|
|
8
|
+
/**
|
|
9
|
+
* **The header cell, and nothing else.** No `DataTable`, no rows, no
|
|
10
|
+
* comparator — the plan is explicit about that, and the reason is
|
|
11
|
+
* that sorting *data* is app logic that differs per column type,
|
|
12
|
+
* while announcing *that* a column is sorted is markup every table
|
|
13
|
+
* gets wrong the same way.
|
|
14
|
+
*
|
|
15
|
+
* ### `aria-sort` does not exist anywhere in the fleet
|
|
16
|
+
*
|
|
17
|
+
* Not in mux-magic, not in rip-deck, not in castkit. mux-magic's
|
|
18
|
+
* `FileExplorerModal` — the one sortable table there is — renders
|
|
19
|
+
* the state as a character:
|
|
20
|
+
*
|
|
21
|
+
* ```tsx
|
|
22
|
+
* {sortDirection === "asc" ? "▲" : "▼"}
|
|
23
|
+
* ```
|
|
24
|
+
*
|
|
25
|
+
* A screen reader announces that as "black up-pointing triangle", if
|
|
26
|
+
* the font has it at all, and the sandbox's headless Chromium does
|
|
27
|
+
* not have it — so the same glyph measures blank in a screenshot.
|
|
28
|
+
* Sighted users see a direction, everyone else gets nothing, and no
|
|
29
|
+
* automated check anywhere reports it: axe has no rule for a missing
|
|
30
|
+
* `aria-sort`, because a table with none is a table that is simply
|
|
31
|
+
* not sorted as far as the accessibility tree knows.
|
|
32
|
+
*
|
|
33
|
+
* This is therefore the one component in M6 whose payoff is not
|
|
34
|
+
* duplication — there is a single site — but a class of failure
|
|
35
|
+
* that is invisible to every gate the fleet has.
|
|
36
|
+
*
|
|
37
|
+
* ### The cell announces, the button acts
|
|
38
|
+
*
|
|
39
|
+
* `aria-sort` belongs on the `<th>`; the click target is a `<button>`
|
|
40
|
+
* inside it. Putting the role on the button instead is the common
|
|
41
|
+
* mistake and it breaks the grid semantics — a `<th>` that is a
|
|
42
|
+
* button is no longer a column header to anything reading the table
|
|
43
|
+
* structure.
|
|
44
|
+
*
|
|
45
|
+
* The name stays the column's name. It is tempting to append the
|
|
46
|
+
* state — "Name, sorted ascending" — and that duplicates what
|
|
47
|
+
* `aria-sort` already says, so the column is announced twice and
|
|
48
|
+
* `getByRole("columnheader", { name: "Name" })` stops matching the
|
|
49
|
+
* moment somebody sorts it.
|
|
50
|
+
*/
|
|
51
|
+
export const SortableTableHeader = ({ children, className, direction, onSort, }) => (_jsx("th", { "aria-sort": direction ?? "none", className: toClassName("border-border-subtle border-b p-0 text-start", className), scope: "col", children: _jsxs("button", { className: toClassName("flex w-full cursor-pointer items-center gap-1.5 bg-transparent px-3 py-2 text-start font-medium text-content-secondary text-xs uppercase tracking-wide transition-colors duration-(--duration-fast) ease-standard", "hover:text-content-primary", FOCUS_RING_CLASS), onClick: () => {
|
|
52
|
+
onSort(direction === undefined
|
|
53
|
+
? "ascending"
|
|
54
|
+
: NEXT_DIRECTION[direction]);
|
|
55
|
+
}, type: "button", children: [children, _jsx("svg", { "aria-hidden": "true", className: toClassName("size-3.5 shrink-0 transition-[transform,opacity] duration-(--duration-fast) ease-standard", direction === undefined && "opacity-0", direction === "descending" && "rotate-180"), fill: "none", focusable: false, stroke: "currentColor", strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, viewBox: "0 0 24 24", xmlns: "http://www.w3.org/2000/svg", children: _jsx("path", { d: "m6 15 6-6 6 6" }) })] }) }));
|
|
56
|
+
//# sourceMappingURL=SortableTableHeader.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"SortableTableHeader.js","sourceRoot":"","sources":["../../src/SortableTableHeader/SortableTableHeader.tsx"],"names":[],"mappings":";AAEA,OAAO,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAA;AACrD,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAA;AAmB/C,MAAM,cAAc,GAClB;IACE,SAAS,EAAE,YAAY;IACvB,UAAU,EAAE,WAAW;CACxB,CAAA;AAEH;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA0CG;AACH,MAAM,CAAC,MAAM,mBAAmB,GAAG,CAAC,EAClC,QAAQ,EACR,SAAS,EACT,SAAS,EACT,MAAM,GACmB,EAAa,EAAE,CAAC,CACzC,0BAMa,SAAS,IAAI,MAAM,EAC9B,SAAS,EAAE,WAAW,CACpB,8CAA8C,EAC9C,SAAS,CACV,EACD,KAAK,EAAC,KAAK,YAEX,kBACE,SAAS,EAAE,WAAW,CACpB,mNAAmN,EACnN,4BAA4B,EAC5B,gBAAgB,CACjB,EACD,OAAO,EAAE,GAAG,EAAE;YACZ,MAAM,CACJ,SAAS,KAAK,SAAS;gBACrB,CAAC,CAAC,WAAW;gBACb,CAAC,CAAC,cAAc,CAAC,SAAS,CAAC,CAC9B,CAAA;QACH,CAAC,EACD,IAAI,EAAC,QAAQ,aAEZ,QAAQ,EAET,6BAIc,MAAM,EAClB,SAAS,EAAE,WAAW,CACpB,2FAA2F,EAC3F,SAAS,KAAK,SAAS,IAAI,WAAW,EACtC,SAAS,KAAK,YAAY,IAAI,YAAY,CAC3C,EACD,IAAI,EAAC,MAAM,EACX,SAAS,EAAE,KAAK,EAChB,MAAM,EAAC,cAAc,EACrB,aAAa,EAAC,OAAO,EACrB,cAAc,EAAC,OAAO,EACtB,WAAW,EAAE,CAAC,EACd,OAAO,EAAC,WAAW,EACnB,KAAK,EAAC,4BAA4B,YAElC,eAAM,CAAC,EAAC,eAAe,GAAG,GACtB,IACC,GACN,CACN,CAAA"}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { IntentName } from "@charcuterie/tokens";
|
|
2
|
+
import type { ReactNode } from "react";
|
|
3
|
+
export type ToastProps = {
|
|
4
|
+
/** Detail under the title. Optional — many toasts are one line. */
|
|
5
|
+
children?: ReactNode;
|
|
6
|
+
className?: string;
|
|
7
|
+
/**
|
|
8
|
+
* Milliseconds at `visible` before it starts leaving. `0` pins it
|
|
9
|
+
* open, which is what a toast carrying an action has to do — WCAG
|
|
10
|
+
* 2.2.1 gives a user the right to finish reading.
|
|
11
|
+
*/
|
|
12
|
+
duration?: number;
|
|
13
|
+
/** How long `exiting` lasts, matched to the transition below. */
|
|
14
|
+
exitDuration?: number;
|
|
15
|
+
intent?: IntentName;
|
|
16
|
+
/** Called once, when the machine reaches its terminal state. */
|
|
17
|
+
onRemove: () => void;
|
|
18
|
+
/**
|
|
19
|
+
* A string, not a `ReactNode`. It is what the region announces and
|
|
20
|
+
* what names this toast's own dismiss button — a stack of three
|
|
21
|
+
* toasts all offering "Dismiss" is three matches for one query,
|
|
22
|
+
* which `expectAgentDrivable` treats as a failure.
|
|
23
|
+
*/
|
|
24
|
+
title: string;
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* One toast, and its whole job is the four-state machine the plan
|
|
28
|
+
* specified and M2 left unbuilt.
|
|
29
|
+
*
|
|
30
|
+
* ### Why a `Status`, when a boolean and a timeout would do
|
|
31
|
+
*
|
|
32
|
+
* Because a boolean cannot express *why* the node is still mounted.
|
|
33
|
+
* A toast that is leaving is still painted, still occupies its slot,
|
|
34
|
+
* and must not be announced again — and `isVisible === false` says
|
|
35
|
+
* none of that. The states are what a reader needs to know the
|
|
36
|
+
* difference between "arriving", "leaving", and "gone but not yet
|
|
37
|
+
* unmounted", and `createStatus` makes the illegal ones throw:
|
|
38
|
+
* `visible → removed` skips the exit and is rejected, and `removed`
|
|
39
|
+
* is terminal so a late timer cannot remove the same toast twice.
|
|
40
|
+
*
|
|
41
|
+
* That last one is not hypothetical. Hover-to-pause plus a dismiss
|
|
42
|
+
* button means two independent timers can be in flight, and the
|
|
43
|
+
* hand-rolled version of this component reconciles them with a
|
|
44
|
+
* `clearTimeout` and a ref. Here the second one throws.
|
|
45
|
+
*
|
|
46
|
+
* ### It does not announce itself
|
|
47
|
+
*
|
|
48
|
+
* No `role="status"` and no `aria-live` on this element — the
|
|
49
|
+
* `ToastRegion` around it owns that, and a live region inside a live
|
|
50
|
+
* region is a message announced twice. See that component for the
|
|
51
|
+
* reason there is only ever one, and it is polite.
|
|
52
|
+
*/
|
|
53
|
+
export declare const Toast: ({ children, className, duration, exitDuration, intent, onRemove, title, }: ToastProps) => ReactNode;
|
|
54
|
+
//# sourceMappingURL=Toast.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Toast.d.ts","sourceRoot":"","sources":["../../src/Toast/Toast.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,qBAAqB,CAAA;AACrD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AAYtC,MAAM,MAAM,UAAU,GAAG;IACvB,mEAAmE;IACnE,QAAQ,CAAC,EAAE,SAAS,CAAA;IACpB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB;;;;OAIG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,iEAAiE;IACjE,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,MAAM,CAAC,EAAE,UAAU,CAAA;IACnB,gEAAgE;IAChE,QAAQ,EAAE,MAAM,IAAI,CAAA;IACpB;;;;;OAKG;IACH,KAAK,EAAE,MAAM,CAAA;CACd,CAAA;AASD;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,eAAO,MAAM,KAAK,GAAI,2EAQnB,UAAU,KAAG,SA+If,CAAA"}
|