@zeyos/zx 4.4.0 → 5.0.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/CHANGELOG.md +283 -0
- package/README.md +42 -10
- package/dist/types/components/activity-list/activity-list.d.ts +22 -0
- package/dist/types/components/app-sidebar/app-sidebar.d.ts +5 -3
- package/dist/types/components/avatar/avatar.d.ts +46 -7
- package/dist/types/components/badge/badge.d.ts +21 -1
- package/dist/types/components/button/button.d.ts +15 -0
- package/dist/types/components/chart/chart.d.ts +10 -0
- package/dist/types/components/checklist/checklist.d.ts +9 -0
- package/dist/types/components/code-editor/code-editor.d.ts +6 -17
- package/dist/types/components/datebox/date-range-box.d.ts +5 -1
- package/dist/types/components/datebox/datebox.d.ts +7 -2
- package/dist/types/components/dialog/dialog.d.ts +46 -6
- package/dist/types/components/dropdown/dropdown.d.ts +2 -2
- package/dist/types/components/entity-ref/entity-ref.d.ts +2 -2
- package/dist/types/components/field/field.d.ts +24 -2
- package/dist/types/components/fieldset/fieldset.d.ts +19 -1
- package/dist/types/components/file-item/file-item.d.ts +34 -6
- package/dist/types/components/form/form.d.ts +12 -1
- package/dist/types/components/kanban-view/kanban-card.d.ts +8 -0
- package/dist/types/components/launcher/launcher.d.ts +260 -15
- package/dist/types/components/list-toolbar/list-toolbar.d.ts +1 -1
- package/dist/types/components/menu-button/menu-button.d.ts +33 -5
- package/dist/types/components/message/message.d.ts +119 -17
- package/dist/types/components/modal/modal.d.ts +13 -2
- package/dist/types/components/select/color.d.ts +12 -0
- package/dist/types/components/select/glyphs.d.ts +124 -0
- package/dist/types/components/select/select.d.ts +11 -3
- package/dist/types/components/stat-tile/stat-tile.d.ts +16 -5
- package/dist/types/components/tabbox/tabbox.d.ts +9 -0
- package/dist/types/components/table/table.d.ts +46 -1
- package/dist/types/components/tag-picker/tag-picker.d.ts +21 -4
- package/dist/types/components/tree/tree.d.ts +13 -0
- package/dist/types/core/breakpoint.d.ts +1 -1
- package/dist/types/core/icons.d.ts +23 -25
- package/dist/types/core/motion.d.ts +59 -0
- package/dist/types/core/overlay-host.d.ts +15 -1
- package/dist/types/index.d.ts +5 -3
- package/dist/types/zeyos/connect.d.ts +6 -0
- package/dist/types/zeyos/form.d.ts +50 -9
- package/dist/types/zeyos/index.d.ts +1 -1
- package/dist/types/zeyos/query.d.ts +79 -0
- package/dist/types/zeyos/schema.d.ts +24 -1
- package/dist/types/zeyos/table.d.ts +100 -14
- package/dist/types/zeyos/view.d.ts +10 -0
- package/dist/zx-zeyos.esm.js +1621 -443
- package/dist/zx-zeyos.esm.min.js +53 -5
- package/dist/zx.css +1411 -660
- package/dist/zx.esm.js +1976 -703
- package/dist/zx.esm.min.js +56 -8
- package/dist/zx.global.js +1978 -705
- package/dist/zx.global.min.js +56 -8
- package/dist/zx.min.css +1 -1
- package/docs/api.json +219 -34
- package/docs/llms.md +472 -110
- package/docs/llms.txt +3 -3
- package/package.json +1 -1
- package/src/components/account-menu/account-menu.css +35 -10
- package/src/components/activity-item/activity-item.css +1 -1
- package/src/components/activity-item/activity-item.js +3 -0
- package/src/components/activity-list/activity-list.css +38 -3
- package/src/components/activity-list/activity-list.js +34 -0
- package/src/components/app-icon/app-icon.css +1 -1
- package/src/components/app-rail/app-rail.css +40 -79
- package/src/components/app-sidebar/app-sidebar.css +109 -174
- package/src/components/app-sidebar/app-sidebar.js +21 -6
- package/src/components/aurora/aurora.css +5 -3
- package/src/components/avatar/avatar.css +32 -7
- package/src/components/avatar/avatar.js +72 -8
- package/src/components/badge/badge.css +54 -2
- package/src/components/badge/badge.js +29 -2
- package/src/components/button/button.css +67 -9
- package/src/components/button/button.js +38 -0
- package/src/components/calendar/calendar.css +46 -46
- package/src/components/card/card.css +2 -2
- package/src/components/card-view/card-view.css +9 -7
- package/src/components/chart/chart.css +4 -1
- package/src/components/chart/chart.js +12 -0
- package/src/components/check-button/check-button.css +10 -2
- package/src/components/checklist/checklist.css +9 -2
- package/src/components/checklist/checklist.js +11 -5
- package/src/components/checklist/field-adapter.js +3 -1
- package/src/components/code-editor/code-editor.css +6 -5
- package/src/components/code-editor/code-editor.js +19 -1
- package/src/components/context-menu/context-menu.css +9 -4
- package/src/components/copy/copy-button.css +6 -0
- package/src/components/copy/copy-input.css +8 -3
- package/src/components/data-filter/data-filter.css +5 -4
- package/src/components/date-picker/date-picker.css +6 -6
- package/src/components/date-picker/date-range-picker.css +3 -3
- package/src/components/date-picker/time-picker.css +1 -1
- package/src/components/datebox/date-range-box.css +79 -2
- package/src/components/datebox/date-range-box.js +13 -2
- package/src/components/datebox/datebox.css +79 -2
- package/src/components/datebox/datebox.js +13 -2
- package/src/components/dialog/dialog.css +26 -0
- package/src/components/dialog/dialog.js +99 -17
- package/src/components/dropdown/dropdown.css +1 -1
- package/src/components/dropdown/dropdown.js +5 -3
- package/src/components/empty-state/empty-state.css +2 -2
- package/src/components/entity-ref/entity-ref.css +1 -1
- package/src/components/entity-ref/entity-ref.js +1 -1
- package/src/components/field/field.css +28 -9
- package/src/components/field/field.js +68 -26
- package/src/components/field-upload/field-upload.css +1 -1
- package/src/components/fieldset/fieldset.css +5 -3
- package/src/components/fieldset/fieldset.js +28 -5
- package/src/components/file-item/file-item.js +144 -10
- package/src/components/filter/filter.js +7 -3
- package/src/components/filter-panel/filter-panel.css +1 -1
- package/src/components/finder/finder.css +15 -13
- package/src/components/form/form.css +5 -4
- package/src/components/form/form.js +11 -2
- package/src/components/grid/grid.css +1 -1
- package/src/components/groupbox/groupbox.css +1 -1
- package/src/components/kanban-view/kanban-card.js +11 -3
- package/src/components/kanban-view/kanban-view.css +39 -15
- package/src/components/kanban-view/kanban-view.js +4 -4
- package/src/components/launcher/launcher.css +107 -25
- package/src/components/launcher/launcher.js +503 -41
- package/src/components/list-toolbar/list-toolbar.css +13 -8
- package/src/components/list-toolbar/list-toolbar.js +1 -1
- package/src/components/master-panel/master-panel.css +2 -0
- package/src/components/menu-button/menu-button.css +9 -4
- package/src/components/menu-button/menu-button.js +65 -9
- package/src/components/message/message.css +33 -3
- package/src/components/message/message.js +217 -15
- package/src/components/modal/modal.js +18 -3
- package/src/components/navigation-bar/navigation-bar.css +18 -9
- package/src/components/number-field/field-adapter.js +1 -0
- package/src/components/number-field/number-field.css +70 -0
- package/src/components/number-field/number-field.js +2 -0
- package/src/components/pagination/pagination.css +8 -6
- package/src/components/panel/panel.css +1 -1
- package/src/components/rating/rating.css +7 -0
- package/src/components/select/color.js +22 -0
- package/src/components/select/field-adapter.js +2 -0
- package/src/components/select/glyphs.js +165 -0
- package/src/components/select/select.css +138 -45
- package/src/components/select/select.js +83 -47
- package/src/components/sheet/sheet.css +11 -8
- package/src/components/slider/slider.css +16 -2
- package/src/components/slider/slider.js +1 -1
- package/src/components/sort-control/sort-control.css +4 -1
- package/src/components/stat-tile/stat-tile.css +41 -46
- package/src/components/stat-tile/stat-tile.js +17 -6
- package/src/components/tabbox/tabbox.css +32 -27
- package/src/components/tabbox/tabbox.js +5 -1
- package/src/components/table/table.css +104 -29
- package/src/components/table/table.js +90 -16
- package/src/components/tag-picker/tag-picker.css +83 -5
- package/src/components/tag-picker/tag-picker.js +25 -8
- package/src/components/timebox/timebox.css +22 -2
- package/src/components/toggle/toggle.css +12 -1
- package/src/components/toolbar/toolbar.js +2 -2
- package/src/components/tooltip/tooltip.css +4 -0
- package/src/components/tooltip/tooltip.js +10 -3
- package/src/components/tree/tree.css +25 -16
- package/src/components/tree/tree.js +31 -0
- package/src/components/value-list/value-list.css +3 -3
- package/src/components/view/record-view.js +1 -1
- package/src/core/breakpoint.js +1 -1
- package/src/core/icons.js +193 -60
- package/src/core/motion.js +147 -0
- package/src/core/overlay-host.js +21 -2
- package/src/index.js +5 -3
- package/src/internal/app-rail.js +1 -2
- package/src/zeyos/connect.js +6 -2
- package/src/zeyos/form.js +67 -27
- package/src/zeyos/index.js +1 -1
- package/src/zeyos/query.js +153 -0
- package/src/zeyos/schema.js +144 -27
- package/src/zeyos/table.js +68 -5
- package/src/zeyos/view.js +6 -0
- package/styles/base.css +56 -0
- package/styles/icons.css +11 -1
- package/styles/tokens/dark.css +113 -37
- package/styles/tokens/global.css +121 -27
- package/styles/tokens/semantic.css +100 -31
- package/styles/tokens/themes.css +7 -6
- package/styles/zx.css +0 -1
- package/styles/tokens/density.css +0 -9
|
@@ -20,7 +20,7 @@ import { button } from '../button/button.js';
|
|
|
20
20
|
* @property {DialogButton[]} [buttons=[]] Footer buttons.
|
|
21
21
|
* @property {boolean} [closable=true] Whether Escape and the header button may close the dialog.
|
|
22
22
|
* @property {Node|string|number|{toElement: () => Node|null}|null} [content=null] Body content.
|
|
23
|
-
* @property {Element|string|null} [scope=null] Element whose nearest Zx theme scope owns the overlay; defaults to the opener.
|
|
23
|
+
* @property {Element|string|null} [scope=null] Element whose nearest Zx theme scope owns the overlay; defaults to the opener. Resolved on every open, so a scope connected after construction still owns it.
|
|
24
24
|
* @property {(event: CustomEvent<Record<string, never>>) => void} [onopen] Open event listener.
|
|
25
25
|
* @property {(event: CustomEvent<{result: unknown}>) => void} [onclose] Close event listener.
|
|
26
26
|
* @property {(event: CustomEvent<Record<string, never>>) => void} [oncancel] Cancel event listener.
|
|
@@ -51,6 +51,11 @@ import { button } from '../button/button.js';
|
|
|
51
51
|
* @property {string} [okLabel='OK'] Confirmation label.
|
|
52
52
|
* @property {string} [cancelLabel='Cancel'] Cancellation label.
|
|
53
53
|
* @property {boolean} [danger=false] Whether the confirmation action is dangerous.
|
|
54
|
+
* @property {string|null} [confirmText=null] Text the reader must type before the confirm button
|
|
55
|
+
* enables — the name of what is about to be deleted, say. The dialog adds an input labelled
|
|
56
|
+
* "Type <confirmText> to confirm" (message `dialog.typeToConfirm`, `%1` standing for the text),
|
|
57
|
+
* focuses it, and enables the confirm button, and Enter, only while its value matches exactly,
|
|
58
|
+
* ignoring surrounding spaces.
|
|
54
59
|
*/
|
|
55
60
|
/**
|
|
56
61
|
* @typedef {Object} PromptOptions
|
|
@@ -58,6 +63,8 @@ import { button } from '../button/button.js';
|
|
|
58
63
|
* @property {Node|string|number|null} [message=''] Prompt message.
|
|
59
64
|
* @property {string} [value=''] Initial input value.
|
|
60
65
|
* @property {string} [placeholder=''] Input placeholder.
|
|
66
|
+
* @property {string} [okLabel='OK'] Confirmation label.
|
|
67
|
+
* @property {string} [cancelLabel='Cancel'] Cancellation label.
|
|
61
68
|
*/
|
|
62
69
|
|
|
63
70
|
/**
|
|
@@ -138,7 +145,7 @@ export class Dialog extends Modal {
|
|
|
138
145
|
h('footer', { class: 'zx-dialog__footer', ref: 'footer' })
|
|
139
146
|
));
|
|
140
147
|
dialog.dataset.state = 'closed';
|
|
141
|
-
this.
|
|
148
|
+
this._mountInitial(dialog);
|
|
142
149
|
return dialog;
|
|
143
150
|
}
|
|
144
151
|
|
|
@@ -281,12 +288,13 @@ export class Dialog extends Modal {
|
|
|
281
288
|
}
|
|
282
289
|
|
|
283
290
|
/**
|
|
284
|
-
* Opens a confirmation dialog.
|
|
291
|
+
* Opens a confirmation dialog. With `confirmText` it becomes a type-to-confirm dialog: the
|
|
292
|
+
* confirm button stays disabled until the reader has typed that text.
|
|
285
293
|
* @param {ConfirmOptions} [options={}] Confirmation options.
|
|
286
|
-
* @returns {Promise<boolean>}
|
|
294
|
+
* @returns {Promise<boolean>} True when confirmed; false on Cancel, Escape, or the close button.
|
|
287
295
|
*/
|
|
288
296
|
static confirm({
|
|
289
|
-
title = '', message = '', okLabel, cancelLabel, danger = false
|
|
297
|
+
title = '', message = '', okLabel, cancelLabel, danger = false, confirmText = null
|
|
290
298
|
} = {}) {
|
|
291
299
|
return new Promise((resolve) => {
|
|
292
300
|
let settled = false;
|
|
@@ -296,16 +304,58 @@ export class Dialog extends Modal {
|
|
|
296
304
|
dialog.close(value);
|
|
297
305
|
resolve(value);
|
|
298
306
|
};
|
|
299
|
-
const
|
|
300
|
-
dialog
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
307
|
+
const expected = confirmText == null ? '' : String(confirmText).trim();
|
|
308
|
+
const dialog = new Dialog(null, { title, content: expected ? null : message });
|
|
309
|
+
// A destructive confirmation starts on the least destructive choice, as the APG alert dialog
|
|
310
|
+
// has it; type-to-confirm starts in its input.
|
|
311
|
+
const cancel = {
|
|
312
|
+
label: cancelLabel ?? dialog._message('dialog.cancel', 'Cancel'),
|
|
313
|
+
action: () => finish(false),
|
|
314
|
+
autofocus: danger && !expected
|
|
315
|
+
};
|
|
316
|
+
const accept = {
|
|
317
|
+
label: okLabel ?? dialog._message('dialog.ok', 'OK'),
|
|
318
|
+
kind: /** @type {DialogButtonKind} */ (danger ? 'danger' : 'primary'),
|
|
319
|
+
action: () => finish(true),
|
|
320
|
+
autofocus: !danger && !expected
|
|
321
|
+
};
|
|
322
|
+
if (expected) {
|
|
323
|
+
const input = /** @type {HTMLInputElement} */ (h('input', {
|
|
324
|
+
class: 'zx-dialog__confirm-input',
|
|
325
|
+
id: uid('zx-dialog-confirm'),
|
|
326
|
+
type: 'text',
|
|
327
|
+
autocomplete: 'off',
|
|
328
|
+
autocapitalize: 'off',
|
|
329
|
+
spellcheck: false,
|
|
306
330
|
autofocus: true
|
|
307
|
-
}
|
|
308
|
-
|
|
331
|
+
}));
|
|
332
|
+
const [before, after] = typeToConfirmParts(
|
|
333
|
+
dialog._message('dialog.typeToConfirm', 'Type %1 to confirm'), expected);
|
|
334
|
+
const label = h('label', { class: 'zx-dialog__confirm-label', for: input.id },
|
|
335
|
+
before, h('b', {}, expected), after);
|
|
336
|
+
dialog.setContent(h('div', { class: 'zx-dialog__confirm' },
|
|
337
|
+
isEmptyContent(message) ? null : h('div', { class: 'zx-dialog__prompt-message' }, message),
|
|
338
|
+
label,
|
|
339
|
+
input));
|
|
340
|
+
let matched = false;
|
|
341
|
+
const settle = () => dialog.setButtons([cancel, { ...accept, autofocus: false, disabled: !matched }]);
|
|
342
|
+
settle();
|
|
343
|
+
dialog.listen(input, 'input', () => {
|
|
344
|
+
const next = typedConfirmation(input.value, expected);
|
|
345
|
+
if (next === matched) return;
|
|
346
|
+
matched = next;
|
|
347
|
+
settle();
|
|
348
|
+
});
|
|
349
|
+
// Enter confirms only a match; otherwise it does nothing, as the disabled button would.
|
|
350
|
+
dialog.listen(input, 'keydown', (event) => {
|
|
351
|
+
const key = /** @type {KeyboardEvent} */ (event);
|
|
352
|
+
if (key.key !== 'Enter' || key.isComposing) return;
|
|
353
|
+
key.preventDefault();
|
|
354
|
+
if (typedConfirmation(input.value, expected)) finish(true);
|
|
355
|
+
});
|
|
356
|
+
} else {
|
|
357
|
+
dialog.setButtons([cancel, accept]);
|
|
358
|
+
}
|
|
309
359
|
dialog.once('close', () => {
|
|
310
360
|
if (!settled) {
|
|
311
361
|
settled = true;
|
|
@@ -322,7 +372,9 @@ export class Dialog extends Modal {
|
|
|
322
372
|
* @param {PromptOptions} [options={}] Prompt options.
|
|
323
373
|
* @returns {Promise<string|null>}
|
|
324
374
|
*/
|
|
325
|
-
static prompt({
|
|
375
|
+
static prompt({
|
|
376
|
+
title = '', message = '', value = '', placeholder = '', okLabel, cancelLabel
|
|
377
|
+
} = {}) {
|
|
326
378
|
return new Promise((resolve) => {
|
|
327
379
|
let settled = false;
|
|
328
380
|
const input = h('input', {
|
|
@@ -344,8 +396,8 @@ export class Dialog extends Modal {
|
|
|
344
396
|
const dialog = new Dialog(null, { title, content });
|
|
345
397
|
input.setAttribute('aria-label', String(title || dialog._message('dialog.promptValue', 'Prompt value')));
|
|
346
398
|
dialog.setButtons([
|
|
347
|
-
{ label: dialog._message('dialog.cancel', 'Cancel'), action: () => finish(null) },
|
|
348
|
-
{ label: dialog._message('dialog.ok', 'OK'), kind: 'primary', action: () => finish(input.value) }
|
|
399
|
+
{ label: cancelLabel ?? dialog._message('dialog.cancel', 'Cancel'), action: () => finish(null) },
|
|
400
|
+
{ label: okLabel ?? dialog._message('dialog.ok', 'OK'), kind: 'primary', action: () => finish(input.value) }
|
|
349
401
|
]);
|
|
350
402
|
dialog.listen(input, 'keydown', (event) => {
|
|
351
403
|
if (event.key !== 'Enter') return;
|
|
@@ -428,6 +480,36 @@ export class Dialog extends Modal {
|
|
|
428
480
|
}
|
|
429
481
|
}
|
|
430
482
|
|
|
483
|
+
/**
|
|
484
|
+
* Reports whether what the reader typed confirms a type-to-confirm dialog: the same text, exactly
|
|
485
|
+
* — case and inner spacing included — once surrounding spaces are ignored on both sides.
|
|
486
|
+
* @param {unknown} typed Input value.
|
|
487
|
+
* @param {unknown} expected Text the dialog asks for.
|
|
488
|
+
* @returns {boolean}
|
|
489
|
+
*/
|
|
490
|
+
export function typedConfirmation(typed, expected) {
|
|
491
|
+
const target = expected == null ? '' : String(expected).trim();
|
|
492
|
+
return target !== '' && String(typed ?? '').trim() === target;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
/**
|
|
496
|
+
* Splits the type-to-confirm message around `%1`, where the text to type goes. A translation that
|
|
497
|
+
* leaves `%1` out gets the text after it, following a space; a second `%1` is filled in as text.
|
|
498
|
+
* @param {string} template Message such as "Type %1 to confirm".
|
|
499
|
+
* @param {string} expected Text to type.
|
|
500
|
+
* @returns {[string, string]} The text before and after the text to type.
|
|
501
|
+
*/
|
|
502
|
+
export function typeToConfirmParts(template, expected) {
|
|
503
|
+
const parts = String(template).split('%1');
|
|
504
|
+
if (parts.length === 1) return [`${parts[0]} `, ''];
|
|
505
|
+
return [parts[0], parts.slice(1).join(expected)];
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
/** @param {unknown} content @returns {boolean} */
|
|
509
|
+
function isEmptyContent(content) {
|
|
510
|
+
return content == null || content === '';
|
|
511
|
+
}
|
|
512
|
+
|
|
431
513
|
/**
|
|
432
514
|
* Copies footer-button descriptors without mutating the caller's array or objects, and settles
|
|
433
515
|
* the two booleans the footer renders from.
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
max-block-size: calc(100vh - (2 * var(--zx-space-3)));
|
|
4
4
|
overflow: auto;
|
|
5
5
|
border: 1px solid var(--zx-color-overlay-border);
|
|
6
|
-
border-radius: var(--zx-radius-
|
|
6
|
+
border-radius: var(--zx-radius-lg);
|
|
7
7
|
background-color: var(--zx-color-overlay-surface);
|
|
8
8
|
background-image: var(--zx-overlay-surface-image);
|
|
9
9
|
background-clip: padding-box;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Component } from '../../core/component.js';
|
|
2
2
|
import { h, resolveElement } from '../../core/dom.js';
|
|
3
|
-
import { overlayHost } from '../../core/overlay-host.js';
|
|
3
|
+
import { connectedOverlayHost, overlayHost } from '../../core/overlay-host.js';
|
|
4
4
|
import { position } from '../../core/position.js';
|
|
5
5
|
import { uid } from '../../core/util.js';
|
|
6
6
|
|
|
@@ -15,7 +15,7 @@ const HANDLE_ESCAPE_DISMISS = Symbol('zxDropdownEscapeDismiss');
|
|
|
15
15
|
* @property {boolean} [matchWidth=false] Whether the panel matches the anchor width.
|
|
16
16
|
* @property {'click'|'manual'} [openOn='click'] Trigger behavior.
|
|
17
17
|
* @property {boolean} [closeOnSelect=false] Whether a click within the panel closes it.
|
|
18
|
-
* @property {Element|string|null} [scope=null] Element whose nearest Zx theme scope owns the panel; defaults to the anchor.
|
|
18
|
+
* @property {Element|string|null} [scope=null] Element whose nearest Zx theme scope owns the panel; defaults to the anchor. Resolved on every open, so an anchor connected after construction still gets its scope.
|
|
19
19
|
* @property {(event: CustomEvent<Record<string, never>>) => void} [onopen] Open event listener.
|
|
20
20
|
* @property {(event: CustomEvent<Record<string, never>>) => void} [onclose] Close event listener.
|
|
21
21
|
*/
|
|
@@ -58,7 +58,9 @@ export class Dropdown extends Component {
|
|
|
58
58
|
if (!resolved) throw new TypeError('Dropdown anchor could not be resolved');
|
|
59
59
|
super(null, options);
|
|
60
60
|
this.#anchor = resolved;
|
|
61
|
-
|
|
61
|
+
// An anchor that is not in the document yet has no theme scope to read; the panel then waits
|
|
62
|
+
// detached and finds its host on the first open, instead of settling on <body>.
|
|
63
|
+
connectedOverlayHost(this.options.scope ?? resolved)?.append(this.el);
|
|
62
64
|
this.#rememberAnchorAttribute('aria-expanded');
|
|
63
65
|
this.#rememberAnchorAttribute('aria-controls');
|
|
64
66
|
this.#anchor.setAttribute('aria-expanded', 'false');
|
|
@@ -11,14 +11,14 @@
|
|
|
11
11
|
|
|
12
12
|
.zx-empty-state__icon {
|
|
13
13
|
display: flex;
|
|
14
|
-
color: var(--zx-color-text-
|
|
14
|
+
color: var(--zx-color-text-subtle);
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
.zx-empty-state__title {
|
|
18
18
|
margin: 0;
|
|
19
19
|
color: var(--zx-color-text);
|
|
20
20
|
font-size: var(--zx-text-lg);
|
|
21
|
-
font-weight:
|
|
21
|
+
font-weight: var(--zx-weight-semibold);
|
|
22
22
|
line-height: 1.4;
|
|
23
23
|
}
|
|
24
24
|
|
|
@@ -52,7 +52,7 @@ import { isElement } from '../../core/util.js';
|
|
|
52
52
|
* @property {EntityRefMetadata[]} [metadata=[]] Labelled dense metadata.
|
|
53
53
|
* @property {EntityRefLinkValue} [link=null] Optional native primary link.
|
|
54
54
|
* @property {EntityRefActionEntry[]} [actions=[]] Secondary controls; element actions keep their own behavior.
|
|
55
|
-
* @property {'sm'|'md'|'lg'} [size='md']
|
|
55
|
+
* @property {'sm'|'md'|'lg'} [size='md'] Size of the icon and the identity text.
|
|
56
56
|
* @property {boolean} [wrap=false] Whether long identity text may wrap instead of truncating.
|
|
57
57
|
* @property {string} [actionsLabel='Entity actions'] Accessible name for secondary actions.
|
|
58
58
|
* @property {(event: CustomEvent<{id: unknown, entity: Readonly<EntityRefState>, href: string, event: MouseEvent}>) => void} [onactivate]
|
|
@@ -1,5 +1,8 @@
|
|
|
1
|
+
/* Beside a taller field in a multi-column row the field's cell grows, but its label and control
|
|
2
|
+
stay at the top of it rather than spreading over the extra height. */
|
|
1
3
|
.zx-field {
|
|
2
4
|
display: grid;
|
|
5
|
+
align-content: start;
|
|
3
6
|
gap: var(--zx-space-1);
|
|
4
7
|
min-inline-size: 0;
|
|
5
8
|
}
|
|
@@ -10,15 +13,24 @@
|
|
|
10
13
|
align-items: start;
|
|
11
14
|
}
|
|
12
15
|
|
|
16
|
+
/* Beside its control, a label lines up with the control's first line rather than its top edge, so
|
|
17
|
+
"Status" and "In review" share a baseline — and still sits by the first row of a control that
|
|
18
|
+
grows taller. */
|
|
19
|
+
.zx-field[data-layout="inline"] .zx-field__label {
|
|
20
|
+
padding-block-start: max(0px, (var(--zx-control-height) - 1lh) / 2);
|
|
21
|
+
}
|
|
22
|
+
|
|
13
23
|
.zx-field[data-field-type="hidden"] {
|
|
14
24
|
display: none;
|
|
15
25
|
}
|
|
16
26
|
|
|
27
|
+
/* Labels are quieter than the values they name: the small size, a medium weight, the muted text
|
|
28
|
+
colour. The value is what a reader came for. */
|
|
17
29
|
.zx-field__label {
|
|
18
30
|
min-inline-size: 0;
|
|
19
|
-
color: var(--zx-color-text);
|
|
31
|
+
color: var(--zx-color-text-muted);
|
|
20
32
|
font-size: var(--zx-text-sm);
|
|
21
|
-
font-weight:
|
|
33
|
+
font-weight: var(--zx-weight-medium);
|
|
22
34
|
}
|
|
23
35
|
|
|
24
36
|
.zx-field__required,
|
|
@@ -101,17 +113,24 @@
|
|
|
101
113
|
border-color: var(--zx-color-danger);
|
|
102
114
|
}
|
|
103
115
|
|
|
104
|
-
.
|
|
105
|
-
|
|
116
|
+
/* Display content is not an input, so it draws no frame and no fill. It keeps a control's height
|
|
117
|
+
and centres its first line in it, so it sits level with the fields beside it and with an inline
|
|
118
|
+
label; in a quiet form it is inset like the quiet values around it. */
|
|
119
|
+
.zx-field__static {
|
|
106
120
|
min-block-size: var(--zx-control-height);
|
|
107
|
-
padding:
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
121
|
+
padding-block: max(0px, (var(--zx-control-height) - 1lh) / 2);
|
|
122
|
+
font-size: var(--zx-control-font-size);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
[data-zx-fields="quiet"] .zx-field__static {
|
|
126
|
+
padding-inline: calc(var(--zx-control-pad-x) + 1px);
|
|
111
127
|
}
|
|
112
128
|
|
|
113
129
|
.zx-field__error-control {
|
|
114
|
-
|
|
130
|
+
min-block-size: var(--zx-control-height);
|
|
131
|
+
padding: var(--zx-space-2) var(--zx-control-pad-x);
|
|
132
|
+
border: 1px solid var(--zx-color-danger);
|
|
133
|
+
border-radius: var(--zx-control-radius);
|
|
115
134
|
background: var(--zx-color-danger-bg);
|
|
116
135
|
color: var(--zx-color-danger);
|
|
117
136
|
}
|
|
@@ -4,7 +4,10 @@ import { uid } from '../../core/util.js';
|
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* @typedef {Object} FieldOptions
|
|
7
|
-
* @property {string|null} [id=null]
|
|
7
|
+
* @property {string|null} [id=null] DOM id of the control. Generated when omitted; a Fieldset
|
|
8
|
+
* derives it from its `idPrefix` and the field key, so a key never becomes a DOM id by itself.
|
|
9
|
+
* @property {string|null} [name=null] Field name: the `name` of a native control and the key the
|
|
10
|
+
* field is filed under. A Fieldset sets it to the field key; a standalone field falls back to `id`.
|
|
8
11
|
* @property {string} [type='text'] Registered field type.
|
|
9
12
|
* @property {string} [label=''] Visible label.
|
|
10
13
|
* @property {string} [description=''] Supporting description.
|
|
@@ -28,6 +31,9 @@ import { uid } from '../../core/util.js';
|
|
|
28
31
|
* @property {() => void} focus Focus the control.
|
|
29
32
|
* @property {(disabled: boolean) => void} setDisabled Set disabled state.
|
|
30
33
|
* @property {() => void} [destroy] Release adapter-owned resources.
|
|
34
|
+
* @property {Element} [labelTarget] The element the field's label, description, and validity
|
|
35
|
+
* belong to when it is not `el` — the text input inside a composite control. Left out, a
|
|
36
|
+
* wrapper that cannot be labelled hands them to the first unnamed form control inside it.
|
|
31
37
|
*/
|
|
32
38
|
|
|
33
39
|
/** @typedef {(field: Field, options: Readonly<FieldOptions>) => FieldAdapter} FieldAdapterFactory */
|
|
@@ -45,6 +51,7 @@ export class Field extends Component {
|
|
|
45
51
|
/** @type {FieldOptions & {msg: Record<string, string>}} */
|
|
46
52
|
static defaults = {
|
|
47
53
|
id: null,
|
|
54
|
+
name: null,
|
|
48
55
|
type: 'text',
|
|
49
56
|
label: '',
|
|
50
57
|
description: '',
|
|
@@ -88,9 +95,10 @@ export class Field extends Component {
|
|
|
88
95
|
return Field.#adapters.has(String(type).trim().toLowerCase());
|
|
89
96
|
}
|
|
90
97
|
|
|
91
|
-
// `adapter` (FieldAdapter), `initialValue`,
|
|
92
|
-
//
|
|
93
|
-
// constructor (which calls render())
|
|
98
|
+
// `adapter` (FieldAdapter), `initialValue`, `controlId` (the control's DOM id), and `name` (the
|
|
99
|
+
// field name, '' when there is none) are assigned in render(); declaring them as class fields
|
|
100
|
+
// would re-initialize them to undefined after the base constructor (which calls render())
|
|
101
|
+
// returns — see AGENTS.md.
|
|
94
102
|
|
|
95
103
|
/** @returns {HTMLElement} */
|
|
96
104
|
render() {
|
|
@@ -104,7 +112,10 @@ export class Field extends Component {
|
|
|
104
112
|
root.dataset.layout = layout;
|
|
105
113
|
root.dataset.fieldType = type;
|
|
106
114
|
|
|
115
|
+
/** @type {string} The control's DOM id: `options.id`, or a generated one. */
|
|
107
116
|
this.controlId = this.options.id ? String(this.options.id) : uid('zx-field');
|
|
117
|
+
/** @type {string} The field name (`options.name`, else `options.id`), '' when there is none. */
|
|
118
|
+
this.name = String(this.options.name ?? this.options.id ?? '');
|
|
108
119
|
const labelId = `${this.controlId}-label`;
|
|
109
120
|
const descriptionId = `${this.controlId}-description`;
|
|
110
121
|
const highlightId = `${this.controlId}-highlight`;
|
|
@@ -148,9 +159,14 @@ export class Field extends Component {
|
|
|
148
159
|
|
|
149
160
|
const control = this.adapter.el;
|
|
150
161
|
if (!control.id) control.id = this.controlId;
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
162
|
+
// A wrapper (a `custom` adapter's box) cannot carry a name; the control inside it can.
|
|
163
|
+
const labelled = this.adapter.labelTarget
|
|
164
|
+
?? (isLabelable(control) ? control : unnamedControlWithin(control)) ?? control;
|
|
165
|
+
this._labelled = labelled;
|
|
166
|
+
if (labelled !== control && isLabelable(labelled) && !labelled.id) labelled.id = `${this.controlId}-input`;
|
|
167
|
+
label.htmlFor = isLabelable(labelled) ? labelled.id : control.id;
|
|
168
|
+
if (!isLabelable(labelled)) labelled.setAttribute('aria-labelledby', labelId);
|
|
169
|
+
this._baseDescribedBy = labelled.getAttribute('aria-describedby') ?? '';
|
|
154
170
|
controlSlot.append(control);
|
|
155
171
|
this._descriptionId = String(this.options.description ?? '') ? descriptionId : '';
|
|
156
172
|
this._highlightId = highlightId;
|
|
@@ -231,11 +247,11 @@ export class Field extends Component {
|
|
|
231
247
|
this._highlight.hidden = !message;
|
|
232
248
|
if (normalizedKind === 'danger' && message) {
|
|
233
249
|
this._highlight.setAttribute('role', 'alert');
|
|
234
|
-
this.
|
|
250
|
+
this._labelled.setAttribute('aria-invalid', 'true');
|
|
235
251
|
this.emit('invalid', { message: String(message) });
|
|
236
252
|
} else {
|
|
237
253
|
this._highlight.removeAttribute('role');
|
|
238
|
-
this.
|
|
254
|
+
this._labelled.removeAttribute('aria-invalid');
|
|
239
255
|
}
|
|
240
256
|
this._syncDescription(Boolean(message));
|
|
241
257
|
return this;
|
|
@@ -250,7 +266,7 @@ export class Field extends Component {
|
|
|
250
266
|
this._highlight.textContent = '';
|
|
251
267
|
this._highlight.hidden = true;
|
|
252
268
|
this._highlight.removeAttribute('role');
|
|
253
|
-
this.
|
|
269
|
+
this._labelled.removeAttribute('aria-invalid');
|
|
254
270
|
this._syncDescription(false);
|
|
255
271
|
return this;
|
|
256
272
|
}
|
|
@@ -279,8 +295,8 @@ export class Field extends Component {
|
|
|
279
295
|
_syncDescription(highlighted = false) {
|
|
280
296
|
const ids = [this._baseDescribedBy, this._descriptionId, highlighted ? this._highlightId : '']
|
|
281
297
|
.flatMap((value) => value.split(/\s+/)).filter(Boolean);
|
|
282
|
-
if (ids.length) this.
|
|
283
|
-
else this.
|
|
298
|
+
if (ids.length) this._labelled.setAttribute('aria-describedby', [...new Set(ids)].join(' '));
|
|
299
|
+
else this._labelled.removeAttribute('aria-describedby');
|
|
284
300
|
}
|
|
285
301
|
|
|
286
302
|
/** @returns {void} */
|
|
@@ -400,6 +416,22 @@ function isLabelable(element) {
|
|
|
400
416
|
return ['BUTTON', 'INPUT', 'METER', 'OUTPUT', 'PROGRESS', 'SELECT', 'TEXTAREA'].includes(element.tagName);
|
|
401
417
|
}
|
|
402
418
|
|
|
419
|
+
/**
|
|
420
|
+
* The first form control inside `root` that nothing names yet — no `aria-label`, no
|
|
421
|
+
* `aria-labelledby`, no `<label>` with any text — or null.
|
|
422
|
+
* @param {Element} root
|
|
423
|
+
* @returns {Element|null}
|
|
424
|
+
*/
|
|
425
|
+
function unnamedControlWithin(root) {
|
|
426
|
+
for (const candidate of root.querySelectorAll('input:not([type="hidden"]), select, textarea')) {
|
|
427
|
+
if (candidate.hasAttribute('aria-label') || candidate.hasAttribute('aria-labelledby')) continue;
|
|
428
|
+
const labels = [.../** @type {HTMLInputElement} */ (candidate).labels ?? []];
|
|
429
|
+
if (labels.some((label) => label.textContent?.trim())) continue;
|
|
430
|
+
return candidate;
|
|
431
|
+
}
|
|
432
|
+
return null;
|
|
433
|
+
}
|
|
434
|
+
|
|
403
435
|
/** @param {HTMLElement} element @param {string[]} names @returns {Map<string, string|null>} */
|
|
404
436
|
function rememberAttributes(element, names) {
|
|
405
437
|
return new Map(names.map((name) => [name, element.getAttribute(name)]));
|
|
@@ -413,19 +445,26 @@ function restoreAttributes(element, attributes) {
|
|
|
413
445
|
}
|
|
414
446
|
}
|
|
415
447
|
|
|
416
|
-
/**
|
|
417
|
-
|
|
448
|
+
/**
|
|
449
|
+
* The properties a native control is built with: the adapter's own `base` over the caller's
|
|
450
|
+
* `props`, with the field name as the control's `name` unless `props` gives one.
|
|
451
|
+
* @param {Field} field Field the control belongs to.
|
|
452
|
+
* @param {Readonly<FieldOptions>} options Field options.
|
|
453
|
+
* @param {Record<string, unknown>} base Properties the adapter owns.
|
|
454
|
+
* @returns {Record<string, unknown>}
|
|
455
|
+
*/
|
|
456
|
+
function controlProps(field, options, base) {
|
|
418
457
|
const props = { ...(options.props ?? {}) };
|
|
419
458
|
for (const key of Object.keys(props)) {
|
|
420
459
|
if (/^on[a-z]/i.test(key) || key === 'content') delete props[key];
|
|
421
460
|
}
|
|
422
|
-
return { ...props, ...base };
|
|
461
|
+
return { name: field.name || undefined, ...props, ...base };
|
|
423
462
|
}
|
|
424
463
|
|
|
425
464
|
/** @param {'input'|'textarea'} tag @param {string} type @returns {FieldAdapterFactory} */
|
|
426
465
|
function textAdapter(tag, type) {
|
|
427
|
-
return (
|
|
428
|
-
const el = h(tag, controlProps(options, {
|
|
466
|
+
return (field, options) => {
|
|
467
|
+
const el = h(tag, controlProps(field, options, {
|
|
429
468
|
type: tag === 'input' ? type : undefined,
|
|
430
469
|
placeholder: options.placeholder,
|
|
431
470
|
required: options.required,
|
|
@@ -450,7 +489,7 @@ function elementValueAdapter(el) {
|
|
|
450
489
|
function numericAdapter(kind) {
|
|
451
490
|
return (field, options) => {
|
|
452
491
|
const coerce = kind === 'int' ? coerceInteger : coerceFloat;
|
|
453
|
-
const input = h('input', controlProps(options, {
|
|
492
|
+
const input = h('input', controlProps(field, options, {
|
|
454
493
|
type: 'text',
|
|
455
494
|
inputMode: kind === 'int' ? 'numeric' : 'decimal',
|
|
456
495
|
placeholder: options.placeholder,
|
|
@@ -474,8 +513,8 @@ function numericAdapter(kind) {
|
|
|
474
513
|
}
|
|
475
514
|
|
|
476
515
|
/** @type {FieldAdapterFactory} */
|
|
477
|
-
const checkboxAdapter = (
|
|
478
|
-
const input = h('input', controlProps(options, {
|
|
516
|
+
const checkboxAdapter = (field, options) => {
|
|
517
|
+
const input = h('input', controlProps(field, options, {
|
|
479
518
|
type: 'checkbox',
|
|
480
519
|
required: options.required,
|
|
481
520
|
disabled: options.disabled
|
|
@@ -490,8 +529,8 @@ const checkboxAdapter = (_field, options) => {
|
|
|
490
529
|
};
|
|
491
530
|
|
|
492
531
|
/** @type {FieldAdapterFactory} */
|
|
493
|
-
const selectAdapter = (
|
|
494
|
-
const select = h('select', controlProps(options, {
|
|
532
|
+
const selectAdapter = (field, options) => {
|
|
533
|
+
const select = h('select', controlProps(field, options, {
|
|
495
534
|
required: options.required,
|
|
496
535
|
disabled: options.disabled
|
|
497
536
|
}));
|
|
@@ -502,12 +541,15 @@ const selectAdapter = (_field, options) => {
|
|
|
502
541
|
};
|
|
503
542
|
|
|
504
543
|
/** @type {FieldAdapterFactory} */
|
|
505
|
-
const optionListAdapter = (
|
|
544
|
+
const optionListAdapter = (field, options) => {
|
|
506
545
|
const root = h('div', {
|
|
507
546
|
class: 'zx-field__option-list',
|
|
508
547
|
role: 'radiogroup'
|
|
509
548
|
});
|
|
510
|
-
|
|
549
|
+
// The radios are grouped by the control's id, not by the field key: radios outside a <form>
|
|
550
|
+
// share one group per name across the whole document, so the same fieldset mounted twice
|
|
551
|
+
// would otherwise check one copy's option by clicking the other's.
|
|
552
|
+
const name = field.controlId;
|
|
511
553
|
const radios = normalizeOptions(options.options).map(([value, label], index) => {
|
|
512
554
|
const input = h('input', {
|
|
513
555
|
type: 'radio',
|
|
@@ -534,8 +576,8 @@ const optionListAdapter = (_field, options) => {
|
|
|
534
576
|
};
|
|
535
577
|
|
|
536
578
|
/** @type {FieldAdapterFactory} */
|
|
537
|
-
const htmlAdapter = (
|
|
538
|
-
const el = h('div', controlProps(options, { class: 'zx-field__static' }));
|
|
579
|
+
const htmlAdapter = (field, options) => {
|
|
580
|
+
const el = h('div', controlProps(field, options, { class: 'zx-field__static', name: undefined }));
|
|
539
581
|
let adopted = null;
|
|
540
582
|
const set = (value) => {
|
|
541
583
|
restoreAdopted(adopted);
|
|
@@ -15,11 +15,13 @@
|
|
|
15
15
|
inline-size: 100%;
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
+
/* A caption for the group, in sentence case and the same quiet voice as the field labels inside
|
|
19
|
+
it — the fields are the content. */
|
|
18
20
|
.zx-fieldset__legend {
|
|
19
21
|
padding-inline: var(--zx-space-2);
|
|
20
|
-
color: var(--zx-color-text);
|
|
21
|
-
font-size: var(--zx-text-
|
|
22
|
-
font-weight:
|
|
22
|
+
color: var(--zx-color-text-muted);
|
|
23
|
+
font-size: var(--zx-text-sm);
|
|
24
|
+
font-weight: var(--zx-weight-medium);
|
|
23
25
|
}
|
|
24
26
|
|
|
25
27
|
.zx-fieldset__grid {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Component } from '../../core/component.js';
|
|
2
2
|
import { h } from '../../core/dom.js';
|
|
3
|
-
import { isElement } from '../../core/util.js';
|
|
3
|
+
import { isElement, uid } from '../../core/util.js';
|
|
4
4
|
import { Field } from '../field/field.js';
|
|
5
5
|
|
|
6
6
|
/**
|
|
@@ -8,6 +8,9 @@ import { Field } from '../field/field.js';
|
|
|
8
8
|
* @property {string} [title=''] Legend text.
|
|
9
9
|
* @property {1|2|3} [columns=1] Grid column count.
|
|
10
10
|
* @property {Record<string, FieldOptions|Field|Element|Component>} [fields={}] Initial fields.
|
|
11
|
+
* @property {string|null} [idPrefix=null] Prefix of the DOM ids generated for the fields, as
|
|
12
|
+
* `<idPrefix>-<key>`. Null generates one per instance, so the same fieldset mounted twice
|
|
13
|
+
* never repeats an id and a field key never collides with an element id elsewhere on the page.
|
|
11
14
|
*/
|
|
12
15
|
|
|
13
16
|
/** @typedef {import('../field/field.js').FieldOptions} FieldOptions */
|
|
@@ -20,7 +23,7 @@ export class Fieldset extends Component {
|
|
|
20
23
|
static cssName = 'fieldset';
|
|
21
24
|
|
|
22
25
|
/** @type {FieldsetOptions} */
|
|
23
|
-
static defaults = { title: '', columns: 1, fields: {} };
|
|
26
|
+
static defaults = { title: '', columns: 1, fields: {}, idPrefix: null };
|
|
24
27
|
|
|
25
28
|
/** @returns {HTMLElement} */
|
|
26
29
|
render() {
|
|
@@ -30,6 +33,7 @@ export class Fieldset extends Component {
|
|
|
30
33
|
this._origins = new Map();
|
|
31
34
|
this._fieldIdAttributes = new Map();
|
|
32
35
|
this._ownedNodes = [];
|
|
36
|
+
this._idPrefix = this.options.idPrefix ? String(this.options.idPrefix) : uid('zx-fieldset');
|
|
33
37
|
this._columnAttribute = root.getAttribute('data-columns');
|
|
34
38
|
const columns = [1, 2, 3].includes(Number(this.options.columns)) ? Number(this.options.columns) : 1;
|
|
35
39
|
root.dataset.columns = String(columns);
|
|
@@ -49,7 +53,9 @@ export class Fieldset extends Component {
|
|
|
49
53
|
}
|
|
50
54
|
|
|
51
55
|
/**
|
|
52
|
-
* Adds a field from options, an existing field, an element, or a component.
|
|
56
|
+
* Adds a field from options, an existing field, an element, or a component. The key becomes the
|
|
57
|
+
* field's `name` and its `data-field-id`; the control's DOM id is `<idPrefix>-<key>`, unless the
|
|
58
|
+
* field options give an `id` of their own that differs from the key.
|
|
53
59
|
* @param {string} id Logical field identifier.
|
|
54
60
|
* @param {FieldOptions|Field|Element|Component} field Field source.
|
|
55
61
|
* @returns {Field}
|
|
@@ -64,7 +70,8 @@ export class Fieldset extends Component {
|
|
|
64
70
|
} else if (isElement(field) || field instanceof Component) {
|
|
65
71
|
const source = field;
|
|
66
72
|
instance = new Field(null, {
|
|
67
|
-
id: key,
|
|
73
|
+
id: this._controlId(key),
|
|
74
|
+
name: key,
|
|
68
75
|
type: 'custom',
|
|
69
76
|
label: key,
|
|
70
77
|
value: readExternalValue(source),
|
|
@@ -74,7 +81,14 @@ export class Fieldset extends Component {
|
|
|
74
81
|
owned = true;
|
|
75
82
|
} else {
|
|
76
83
|
const options = field && typeof field === 'object' ? field : {};
|
|
77
|
-
|
|
84
|
+
// An `id` equal to the key is the key restated (schema descriptors carry both), not a
|
|
85
|
+
// request for that DOM id.
|
|
86
|
+
const explicit = options.id != null && String(options.id) !== '' && String(options.id) !== key;
|
|
87
|
+
instance = new Field(null, {
|
|
88
|
+
...options,
|
|
89
|
+
id: explicit ? String(options.id) : this._controlId(key),
|
|
90
|
+
name: options.name ?? key
|
|
91
|
+
});
|
|
78
92
|
owned = true;
|
|
79
93
|
}
|
|
80
94
|
|
|
@@ -218,6 +232,15 @@ export class Fieldset extends Component {
|
|
|
218
232
|
return this;
|
|
219
233
|
}
|
|
220
234
|
|
|
235
|
+
/**
|
|
236
|
+
* The DOM id generated for a field key: unique per instance unless `idPrefix` was given.
|
|
237
|
+
* @param {string} key Field key.
|
|
238
|
+
* @returns {string}
|
|
239
|
+
*/
|
|
240
|
+
_controlId(key) {
|
|
241
|
+
return `${this._idPrefix}-${key.trim().replace(/\s+/g, '-')}`;
|
|
242
|
+
}
|
|
243
|
+
|
|
221
244
|
/** @param {Field} field @returns {void} */
|
|
222
245
|
_releaseField(field) {
|
|
223
246
|
const fieldId = this._fieldIdAttributes.get(field);
|