@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.
Files changed (115) hide show
  1. package/README.md +9 -0
  2. package/dist/Accordion/Accordion.d.ts +82 -0
  3. package/dist/Accordion/Accordion.d.ts.map +1 -0
  4. package/dist/Accordion/Accordion.js +108 -0
  5. package/dist/Accordion/Accordion.js.map +1 -0
  6. package/dist/Accordion/AccordionSection.d.ts +28 -0
  7. package/dist/Accordion/AccordionSection.d.ts.map +1 -0
  8. package/dist/Accordion/AccordionSection.js +53 -0
  9. package/dist/Accordion/AccordionSection.js.map +1 -0
  10. package/dist/Field/Field.d.ts +104 -0
  11. package/dist/Field/Field.d.ts.map +1 -0
  12. package/dist/Field/Field.js +115 -0
  13. package/dist/Field/Field.js.map +1 -0
  14. package/dist/Field/FieldGroup.d.ts +58 -0
  15. package/dist/Field/FieldGroup.d.ts.map +1 -0
  16. package/dist/Field/FieldGroup.js +65 -0
  17. package/dist/Field/FieldGroup.js.map +1 -0
  18. package/dist/FileDropZone/FileDropZone.d.ts +63 -0
  19. package/dist/FileDropZone/FileDropZone.d.ts.map +1 -0
  20. package/dist/FileDropZone/FileDropZone.js +122 -0
  21. package/dist/FileDropZone/FileDropZone.js.map +1 -0
  22. package/dist/LogViewer/LogViewer.d.ts +90 -0
  23. package/dist/LogViewer/LogViewer.d.ts.map +1 -0
  24. package/dist/LogViewer/LogViewer.js +165 -0
  25. package/dist/LogViewer/LogViewer.js.map +1 -0
  26. package/dist/Menu/Menu.d.ts +81 -0
  27. package/dist/Menu/Menu.d.ts.map +1 -0
  28. package/dist/Menu/Menu.js +199 -0
  29. package/dist/Menu/Menu.js.map +1 -0
  30. package/dist/Menu/MenuAction.d.ts +24 -0
  31. package/dist/Menu/MenuAction.d.ts.map +1 -0
  32. package/dist/Menu/MenuAction.js +37 -0
  33. package/dist/Menu/MenuAction.js.map +1 -0
  34. package/dist/Select/Select.d.ts +87 -0
  35. package/dist/Select/Select.d.ts.map +1 -0
  36. package/dist/Select/Select.js +51 -0
  37. package/dist/Select/Select.js.map +1 -0
  38. package/dist/SortableTableHeader/SortableTableHeader.d.ts +61 -0
  39. package/dist/SortableTableHeader/SortableTableHeader.d.ts.map +1 -0
  40. package/dist/SortableTableHeader/SortableTableHeader.js +56 -0
  41. package/dist/SortableTableHeader/SortableTableHeader.js.map +1 -0
  42. package/dist/Toast/Toast.d.ts +54 -0
  43. package/dist/Toast/Toast.d.ts.map +1 -0
  44. package/dist/Toast/Toast.js +121 -0
  45. package/dist/Toast/Toast.js.map +1 -0
  46. package/dist/Toast/ToastRegion.d.ts +57 -0
  47. package/dist/Toast/ToastRegion.d.ts.map +1 -0
  48. package/dist/Toast/ToastRegion.js +41 -0
  49. package/dist/Toast/ToastRegion.js.map +1 -0
  50. package/dist/Toast/toastLifecycle.d.ts +34 -0
  51. package/dist/Toast/toastLifecycle.d.ts.map +1 -0
  52. package/dist/Toast/toastLifecycle.js +25 -0
  53. package/dist/Toast/toastLifecycle.js.map +1 -0
  54. package/dist/Tooltip/Tooltip.d.ts +82 -0
  55. package/dist/Tooltip/Tooltip.d.ts.map +1 -0
  56. package/dist/Tooltip/Tooltip.js +124 -0
  57. package/dist/Tooltip/Tooltip.js.map +1 -0
  58. package/dist/index.d.ts +26 -0
  59. package/dist/index.d.ts.map +1 -1
  60. package/dist/index.js +13 -0
  61. package/dist/index.js.map +1 -1
  62. package/dist/mountStory.testHelpers.d.ts +0 -11
  63. package/dist/mountStory.testHelpers.d.ts.map +1 -1
  64. package/dist/mountStory.testHelpers.js +27 -0
  65. package/dist/mountStory.testHelpers.js.map +1 -1
  66. package/dist/slotProps.d.ts +110 -0
  67. package/dist/slotProps.d.ts.map +1 -0
  68. package/dist/slotProps.js +39 -0
  69. package/dist/slotProps.js.map +1 -0
  70. package/package.json +3 -3
  71. package/src/Accordion/Accordion.mdx +83 -0
  72. package/src/Accordion/Accordion.stories.tsx +146 -0
  73. package/src/Accordion/Accordion.test.tsx +163 -0
  74. package/src/Accordion/Accordion.tsx +182 -0
  75. package/src/Accordion/AccordionSection.tsx +163 -0
  76. package/src/Field/Field.mdx +144 -0
  77. package/src/Field/Field.stories.tsx +280 -0
  78. package/src/Field/Field.test.tsx +344 -0
  79. package/src/Field/Field.tsx +212 -0
  80. package/src/Field/FieldGroup.tsx +138 -0
  81. package/src/FileDropZone/FileDropZone.mdx +78 -0
  82. package/src/FileDropZone/FileDropZone.stories.tsx +136 -0
  83. package/src/FileDropZone/FileDropZone.test.tsx +230 -0
  84. package/src/FileDropZone/FileDropZone.tsx +234 -0
  85. package/src/LogViewer/LogViewer.mdx +123 -0
  86. package/src/LogViewer/LogViewer.stories.tsx +169 -0
  87. package/src/LogViewer/LogViewer.test.tsx +236 -0
  88. package/src/LogViewer/LogViewer.tsx +261 -0
  89. package/src/Menu/Menu.mdx +103 -0
  90. package/src/Menu/Menu.stories.tsx +161 -0
  91. package/src/Menu/Menu.test.tsx +202 -0
  92. package/src/Menu/Menu.tsx +300 -0
  93. package/src/Menu/MenuAction.tsx +95 -0
  94. package/src/Select/Select.mdx +74 -0
  95. package/src/Select/Select.stories.tsx +126 -0
  96. package/src/Select/Select.test.tsx +129 -0
  97. package/src/Select/Select.tsx +200 -0
  98. package/src/SortableTableHeader/SortableTableHeader.mdx +67 -0
  99. package/src/SortableTableHeader/SortableTableHeader.stories.tsx +207 -0
  100. package/src/SortableTableHeader/SortableTableHeader.test.tsx +180 -0
  101. package/src/SortableTableHeader/SortableTableHeader.tsx +131 -0
  102. package/src/Toast/Toast.mdx +84 -0
  103. package/src/Toast/Toast.stories.tsx +192 -0
  104. package/src/Toast/Toast.test.tsx +217 -0
  105. package/src/Toast/Toast.tsx +224 -0
  106. package/src/Toast/ToastRegion.tsx +98 -0
  107. package/src/Toast/toastLifecycle.ts +45 -0
  108. package/src/Tooltip/Tooltip.mdx +69 -0
  109. package/src/Tooltip/Tooltip.stories.tsx +104 -0
  110. package/src/Tooltip/Tooltip.test.tsx +138 -0
  111. package/src/Tooltip/Tooltip.tsx +193 -0
  112. package/src/index.ts +47 -0
  113. package/src/mountStory.testHelpers.ts +30 -0
  114. package/src/slotProps.ts +143 -0
  115. package/src/sourceRules.test.ts +221 -30
