@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 +1 -1
- package/react/README.md +11 -0
- package/react/dist/index.d.ts +5 -1
- package/react/dist/index.js +23 -6
- package/src/components/index.js +48 -9
package/package.json
CHANGED
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
|
package/react/dist/index.d.ts
CHANGED
|
@@ -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. */
|
package/react/dist/index.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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 =
|
|
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 =
|
|
3423
|
+
const push = useContext3(Context);
|
|
3407
3424
|
if (!push) throw new Error("useToast requires a Toast provider");
|
|
3408
3425
|
return push;
|
|
3409
3426
|
}
|
package/src/components/index.js
CHANGED
|
@@ -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
|
|
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(
|
|
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 =
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
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
|