@oprt/react 0.1.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -8,6 +8,7 @@ import { Badge } from "../Badge/Badge.js";
8
8
  import { Button } from "../Button/Button.js";
9
9
  import { useActionFocus } from "../_internal/useActionFocus.js";
10
10
  import { devValidate } from "../_internal/devValidate.js";
11
+ import { useHoldBackNewText } from "../_internal/liveRegion.js";
11
12
  import { ActionStatusManifest } from "./ActionStatus.manifest.js";
12
13
  import { jsx, jsxs } from "react/jsx-runtime";
13
14
  var statusStyles = cva(
@@ -34,6 +35,8 @@ function ActionStatusLine({
34
35
  requestId,
35
36
  children
36
37
  }) {
38
+ const spoken = [`${label}, ${state}.`, requestId && `Request: ${requestId}.`, message].filter(Boolean).join(" ");
39
+ const holdBack = useHoldBackNewText(true);
37
40
  return /* @__PURE__ */ jsxs(
38
41
  "div",
39
42
  {
@@ -42,13 +45,21 @@ function ActionStatusLine({
42
45
  "aria-atomic": "true",
43
46
  className: "flex min-w-0 flex-col gap-1u",
44
47
  children: [
45
- /* @__PURE__ */ jsx("span", { className: "min-w-0 oprt-wrap-anywhere font-medium uppercase tracking-oprt-caps", children: label }),
46
- /* @__PURE__ */ jsx("div", { children: /* @__PURE__ */ jsx(Badge, { variant: variants[state], indicator: "dot", children: state }) }),
47
- requestId && /* @__PURE__ */ jsxs("span", { className: "min-w-0 oprt-wrap-anywhere font-mono", children: [
48
+ /* @__PURE__ */ jsx("span", { className: "sr-only", children: holdBack ? null : spoken }),
49
+ /* @__PURE__ */ jsx(
50
+ "span",
51
+ {
52
+ "aria-hidden": "true",
53
+ className: "min-w-0 oprt-wrap-anywhere font-medium uppercase tracking-oprt-caps",
54
+ children: label
55
+ }
56
+ ),
57
+ /* @__PURE__ */ jsx("div", { "aria-hidden": "true", children: /* @__PURE__ */ jsx(Badge, { variant: variants[state], indicator: "dot", children: state }) }),
58
+ requestId && /* @__PURE__ */ jsxs("span", { "aria-hidden": "true", className: "min-w-0 oprt-wrap-anywhere font-mono", children: [
48
59
  "Request: ",
49
60
  requestId
50
61
  ] }),
51
- message && /* @__PURE__ */ jsx("p", { className: "m-0 min-w-0 oprt-wrap-anywhere", children: message }),
62
+ message && /* @__PURE__ */ jsx("p", { "aria-hidden": "true", className: "m-0 min-w-0 oprt-wrap-anywhere", children: message }),
52
63
  children
53
64
  ]
54
65
  }
@@ -7,6 +7,7 @@ import { forwardRef } from "react";
7
7
  import clsx from "clsx";
8
8
  import { AlertManifest } from "./Alert.manifest.js";
9
9
  import { devValidate } from "../_internal/devValidate.js";
10
+ import { useHoldBackNewText } from "../_internal/liveRegion.js";
10
11
  import { jsx, jsxs } from "react/jsx-runtime";
11
12
  var ICONS = {
12
13
  info: Info,
@@ -48,6 +49,7 @@ var Alert = forwardRef((allProps, ref) => {
48
49
  ...props
49
50
  } = allProps;
50
51
  const role = variant === "error" || variant === "warning" ? "alert" : "status";
52
+ const holdBack = useHoldBackNewText(role === "status");
51
53
  const Icon = ICONS[variant];
52
54
  const hasBody = children != null && typeof children !== "boolean" && children !== "";
53
55
  if (process.env.NODE_ENV !== "production") {
@@ -55,19 +57,26 @@ var Alert = forwardRef((allProps, ref) => {
55
57
  }
56
58
  return /* @__PURE__ */ jsxs("div", { ref, role, className: clsx(containerStyles({ variant }), className), ...props, children: [
57
59
  /* @__PURE__ */ jsx("span", { "aria-hidden": "true", className: clsx("shrink-0", accentStyles({ variant })), children: /* @__PURE__ */ jsx(Icon, { size: 16 }) }),
58
- /* @__PURE__ */ jsxs("div", { className: "flex min-w-0 flex-1 flex-col gap-1u oprt-wrap-anywhere", children: [
59
- title && /* @__PURE__ */ jsx(
60
- "p",
61
- {
62
- className: clsx(
63
- "m-0 text-oprt-sm uppercase tracking-oprt-sm",
64
- accentStyles({ variant })
60
+ /* @__PURE__ */ jsxs(
61
+ "div",
62
+ {
63
+ className: "flex min-w-0 flex-1 flex-col gap-1u oprt-wrap-anywhere",
64
+ ...holdBack ? { "aria-hidden": true } : {},
65
+ children: [
66
+ title && /* @__PURE__ */ jsx(
67
+ "p",
68
+ {
69
+ className: clsx(
70
+ "m-0 text-oprt-sm uppercase tracking-oprt-sm",
71
+ accentStyles({ variant })
72
+ ),
73
+ children: title
74
+ }
65
75
  ),
66
- children: title
67
- }
68
- ),
69
- hasBody && /* @__PURE__ */ jsx("div", { className: "text-oprt-base normal-case text-oprt-text", children })
70
- ] }),
76
+ hasBody && /* @__PURE__ */ jsx("div", { className: "text-oprt-base normal-case text-oprt-text", children })
77
+ ]
78
+ }
79
+ ),
71
80
  dismissible && /* @__PURE__ */ jsx(
72
81
  "button",
73
82
  {
@@ -77,18 +77,21 @@ var AttachmentPreview = forwardRef(
77
77
  /* @__PURE__ */ jsx("span", { className: "oprt-wrap-anywhere", children: attachment.mediaType })
78
78
  ] })
79
79
  ] }),
80
- preview.kind === "image" && (imageError ? /* @__PURE__ */ jsx("p", { role: "status", className: "m-0 text-oprt-error", children: imageError }) : /* @__PURE__ */ jsx(
81
- "img",
82
- {
83
- src: preview.src,
84
- alt: preview.alt,
85
- onError: () => setImageState(
86
- (current) => current.key === imageKey ? { key: imageKey, failed: true } : current
87
- ),
88
- className: clsx(PREVIEW_HEIGHT, "max-w-full self-start object-contain")
89
- },
90
- imageKey
91
- )),
80
+ preview.kind === "image" && /* @__PURE__ */ jsxs(Fragment, { children: [
81
+ /* @__PURE__ */ jsx("p", { role: "status", className: imageError ? "m-0 text-oprt-error" : "sr-only", children: imageError }),
82
+ !imageError && /* @__PURE__ */ jsx(
83
+ "img",
84
+ {
85
+ src: preview.src,
86
+ alt: preview.alt,
87
+ onError: () => setImageState(
88
+ (current) => current.key === imageKey ? { key: imageKey, failed: true } : current
89
+ ),
90
+ className: clsx(PREVIEW_HEIGHT, "max-w-full self-start object-contain")
91
+ },
92
+ imageKey
93
+ )
94
+ ] }),
92
95
  preview.kind === "text" && /* @__PURE__ */ jsxs(Fragment, { children: [
93
96
  /* @__PURE__ */ jsx(
94
97
  "pre",
@@ -874,7 +874,15 @@ var Chart = forwardRef((allProps, ref) => {
874
874
  onBlur: () => setInspectionFocused(false)
875
875
  }
876
876
  ),
877
- empty ? /* @__PURE__ */ jsx("p", { className: "m-0 py-6u text-oprt-base text-oprt-text-dim", role: "status", children: empty }) : /* @__PURE__ */ jsxs(
877
+ /* @__PURE__ */ jsx(
878
+ "p",
879
+ {
880
+ role: "status",
881
+ className: empty ? "m-0 py-6u text-oprt-base text-oprt-text-dim" : "sr-only",
882
+ children: empty
883
+ }
884
+ ),
885
+ empty ? null : /* @__PURE__ */ jsxs(
878
886
  "svg",
879
887
  {
880
888
  role: "img",
@@ -1092,7 +1100,7 @@ var Chart = forwardRef((allProps, ref) => {
1092
1100
  variant: "ghost",
1093
1101
  size: "sm",
1094
1102
  className: "min-w-0 max-w-full oprt-wrap-anywhere",
1095
- "aria-label": variant === "heatmap" ? `${seriesNumber.get(item.id)} ${item.label}` : item.label,
1103
+ "aria-label": `${variant === "heatmap" ? `${seriesNumber.get(item.id)} ${item.label}` : item.label}${hidden.has(item.id) ? " Hidden" : ""}`,
1096
1104
  "aria-pressed": !hidden.has(item.id),
1097
1105
  disabled,
1098
1106
  onClick: () => {
@@ -21,9 +21,28 @@ var feedbackStyles = cva("min-w-0 break-words font-sans text-oprt-base normal-ca
21
21
  }
22
22
  }
23
23
  });
24
+ var reportedLabels = /* @__PURE__ */ new Set();
25
+ function reportLabelWithoutVisibleText(label) {
26
+ if (process.env.NODE_ENV === "production" || reportedLabels.has(label)) return;
27
+ reportedLabels.add(label);
28
+ console.error(
29
+ JSON.stringify(
30
+ {
31
+ component: "CopyButton",
32
+ error: "LABEL_OMITS_VISIBLE_TEXT",
33
+ prop: "label",
34
+ received: label,
35
+ suggestion: 'The button shows the word "Copy", so its label must contain it (WCAG 2.5.3, Label in Name). Start the label with it: "Copy equipment ID".'
36
+ },
37
+ null,
38
+ 2
39
+ )
40
+ );
41
+ }
24
42
  var CopyButton = forwardRef((allProps, ref) => {
25
43
  const { value, label, onCopy, onCopyError, disabled = false, size = "md", className } = allProps;
26
44
  const [pending, setPending] = useState(false);
45
+ if (!/copy/i.test(label)) reportLabelWithoutVisibleText(label);
27
46
  const [feedback, setFeedback] = useState(null);
28
47
  const pendingRef = useRef(false);
29
48
  const isMounted = useIsMounted();
@@ -11,7 +11,7 @@ var CopyButtonManifest = {
11
11
  label: {
12
12
  type: "string",
13
13
  required: true,
14
- description: 'Accessible action name identifying the value. Feedback reads this label, then the outcome: "Copy equipment ID: copied".'
14
+ description: 'Accessible action name identifying the value. It must contain "Copy", the word the button shows. Feedback reads this label, then the outcome: "Copy equipment ID: copied".'
15
15
  },
16
16
  size: { type: "enum", values: ["sm", "md", "lg"], default: "md" },
17
17
  disabled: { type: "boolean", default: false },
@@ -172,7 +172,15 @@ var EventTimeline = forwardRef(
172
172
  "aria-label": label,
173
173
  className: clsx(timelineStyles(), className),
174
174
  children: [
175
- !validRange ? /* @__PURE__ */ jsx("p", { role: "status", className: "m-0 text-oprt-base text-oprt-text-dim", children: "Choose a valid time range with start before end." }) : /* @__PURE__ */ jsxs(Fragment, { children: [
175
+ /* @__PURE__ */ jsx(
176
+ "p",
177
+ {
178
+ role: "status",
179
+ className: !validRange ? "m-0 text-oprt-base text-oprt-text-dim" : "sr-only",
180
+ children: !validRange ? "Choose a valid time range with start before end." : ""
181
+ }
182
+ ),
183
+ !validRange ? null : /* @__PURE__ */ jsxs(Fragment, { children: [
176
184
  /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap justify-between gap-1u font-mono text-oprt-base text-oprt-text-dim", children: [
177
185
  /* @__PURE__ */ jsx("time", { dateTime: new Date(start).toISOString(), children: utc(start) }),
178
186
  /* @__PURE__ */ jsx("time", { dateTime: new Date(end).toISOString(), children: utc(end) })
@@ -165,7 +165,7 @@ var LayerControl = forwardRef(
165
165
  {
166
166
  size: "sm",
167
167
  variant: "tertiary",
168
- "aria-label": `Move ${item.label} up`,
168
+ "aria-label": `Move up ${item.label}`,
169
169
  disabled: !canMove(index, -1),
170
170
  onClick: () => move(index, -1),
171
171
  children: "Move up"
@@ -176,7 +176,7 @@ var LayerControl = forwardRef(
176
176
  {
177
177
  size: "sm",
178
178
  variant: "tertiary",
179
- "aria-label": `Move ${item.label} down`,
179
+ "aria-label": `Move down ${item.label}`,
180
180
  disabled: !canMove(index, 1),
181
181
  onClick: () => move(index, 1),
182
182
  children: "Move down"
@@ -165,7 +165,7 @@ var Toast = forwardRef((allProps, ref) => {
165
165
  /* @__PURE__ */ jsxs("div", { className: "flex min-w-0 flex-1 select-text flex-col gap-1u oprt-wrap-anywhere", children: [
166
166
  /* @__PURE__ */ jsx(RadixToast.Title, { className: titleStyles({ variant: severity }), children: title }),
167
167
  description && /* @__PURE__ */ jsxs(RadixToast.Description, { className: "m-0 text-oprt-base normal-case text-oprt-text", children: [
168
- /* @__PURE__ */ jsx("span", { className: "sr-only", children: ". " }),
168
+ /* @__PURE__ */ jsx("span", { "data-radix-toast-announce-exclude": "", "data-radix-toast-announce-alt": ". " }),
169
169
  description
170
170
  ] })
171
171
  ] }),
@@ -23,6 +23,7 @@ import { isSingleElement, SCREEN_MARGIN, toPlacement, TOOLTIP_WIDTH } from "../_
23
23
  import { FocusableWhenUnavailableProvider } from "../_internal/unavailable.js";
24
24
  import { jsx, jsxs } from "react/jsx-runtime";
25
25
  var PLAIN_TRIGGER = "inline-flex min-h-3u items-center border-0 bg-transparent p-0 [color:inherit] [font:inherit] [letter-spacing:inherit] underline decoration-dotted underline-offset-2 cursor-help outline-hidden focus-visible:shadow-oprt-focus";
26
+ var CLOSE_DELAY = 150;
26
27
  var Tooltip = forwardRef(
27
28
  ({
28
29
  content,
@@ -78,7 +79,7 @@ var Tooltip = forwardRef(
78
79
  isOpen,
79
80
  onOpenChange: setOpen,
80
81
  delay: delayDuration,
81
- closeDelay: 0,
82
+ closeDelay: CLOSE_DELAY,
82
83
  shouldCloseOnPress: !unavailable,
83
84
  children: [
84
85
  unavailable ? /* @__PURE__ */ jsx(FocusableWhenUnavailableProvider, { value: true, children: focusable }) : focusable,
@@ -1,7 +1,7 @@
1
1
  // src/Tooltip/Tooltip.manifest.ts
2
2
  var TooltipManifest = {
3
3
  Tooltip: {
4
- description: "Supplementary text on hover or keyboard focus of a trigger, wrapping at about 20rem. A disabled Button trigger stays focusable and announced unavailable, so focus or a tap shows the explanation. Built on React Aria Components (TooltipTrigger + Tooltip), portal'd, with a bordered surface.",
4
+ description: "Supplementary text on hover or keyboard focus of a trigger, wrapping at about 20rem. The pointer can move onto the tooltip without closing it; it closes 150 ms after the pointer has left both. A disabled Button trigger stays focusable and announced unavailable, so focus or a tap shows the explanation. Built on React Aria Components (TooltipTrigger + Tooltip), portal'd, with a bordered surface.",
5
5
  props: {
6
6
  side: { type: "enum", values: ["top", "right", "bottom", "left"], default: "top" },
7
7
  align: { type: "enum", values: ["start", "center", "end"], default: "center" },
@@ -223,7 +223,7 @@ var UploadQueue = forwardRef(
223
223
  }
224
224
  )
225
225
  ] }),
226
- /* @__PURE__ */ jsx("p", { className: "m-0 text-oprt-text-dim", children: /* @__PURE__ */ jsx(ByteCountLabel, { value: item.sizeBytes }) }),
226
+ /* @__PURE__ */ jsx("p", { className: "m-0 text-oprt-text-dim oprt-wrap-anywhere", children: /* @__PURE__ */ jsx(ByteCountLabel, { value: item.sizeBytes }) }),
227
227
  item.message && /* @__PURE__ */ jsx("p", { className: "m-0 oprt-wrap-anywhere", children: item.message }),
228
228
  transferring && (invalidProgress ? /* @__PURE__ */ jsx("p", { className: "m-0 text-oprt-error", children: "Invalid progress counts." }) : item.totalBytes === 0 ? /* @__PURE__ */ jsx("p", { className: "m-0 text-oprt-text-dim", children: `0${NUMBER_UNIT_SPACE}bytes total; no percentage available.` }) : /* @__PURE__ */ jsxs(Fragment, { children: [
229
229
  /* @__PURE__ */ jsx(
@@ -233,7 +233,7 @@ var UploadQueue = forwardRef(
233
233
  ...item.totalBytes !== void 0 && validLoaded ? { value: item.loadedBytes, max: item.totalBytes } : {}
234
234
  }
235
235
  ),
236
- /* @__PURE__ */ jsxs("p", { className: "m-0 font-mono tabular-nums text-oprt-text-dim", children: [
236
+ /* @__PURE__ */ jsxs("p", { className: "m-0 font-mono tabular-nums text-oprt-text-dim oprt-wrap-anywhere", children: [
237
237
  validLoaded ? `${item.loadedBytes}${NUMBER_UNIT_SPACE}bytes transferred` : "Transferred byte count unavailable",
238
238
  item.totalBytes !== void 0 ? ` of ${item.totalBytes}${NUMBER_UNIT_SPACE}bytes` : "; total unknown",
239
239
  "."
@@ -0,0 +1,27 @@
1
+ "use client";
2
+
3
+ // src/_internal/liveRegion.ts
4
+ import { useEffect, useRef, useState, useSyncExternalStore } from "react";
5
+ var neverChanges = () => () => {
6
+ };
7
+ var LIVE_REGION_LEAD_MS = 100;
8
+ function useHoldBackNewText(enabled) {
9
+ const hydrating = useSyncExternalStore(
10
+ neverChanges,
11
+ () => false,
12
+ () => true
13
+ );
14
+ const lateArrival = useRef(null);
15
+ if (lateArrival.current === null) lateArrival.current = !hydrating;
16
+ const [held, setHeld] = useState(Boolean(lateArrival.current));
17
+ useEffect(() => {
18
+ if (!held) return;
19
+ const timer = window.setTimeout(() => setHeld(false), LIVE_REGION_LEAD_MS);
20
+ return () => window.clearTimeout(timer);
21
+ }, [held]);
22
+ return enabled && held;
23
+ }
24
+ export {
25
+ LIVE_REGION_LEAD_MS,
26
+ useHoldBackNewText
27
+ };