@@ -0,0 +1,104 @@
1
+ import type { Meta, StoryObj } from "@storybook/react"
2
+
3
+ import { placementArgType } from "../argTypes.storyHelpers.ts"
4
+ import { Button } from "../Button/Button.tsx"
5
+ import {
6
+ StoryCell,
7
+ StoryGrid,
8
+ } from "../board.storyHelpers.tsx"
9
+ import { IconButton } from "../IconButton/IconButton.tsx"
10
+ import { SettingsIcon } from "../icons.storyHelpers.tsx"
11
+ import { Tooltip } from "./Tooltip.tsx"
12
+
13
+ const meta = {
14
+ title: "Components/Tooltip",
15
+ component: Tooltip,
16
+ parameters: { layout: "centered" },
17
+ argTypes: { placement: placementArgType },
18
+ args: {
19
+ delay: 200,
20
+ label: "Re-reads the disc from title 1.",
21
+ placement: "top",
22
+ },
23
+ } satisfies Meta<typeof Tooltip>
24
+
25
+ export default meta
26
+
27
+ type Story = StoryObj<typeof meta>
28
+
29
+ export const Default: Story = {
30
+ args: {
31
+ children: <Button appearance="outline">Retry</Button>,
32
+ label: "Re-reads the disc from title 1.",
33
+ },
34
+ }
35
+
36
+ /**
37
+ * A tooltip **adds**; it never *is* the name. The icon button below
38
+ * still carries its own `aria-label` — the tip is the sentence you
39
+ * would otherwise have had no room for.
40
+ */
41
+ export const AllVariants: Story = {
42
+ args: {
43
+ children: <Button appearance="outline">Retry</Button>,
44
+ label: "Re-reads the disc from title 1.",
45
+ },
46
+ render: () => (
47
+ <StoryGrid columns={3}>
48
+ <StoryCell label="on a button">
49
+ <Tooltip label="Re-reads the disc from title 1.">
50
+ <Button appearance="outline">Retry</Button>
51
+ </Tooltip>
52
+ </StoryCell>
53
+
54
+ <StoryCell label="on an IconButton">
55
+ <Tooltip label="Opens the bay's rip settings.">
56
+ <IconButton label="Bay settings">
57
+ <SettingsIcon />
58
+ </IconButton>
59
+ </Tooltip>
60
+ </StoryCell>
61
+
62
+ <StoryCell label="placed below">
63
+ <Tooltip
64
+ label="Skips this title and carries on."
65
+ placement="bottom"
66
+ >
67
+ <Button appearance="outline">Skip</Button>
68
+ </Tooltip>
69
+ </StoryCell>
70
+ </StoryGrid>
71
+ ),
72
+ }
73
+
74
+ /**
75
+ * Closed until something asks for it. There is no "open" prop: a
76
+ * tooltip's visibility is entirely a consequence of hover or focus,
77
+ * which is the one piece of state in this library a consumer has no
78
+ * business holding.
79
+ */
80
+ export const AllStates: Story = {
81
+ args: {
82
+ children: (
83
+ <Button appearance="outline">
84
+ Hover or Tab to me
85
+ </Button>
86
+ ),
87
+ label: "Re-reads the disc from title 1.",
88
+ },
89
+ }
90
+
91
+ /**
92
+ * Tab to the button. The tip opens **on focus** — the line
93
+ * mux-magic's `FieldTooltip` is missing, which makes its version
94
+ * pointer-only and a WCAG 2.1.1 failure on the control whose
95
+ * explanation lives there. Escape closes it, per WCAG 1.4.13.
96
+ */
97
+ export const Interactive: Story = {
98
+ args: {
99
+ children: (
100
+ <Button appearance="outline">Keyboard tip</Button>
101
+ ),
102
+ label: "Opened by focus, closed by Escape.",
103
+ },
104
+ }
@@ -0,0 +1,138 @@
1
+ import { composeStories } from "@storybook/react"
2
+ import { expect, userEvent, waitFor } from "storybook/test"
3
+ import { test } from "vitest"
4
+
5
+ import { expectNoAxeViolations } from "../expectNoAxeViolations.testHelpers.ts"
6
+ import { mountStory } from "../mountStory.testHelpers.ts"
7
+ import { expectAgentDrivable } from "../testing/index.ts"
8
+ import * as stories from "./Tooltip.stories.tsx"
9
+
10
+ const { AllVariants, Default, Interactive } =
11
+ composeStories(stories)
12
+
13
+ test("nothing is shown until something asks", async () => {
14
+ const { canvas, canvasElement } =
15
+ await mountStory(Default)
16
+
17
+ expectAgentDrivable(canvas, {
18
+ name: "Retry",
19
+ role: "button",
20
+ })
21
+
22
+ await expect(
23
+ canvas.queryAllByRole("tooltip"),
24
+ ).toHaveLength(0)
25
+
26
+ await expectNoAxeViolations(canvasElement)
27
+ })
28
+
29
+ /**
30
+ * The defect that matters most in mux-magic's `FieldTooltip`: it
31
+ * renders `role="tooltip"` and **no `aria-describedby` anywhere**,
32
+ * so the control it belongs to never mentions it. `role="tooltip"`
33
+ * is not a live region and does nothing on its own — the tip is a
34
+ * floating node nobody is pointed at.
35
+ */
36
+ test("the trigger is described by the tip that opened", async () => {
37
+ const { canvas, canvasElement } =
38
+ await mountStory(Interactive)
39
+
40
+ const trigger = expectAgentDrivable(canvas, {
41
+ name: "Keyboard tip",
42
+ role: "button",
43
+ })
44
+
45
+ await expect(trigger).not.toHaveAttribute(
46
+ "aria-describedby",
47
+ )
48
+
49
+ await userEvent.tab()
50
+
51
+ await waitFor(() => {
52
+ expect(trigger).toHaveFocus()
53
+ })
54
+
55
+ const tip = await waitFor(() =>
56
+ expectAgentDrivable(canvas, { role: "tooltip" }),
57
+ )
58
+
59
+ await expect(trigger).toHaveAttribute(
60
+ "aria-describedby",
61
+ tip.id,
62
+ )
63
+
64
+ await expect(tip).toHaveTextContent(
65
+ "Opened by focus, closed by Escape.",
66
+ )
67
+
68
+ // Audited in the *driven* state, not at mount. The a11y addon
69
+ // runs axe once, when `run()` resolves — before anything has been
70
+ // focused — so an open tip would never be seen by it.
71
+ await expectNoAxeViolations(canvasElement)
72
+ })
73
+
74
+ /**
75
+ * Focus, not just hover. `useFocus` is the line mux-magic is
76
+ * missing, and without it the tip is pointer-only — a WCAG 2.1.1
77
+ * failure on the one control whose explanation lives there.
78
+ */
79
+ test("focus opens it and blur closes it", async () => {
80
+ const { canvas } = await mountStory(Interactive)
81
+
82
+ await userEvent.tab()
83
+
84
+ await waitFor(() => {
85
+ expectAgentDrivable(canvas, { role: "tooltip" })
86
+ })
87
+
88
+ await userEvent.tab()
89
+
90
+ await waitFor(() => {
91
+ expect(canvas.queryAllByRole("tooltip")).toHaveLength(0)
92
+ })
93
+ })
94
+
95
+ test("Escape dismisses it", async () => {
96
+ const { canvas } = await mountStory(Interactive)
97
+
98
+ await userEvent.tab()
99
+
100
+ await waitFor(() => {
101
+ expectAgentDrivable(canvas, { role: "tooltip" })
102
+ })
103
+
104
+ // WCAG 1.4.13 requires content shown on hover or focus to be
105
+ // dismissable without moving the pointer or the caret.
106
+ await userEvent.keyboard("{Escape}")
107
+
108
+ await waitFor(() => {
109
+ expect(canvas.queryAllByRole("tooltip")).toHaveLength(0)
110
+ })
111
+ })
112
+
113
+ /**
114
+ * A tooltip **adds**; it is never the name. An `IconButton` still
115
+ * carries its own `aria-label`, and the tip is the sentence there
116
+ * was no room for — so the button stays findable by the same query
117
+ * whether or not the tip is open.
118
+ */
119
+ test("the tip does not become the control's name", async () => {
120
+ const { canvas } = await mountStory(AllVariants)
121
+
122
+ const iconButton = expectAgentDrivable(canvas, {
123
+ name: "Bay settings",
124
+ role: "button",
125
+ })
126
+
127
+ await userEvent.hover(iconButton)
128
+
129
+ await waitFor(() => {
130
+ expectAgentDrivable(canvas, { role: "tooltip" })
131
+ })
132
+
133
+ // Still "Bay settings", not "Opens the bay's rip settings."
134
+ expectAgentDrivable(canvas, {
135
+ name: "Bay settings",
136
+ role: "button",
137
+ })
138
+ })
@@ -0,0 +1,193 @@
1
+ import {
2
+ useClonedChild,
3
+ useVisibility,
4
+ } from "@charcuterie/logic"
5
+ import type { Placement } from "@floating-ui/react"
6
+ import {
7
+ autoUpdate,
8
+ flip,
9
+ offset,
10
+ shift,
11
+ useDismiss,
12
+ useFloating,
13
+ useFocus,
14
+ useHover,
15
+ useInteractions,
16
+ useRole,
17
+ } from "@floating-ui/react"
18
+ import type { ReactElement, ReactNode } from "react"
19
+
20
+ import type { SlotProps } from "../slotProps.ts"
21
+ import { mergeSlotProps } from "../slotProps.ts"
22
+ import { toClassName } from "../toClassName.ts"
23
+
24
+ export type TooltipProps = SlotProps & {
25
+ /**
26
+ * The control the tip describes. **Cloned, not wrapped** — the
27
+ * same slot contract as `Popover`'s trigger.
28
+ *
29
+ * It has to be something focusable. A tooltip on a `<span>` is
30
+ * pointer-only, which is the state mux-magic's is in today.
31
+ */
32
+ children: ReactElement
33
+ className?: string
34
+ /** Milliseconds of hover before it opens. */
35
+ delay?: number
36
+ /**
37
+ * Text, and text only. `role="tooltip"` has no way to move focus
38
+ * into itself, so a link or a button inside one is unreachable by
39
+ * keyboard and unreadable by touch — if it needs a control, it is
40
+ * a `Popover`.
41
+ */
42
+ label: string
43
+ placement?: Placement
44
+ }
45
+
46
+ /**
47
+ * Replaces mux-magic's `FieldTooltip`, which is 130 lines of
48
+ * hand-rolled positioning — a `computePosition` that reimplements
49
+ * `flip` and `shift` against `window.innerHeight` — and has three
50
+ * defects that all render perfectly:
51
+ *
52
+ * 1. **Nothing references the tip.** It sets `role="tooltip"` and
53
+ * no `aria-describedby` anywhere, so the control it belongs to
54
+ * never mentions it. A screen reader reads the button, says
55
+ * nothing about the tip, and the tip itself is a floating node
56
+ * nobody is pointed at. `role="tooltip"` is not a live region;
57
+ * it does nothing on its own.
58
+ * 2. **Pointer only.** `onPointerEnter`/`onPointerLeave` with no
59
+ * `onFocus`/`onBlur`, so a keyboard user cannot open it at all.
60
+ * 3. **No dismiss.** Escape does nothing, which WCAG 1.4.13
61
+ * requires of any content shown on hover.
62
+ *
63
+ * All three come from the same place: the ARIA is written by hand
64
+ * beside the behaviour instead of falling out of it. Here `useRole`
65
+ * writes `aria-describedby` on the trigger and `role` + `id` on the
66
+ * tip from one source, `useFocus` is what makes it keyboard-openable,
67
+ * and `useDismiss` is Escape.
68
+ *
69
+ * ### Not a `title`, and not a substitute for a name
70
+ *
71
+ * A `title` attribute is unstyleable, has a ~1s delay nobody can
72
+ * change, and does not exist on touch. `Badge` still uses one — for
73
+ * a *truncation* readout, where the full text is already in
74
+ * `textContent` and the tip is a convenience
75
+ * ([decision](../../../docs/decisions/2026-07-30-a-badge-truncates-with-text-overflow.md)).
76
+ * That is the line: a `title` may repeat something already
77
+ * available, and a `Tooltip` may add something, but **neither may be
78
+ * the only place a control's name exists**. An icon button still
79
+ * needs its `aria-label`; the tip is extra.
80
+ *
81
+ * ### Touch has no hover, and this component does not pretend
82
+ *
83
+ * There is no long-press fallback here. A tooltip is supplementary
84
+ * by construction, so on the kiosk it simply never appears, and
85
+ * anything a touch user *must* read belongs in a `Field`'s
86
+ * `description` instead. Building a long-press would make the tip
87
+ * load-bearing on exactly the surface where it is least reliable.
88
+ *
89
+ * ### A slot is a pass-through
90
+ *
91
+ * A `Tooltip` is the natural thing to put between a `Field` and its
92
+ * control — mux-magic's `FieldLabel` renders a `FieldTooltip` for
93
+ * exactly that reason. Both components clone onto their one child, so
94
+ * an outer `Field` used to hand *this component* the `id`,
95
+ * `aria-describedby`, `aria-invalid` and `required` meant for the
96
+ * `<input>`, and this component dropped all four without a word.
97
+ *
98
+ * Anything in `SlotProps` that arrives from above is forwarded to the
99
+ * trigger, and `aria-describedby` is **merged** with the tip's own id
100
+ * rather than overwriting it. `slotProps.ts` has the reasoning.
101
+ */
102
+ export const Tooltip = ({
103
+ children,
104
+ className,
105
+ delay = 200,
106
+ label,
107
+ placement = "top",
108
+ ...receivedSlotProps
109
+ }: TooltipProps): ReactNode => {
110
+ const { isVisible, setIsVisible } = useVisibility()
111
+
112
+ const { context, floatingStyles, refs } = useFloating({
113
+ // Read-only, as everywhere: floating-ui is told what is true and
114
+ // never decides it.
115
+ open: isVisible,
116
+ onOpenChange: setIsVisible,
117
+ placement,
118
+ middleware: [offset(6), flip(), shift({ padding: 8 })],
119
+ strategy: "fixed",
120
+ whileElementsMounted: autoUpdate,
121
+ })
122
+
123
+ const { getFloatingProps, getReferenceProps } =
124
+ useInteractions([
125
+ useHover(context, {
126
+ // `move: false` — a tooltip that re-opens because the
127
+ // pointer twitched inside the trigger is the flicker every
128
+ // hand-rolled one has.
129
+ move: false,
130
+ restMs: delay,
131
+ }),
132
+ // The line mux-magic is missing. Without it the tip is
133
+ // pointer-only, which is a WCAG 2.1.1 failure on a control
134
+ // whose explanation lives there.
135
+ useFocus(context),
136
+ useDismiss(context, { referencePress: false }),
137
+ useRole(context, { role: "tooltip" }),
138
+ ])
139
+
140
+ const clonedTrigger = useClonedChild(
141
+ children,
142
+ // `mergeSlotProps` rather than a spread, because
143
+ // `getReferenceProps()` writes an `aria-describedby` of its own —
144
+ // `useRole` pointing the trigger at the tip — and an outer
145
+ // `Field` writes one naming its description and its error. A
146
+ // spread keeps whichever went last.
147
+ mergeSlotProps(receivedSlotProps, {
148
+ ...getReferenceProps(),
149
+ ref: refs.setReference,
150
+ }),
151
+ )
152
+
153
+ return (
154
+ <>
155
+ {clonedTrigger}
156
+
157
+ {isVisible ? (
158
+ <div
159
+ {...getFloatingProps()}
160
+ className={toClassName(
161
+ // `intent-neutral-solid` + its own `on-solid`, rather
162
+ // than `surface-inverse` + `content-on-accent`. The
163
+ // second pair looks like the obvious one and is not a
164
+ // *pair* at all — nothing in the token set promises those
165
+ // two contrast, and in `daylight` dark they land at
166
+ // 2.9:1. The `solid`/`on-solid` roles exist precisely to
167
+ // be a guaranteed couple, and the contrast gate checks
168
+ // them as one.
169
+ "inset-auto m-0 max-w-xs rounded-md bg-intent-neutral-solid px-2 py-1 text-intent-neutral-on-solid text-xs shadow-medium",
170
+ className,
171
+ )}
172
+ // No `popover="manual"` and no `FloatingFocusManager`,
173
+ // unlike `Popover`. Both exist to manage focus, and focus
174
+ // never enters a tooltip — moving it here is what makes
175
+ // one unescapable. The trade is that a tooltip is not in
176
+ // the top layer, so it can be clipped by an
177
+ // `overflow: hidden` ancestor; `strategy: "fixed"` keeps
178
+ // that to ancestors that actually clip fixed descendants.
179
+ ref={refs.setFloating}
180
+ // Duplicated from `getFloatingProps()` deliberately, and
181
+ // the two cannot differ — `useRole(context, { role:
182
+ // "tooltip" })` above is where it comes from. Stated here
183
+ // because a linter cannot see a role through a spread, and
184
+ // neither can the next reader.
185
+ role="tooltip"
186
+ style={floatingStyles}
187
+ >
188
+ {label}
189
+ </div>
190
+ ) : null}
191
+ </>
192
+ )
193
+ }
package/src/index.ts CHANGED
@@ -31,6 +31,12 @@
31
31
  * this package's README has always told it to do.
