@oprt/react 0.2.1 → 0.4.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 CHANGED
@@ -35,7 +35,8 @@ import "@oprt/tokens";
35
35
  import "./globals.css";
36
36
  ```
37
37
 
38
- Load the tokens from JavaScript or a `<link>`, not with an `@import` inside the Tailwind
38
+ Load the tokens from
39
+ JavaScript or a `<link>`, not with an `@import` inside the Tailwind
39
40
  stylesheet: the Tailwind CLI inlines an imported file without rewriting its font URLs, and the
40
41
  bundled fonts would not load. Inside Tailwind Typography's `prose`, wrap components in an element
41
42
  with the `not-prose` class.
@@ -179,7 +179,15 @@ var ComboBox = forwardRef((props, ref) => {
179
179
  }
180
180
  )
181
181
  ] }),
182
- /* @__PURE__ */ jsx("p", { role: "status", className: loading ? "m-0 text-oprt-text-dim" : "sr-only", children: loading ? "Loading options." : null }),
182
+ /* @__PURE__ */ jsx(
183
+ "p",
184
+ {
185
+ role: "status",
186
+ "data-react-aria-top-layer": "",
187
+ className: loading ? "m-0 text-oprt-text-dim" : "sr-only",
188
+ children: loading ? "Loading options." : null
189
+ }
190
+ ),
183
191
  /* @__PURE__ */ jsx(
184
192
  MotionPopover,
185
193
  {
@@ -18,6 +18,7 @@ import {
18
18
  import { Select, SelectOption } from "../Select/Select.js";
19
19
  import { sortedOrder } from "../DataTable/sort.js";
20
20
  import { devValidate } from "../_internal/devValidate.js";
21
+ import { useLoadingStatusText } from "../_internal/loadingStatus.js";
21
22
  import { DataGridManifest } from "./DataGrid.manifest.js";
22
23
  import { PageControls } from "./PageControls.js";
23
24
  import { jsx, jsxs } from "react/jsx-runtime";
@@ -76,7 +77,7 @@ var DataGrid = forwardRef((allProps, ref) => {
76
77
  columns,
77
78
  data,
78
79
  getRowId,
79
- caption = "Data grid",
80
+ caption: captionProp,
80
81
  selectedRowIds,
81
82
  onSelectionChange,
82
83
  sort,
@@ -106,6 +107,7 @@ var DataGrid = forwardRef((allProps, ref) => {
106
107
  if (process.env.NODE_ENV !== "production") {
107
108
  devValidate("DataGrid", DataGridManifest.DataGrid.props, allProps);
108
109
  }
110
+ const caption = captionProp ?? "Data grid";
109
111
  const [localSelection, setLocalSelection] = useState([]);
110
112
  const [localSort, setLocalSort] = useState(null);
111
113
  const [localPage, setLocalPage] = useState(1);
@@ -152,6 +154,13 @@ var DataGrid = forwardRef((allProps, ref) => {
152
154
  const safePageSize = Number.isFinite(pageSize) && pageSize >= 1 ? Math.floor(pageSize) : 10;
153
155
  const count = mode === "server" && rowCount !== void 0 && Number.isFinite(rowCount) ? Math.max(0, Math.floor(rowCount)) : data.length;
154
156
  const pageCount = Math.max(1, Math.ceil(count / safePageSize));
157
+ const what = captionProp ?? "results";
158
+ const loadingNote = `Loading ${what}.`;
159
+ const loadingStatus = useLoadingStatusText({
160
+ loading,
161
+ loadingText: loadingNote,
162
+ loadedText: `${captionProp ?? "Results"} loaded. ${count} ${count === 1 ? "row" : "rows"}.`
163
+ });
155
164
  const requestedPage = page ?? localPage;
156
165
  const currentPage = Math.min(
157
166
  pageCount,
@@ -431,7 +440,8 @@ var DataGrid = forwardRef((allProps, ref) => {
431
440
  } : {}
432
441
  }
433
442
  ),
434
- /* @__PURE__ */ jsx("p", { role: "status", className: loading ? "m-0 text-oprt-text-dim" : "sr-only", children: loading ? "Loading results." : "" }),
443
+ /* @__PURE__ */ jsx("p", { role: "status", className: "sr-only", children: loadingStatus }),
444
+ loading && /* @__PURE__ */ jsx("p", { "aria-hidden": "true", className: "m-0 text-oprt-text-dim", children: loadingNote }),
435
445
  /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center justify-between gap-2u", children: [
436
446
  /* @__PURE__ */ jsxs("p", { className: "m-0 font-mono tabular-nums text-oprt-text-dim", children: [
437
447
  count,
@@ -34,7 +34,7 @@ var DataGridManifest = {
34
34
  caption: {
35
35
  type: "string",
36
36
  default: "Data grid",
37
- description: "Accessible native table caption and controls group name."
37
+ description: "Accessible native table caption and controls group name. Also names what is loading in the loading and loaded messages."
38
38
  },
39
39
  selectedRowIds: {
40
40
  type: "array",
@@ -111,7 +111,7 @@ var DataGridManifest = {
111
111
  loading: {
112
112
  type: "boolean",
113
113
  default: false,
114
- description: "Marks the table busy, announces loading, and disables grid-owned controls and row activation while retaining supplied data."
114
+ description: 'Marks the table busy and disables grid-owned controls and row activation while retaining supplied data. A load the person asks for (a reload, a page, a filter: `loading` turns on after the grid has rendered) says "Loading <caption>." once (or "Loading results." when no caption is given) and, when it ends, "<caption> loaded. N rows." once. The load that is under way when the grid first renders is busy but silent, as is the arrival of its rows.'
115
115
  },
116
116
  disabled: {
117
117
  type: "boolean",
@@ -69,11 +69,12 @@ var Popover = forwardRef(
69
69
  const dialog = useRef(null);
70
70
  const lastDialog = useRef(null);
71
71
  const wasOpen = useRef(false);
72
- const focusFrame = useRef();
72
+ const focusFrame = useRef(void 0);
73
73
  const focusScrollClose = useRef(false);
74
74
  const current = useRef({ isOpen, onCloseAutoFocus, returnFocusRef });
75
75
  current.current = { isOpen, onCloseAutoFocus, returnFocusRef };
76
- const childRef = parseInt(version, 10) < 19 ? children.ref : children.props.ref;
76
+ const triggerChild = children;
77
+ const childRef = parseInt(version, 10) < 19 ? triggerChild.ref : triggerChild.props.ref;
77
78
  const rememberTrigger = useCallback((node) => {
78
79
  trigger.current = node;
79
80
  if (node) lastTrigger.current = node;
@@ -209,10 +210,10 @@ var Popover = forwardRef(
209
210
  document.removeEventListener("pointercancel", cancel, true);
210
211
  };
211
212
  }, [isOpen, requestOpen]);
212
- const triggerElement = cloneElement(children, {
213
+ const triggerElement = cloneElement(triggerChild, {
213
214
  ref: setTrigger,
214
- disabled: disabled || children.props.disabled,
215
- "aria-disabled": disabled || children.props["aria-disabled"],
215
+ disabled: disabled || triggerChild.props.disabled,
216
+ "aria-disabled": disabled || triggerChild.props["aria-disabled"],
216
217
  "aria-controls": isOpen ? id : void 0
217
218
  });
218
219
  const pressable = /* @__PURE__ */ jsx(Pressable, { isDisabled: disabled, children: triggerElement });
@@ -19,26 +19,55 @@ var barStyles = cva(
19
19
  defaultVariants: { variant: "block" }
20
20
  }
21
21
  );
22
+ var NOT_APPLIED = [
23
+ "aria-label",
24
+ "aria-labelledby",
25
+ "aria-describedby",
26
+ "role",
27
+ "aria-live",
28
+ "aria-busy"
29
+ ];
30
+ var reportedNotApplied = /* @__PURE__ */ new Set();
31
+ function reportNotApplied(prop, received) {
32
+ if (process.env.NODE_ENV === "production" || reportedNotApplied.has(prop)) return;
33
+ reportedNotApplied.add(prop);
34
+ console.error(
35
+ JSON.stringify(
36
+ {
37
+ component: "Skeleton",
38
+ error: "PROP_NOT_APPLIED",
39
+ prop,
40
+ received: typeof received === "string" ? received : String(received),
41
+ suggestion: 'Skeleton is decorative: hidden from assistive technology, with no role or name, so it stays silent and a page of them does not say "Loading" once each. Mark the area that is loading aria-busy and say "Loading <what>" once in a status that is already on the page (see the Skeleton page).'
42
+ },
43
+ null,
44
+ 2
45
+ )
46
+ );
47
+ }
22
48
  var Skeleton = forwardRef((allProps, ref) => {
23
- const { className, variant = "block", width, height, lines = 3, ...props } = allProps;
49
+ const { className, variant = "block", width, height, lines = 3, ...rest } = allProps;
50
+ const props = { ...rest };
24
51
  if (process.env.NODE_ENV !== "production") {
25
52
  devValidate("Skeleton", SkeletonManifest.Skeleton.props, { ...allProps });
53
+ for (const name of NOT_APPLIED) {
54
+ if (props[name] !== void 0) reportNotApplied(name, props[name]);
55
+ }
26
56
  }
57
+ for (const name of NOT_APPLIED) delete props[name];
58
+ delete props["aria-hidden"];
27
59
  if (variant === "text") {
28
60
  const count = Number.isFinite(lines) ? Math.max(1, Math.floor(lines)) : 3;
29
61
  return /* @__PURE__ */ jsx(
30
62
  "div",
31
63
  {
64
+ ...props,
32
65
  ref,
33
- role: "status",
34
- "aria-busy": "true",
35
- "aria-label": "Loading",
66
+ "aria-hidden": "true",
36
67
  className: clsx("flex flex-col gap-1u", className),
37
- ...props,
38
68
  children: Array.from({ length: count }, (_, i) => /* @__PURE__ */ jsx(
39
69
  "div",
40
70
  {
41
- "aria-hidden": "true",
42
71
  className: clsx(barStyles({ variant: "text" }), i === count - 1 && "w-2/3")
43
72
  },
44
73
  i
@@ -50,25 +79,7 @@ var Skeleton = forwardRef((allProps, ref) => {
50
79
  if (width !== void 0) style.width = width;
51
80
  if (height !== void 0) style.height = height;
52
81
  const hasStyle = width !== void 0 || height !== void 0;
53
- return /* @__PURE__ */ jsx(
54
- "div",
55
- {
56
- ref,
57
- role: "status",
58
- "aria-busy": "true",
59
- "aria-label": "Loading",
60
- className: clsx("flex flex-col", className),
61
- ...props,
62
- children: /* @__PURE__ */ jsx(
63
- "div",
64
- {
65
- "aria-hidden": "true",
66
- className: barStyles({ variant: "block" }),
67
- ...hasStyle ? { style } : {}
68
- }
69
- )
70
- }
71
- );
82
+ return /* @__PURE__ */ jsx("div", { ...props, ref, "aria-hidden": "true", className: clsx("flex flex-col", className), children: /* @__PURE__ */ jsx("div", { className: barStyles({ variant: "block" }), ...hasStyle ? { style } : {} }) });
72
83
  });
73
84
  Skeleton.displayName = "Skeleton";
74
85
  export {
@@ -1,7 +1,7 @@
1
1
  // src/Skeleton/Skeleton.manifest.ts
2
2
  var SkeletonManifest = {
3
3
  Skeleton: {
4
- description: "Loading placeholder. block renders one pulsing bar; text stacks `lines` bars with a shortened last line. Bars fill their container and never widen it, whatever width the caller asks for, and are drawn in a system color in forced-colors mode.",
4
+ description: 'Loading placeholder, purely visual and silent: hidden from assistive technology, with no role, name or text, so a screen of placeholders does not say "Loading" once each. block renders one pulsing bar; text stacks `lines` bars with a shortened last line. Bars fill their container and never widen it, whatever width the caller asks for, and are drawn in a system color in forced-colors mode. The area that is loading says it: mark the area `aria-busy`, keep one polite status outside it (on the page before it has anything to say) that says "Loading <what>" and then that the content has arrived.',
5
5
  props: {
6
6
  variant: { type: "enum", values: ["block", "text"], default: "block" },
7
7
  width: {
@@ -18,11 +18,19 @@ var SkeletonManifest = {
18
18
  type: "number",
19
19
  default: 3,
20
20
  description: "Number of stacked bars in the text variant, rounded down with a minimum of 1. Non-finite values fall back to 3."
21
+ },
22
+ "aria-label": {
23
+ type: "string",
24
+ description: 'Deprecated and not applied. A Skeleton used to be a status named "Loading" (or this label); it is decorative now. In development a structured error says so, once. Name the loading area instead. The same holds for aria-labelledby, aria-describedby, role, aria-live and aria-busy.'
25
+ },
26
+ role: {
27
+ type: "string",
28
+ description: "Deprecated and not applied: a Skeleton is decorative and has no role. See aria-label."
21
29
  }
22
30
  },
23
31
  composable_with: ["Card", "DataTable"],
24
32
  never_inside: ["Button", "Badge"],
25
- example: '<Skeleton variant="text" lines={3} />'
33
+ example: '<section aria-labelledby="tracks-title">\n <h2 id="tracks-title">Tracks</h2>\n <p role="status" className="sr-only">{note}</p>\n <div aria-busy={loading}>\n {loading ? <Skeleton variant="text" lines={3} /> : <TrackList tracks={tracks} />}\n </div>\n</section>'
26
34
  }
27
35
  };
28
36
  export {
@@ -6,7 +6,7 @@ import { createResizeObserver } from "../_internal/useElementWidth.js";
6
6
  import { useIsomorphicLayoutEffect } from "../_internal/useIsomorphicLayoutEffect.js";
7
7
  function useTabsIndicator(list, vertical) {
8
8
  const [geometry, setGeometry] = useState();
9
- const selectedId = useRef();
9
+ const selectedId = useRef(void 0);
10
10
  useIsomorphicLayoutEffect(() => {
11
11
  const node = list.current;
12
12
  if (!node) return;
@@ -35,11 +35,11 @@ function warnProbeMissing(origin) {
35
35
  function useHydrationRoot(...forwardedIds) {
36
36
  const probe = useId();
37
37
  const hydrated = useSyncExternalStore(subscribeNoop, getClientSnapshot, getServerSnapshot);
38
- const hydratingMount = useRef();
38
+ const hydratingMount = useRef(void 0);
39
39
  if (hydratingMount.current === void 0) hydratingMount.current = !hydrated;
40
40
  const [disagrees, setDisagrees] = useState(false);
41
41
  const clientOnly = useRef(hydrated);
42
- const origin = useRef();
42
+ const origin = useRef(void 0);
43
43
  if (process.env.NODE_ENV !== "production" && origin.current === void 0) {
44
44
  origin.current = callerOfHook();
45
45
  }
@@ -0,0 +1,30 @@
1
+ "use client";
2
+
3
+ // src/_internal/loadingStatus.ts
4
+ import { useState } from "react";
5
+ import { useHoldBackNewText } from "./liveRegion.js";
6
+ function useLoadingStatusText({
7
+ loading,
8
+ loadingText,
9
+ loadedText
10
+ }) {
11
+ const held = useHoldBackNewText(true);
12
+ const [seen, setSeen] = useState({ loading, first: loading, arrival: "" });
13
+ let current = seen;
14
+ if (seen.loading !== loading) {
15
+ current = {
16
+ loading,
17
+ first: false,
18
+ arrival: loading || seen.first ? "" : loadedText
19
+ };
20
+ setSeen(current);
21
+ } else if (!loading && seen.arrival !== "" && seen.arrival !== loadedText) {
22
+ current = { ...seen, arrival: "" };
23
+ setSeen(current);
24
+ }
25
+ if (held || current.first) return "";
26
+ return loading ? loadingText : current.arrival;
27
+ }
28
+ export {
29
+ useLoadingStatusText
30
+ };
@@ -25,8 +25,8 @@ function useMotion(mode = "auto", component) {
25
25
  const [ready, setReady] = useState(false);
26
26
  const [keyboard, setKeyboard] = useState(false);
27
27
  const pointer = useRef(false);
28
- const frame = useRef();
29
- const contactCleanup = useRef();
28
+ const frame = useRef(void 0);
29
+ const contactCleanup = useRef(void 0);
30
30
  const clearContact = useCallback(() => {
31
31
  contactCleanup.current?.();
32
32
  contactCleanup.current = void 0;