@stonedogcode/style 0.9.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/LICENSE +201 -0
- package/NOTICE +18 -0
- package/README.md +699 -0
- package/package.json +95 -0
- package/src/components/DictationControls.tsx +141 -0
- package/src/components/DictationPrompt.tsx +78 -0
- package/src/components/StyledBox.tsx +174 -0
- package/src/components/StyledButton.tsx +144 -0
- package/src/components/StyledCollapsible.tsx +127 -0
- package/src/components/StyledDefinitionList.tsx +134 -0
- package/src/components/StyledFieldset.tsx +157 -0
- package/src/components/StyledFlex.tsx +13 -0
- package/src/components/StyledFooter.tsx +399 -0
- package/src/components/StyledFormLabel.tsx +141 -0
- package/src/components/StyledGrid.tsx +109 -0
- package/src/components/StyledGridItem.tsx +19 -0
- package/src/components/StyledHStack.tsx +145 -0
- package/src/components/StyledHeading.tsx +79 -0
- package/src/components/StyledHrRule.tsx +33 -0
- package/src/components/StyledIcon.tsx +172 -0
- package/src/components/StyledIconButton.tsx +135 -0
- package/src/components/StyledInputBool.tsx +81 -0
- package/src/components/StyledInputRadio.tsx +141 -0
- package/src/components/StyledInputSelect.tsx +115 -0
- package/src/components/StyledInputSlider.tsx +83 -0
- package/src/components/StyledInputText.tsx +146 -0
- package/src/components/StyledInputTextArea.tsx +119 -0
- package/src/components/StyledInputToggle.tsx +224 -0
- package/src/components/StyledList.tsx +188 -0
- package/src/components/StyledScrollbar.tsx +53 -0
- package/src/components/StyledSearch.tsx +78 -0
- package/src/components/StyledSeparator.tsx +38 -0
- package/src/components/StyledSidebar.tsx +555 -0
- package/src/components/StyledSimpleGrid.tsx +99 -0
- package/src/components/StyledSparkLine.tsx +119 -0
- package/src/components/StyledSpinner.tsx +91 -0
- package/src/components/StyledStack.tsx +62 -0
- package/src/components/StyledText.tsx +99 -0
- package/src/components/StyledTooltip.tsx +398 -0
- package/src/components/StyledVStack.tsx +143 -0
- package/src/components/TitleLogo.tsx +223 -0
- package/src/components/create-icon.tsx +66 -0
- package/src/components/create-intent-button.tsx +134 -0
- package/src/components/dictation.ts +71 -0
- package/src/components/intent-buttons.ts +154 -0
- package/src/config/can-hover.ts +75 -0
- package/src/config/density.ts +138 -0
- package/src/config/font-size.ts +113 -0
- package/src/config/intent-icons.tsx +116 -0
- package/src/config/logger.ts +60 -0
- package/src/config/style-config.tsx +263 -0
- package/src/config/types.ts +137 -0
- package/src/index.ts +259 -0
- package/src/preset/index.ts +243 -0
- package/src/preset/recipes/arrows.ts +29 -0
- package/src/preset/recipes/box.ts +122 -0
- package/src/preset/recipes/button.ts +161 -0
- package/src/preset/recipes/dl-list.ts +109 -0
- package/src/preset/recipes/drawer.ts +125 -0
- package/src/preset/recipes/form.ts +95 -0
- package/src/preset/recipes/icon-button.ts +161 -0
- package/src/preset/recipes/icon.ts +34 -0
- package/src/preset/recipes/input-bool.ts +184 -0
- package/src/preset/recipes/input-dropdown.ts +93 -0
- package/src/preset/recipes/input-radio.ts +158 -0
- package/src/preset/recipes/input-surface.ts +152 -0
- package/src/preset/recipes/input-text.ts +17 -0
- package/src/preset/recipes/list.ts +196 -0
- package/src/preset/recipes/menu.ts +28 -0
- package/src/preset/recipes/separator.ts +89 -0
- package/src/preset/recipes/stack.ts +89 -0
- package/src/preset/recipes/striped.ts +34 -0
- package/src/preset/recipes/text.ts +41 -0
- package/src/preset/recipes/tooltip.ts +77 -0
- package/src/preset/semantic-variables.ts +283 -0
|
@@ -0,0 +1,398 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { log } from "../config/logger";
|
|
4
|
+
import React, { useRef, useState, useLayoutEffect, useEffect } from "react";
|
|
5
|
+
import { createPortal } from "react-dom";
|
|
6
|
+
import { styled } from "styled-system/jsx";
|
|
7
|
+
import StyledText from "./StyledText";
|
|
8
|
+
import type { AllowedVariant } from "../config/types";
|
|
9
|
+
import { tooltipRecipe } from "styled-system/recipes";
|
|
10
|
+
import { useCanHover } from "../config/can-hover";
|
|
11
|
+
import { useResolvedVariant } from "../config/style-config";
|
|
12
|
+
|
|
13
|
+
const TooltipTrigger = styled("div", {
|
|
14
|
+
base: {
|
|
15
|
+
display: "inline-block",
|
|
16
|
+
position: "relative",
|
|
17
|
+
cursor: "pointer",
|
|
18
|
+
},
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
const TooltipContent = styled("div");
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* The click-mode affordance.
|
|
25
|
+
*
|
|
26
|
+
* In click mode the tooltip cannot be opened by clicking the wrapper, because
|
|
27
|
+
* the wrapper usually contains a button and that click belongs to the button.
|
|
28
|
+
* So click mode renders this next to the child: a separate, visible, focusable
|
|
29
|
+
* control whose only job is to reveal the explanation. The reader can see that
|
|
30
|
+
* there is help, and where to press for it, without discovering that pressing
|
|
31
|
+
* the thing itself does something else entirely.
|
|
32
|
+
*
|
|
33
|
+
* Deliberately not `StyledButton` — that imports this module, and a cycle
|
|
34
|
+
* between two components that render each other is a module-init hazard for
|
|
35
|
+
* every consumer. Layout only, no colours, per the package rule.
|
|
36
|
+
*/
|
|
37
|
+
const HelpTrigger = styled("button", {
|
|
38
|
+
base: {
|
|
39
|
+
display: "inline-flex",
|
|
40
|
+
alignItems: "center",
|
|
41
|
+
justifyContent: "center",
|
|
42
|
+
// 48 rather than the WCAG 2.5.5 floor of 44: this component is used by
|
|
43
|
+
// applications whose readers are frequently older, and a help control that
|
|
44
|
+
// is hard to hit is a help control that does not get used.
|
|
45
|
+
minWidth: "48px",
|
|
46
|
+
minHeight: "48px",
|
|
47
|
+
marginLeft: "4px",
|
|
48
|
+
borderRadius: "9999px",
|
|
49
|
+
borderWidth: "1px",
|
|
50
|
+
borderStyle: "solid",
|
|
51
|
+
cursor: "pointer",
|
|
52
|
+
fontWeight: "bold",
|
|
53
|
+
lineHeight: "1",
|
|
54
|
+
verticalAlign: "middle",
|
|
55
|
+
},
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Everything the browser already puts in the tab sequence, plus anything given
|
|
60
|
+
* an explicit non-negative tabindex. Used to decide whether the trigger needs a
|
|
61
|
+
* tab stop of its own, or whether the child already provides one.
|
|
62
|
+
*/
|
|
63
|
+
const FOCUSABLE_SELECTOR = [
|
|
64
|
+
"a[href]",
|
|
65
|
+
"button:not([disabled])",
|
|
66
|
+
"input:not([disabled])",
|
|
67
|
+
"select:not([disabled])",
|
|
68
|
+
"textarea:not([disabled])",
|
|
69
|
+
'[tabindex]:not([tabindex="-1"])',
|
|
70
|
+
'[contenteditable="true"]',
|
|
71
|
+
].join(", ");
|
|
72
|
+
|
|
73
|
+
export interface StyledTooltipProps {
|
|
74
|
+
tooltip: React.ReactNode;
|
|
75
|
+
children: React.ReactNode;
|
|
76
|
+
delay?: number | undefined;
|
|
77
|
+
placement?: "top" | "bottom" | "left" | "right" | undefined;
|
|
78
|
+
boxBgAccent?: string | undefined; // theme color or fallback
|
|
79
|
+
size?: string | undefined;
|
|
80
|
+
"aria-label"?: string;
|
|
81
|
+
variant?: AllowedVariant | undefined;
|
|
82
|
+
style?: React.CSSProperties | undefined;
|
|
83
|
+
/**
|
|
84
|
+
* How the tooltip opens.
|
|
85
|
+
*
|
|
86
|
+
* `"hover"` (the default, and what an unset value means) preserves the
|
|
87
|
+
* long-standing behaviour: reveal on hover or focus, hide on leave or blur.
|
|
88
|
+
*
|
|
89
|
+
* `"click"` renders a separate help control beside the child and opens only
|
|
90
|
+
* when that control is pressed. Hover does nothing at all. This exists for
|
|
91
|
+
* readers whose pointer drifts — a hover tooltip closes itself before they
|
|
92
|
+
* arrive, and a hover tooltip they *are* reading vanishes when their hand
|
|
93
|
+
* moves. Hosts typically wire this to a user preference rather than setting
|
|
94
|
+
* it per call site.
|
|
95
|
+
*/
|
|
96
|
+
trigger?: "hover" | "click" | undefined;
|
|
97
|
+
/**
|
|
98
|
+
* Accessible name for the click-mode help control. Defaults to
|
|
99
|
+
* "More information". Give it something specific where the surrounding
|
|
100
|
+
* context does not already make the subject obvious.
|
|
101
|
+
*/
|
|
102
|
+
helpLabel?: string | undefined;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const StyledTooltip: React.FC<StyledTooltipProps> = ({
|
|
106
|
+
tooltip,
|
|
107
|
+
children,
|
|
108
|
+
delay = 120,
|
|
109
|
+
placement = "top",
|
|
110
|
+
size = "md",
|
|
111
|
+
"aria-label": ariaLabel,
|
|
112
|
+
variant,
|
|
113
|
+
trigger = "hover",
|
|
114
|
+
helpLabel = "More information",
|
|
115
|
+
...rest
|
|
116
|
+
}) => {
|
|
117
|
+
// Caller's variant, else the app-wide one, else `solid` — and anything the
|
|
118
|
+
// tooltip recipe has no case for (`ghost`, `selected`, `link`, …) coerces to
|
|
119
|
+
// `solid` rather than rendering an unstyled floating box. The original coerced
|
|
120
|
+
// only `ghost` and `selected` by name; useResolvedVariant generalises that to
|
|
121
|
+
// "any variant this recipe does not define", which is the rule that was meant.
|
|
122
|
+
const finalVariant = useResolvedVariant(variant);
|
|
123
|
+
log.trace("StyledTooltip rendered");
|
|
124
|
+
const tooltipId = React.useId(); // <-- Move useId to top-level, before any returns or conditionals
|
|
125
|
+
const [visible, setVisible] = useState(false);
|
|
126
|
+
const [styles, setStyles] = useState({});
|
|
127
|
+
|
|
128
|
+
// `ReturnType<typeof setTimeout>`, not `NodeJS.Timeout`: this is browser
|
|
129
|
+
// code, and naming the NodeJS namespace makes the whole package fail to
|
|
130
|
+
// typecheck for any consumer that has not installed @types/node. It also
|
|
131
|
+
// happens to be wrong — in a browser this is a number, not a Timeout object.
|
|
132
|
+
const timeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
133
|
+
const triggerRef = useRef<HTMLDivElement>(null);
|
|
134
|
+
const tooltipRef = useRef<HTMLDivElement>(null);
|
|
135
|
+
const helpRef = useRef<HTMLButtonElement>(null);
|
|
136
|
+
/**
|
|
137
|
+
* A hover trigger on a device that cannot hover is not a worse experience —
|
|
138
|
+
* it is an unreachable one. There is no hover event, and tapping the control
|
|
139
|
+
* activates it rather than explaining it, so the help is rendered, correct
|
|
140
|
+
* and impossible to see.
|
|
141
|
+
*
|
|
142
|
+
* So `hover` becomes `click` there, which renders the explicit control the
|
|
143
|
+
* click path already has. This only ever changes cases that were broken:
|
|
144
|
+
* `click` is unaffected, and a device that can hover is unaffected.
|
|
145
|
+
*/
|
|
146
|
+
const canHover = useCanHover();
|
|
147
|
+
const isClick = trigger === "click" || !canHover;
|
|
148
|
+
|
|
149
|
+
// The child may be any component (StyledIconButton, a link, a bare span), so
|
|
150
|
+
// whether it is focusable can only be known from the rendered DOM — React
|
|
151
|
+
// cannot see inside a child component's output. Starts as "yes" so the common
|
|
152
|
+
// case (an icon button) never renders a spurious tab stop, not even for the
|
|
153
|
+
// one frame before this layout effect runs.
|
|
154
|
+
const [focusableChild, setFocusableChild] = useState<HTMLElement | null>(null);
|
|
155
|
+
const [hasFocusableChild, setHasFocusableChild] = useState(true);
|
|
156
|
+
|
|
157
|
+
// When the trigger KEEPS its tab stop it must have a role and a name (WCAG
|
|
158
|
+
// 2.2 4.1.2) — but only if nothing else already provides one. Borrowing the
|
|
159
|
+
// tooltip text unconditionally is what broke SharedWithIndicator, which names
|
|
160
|
+
// an ANCESTOR: two elements then claimed the same label, with role="button"
|
|
161
|
+
// nested inside role="img" (NEH-151).
|
|
162
|
+
//
|
|
163
|
+
// Defaults to false so the component never invents a name before it has
|
|
164
|
+
// measured. Silence is the safe direction — a missing name is the status quo,
|
|
165
|
+
// a duplicated one is a new bug.
|
|
166
|
+
const [needsFallbackName, setNeedsFallbackName] = useState(false);
|
|
167
|
+
|
|
168
|
+
useLayoutEffect(() => {
|
|
169
|
+
const node = triggerRef.current;
|
|
170
|
+
const found = node?.querySelector<HTMLElement>(FOCUSABLE_SELECTOR) ?? null;
|
|
171
|
+
// Same-value setState is a no-op in React, so this cannot loop.
|
|
172
|
+
setFocusableChild((prev) => (prev === found ? prev : found));
|
|
173
|
+
setHasFocusableChild(found !== null);
|
|
174
|
+
|
|
175
|
+
if (!node) return;
|
|
176
|
+
// parentElement, not the node itself: closest() would match our own
|
|
177
|
+
// aria-label once we set one, and the answer would flip every render.
|
|
178
|
+
const namedByAncestor = Boolean(
|
|
179
|
+
node.parentElement?.closest("[aria-label], [aria-labelledby]"),
|
|
180
|
+
);
|
|
181
|
+
// Text content names an element for free; so does a labelled descendant
|
|
182
|
+
// (an icon carrying its own aria-label, an <img alt>).
|
|
183
|
+
const namedByContent =
|
|
184
|
+
(node.textContent ?? "").trim().length > 0 ||
|
|
185
|
+
Boolean(
|
|
186
|
+
node.querySelector('[aria-label], [aria-labelledby], img[alt]:not([alt=""])'),
|
|
187
|
+
);
|
|
188
|
+
setNeedsFallbackName(!namedByAncestor && !namedByContent);
|
|
189
|
+
}, [children]);
|
|
190
|
+
|
|
191
|
+
// aria-describedby has to sit on whatever actually receives focus, or a screen
|
|
192
|
+
// reader announces the control with no description. Set imperatively rather
|
|
193
|
+
// than by cloning the child: cloneElement would depend on every child
|
|
194
|
+
// component forwarding the prop, and a child that quietly drops it would fail
|
|
195
|
+
// invisibly.
|
|
196
|
+
useLayoutEffect(() => {
|
|
197
|
+
const node = focusableChild;
|
|
198
|
+
if (!node || !visible) return;
|
|
199
|
+
const previous = node.getAttribute("aria-describedby");
|
|
200
|
+
node.setAttribute(
|
|
201
|
+
"aria-describedby",
|
|
202
|
+
previous ? `${previous} ${tooltipId}` : tooltipId,
|
|
203
|
+
);
|
|
204
|
+
return () => {
|
|
205
|
+
if (previous === null) node.removeAttribute("aria-describedby");
|
|
206
|
+
else node.setAttribute("aria-describedby", previous);
|
|
207
|
+
};
|
|
208
|
+
}, [focusableChild, visible, tooltipId]);
|
|
209
|
+
|
|
210
|
+
useLayoutEffect(() => {
|
|
211
|
+
if (visible && triggerRef.current && tooltipRef.current) {
|
|
212
|
+
const triggerRect = triggerRef.current.getBoundingClientRect();
|
|
213
|
+
const tooltipRect = tooltipRef.current.getBoundingClientRect();
|
|
214
|
+
const margin = 8;
|
|
215
|
+
|
|
216
|
+
// Try placements in order of preference
|
|
217
|
+
const placements = [placement, "top", "bottom", "left", "right"];
|
|
218
|
+
|
|
219
|
+
let top = 0,
|
|
220
|
+
left = 0;
|
|
221
|
+
|
|
222
|
+
for (const tryPlacement of placements) {
|
|
223
|
+
if (tryPlacement === "top") {
|
|
224
|
+
top = triggerRect.top - tooltipRect.height - margin;
|
|
225
|
+
left =
|
|
226
|
+
triggerRect.left + triggerRect.width / 2 - tooltipRect.width / 2;
|
|
227
|
+
if (top >= 0) {
|
|
228
|
+
|
|
229
|
+
break;
|
|
230
|
+
}
|
|
231
|
+
} else if (tryPlacement === "bottom") {
|
|
232
|
+
top = triggerRect.bottom + margin;
|
|
233
|
+
left =
|
|
234
|
+
triggerRect.left + triggerRect.width / 2 - tooltipRect.width / 2;
|
|
235
|
+
if (top + tooltipRect.height <= window.innerHeight) {
|
|
236
|
+
|
|
237
|
+
break;
|
|
238
|
+
}
|
|
239
|
+
} else if (tryPlacement === "left") {
|
|
240
|
+
top =
|
|
241
|
+
triggerRect.top + triggerRect.height / 2 - tooltipRect.height / 2;
|
|
242
|
+
left = triggerRect.left - tooltipRect.width - margin;
|
|
243
|
+
if (left >= 0) {
|
|
244
|
+
|
|
245
|
+
break;
|
|
246
|
+
}
|
|
247
|
+
} else if (tryPlacement === "right") {
|
|
248
|
+
top =
|
|
249
|
+
triggerRect.top + triggerRect.height / 2 - tooltipRect.height / 2;
|
|
250
|
+
left = triggerRect.right + margin;
|
|
251
|
+
if (left + tooltipRect.width <= window.innerWidth) {
|
|
252
|
+
|
|
253
|
+
break;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// Clamp to viewport
|
|
259
|
+
top = Math.max(
|
|
260
|
+
margin,
|
|
261
|
+
Math.min(top, window.innerHeight - tooltipRect.height - margin),
|
|
262
|
+
);
|
|
263
|
+
left = Math.max(
|
|
264
|
+
margin,
|
|
265
|
+
Math.min(left, window.innerWidth - tooltipRect.width - margin),
|
|
266
|
+
);
|
|
267
|
+
|
|
268
|
+
setStyles({ top, left });
|
|
269
|
+
}
|
|
270
|
+
}, [visible, placement]);
|
|
271
|
+
|
|
272
|
+
// Click mode owns its own dismissal. Hover mode needs none of this: it
|
|
273
|
+
// closes when the pointer leaves. A panel opened by a deliberate press has
|
|
274
|
+
// to be closable by a deliberate action, and Escape has to work, or a
|
|
275
|
+
// keyboard user is stuck with it open.
|
|
276
|
+
useEffect(() => {
|
|
277
|
+
if (!isClick || !visible || typeof document === "undefined") return;
|
|
278
|
+
|
|
279
|
+
const onKeyDown = (event: KeyboardEvent) => {
|
|
280
|
+
if (event.key !== "Escape") return;
|
|
281
|
+
setVisible(false);
|
|
282
|
+
// Focus goes back to what opened it — never to the top of the document.
|
|
283
|
+
helpRef.current?.focus();
|
|
284
|
+
};
|
|
285
|
+
const onPointerDown = (event: MouseEvent) => {
|
|
286
|
+
const target = event.target as Node;
|
|
287
|
+
// A press inside the tooltip is not a dismissal: the explanation may
|
|
288
|
+
// contain a link, and text worth selecting.
|
|
289
|
+
if (helpRef.current?.contains(target) || tooltipRef.current?.contains(target)) return;
|
|
290
|
+
setVisible(false);
|
|
291
|
+
};
|
|
292
|
+
|
|
293
|
+
document.addEventListener("keydown", onKeyDown);
|
|
294
|
+
document.addEventListener("mousedown", onPointerDown);
|
|
295
|
+
return () => {
|
|
296
|
+
document.removeEventListener("keydown", onKeyDown);
|
|
297
|
+
document.removeEventListener("mousedown", onPointerDown);
|
|
298
|
+
};
|
|
299
|
+
}, [isClick, visible]);
|
|
300
|
+
|
|
301
|
+
if (!tooltip) {
|
|
302
|
+
return <>{children}</>;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// Only a string tooltip can serve as a name — stringifying a React element
|
|
306
|
+
// would produce "[object Object]" in the accessibility tree.
|
|
307
|
+
const tooltipLabel = typeof tooltip === "string" ? tooltip : undefined;
|
|
308
|
+
|
|
309
|
+
const show = () => {
|
|
310
|
+
timeoutRef.current = setTimeout(() => setVisible(true), delay);
|
|
311
|
+
};
|
|
312
|
+
const hide = () => {
|
|
313
|
+
if (timeoutRef.current) clearTimeout(timeoutRef.current);
|
|
314
|
+
setVisible(false);
|
|
315
|
+
};
|
|
316
|
+
|
|
317
|
+
return (
|
|
318
|
+
<>
|
|
319
|
+
<TooltipTrigger
|
|
320
|
+
ref={triggerRef}
|
|
321
|
+
// When the child is focusable it already owns the tab stop, and
|
|
322
|
+
// onFocus/onBlur use bubbling focusin/focusout semantics, so the
|
|
323
|
+
// tooltip still fires without the wrapper taking focus itself. Adding
|
|
324
|
+
// tabIndex here regardless is what gave every tooltipped control two
|
|
325
|
+
// tab stops, the second of them silent (NEH-127).
|
|
326
|
+
tabIndex={isClick || hasFocusableChild ? undefined : 0}
|
|
327
|
+
// A focusable element needs a role and a name (WCAG 2.2 4.1.2). Applied
|
|
328
|
+
// only when the trigger keeps the tab stop AND nothing else names it —
|
|
329
|
+
// see needsFallbackName above for why the condition matters (NEH-151).
|
|
330
|
+
//
|
|
331
|
+
// role="button" rather than no role: the trigger is focusable and
|
|
332
|
+
// reveals content on focus, which is the closest standard role and what
|
|
333
|
+
// the ARIA tooltip pattern assumes of a trigger. A focusable generic
|
|
334
|
+
// with only a name still fails 4.1.2, which asks for both.
|
|
335
|
+
role={!isClick && !hasFocusableChild && needsFallbackName ? "button" : undefined}
|
|
336
|
+
aria-label={
|
|
337
|
+
isClick || hasFocusableChild
|
|
338
|
+
? undefined
|
|
339
|
+
: ariaLabel ?? (needsFallbackName ? tooltipLabel : undefined)
|
|
340
|
+
}
|
|
341
|
+
// Hover handlers exist only in hover mode. In click mode a drifting
|
|
342
|
+
// pointer must change nothing at all — that is the entire point.
|
|
343
|
+
onMouseEnter={isClick ? undefined : show}
|
|
344
|
+
onMouseLeave={isClick ? undefined : hide}
|
|
345
|
+
onFocus={isClick ? undefined : show}
|
|
346
|
+
onBlur={isClick ? undefined : hide}
|
|
347
|
+
aria-describedby={
|
|
348
|
+
!isClick && !hasFocusableChild && visible ? tooltipId : undefined
|
|
349
|
+
}
|
|
350
|
+
{...rest}
|
|
351
|
+
>
|
|
352
|
+
{children}
|
|
353
|
+
{isClick && (
|
|
354
|
+
<HelpTrigger
|
|
355
|
+
ref={helpRef}
|
|
356
|
+
type="button"
|
|
357
|
+
aria-label={helpLabel}
|
|
358
|
+
aria-expanded={visible}
|
|
359
|
+
aria-controls={visible ? tooltipId : undefined}
|
|
360
|
+
onClick={() => setVisible((open) => !open)}
|
|
361
|
+
>
|
|
362
|
+
?
|
|
363
|
+
</HelpTrigger>
|
|
364
|
+
)}
|
|
365
|
+
</TooltipTrigger>
|
|
366
|
+
{visible && typeof document !== "undefined" &&
|
|
367
|
+
createPortal(
|
|
368
|
+
<TooltipContent
|
|
369
|
+
id={tooltipId}
|
|
370
|
+
role="tooltip"
|
|
371
|
+
ref={tooltipRef}
|
|
372
|
+
pt={3}
|
|
373
|
+
pl={8}
|
|
374
|
+
pb={3}
|
|
375
|
+
pr={8}
|
|
376
|
+
className={tooltipRecipe({
|
|
377
|
+
variant: finalVariant,
|
|
378
|
+
})}
|
|
379
|
+
style={{
|
|
380
|
+
...styles,
|
|
381
|
+
position: "fixed",
|
|
382
|
+
opacity: 1,
|
|
383
|
+
pointerEvents: "auto",
|
|
384
|
+
zIndex: 200000,
|
|
385
|
+
}}
|
|
386
|
+
onMouseEnter={isClick ? undefined : show}
|
|
387
|
+
onMouseLeave={isClick ? undefined : hide}
|
|
388
|
+
>
|
|
389
|
+
<StyledText size={size}>{tooltip}</StyledText>
|
|
390
|
+
</TooltipContent>,
|
|
391
|
+
document.body,
|
|
392
|
+
)
|
|
393
|
+
}
|
|
394
|
+
</>
|
|
395
|
+
);
|
|
396
|
+
};
|
|
397
|
+
|
|
398
|
+
export default StyledTooltip;
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { vstack } from "styled-system/patterns";
|
|
3
|
+
import type { ConditionalValue } from "styled-system/types";
|
|
4
|
+
import { cx } from "styled-system/css";
|
|
5
|
+
import { stripedRecipe } from "styled-system/recipes";
|
|
6
|
+
import { Property } from "csstype";
|
|
7
|
+
|
|
8
|
+
export interface StyledVStackProps
|
|
9
|
+
extends React.HTMLAttributes<HTMLDivElement> {
|
|
10
|
+
opacity?: ConditionalValue<number> | undefined;
|
|
11
|
+
gap?: ConditionalValue<string | number> | undefined;
|
|
12
|
+
align?: ConditionalValue<string> | undefined;
|
|
13
|
+
justify?: ConditionalValue<string> | undefined;
|
|
14
|
+
width?: ConditionalValue<string> | undefined;
|
|
15
|
+
w?: ConditionalValue<string> | undefined;
|
|
16
|
+
marginBottom?: ConditionalValue<string> | undefined;
|
|
17
|
+
mb?: ConditionalValue<string | number> | undefined;
|
|
18
|
+
marginTop?: ConditionalValue<string | number> | undefined;
|
|
19
|
+
mt?: ConditionalValue<string | number> | undefined;
|
|
20
|
+
flexWrap?: ConditionalValue<string> | undefined;
|
|
21
|
+
display?: ConditionalValue<string> | undefined;
|
|
22
|
+
alignItems?: ConditionalValue<string> | undefined;
|
|
23
|
+
justifyContent?: ConditionalValue<string> | undefined;
|
|
24
|
+
p?: ConditionalValue<string | number> | undefined;
|
|
25
|
+
py?: ConditionalValue<string | number> | undefined;
|
|
26
|
+
px?: ConditionalValue<string | number> | undefined;
|
|
27
|
+
pt?: ConditionalValue<string | number> | undefined;
|
|
28
|
+
pb?: ConditionalValue<string | number> | undefined;
|
|
29
|
+
pl?: ConditionalValue<string | number> | undefined;
|
|
30
|
+
pr?: ConditionalValue<string | number> | undefined;
|
|
31
|
+
minH?: ConditionalValue<string | number> | undefined;
|
|
32
|
+
minHeight?: ConditionalValue<string | number> | undefined;
|
|
33
|
+
minWidth?: ConditionalValue<string | number> | undefined;
|
|
34
|
+
maxW?: ConditionalValue<string | number> | undefined;
|
|
35
|
+
maxWidth?: ConditionalValue<string | number> | undefined;
|
|
36
|
+
flex?: ConditionalValue<string | number> | undefined;
|
|
37
|
+
position?: ConditionalValue<string> | undefined;
|
|
38
|
+
textAlign?: ConditionalValue<string> | undefined;
|
|
39
|
+
borderWidth?: ConditionalValue<string | number> | undefined;
|
|
40
|
+
borderRadius?: ConditionalValue<string | number> | undefined;
|
|
41
|
+
flexDirection?: ConditionalValue<Property.FlexDirection> | undefined;
|
|
42
|
+
height?: ConditionalValue<string | number> | undefined;
|
|
43
|
+
h?: ConditionalValue<string | number> | undefined;
|
|
44
|
+
bg?: ConditionalValue<string> | undefined;
|
|
45
|
+
backgroundColor?: ConditionalValue<string> | undefined;
|
|
46
|
+
isStriped?: boolean | undefined;
|
|
47
|
+
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export const StyledVStack: React.FC<StyledVStackProps> = ({
|
|
51
|
+
gap = "2",
|
|
52
|
+
align,
|
|
53
|
+
justify,
|
|
54
|
+
width,
|
|
55
|
+
w,
|
|
56
|
+
marginBottom,
|
|
57
|
+
mb,
|
|
58
|
+
marginTop,
|
|
59
|
+
mt,
|
|
60
|
+
flexWrap,
|
|
61
|
+
display,
|
|
62
|
+
alignItems,
|
|
63
|
+
justifyContent,
|
|
64
|
+
p,
|
|
65
|
+
py,
|
|
66
|
+
px,
|
|
67
|
+
pt,
|
|
68
|
+
pb,
|
|
69
|
+
pl,
|
|
70
|
+
pr,
|
|
71
|
+
minH,
|
|
72
|
+
minHeight,
|
|
73
|
+
minWidth,
|
|
74
|
+
maxW,
|
|
75
|
+
maxWidth,
|
|
76
|
+
flex,
|
|
77
|
+
position,
|
|
78
|
+
textAlign,
|
|
79
|
+
borderWidth,
|
|
80
|
+
borderRadius,
|
|
81
|
+
flexDirection,
|
|
82
|
+
height,
|
|
83
|
+
h,
|
|
84
|
+
bg,
|
|
85
|
+
backgroundColor,
|
|
86
|
+
isStriped,
|
|
87
|
+
opacity,
|
|
88
|
+
className,
|
|
89
|
+
children,
|
|
90
|
+
...rest
|
|
91
|
+
}) => { // Map React-style props to styled-system pattern props
|
|
92
|
+
const mappedProps = {
|
|
93
|
+
opacity,
|
|
94
|
+
gap: typeof gap === "number" ? String(gap) : gap,
|
|
95
|
+
alignItems: align || alignItems,
|
|
96
|
+
justify: justify || justifyContent,
|
|
97
|
+
width: width || w,
|
|
98
|
+
marginBottom: marginBottom || mb,
|
|
99
|
+
marginTop: marginTop || mt,
|
|
100
|
+
flexWrap,
|
|
101
|
+
display,
|
|
102
|
+
p,
|
|
103
|
+
py,
|
|
104
|
+
px,
|
|
105
|
+
pt,
|
|
106
|
+
pb,
|
|
107
|
+
pl,
|
|
108
|
+
pr,
|
|
109
|
+
minH,
|
|
110
|
+
minHeight,
|
|
111
|
+
minWidth,
|
|
112
|
+
maxWidth: maxWidth || maxW,
|
|
113
|
+
flex,
|
|
114
|
+
position,
|
|
115
|
+
textAlign,
|
|
116
|
+
borderWidth,
|
|
117
|
+
borderRadius,
|
|
118
|
+
flexDirection,
|
|
119
|
+
height: height || h,
|
|
120
|
+
background: bg || backgroundColor,
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
// Remove undefined values
|
|
124
|
+
Object.keys(mappedProps).forEach(
|
|
125
|
+
(key) => mappedProps[key as keyof typeof mappedProps] === undefined && delete mappedProps[key as keyof typeof mappedProps],
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
const combinedClassName = cx(
|
|
129
|
+
vstack(mappedProps as unknown as Parameters<typeof vstack>[0]),
|
|
130
|
+
isStriped ? stripedRecipe() : undefined,
|
|
131
|
+
className,
|
|
132
|
+
);
|
|
133
|
+
const { ...divProps } = rest;
|
|
134
|
+
return (
|
|
135
|
+
<div className={combinedClassName} {...divProps}>
|
|
136
|
+
{children}
|
|
137
|
+
</div>
|
|
138
|
+
);
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
StyledVStack.displayName = "StyledVStack";
|
|
142
|
+
|
|
143
|
+
export default StyledVStack;
|