32
32
  */
33
33
 
34
+ export type {
35
+ AccordionHeadingLevel,
36
+ AccordionItem,
37
+ AccordionProps,
38
+ } from "./Accordion/Accordion.tsx"
39
+ export { Accordion } from "./Accordion/Accordion.tsx"
34
40
  export type {
35
41
  AlertProps,
36
42
  AlertSize,
@@ -60,6 +66,12 @@ export type {
60
66
  EmptyStateSize,
61
67
  } from "./EmptyState/EmptyState.tsx"
62
68
  export { EmptyState } from "./EmptyState/EmptyState.tsx"
69
+ export type { FieldProps } from "./Field/Field.tsx"
70
+ export { Field } from "./Field/Field.tsx"
71
+ export type { FieldGroupProps } from "./Field/FieldGroup.tsx"
72
+ export { FieldGroup } from "./Field/FieldGroup.tsx"
73
+ export type { FileDropZoneProps } from "./FileDropZone/FileDropZone.tsx"
74
+ export { FileDropZone } from "./FileDropZone/FileDropZone.tsx"
63
75
  export type { IconButtonProps } from "./IconButton/IconButton.tsx"
64
76
  export { IconButton } from "./IconButton/IconButton.tsx"
65
77
  export type { IntentAppearance } from "./intentStyles.ts"
@@ -76,6 +88,11 @@ export type {
76
88
  LiveStatusIndicatorSize,
77
89
  } from "./LiveStatusIndicator/LiveStatusIndicator.tsx"
78
90
  export { LiveStatusIndicator } from "./LiveStatusIndicator/LiveStatusIndicator.tsx"
91
+ export type {
92
+ LogLine,
93
+ LogViewerProps,
94
+ } from "./LogViewer/LogViewer.tsx"
95
+ export { LogViewer } from "./LogViewer/LogViewer.tsx"
79
96
  export type {
80
97
  MediaTileProps,
81
98
  MediaTileRatio,
@@ -83,6 +100,11 @@ export type {
83
100
  export { MediaTile } from "./MediaTile/MediaTile.tsx"
84
101
  export type { MediaStatus } from "./MediaTile/mediaStatus.ts"
85
102
  export { mediaTransitions } from "./MediaTile/mediaStatus.ts"
103
+ export type {
104
+ MenuItem,
105
+ MenuProps,
106
+ } from "./Menu/Menu.tsx"
107
+ export { Menu } from "./Menu/Menu.tsx"
86
108
  export type {
87
109
  ModalProps,
88
110
  ModalSize,
@@ -106,13 +128,27 @@ export type {
106
128
  SegmentedItem,
107
129
  } from "./SegmentedControl/SegmentedControl.tsx"
108
130
  export { SegmentedControl } from "./SegmentedControl/SegmentedControl.tsx"
131
+ export type {
132
+ SelectItem,
133
+ SelectOption,
134
+ SelectOptionGroup,
135
+ SelectProps,
136
+ } from "./Select/Select.tsx"
137
+ export { Select } from "./Select/Select.tsx"
109
138
  export type {
110
139
  SkeletonProps,
111
140
  SkeletonShape,
112
141
  } from "./Skeleton/Skeleton.tsx"
113
142
  export { Skeleton } from "./Skeleton/Skeleton.tsx"
143
+ export type {
144
+ SortableTableHeaderProps,
145
+ SortDirection,
146
+ } from "./SortableTableHeader/SortableTableHeader.tsx"
147
+ export { SortableTableHeader } from "./SortableTableHeader/SortableTableHeader.tsx"
114
148
  export type { SpinnerProps } from "./Spinner/Spinner.tsx"
115
149
  export { Spinner } from "./Spinner/Spinner.tsx"
150
+ export type { SlotProps } from "./slotProps.ts"
151
+ export { mergeSlotProps } from "./slotProps.ts"
116
152
  export {
117
153
  getAsyncIntent,
118
154
  getAsyncLabel,
@@ -127,6 +163,17 @@ export type {
127
163
  TabsProps,
128
164
  } from "./Tabs/Tabs.tsx"
129
165
  export { Tabs } from "./Tabs/Tabs.tsx"
166
+ export type { ToastProps } from "./Toast/Toast.tsx"
167
+ export { Toast } from "./Toast/Toast.tsx"
168
+ export type {
169
+ ToastRecord,
170
+ ToastRegionProps,
171
+ } from "./Toast/ToastRegion.tsx"
172
+ export { ToastRegion } from "./Toast/ToastRegion.tsx"
173
+ export type { ToastStatus } from "./Toast/toastLifecycle.ts"
174
+ export { toastTransitions } from "./Toast/toastLifecycle.ts"
175
+ export type { TooltipProps } from "./Tooltip/Tooltip.tsx"
176
+ export { Tooltip } from "./Tooltip/Tooltip.tsx"
130
177
  export { toClassName } from "./toClassName.ts"
131
178
  export type { VisuallyHiddenProps } from "./VisuallyHidden/VisuallyHidden.tsx"
132
179
  export { VisuallyHidden } from "./VisuallyHidden/VisuallyHidden.tsx"
@@ -25,16 +25,46 @@ type MountableStory = {
25
25
  * drift from it. It is also what triggers the a11y addon's
26
26
  * `afterEach`, so every mount here is an axe run.
27
27
  */
28
+ /**
29
+ * Every canvas this helper has created, so the next mount can take
30
+ * the previous ones out of the document.
31
+ *
32
+ * They used to accumulate — one detached-but-attached `<div>` per
33
+ * test, for the whole file — and nothing noticed for three
34
+ * milestones, because every component up to M6 lays out **in
35
+ * flow**: a stale copy sits harmlessly below the live one and every
36
+ * query is scoped to its own canvas anyway.
37
+ *
38
+ * `ToastRegion` is the first `position: fixed` component in the
39
+ * library, and it broke that immediately. A previous test's toast
40
+ * stack is pinned to the same corner of the viewport as the live
41
+ * one, so `userEvent.click` — which clicks by **coordinates** —
42
+ * drove the wrong element while every query still returned the
43
+ * right one. The test failed with "the toast did not go away", one
44
+ * test after the toast it was actually clicking.
45
+ *
46
+ * The stale tree is removed rather than unmounted because portable
47
+ * stories expose no unmount. Detaching is enough: it takes the node
48
+ * out of the layout, which is the entire problem.
49
+ */
50
+ const mountedCanvases: HTMLElement[] = []
51
+
28
52
  export const mountStory = async (
29
53
  story: MountableStory,
30
54
  ): Promise<{
31
55
  canvas: Canvas
32
56
  canvasElement: HTMLElement
33
57
  }> => {
58
+ for (const previous of mountedCanvases.splice(0)) {
59
+ previous.remove()
60
+ }
61
+
34
62
  const canvasElement = document.createElement("div")
35
63
 
36
64
  document.body.append(canvasElement)
37
65
 
66
+ mountedCanvases.push(canvasElement)
67
+
38
68
  await story.run({ canvasElement })
39
69
 
40
70
  return { canvas: within(canvasElement), canvasElement }