@apliteni/apliteni-ui 0.124.2 → 0.125.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@apliteni/apliteni-ui",
3
- "version": "0.124.2",
3
+ "version": "0.125.0",
4
4
  "workspaces": [
5
5
  "react"
6
6
  ],
package/react/README.md CHANGED
@@ -545,6 +545,17 @@ class and invalid styling as appropriate:
545
545
  The frame supplies a stable ID, required state, and linked hint/error messages.
546
546
  An optional `id` lets the caller choose the control ID; it must be unique.
547
547
 
548
+ A `Dropdown` is a control too. Spread the attributes onto it and its trigger takes
549
+ `aria-describedby` and `aria-invalid`, so the error and hint are read when it gets focus.
550
+ Its accessible name is the label and then the chosen value. Outside a `Field`,
551
+ `Dropdown` takes `aria-describedby` and `aria-invalid` as props and forwards them to the trigger.
552
+
553
+ ```tsx
554
+ <Field label="Owner" error="That option is no longer available.">
555
+ {control => <Dropdown {...control} variant="select" items={owners} />}
556
+ </Field>
557
+ ```
558
+
548
559
  ### Search
549
560
 
550
561
  `SearchField` is the toolbar's search box: a search glyph, a native
@@ -501,6 +501,10 @@ type DropdownProps = {
501
501
  /** A field above the rows that filters them as the reader types. */
502
502
  search?: boolean | DropdownSearch;
503
503
  ariaLabel?: string;
504
+ /** Names the elements that describe the trigger, e.g. a field's error and hint. */
505
+ 'aria-describedby'?: string;
506
+ /** Marks the trigger invalid. `Field` supplies it while its error shows. */
507
+ 'aria-invalid'?: boolean | 'true' | 'false';
504
508
  id?: string;
505
509
  panelClass?: string;
506
510
  /** Controlled: the host owns the open state and answers `onOpenChange`. */
@@ -512,7 +516,7 @@ type DropdownProps = {
512
516
  /** Draw the row yourself. `props` is everything the row needs; spread it. */
513
517
  row?: (item: DropdownItem, props: DropdownRowProps) => ReactNode;
514
518
  };
515
- declare function Dropdown({ label, value, placeholder, variant, items, sections, header, footer, triggerContent, triggerClass, disabled, chevron, align, direction, scroll, search, ariaLabel, id, panelClass, open: openProp, defaultOpen, onOpenChange, onSelect, row, }: DropdownProps): react.JSX.Element;
519
+ declare function Dropdown({ label, value, placeholder, variant, items, sections, header, footer, triggerContent, triggerClass, disabled, chevron, align, direction, scroll, search, ariaLabel, id, panelClass, 'aria-describedby': describedBy, 'aria-invalid': invalid, open: openProp, defaultOpen, onOpenChange, onSelect, row, }: DropdownProps): react.JSX.Element;
516
520
 
517
521
  type BackLinkOwnProps = {
518
522
  /** The address. No address, or a `javascript:` one, and nothing is rendered. */
@@ -2072,6 +2072,7 @@ function DatePicker({
2072
2072
  import {
2073
2073
  Fragment as Fragment5,
2074
2074
  useCallback as useCallback4,
2075
+ useContext as useContext2,
2075
2076
  useEffect as useEffect7,
2076
2077
  useId as useId8,
2077
2078
  useRef as useRef11,
@@ -2087,6 +2088,12 @@ import {
2087
2088
  dropdownHeightFit,
2088
2089
  filterPanelRow
2089
2090
  } from "@apliteni/apliteni-ui";
2091
+
2092
+ // src/fieldContext.ts
2093
+ import { createContext as createContext2 } from "react";
2094
+ var FieldContext = createContext2(null);
2095
+
2096
+ // src/Dropdown.tsx
2090
2097
  import { Fragment as Fragment6, jsx as jsx20, jsxs as jsxs16 } from "react/jsx-runtime";
2091
2098
  var EXIT_SLACK_MS2 = 50;
2092
2099
  var SETTLE_MS = 450;
@@ -2140,6 +2147,8 @@ function Dropdown({
2140
2147
  ariaLabel,
2141
2148
  id,
2142
2149
  panelClass = "",
2150
+ "aria-describedby": describedBy,
2151
+ "aria-invalid": invalid,
2143
2152
  open: openProp,
2144
2153
  defaultOpen = false,
2145
2154
  onOpenChange,
@@ -2159,6 +2168,8 @@ function Dropdown({
2159
2168
  const at = useRef11("");
2160
2169
  const auto = useId8().replace(/:/g, "");
2161
2170
  const uid = id ?? auto;
2171
+ const inField = useContext2(FieldContext);
2172
+ const fieldName = inField && id === inField.id && !(ariaLabel && triggerContent != null) ? { id: `${uid}-trigger`, labelId: inField.labelId, labelledBy: `${inField.labelId} ${uid}-trigger` } : null;
2162
2173
  const setOpen = useCallback4((next) => {
2163
2174
  if (openProp === void 0) setSelfOpen(next);
2164
2175
  onOpenChange?.(next);
@@ -2559,7 +2570,11 @@ function Dropdown({
2559
2570
  disabled: disabled || void 0,
2560
2571
  "aria-haspopup": sx ? "dialog" : listRole,
2561
2572
  "aria-expanded": open,
2573
+ id: fieldName?.id,
2562
2574
  "aria-label": ariaLabel && triggerContent != null ? ariaLabel : void 0,
2575
+ "aria-labelledby": fieldName?.labelledBy,
2576
+ "aria-describedby": describedBy,
2577
+ "aria-invalid": invalid,
2563
2578
  ref: trigger,
2564
2579
  onClick: (e) => {
2565
2580
  e.stopPropagation();
@@ -2588,6 +2603,7 @@ function Dropdown({
2588
2603
  "data-dropdown-panel": "",
2589
2604
  role: sx ? "dialog" : listRole,
2590
2605
  "aria-label": sx ? name : ariaLabel,
2606
+ "aria-labelledby": !sx && !ariaLabel ? fieldName?.labelId : void 0,
2591
2607
  style: sx ? void 0 : cap,
2592
2608
  ref: panel,
2593
2609
  onClick: (e) => e.stopPropagation(),
@@ -3084,9 +3100,9 @@ import { jsx as jsx29, jsxs as jsxs24 } from "react/jsx-runtime";
3084
3100
  function messages(id, hint, error) {
3085
3101
  return [error && `${id}-error`, hint && `${id}-hint`].filter(Boolean).join(" ") || void 0;
3086
3102
  }
3087
- function Frame({ id, label, hint, error, required, children }) {
3103
+ function Frame({ id, labelId, label, hint, error, required, children }) {
3088
3104
  return /* @__PURE__ */ jsxs24("div", { className: "ui-field", children: [
3089
- /* @__PURE__ */ jsxs24("label", { className: "ui-field__label", htmlFor: id, children: [
3105
+ /* @__PURE__ */ jsxs24("label", { className: "ui-field__label", id: labelId, htmlFor: id, children: [
3090
3106
  label,
3091
3107
  required && /* @__PURE__ */ jsx29("span", { className: "ui-field__req", "aria-hidden": "true", children: "*" })
3092
3108
  ] }),
@@ -3109,7 +3125,8 @@ function wiring(id, hint, error) {
3109
3125
  function Field({ id: suppliedId, label, hint, error, required, children }) {
3110
3126
  const generatedId = useId10();
3111
3127
  const id = suppliedId ?? generatedId;
3112
- return /* @__PURE__ */ jsx29(Frame, { ...{ id, label, hint, error, required }, children: children({ ...wiring(id, hint, error), required }) });
3128
+ const labelId = `${id}-label`;
3129
+ return /* @__PURE__ */ jsx29(Frame, { ...{ id, labelId, label, hint, error, required }, children: /* @__PURE__ */ jsx29(FieldContext.Provider, { value: { id, labelId }, children: children({ ...wiring(id, hint, error), required }) }) });
3113
3130
  }
3114
3131
  var TextField = forwardRef6(function TextField2({ label, hint, error, required, type = "text", unit, icon: glyph, className = "", ...props }, ref) {
3115
3132
  const id = useId10();
@@ -3397,13 +3414,13 @@ var SearchField = forwardRef7(function SearchField2({ ariaLabel, className, ...p
3397
3414
  });
3398
3415
 
3399
3416
  // src/Toast.tsx
3400
- import { createContext as createContext2, useCallback as useCallback5, useContext as useContext2, useEffect as useEffect12, useLayoutEffect as useLayoutEffect4, useRef as useRef18, useState as useState12 } from "react";
3417
+ import { createContext as createContext3, useCallback as useCallback5, useContext as useContext3, useEffect as useEffect12, useLayoutEffect as useLayoutEffect4, useRef as useRef18, useState as useState12 } from "react";
3401
3418
  import { createPortal as createPortal4 } from "react-dom";
3402
3419
  import { jsx as jsx32, jsxs as jsxs27 } from "react/jsx-runtime";
3403
- var Context = createContext2(null);
3420
+ var Context = createContext3(null);
3404
3421
  var glyphs = { success: "circleCheck", danger: "circleX", warn: "circleAlert", info: "info", neutral: "bolt" };
3405
3422
  function useToast() {
3406
- const push = useContext2(Context);
3423
+ const push = useContext3(Context);
3407
3424
  if (!push) throw new Error("useToast requires a Toast provider");
3408
3425
  return push;
3409
3426
  }
@@ -132,10 +132,10 @@ function withControlId(html) {
132
132
  // aria-describedby, aria-invalid, required. Anything the control already spells
133
133
  // out for itself wins — a caller who passed `invalid` to input() keeps their own
134
134
  // aria-invalid. `true` renders a bare boolean attribute.
135
- function withControlAttrs(html, attrs) {
135
+ function withAttrsOn(html, openTag, attrs) {
136
136
  const pairs = Object.entries(attrs).filter(([, v]) => v != null && v !== false);
137
137
  if (!pairs.length) return html;
138
- return html.replace(/<(input|textarea|select)\b([^>]*?)(\s*\/?)>/, (m, tag, rest, tail) => {
138
+ return html.replace(openTag, (m, tag, rest, tail) => {
139
139
  const add = pairs
140
140
  .filter(([k]) => !new RegExp(`\\s${k}(?=[\\s=>/]|$)`).test(rest))
141
141
  .map(([k, v]) => (v === true ? ` ${k}` : ` ${k}="${esc(v)}"`))
@@ -143,6 +143,38 @@ function withControlAttrs(html, attrs) {
143
143
  return `<${tag}${rest}${add}${tail}>`;
144
144
  });
145
145
  }
146
+ const withControlAttrs = (html, attrs) => withAttrsOn(html, /<(input|textarea|select)\b([^>]*?)(\s*\/?)>/, attrs);
147
+
148
+ // A dropdown() is not a form element: its id sits on the outer div, which a
149
+ // <label for> cannot name, and its one input (the search field) is inside the
150
+ // panel. So field() wires the trigger button instead: the label and the value
151
+ // name it, and the hint or error describes it. why: docs/components.md#forms
152
+ const DD_ROOT = /<(div)\b((?=[^>]*\bdata-dropdown(?=[\s>]))[^>]*?)(\s*\/?)>/;
153
+ const DD_TRIGGER = /<(button)\b((?=[^>]*\bdata-dropdown-trigger\b)[^>]*?)(\s*\/?)>/;
154
+ const DD_PANEL = /<(div)\b((?=[^>]*\bdata-dropdown-panel\b)[^>]*?)(\s*\/?)>/;
155
+ const attrId = (tag) => tag?.match(/\sid="([^"]+)"/)?.[1] || null;
156
+ function withDropdownId(html, id) {
157
+ const root = html.match(DD_ROOT)[0];
158
+ const existing = attrId(root);
159
+ if (existing) return { html, id: existing };
160
+ const own = id || nextId('field');
161
+ return { html: html.replace(DD_ROOT, (m) => m.replace('<div', `<div id="${esc(own)}"`)), id: own };
162
+ }
163
+ function withDropdownAttrs(html, { rootId, labelId, describedby, invalid }) {
164
+ const trigger = html.match(DD_TRIGGER)?.[0] || '';
165
+ const triggerId = attrId(trigger) || `${rootId}-trigger`;
166
+ // A trigger that carries its own aria-label keeps it: the caller named it.
167
+ const named = labelId && !/\saria-label(?:ledby)?=/.test(trigger);
168
+ let out = withAttrsOn(html, DD_TRIGGER, {
169
+ id: named ? triggerId : null,
170
+ 'aria-labelledby': named ? `${labelId} ${triggerId}` : null,
171
+ 'aria-describedby': describedby,
172
+ 'aria-invalid': invalid,
173
+ });
174
+ const panel = html.match(DD_PANEL)?.[0] || '';
175
+ if (labelId && !/\saria-label(?:ledby)?=/.test(panel)) out = withAttrsOn(out, DD_PANEL, { 'aria-labelledby': labelId });
176
+ return out;
177
+ }
146
178
 
147
179
  // A labelled control with, optionally, a hint or an error under it.
148
180
  //
@@ -156,21 +188,28 @@ function withControlAttrs(html, attrs) {
156
188
  export function field({ label, hint, error, control = '', id, required = false } = {}) {
157
189
  let ctl = control;
158
190
  let forId = id;
191
+ const dd = DD_ROOT.test(control);
192
+ let ddId = null;
159
193
  if (label && control) {
160
- const r = withControlId(control);
194
+ const r = dd ? withDropdownId(control, id) : withControlId(control);
161
195
  ctl = r.html;
162
196
  forId = id || r.id;
197
+ if (dd) ddId = r.id;
163
198
  }
164
199
  // Error wins over hint — only one message shows, so only one id is minted.
165
200
  const msg = error || hint;
166
201
  const msgId = msg && control ? nextId(error ? 'field-err' : 'field-hint') : null;
167
- ctl = withControlAttrs(ctl, {
168
- 'aria-describedby': msgId,
169
- 'aria-invalid': error ? 'true' : null,
170
- required: required || null,
171
- });
202
+ ctl = dd
203
+ ? withDropdownAttrs(ctl, {
204
+ rootId: ddId, labelId: ddId && `${ddId}-label`, describedby: msgId, invalid: error ? 'true' : null,
205
+ })
206
+ : withControlAttrs(ctl, {
207
+ 'aria-describedby': msgId,
208
+ 'aria-invalid': error ? 'true' : null,
209
+ required: required || null,
210
+ });
172
211
  const lab = label
173
- ? `<label class="ui-field__label"${forId ? ` for="${id ? esc(id) : forId}"` : ''}>${esc(label)}`
212
+ ? `<label class="ui-field__label"${ddId ? ` id="${esc(ddId)}-label"` : ''}${forId ? ` for="${id ? esc(id) : forId}"` : ''}>${esc(label)}`
174
213
  + `${required ? '<span class="ui-field__req" aria-hidden="true">*</span>' : ''}</label>`
175
214
  : '';
176
215
  const foot = error