@zeyos/zx 4.3.0 → 4.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/CHANGELOG.md +266 -1
- package/README.md +7 -3
- package/dist/types/components/activity-item/activity-item.d.ts +351 -0
- package/dist/types/components/activity-list/activity-list.d.ts +195 -0
- package/dist/types/components/app-sidebar/app-sidebar.d.ts +28 -0
- package/dist/types/components/button/button.d.ts +8 -0
- package/dist/types/components/calendar/calendar-model.d.ts +22 -0
- package/dist/types/components/calendar/calendar.d.ts +19 -0
- package/dist/types/components/card/card.d.ts +7 -0
- package/dist/types/components/card-view/card-view.d.ts +12 -4
- package/dist/types/components/card-view/record-card.d.ts +53 -5
- package/dist/types/components/code-editor/code-editor.d.ts +160 -0
- package/dist/types/components/code-editor/field-adapter.d.ts +5 -0
- package/dist/types/components/context-menu/context-menu.d.ts +18 -2
- package/dist/types/components/date-picker/date-picker.d.ts +8 -0
- package/dist/types/components/date-picker/time-picker.d.ts +7 -0
- package/dist/types/components/dialog/dialog.d.ts +29 -0
- package/dist/types/components/entity-ref/entity-ref.d.ts +331 -0
- package/dist/types/components/file-item/file-item.d.ts +234 -0
- package/dist/types/components/file-list/file-list.d.ts +171 -0
- package/dist/types/components/filter/filter.d.ts +91 -36
- package/dist/types/components/filter-panel/filter-panel.d.ts +449 -0
- package/dist/types/components/kanban-view/kanban-card.d.ts +138 -0
- package/dist/types/components/kanban-view/kanban-policy.d.ts +268 -0
- package/dist/types/components/kanban-view/kanban-view.d.ts +837 -26
- package/dist/types/components/launcher/launcher.d.ts +19 -12
- package/dist/types/components/list-toolbar/list-toolbar.d.ts +599 -0
- package/dist/types/components/menu-button/menu-button.d.ts +4 -37
- package/dist/types/components/message/message.d.ts +7 -0
- package/dist/types/components/modal/modal.d.ts +48 -0
- package/dist/types/components/navigation-bar/navigation-bar.d.ts +105 -21
- package/dist/types/components/panel/panel.d.ts +7 -0
- package/dist/types/components/search/search.d.ts +7 -0
- package/dist/types/components/select/select.d.ts +4 -0
- package/dist/types/components/sort-control/sort-control.d.ts +105 -0
- package/dist/types/components/stat-tile/stat-tile.d.ts +277 -0
- package/dist/types/components/tabbox/tabbox.d.ts +7 -0
- package/dist/types/components/table/table.d.ts +144 -0
- package/dist/types/components/table-view/table-view.d.ts +11 -5
- package/dist/types/components/timebox/timebox.d.ts +7 -0
- package/dist/types/components/value-list/value-list.d.ts +8 -0
- package/dist/types/components/view/record-view.d.ts +96 -5
- package/dist/types/core/component.d.ts +6 -1
- package/dist/types/core/icons.d.ts +17 -0
- package/dist/types/index.d.ts +13 -1
- package/dist/types/internal/app-rail.d.ts +10 -0
- package/dist/types/internal/menu-items.d.ts +134 -0
- package/dist/zx-zeyos.esm.js +651 -88
- package/dist/zx-zeyos.esm.min.js +8 -1
- package/dist/zx.css +6266 -3900
- package/dist/zx.esm.js +8487 -1805
- package/dist/zx.esm.min.js +10 -3
- package/dist/zx.global.js +8487 -1805
- package/dist/zx.global.min.js +10 -3
- package/dist/zx.min.css +1 -1
- package/docs/api.json +1811 -247
- package/docs/llms.md +345 -40
- package/docs/llms.txt +11 -7
- package/package.json +1 -2
- package/src/components/activity-item/activity-item.css +137 -0
- package/src/components/activity-item/activity-item.js +599 -0
- package/src/components/activity-list/activity-list.css +64 -0
- package/src/components/activity-list/activity-list.js +375 -0
- package/src/components/app-rail/app-rail.css +227 -19
- package/src/components/app-sidebar/app-sidebar.css +414 -24
- package/src/components/app-sidebar/app-sidebar.js +65 -5
- package/src/components/button/button.css +8 -0
- package/src/components/button/button.js +6 -1
- package/src/components/calendar/calendar-model.js +45 -0
- package/src/components/calendar/calendar.css +30 -10
- package/src/components/calendar/calendar.js +41 -13
- package/src/components/card/card.js +12 -1
- package/src/components/card-view/card-view.css +212 -9
- package/src/components/card-view/card-view.js +32 -16
- package/src/components/card-view/record-card.js +98 -21
- package/src/components/code-editor/code-editor.css +67 -0
- package/src/components/code-editor/code-editor.js +288 -0
- package/src/components/code-editor/field-adapter.js +34 -0
- package/src/components/context-menu/context-menu.css +80 -3
- package/src/components/context-menu/context-menu.js +22 -30
- package/src/components/date-picker/date-picker.js +22 -8
- package/src/components/date-picker/month-picker.js +3 -3
- package/src/components/date-picker/time-picker.js +20 -8
- package/src/components/dialog/dialog.js +88 -66
- package/src/components/dropdown/dropdown.js +2 -2
- package/src/components/entity-ref/entity-ref.css +182 -0
- package/src/components/entity-ref/entity-ref.js +496 -0
- package/src/components/file-item/file-item.css +88 -0
- package/src/components/file-item/file-item.js +371 -0
- package/src/components/file-list/file-list.css +51 -0
- package/src/components/file-list/file-list.js +320 -0
- package/src/components/filter/filter.css +27 -1
- package/src/components/filter/filter.js +199 -45
- package/src/components/filter-panel/filter-panel.css +104 -0
- package/src/components/filter-panel/filter-panel.js +964 -0
- package/src/components/kanban-view/kanban-card.js +239 -0
- package/src/components/kanban-view/kanban-policy.js +412 -0
- package/src/components/kanban-view/kanban-view.css +674 -36
- package/src/components/kanban-view/kanban-view.js +1762 -240
- package/src/components/launcher/launcher.js +59 -27
- package/src/components/list-toolbar/list-toolbar.css +189 -0
- package/src/components/list-toolbar/list-toolbar.js +851 -0
- package/src/components/menu-button/menu-button.css +80 -3
- package/src/components/menu-button/menu-button.js +21 -37
- package/src/components/message/message.css +14 -12
- package/src/components/message/message.js +13 -2
- package/src/components/modal/modal.css +15 -0
- package/src/components/modal/modal.js +141 -0
- package/src/components/navigation-bar/navigation-bar.css +77 -2
- package/src/components/navigation-bar/navigation-bar.js +171 -4
- package/src/components/panel/panel.js +13 -2
- package/src/components/search/search.js +17 -5
- package/src/components/select/field-adapter.js +4 -1
- package/src/components/select/select.js +3 -0
- package/src/components/sort-control/sort-control.css +17 -0
- package/src/components/sort-control/sort-control.js +140 -0
- package/src/components/stat-tile/stat-tile.css +213 -0
- package/src/components/stat-tile/stat-tile.js +590 -0
- package/src/components/tabbox/tabbox.js +12 -1
- package/src/components/table/table.css +51 -2
- package/src/components/table/table.js +298 -19
- package/src/components/table-view/table-view.css +8 -2
- package/src/components/table-view/table-view.js +21 -6
- package/src/components/tag-picker/tag-picker.css +3 -1
- package/src/components/timebox/timebox.js +18 -5
- package/src/components/value-list/value-list.js +22 -6
- package/src/components/view/record-view.css +5 -0
- package/src/components/view/record-view.js +100 -7
- package/src/core/component.js +8 -3
- package/src/core/icons.js +74 -5
- package/src/index.js +33 -3
- package/src/internal/app-rail.js +19 -1
- package/src/internal/menu-items.js +142 -0
- package/styles/print.css +192 -0
- package/styles/zx.css +11 -0
- package/DESIGN-SYSTEM.md +0 -182
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
import { Component } from '../../core/component.js';
|
|
2
2
|
import { h, resolveElement } from '../../core/dom.js';
|
|
3
|
-
import { icon } from '../../core/icons.js';
|
|
4
3
|
import { rovingTabindex, typeahead } from '../../core/keyboard.js';
|
|
4
|
+
import {
|
|
5
|
+
isMenuItem,
|
|
6
|
+
MENU_ITEM_SELECTOR,
|
|
7
|
+
renderMenuEntries
|
|
8
|
+
} from '../../internal/menu-items.js';
|
|
5
9
|
import { Dropdown } from '../dropdown/dropdown.js';
|
|
6
10
|
|
|
7
|
-
/** @typedef {import('
|
|
8
|
-
/** @typedef {
|
|
11
|
+
/** @typedef {import('../../internal/menu-items.js').MenuItem} MenuItem */
|
|
12
|
+
/** @typedef {import('../../internal/menu-items.js').MenuEntry} ContextMenuEntry */
|
|
9
13
|
|
|
10
14
|
/**
|
|
11
15
|
* @typedef {Object} ContextMenuOptions
|
|
@@ -22,8 +26,6 @@ import { Dropdown } from '../dropdown/dropdown.js';
|
|
|
22
26
|
* @property {(event: CustomEvent<Record<string, never>>) => void} [onclose] Close listener.
|
|
23
27
|
*/
|
|
24
28
|
|
|
25
|
-
const MENU_ITEM_SELECTOR = '[role="menuitem"]';
|
|
26
|
-
|
|
27
29
|
/**
|
|
28
30
|
* Right-click menu for a region, implementing the APG menu pattern.
|
|
29
31
|
*
|
|
@@ -107,7 +109,7 @@ export class ContextMenu extends Component {
|
|
|
107
109
|
this.listen(this.#target, 'keydown', (event) => this.#onTargetKeydown(event));
|
|
108
110
|
this.listen(this.#panel, 'click', (event) => {
|
|
109
111
|
const item = event.target.closest?.(MENU_ITEM_SELECTOR);
|
|
110
|
-
if (item && this.#panel.contains(item)) this.#select(item);
|
|
112
|
+
if (item && this.#panel.contains(item)) this.#select(item, event);
|
|
111
113
|
});
|
|
112
114
|
this.listen(this.#panel, 'keydown', (event) => this.#onMenuKeydown(event));
|
|
113
115
|
}
|
|
@@ -127,25 +129,7 @@ export class ContextMenu extends Component {
|
|
|
127
129
|
*/
|
|
128
130
|
setItems(items) {
|
|
129
131
|
this.#items = Array.isArray(items) ? items.slice() : [];
|
|
130
|
-
this.#panel.replaceChildren();
|
|
131
|
-
this.#items.forEach((item, index) => {
|
|
132
|
-
if (item === '-') {
|
|
133
|
-
this.#panel.append(h('div', { class: 'zx-context-menu__separator', role: 'separator' }));
|
|
134
|
-
return;
|
|
135
|
-
}
|
|
136
|
-
const children = [];
|
|
137
|
-
if (item.icon) children.push(h('span', { class: 'zx-context-menu__icon' }, icon(item.icon)));
|
|
138
|
-
children.push(h('span', { class: 'zx-context-menu__label' }, String(item.label ?? '')));
|
|
139
|
-
this.#panel.append(h('button', {
|
|
140
|
-
class: 'zx-context-menu__item',
|
|
141
|
-
type: 'button',
|
|
142
|
-
role: 'menuitem',
|
|
143
|
-
tabindex: '-1',
|
|
144
|
-
'data-menu-item': String(index),
|
|
145
|
-
'data-danger': item.danger ? 'true' : null,
|
|
146
|
-
ariaDisabled: item.disabled ? 'true' : null
|
|
147
|
-
}, children));
|
|
148
|
-
});
|
|
132
|
+
this.#panel.replaceChildren(...renderMenuEntries(this.#items, 'context-menu'));
|
|
149
133
|
return this;
|
|
150
134
|
}
|
|
151
135
|
|
|
@@ -292,20 +276,28 @@ export class ContextMenu extends Component {
|
|
|
292
276
|
if (key.key === 'Enter' || key.key === ' ') {
|
|
293
277
|
const item = /** @type {Element} */ (key.target).closest?.(MENU_ITEM_SELECTOR);
|
|
294
278
|
if (!item || !this.#panel.contains(item)) return;
|
|
279
|
+
if (key.key === 'Enter' && item.tagName === 'A') return;
|
|
295
280
|
event.preventDefault();
|
|
296
|
-
|
|
281
|
+
/** @type {HTMLElement} */ (item).click();
|
|
297
282
|
return;
|
|
298
283
|
}
|
|
299
284
|
this.#typeahead(key);
|
|
300
285
|
}
|
|
301
286
|
|
|
302
|
-
/** @param {Element} element @returns {void} */
|
|
303
|
-
#select(element) {
|
|
287
|
+
/** @param {Element} element @param {Event|null} [event=null] @returns {void} */
|
|
288
|
+
#select(element, event = null) {
|
|
304
289
|
const index = Number(element.getAttribute('data-menu-item'));
|
|
305
290
|
const item = this.#items[index];
|
|
306
|
-
if (!item || item
|
|
291
|
+
if (!isMenuItem(item) || item.disabled) {
|
|
292
|
+
event?.preventDefault();
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
307
295
|
const context = this.#context;
|
|
308
|
-
this.emit('select', { value: item.value, item, context });
|
|
296
|
+
const selected = this.emit('select', { value: item.value, item, context });
|
|
297
|
+
if (selected.defaultPrevented) {
|
|
298
|
+
event?.preventDefault();
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
309
301
|
try {
|
|
310
302
|
item.onselect?.(item.value, item, this);
|
|
311
303
|
} finally {
|
|
@@ -2,6 +2,7 @@ import { Component } from '../../core/component.js';
|
|
|
2
2
|
import { addDays, addMonths, formatDate, getWeekStart, isSameDay, parseDate } from '../../core/date.js';
|
|
3
3
|
import { h } from '../../core/dom.js';
|
|
4
4
|
import { icon } from '../../core/icons.js';
|
|
5
|
+
import { printf } from '../../core/i18n.js';
|
|
5
6
|
import { TimePicker } from './time-picker.js';
|
|
6
7
|
|
|
7
8
|
/**
|
|
@@ -55,7 +56,7 @@ export class DatePicker extends Component {
|
|
|
55
56
|
ref: 'previous',
|
|
56
57
|
class: 'zx-date-picker__nav',
|
|
57
58
|
type: 'button',
|
|
58
|
-
ariaLabel: 'Previous month'
|
|
59
|
+
ariaLabel: this._message('datePicker.previousMonth', 'Previous month')
|
|
59
60
|
}, icon('chevron-left')),
|
|
60
61
|
h('button', {
|
|
61
62
|
ref: 'heading',
|
|
@@ -68,7 +69,7 @@ export class DatePicker extends Component {
|
|
|
68
69
|
ref: 'next',
|
|
69
70
|
class: 'zx-date-picker__nav',
|
|
70
71
|
type: 'button',
|
|
71
|
-
ariaLabel: 'Next month'
|
|
72
|
+
ariaLabel: this._message('datePicker.nextMonth', 'Next month')
|
|
72
73
|
}, icon('chevron-right'))
|
|
73
74
|
),
|
|
74
75
|
h('div', {
|
|
@@ -81,14 +82,14 @@ export class DatePicker extends Component {
|
|
|
81
82
|
ref: 'quickPrevious',
|
|
82
83
|
class: 'zx-date-picker__nav',
|
|
83
84
|
type: 'button',
|
|
84
|
-
ariaLabel: 'Previous year'
|
|
85
|
+
ariaLabel: this._message('datePicker.previousYear', 'Previous year')
|
|
85
86
|
}, icon('chevron-left')),
|
|
86
87
|
h('span', { ref: 'quickYear', class: 'zx-date-picker__quick-year', ariaLive: 'polite' }),
|
|
87
88
|
h('button', {
|
|
88
89
|
ref: 'quickNext',
|
|
89
90
|
class: 'zx-date-picker__nav',
|
|
90
91
|
type: 'button',
|
|
91
|
-
ariaLabel: 'Next year'
|
|
92
|
+
ariaLabel: this._message('datePicker.nextYear', 'Next year')
|
|
92
93
|
}, icon('chevron-right'))
|
|
93
94
|
),
|
|
94
95
|
h('div', { ref: 'quickGrid', class: 'zx-date-picker__quick-grid', role: 'grid' })),
|
|
@@ -96,7 +97,7 @@ export class DatePicker extends Component {
|
|
|
96
97
|
ref: 'grid',
|
|
97
98
|
class: 'zx-date-picker__grid',
|
|
98
99
|
role: 'grid',
|
|
99
|
-
ariaLabel: 'Calendar'
|
|
100
|
+
ariaLabel: this._message('datePicker.calendar', 'Calendar')
|
|
100
101
|
},
|
|
101
102
|
h('thead', { ref: 'weekdays' }),
|
|
102
103
|
h('tbody', { ref: 'days' }))
|
|
@@ -203,7 +204,7 @@ export class DatePicker extends Component {
|
|
|
203
204
|
row.append(h('th', {
|
|
204
205
|
class: 'zx-date-picker__week-heading',
|
|
205
206
|
scope: 'col',
|
|
206
|
-
ariaLabel: 'Week number'
|
|
207
|
+
ariaLabel: this._message('datePicker.weekNumber', 'Week number')
|
|
207
208
|
}, '#'));
|
|
208
209
|
}
|
|
209
210
|
const sunday = new Date(2023, 0, 1);
|
|
@@ -224,7 +225,8 @@ export class DatePicker extends Component {
|
|
|
224
225
|
const offset = (first.getDay() - this._weekStart + 7) % 7;
|
|
225
226
|
const start = addDays(first, -offset);
|
|
226
227
|
this.refs.heading.textContent = formatDate(first, '%B %Y');
|
|
227
|
-
this.refs.heading.setAttribute('aria-label',
|
|
228
|
+
this.refs.heading.setAttribute('aria-label',
|
|
229
|
+
this._message('datePicker.chooseMonthYear', 'Choose month and year, %1', formatDate(first, '%B %Y')));
|
|
228
230
|
const rows = [];
|
|
229
231
|
|
|
230
232
|
for (let week = 0; week < 6; week += 1) {
|
|
@@ -234,7 +236,7 @@ export class DatePicker extends Component {
|
|
|
234
236
|
row.append(h('th', {
|
|
235
237
|
class: 'zx-date-picker__week',
|
|
236
238
|
scope: 'row',
|
|
237
|
-
ariaLabel:
|
|
239
|
+
ariaLabel: this._message('datePicker.week', 'Week %1', isoWeekNumber(rowStart))
|
|
238
240
|
}, String(isoWeekNumber(rowStart))));
|
|
239
241
|
}
|
|
240
242
|
for (let column = 0; column < 7; column += 1) {
|
|
@@ -450,6 +452,18 @@ export class DatePicker extends Component {
|
|
|
450
452
|
_buttonForDate(date) {
|
|
451
453
|
return this.refs.days.querySelector(`[data-date="${formatDate(date, '%Y-%m-%d')}"]`);
|
|
452
454
|
}
|
|
455
|
+
|
|
456
|
+
/**
|
|
457
|
+
* Resolves a message through the host translator, falling back to the built-in English text.
|
|
458
|
+
* @param {string} key Message key.
|
|
459
|
+
* @param {string} fallback Built-in text, with `%1`-style placeholders.
|
|
460
|
+
* @param {...unknown} args Interpolation values.
|
|
461
|
+
* @returns {string}
|
|
462
|
+
*/
|
|
463
|
+
_message(key, fallback, ...args) {
|
|
464
|
+
const message = this.msg(key, ...args);
|
|
465
|
+
return message === key ? printf(fallback, args) : message;
|
|
466
|
+
}
|
|
453
467
|
}
|
|
454
468
|
|
|
455
469
|
/**
|
|
@@ -47,21 +47,21 @@ export class MonthPicker extends DatePicker {
|
|
|
47
47
|
ref: 'previousYear',
|
|
48
48
|
class: 'zx-month-picker__nav',
|
|
49
49
|
type: 'button',
|
|
50
|
-
ariaLabel: 'Previous year'
|
|
50
|
+
ariaLabel: this._message('monthPicker.previousYear', 'Previous year')
|
|
51
51
|
}, icon('chevron-left')),
|
|
52
52
|
h('span', { ref: 'year', class: 'zx-month-picker__year', ariaLive: 'polite' }),
|
|
53
53
|
h('button', {
|
|
54
54
|
ref: 'nextYear',
|
|
55
55
|
class: 'zx-month-picker__nav',
|
|
56
56
|
type: 'button',
|
|
57
|
-
ariaLabel: 'Next year'
|
|
57
|
+
ariaLabel: this._message('monthPicker.nextYear', 'Next year')
|
|
58
58
|
}, icon('chevron-right'))
|
|
59
59
|
),
|
|
60
60
|
h('div', {
|
|
61
61
|
ref: 'grid',
|
|
62
62
|
class: 'zx-month-picker__grid',
|
|
63
63
|
role: 'grid',
|
|
64
|
-
ariaLabel: 'Choose month'
|
|
64
|
+
ariaLabel: this._message('monthPicker.chooseMonth', 'Choose month')
|
|
65
65
|
})
|
|
66
66
|
);
|
|
67
67
|
this._content = content;
|
|
@@ -48,15 +48,15 @@ export class TimePicker extends Component {
|
|
|
48
48
|
const content = h('div', {
|
|
49
49
|
class: 'zx-time-picker__segments',
|
|
50
50
|
role: 'group',
|
|
51
|
-
ariaLabel: 'Time'
|
|
51
|
+
ariaLabel: this._message('timePicker.label', 'Time')
|
|
52
52
|
});
|
|
53
53
|
this._content = content;
|
|
54
|
-
this._addSpin(content, 'h', 'Hour', 23);
|
|
54
|
+
this._addSpin(content, 'h', this._message('timePicker.hour', 'Hour'), 23);
|
|
55
55
|
content.append(h('span', { class: 'zx-time-picker__separator', ariaHidden: 'true' }, ':'));
|
|
56
|
-
this._addSpin(content, 'm', 'Minute', 59);
|
|
56
|
+
this._addSpin(content, 'm', this._message('timePicker.minute', 'Minute'), 59);
|
|
57
57
|
if (this.options.seconds) {
|
|
58
58
|
content.append(h('span', { class: 'zx-time-picker__separator', ariaHidden: 'true' }, ':'));
|
|
59
|
-
this._addSpin(content, 's', 'Second', 59);
|
|
59
|
+
this._addSpin(content, 's', this._message('timePicker.second', 'Second'), 59);
|
|
60
60
|
}
|
|
61
61
|
root.append(content);
|
|
62
62
|
|
|
@@ -66,7 +66,7 @@ export class TimePicker extends Component {
|
|
|
66
66
|
ref: 'clockToggle',
|
|
67
67
|
class: 'zx-icon-btn zx-time-picker__clock-toggle',
|
|
68
68
|
type: 'button',
|
|
69
|
-
ariaLabel: 'Pick the time on a clock',
|
|
69
|
+
ariaLabel: this._message('timePicker.clock', 'Pick the time on a clock'),
|
|
70
70
|
ariaExpanded: 'false',
|
|
71
71
|
ariaControls: clockId
|
|
72
72
|
}, icon('clock', { size: 15 })));
|
|
@@ -223,7 +223,7 @@ export class TimePicker extends Component {
|
|
|
223
223
|
ref: 'headHour',
|
|
224
224
|
class: 'zx-time-picker__clock-unit',
|
|
225
225
|
type: 'button',
|
|
226
|
-
ariaLabel: 'Select the hour',
|
|
226
|
+
ariaLabel: this._message('timePicker.selectHour', 'Select the hour'),
|
|
227
227
|
dataset: { unit: 'h' }
|
|
228
228
|
}),
|
|
229
229
|
h('span', { class: 'zx-time-picker__clock-colon', ariaHidden: 'true' }, ':'),
|
|
@@ -231,7 +231,7 @@ export class TimePicker extends Component {
|
|
|
231
231
|
ref: 'headMinute',
|
|
232
232
|
class: 'zx-time-picker__clock-unit',
|
|
233
233
|
type: 'button',
|
|
234
|
-
ariaLabel: 'Select the minutes',
|
|
234
|
+
ariaLabel: this._message('timePicker.selectMinutes', 'Select the minutes'),
|
|
235
235
|
dataset: { unit: 'm' }
|
|
236
236
|
})),
|
|
237
237
|
h('div', { ref: 'dial', class: 'zx-time-picker__dial', role: 'radiogroup' },
|
|
@@ -290,7 +290,8 @@ export class TimePicker extends Component {
|
|
|
290
290
|
this.refs.headMinute.textContent = pad(this._display.m);
|
|
291
291
|
this.refs.headHour.setAttribute('aria-pressed', String(hours));
|
|
292
292
|
this.refs.headMinute.setAttribute('aria-pressed', String(!hours));
|
|
293
|
-
this.refs.dial.setAttribute('aria-label', hours
|
|
293
|
+
this.refs.dial.setAttribute('aria-label', hours
|
|
294
|
+
? this._message('timePicker.hour', 'Hour') : this._message('timePicker.minutes', 'Minutes'));
|
|
294
295
|
if (!this._clockOpen) return;
|
|
295
296
|
|
|
296
297
|
const selected = hours ? this._display.h : this._display.m;
|
|
@@ -330,6 +331,17 @@ export class TimePicker extends Component {
|
|
|
330
331
|
element.setAttribute('aria-checked', String(checked));
|
|
331
332
|
element.tabIndex = checked ? 0 : -1;
|
|
332
333
|
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Resolves a message through the host translator, falling back to the built-in English text.
|
|
337
|
+
* @param {string} key Message key.
|
|
338
|
+
* @param {string} fallback English default.
|
|
339
|
+
* @returns {string}
|
|
340
|
+
*/
|
|
341
|
+
_message(key, fallback) {
|
|
342
|
+
const message = this.msg(key);
|
|
343
|
+
return message === key ? fallback : message;
|
|
344
|
+
}
|
|
333
345
|
}
|
|
334
346
|
|
|
335
347
|
/**
|
|
@@ -1,11 +1,7 @@
|
|
|
1
1
|
import { h } from '../../core/dom.js';
|
|
2
2
|
import { uid } from '../../core/util.js';
|
|
3
3
|
import { Modal } from '../modal/modal.js';
|
|
4
|
-
|
|
5
|
-
const FOCUSABLE_SELECTOR = [
|
|
6
|
-
'a[href]', 'button:not([disabled])', 'input:not([disabled])', 'select:not([disabled])',
|
|
7
|
-
'textarea:not([disabled])', '[contenteditable="true"]', '[tabindex]:not([tabindex="-1"])'
|
|
8
|
-
].join(',');
|
|
4
|
+
import { button } from '../button/button.js';
|
|
9
5
|
|
|
10
6
|
/** @typedef {'default'|'primary'|'danger'|'ghost'} DialogButtonKind */
|
|
11
7
|
/**
|
|
@@ -14,6 +10,8 @@ const FOCUSABLE_SELECTOR = [
|
|
|
14
10
|
* @property {DialogButtonKind} [kind='default'] Visual button kind.
|
|
15
11
|
* @property {'close'|'cancel'|((dialog: Dialog) => void)} [action='close'] Button action.
|
|
16
12
|
* @property {boolean} [autofocus=false] Whether this button receives initial focus.
|
|
13
|
+
* @property {boolean} [disabled=false] Whether the footer button is disabled.
|
|
14
|
+
* @property {import('../button/button.js').ButtonShortcut} [shortcut] Visible shortcut hint; the host binds the action.
|
|
17
15
|
*/
|
|
18
16
|
/**
|
|
19
17
|
* @typedef {Object} DialogOptions
|
|
@@ -86,8 +84,6 @@ export class Dialog extends Modal {
|
|
|
86
84
|
/** @type {DialogButton[]} */
|
|
87
85
|
#baseButtons = [];
|
|
88
86
|
#baseTitle = '';
|
|
89
|
-
/** @type {Element|null} */
|
|
90
|
-
#opener = null;
|
|
91
87
|
|
|
92
88
|
/**
|
|
93
89
|
* Creates a structured dialog in the configured theme scope, or at document level when unscoped.
|
|
@@ -99,7 +95,7 @@ export class Dialog extends Modal {
|
|
|
99
95
|
this._applySize(this.options.size);
|
|
100
96
|
this.#baseTitle = String(this.options.title ?? '');
|
|
101
97
|
this.#applyTitle(this.#baseTitle);
|
|
102
|
-
this.#baseButtons =
|
|
98
|
+
this.#baseButtons = normalizeDialogButtons(this.options.buttons);
|
|
103
99
|
this.#renderButtons(this.#baseButtons);
|
|
104
100
|
this.refs.close.hidden = !this.options.closable;
|
|
105
101
|
|
|
@@ -111,7 +107,6 @@ export class Dialog extends Modal {
|
|
|
111
107
|
if (!definition) return;
|
|
112
108
|
this.#runButton(definition);
|
|
113
109
|
});
|
|
114
|
-
this.on('close', () => this.#restoreFocus());
|
|
115
110
|
}
|
|
116
111
|
|
|
117
112
|
/** @type {DialogButton[]} */
|
|
@@ -123,6 +118,7 @@ export class Dialog extends Modal {
|
|
|
123
118
|
*/
|
|
124
119
|
render() {
|
|
125
120
|
const titleId = uid('zx-dialog-title');
|
|
121
|
+
const close = this._message('dialog.close', 'Close');
|
|
126
122
|
const dialog = /** @type {HTMLDialogElement} */ (h('dialog', {
|
|
127
123
|
class: 'zx-modal zx-dialog',
|
|
128
124
|
ariaLabelledby: titleId
|
|
@@ -134,8 +130,8 @@ export class Dialog extends Modal {
|
|
|
134
130
|
ref: 'close',
|
|
135
131
|
type: 'button',
|
|
136
132
|
'data-kind': 'ghost',
|
|
137
|
-
ariaLabel:
|
|
138
|
-
title:
|
|
133
|
+
ariaLabel: close,
|
|
134
|
+
title: close
|
|
139
135
|
}, '×')
|
|
140
136
|
),
|
|
141
137
|
h('div', { class: 'zx-modal__content zx-dialog__body', ref: 'content' }),
|
|
@@ -151,16 +147,13 @@ export class Dialog extends Modal {
|
|
|
151
147
|
* @returns {this}
|
|
152
148
|
*/
|
|
153
149
|
open() {
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
if (first instanceof HTMLElement) first.focus();
|
|
162
|
-
});
|
|
163
|
-
return this;
|
|
150
|
+
/*
|
|
151
|
+
* Capturing the opener, placing initial focus and restoring it afterwards are `Modal`'s, so
|
|
152
|
+
* that every overlay built on it is accessible rather than only the structured one. The
|
|
153
|
+
* override stays because the promise is the dialog's: the header close button is the first
|
|
154
|
+
* focusable descendant, and a footer button declared `autofocus` outranks it.
|
|
155
|
+
*/
|
|
156
|
+
return super.open();
|
|
164
157
|
}
|
|
165
158
|
|
|
166
159
|
/**
|
|
@@ -192,7 +185,7 @@ export class Dialog extends Modal {
|
|
|
192
185
|
* @returns {this}
|
|
193
186
|
*/
|
|
194
187
|
setButtons(list) {
|
|
195
|
-
this.#baseButtons =
|
|
188
|
+
this.#baseButtons = normalizeDialogButtons(list);
|
|
196
189
|
this.#renderButtons(this.#baseButtons);
|
|
197
190
|
return this;
|
|
198
191
|
}
|
|
@@ -216,7 +209,7 @@ export class Dialog extends Modal {
|
|
|
216
209
|
const view = {
|
|
217
210
|
key: normalizedKey,
|
|
218
211
|
content,
|
|
219
|
-
buttons: options && Object.hasOwn(options, 'buttons') ?
|
|
212
|
+
buttons: options && Object.hasOwn(options, 'buttons') ? normalizeDialogButtons(options.buttons ?? []) : null,
|
|
220
213
|
title: options && Object.hasOwn(options, 'title') ? String(options.title ?? '') : null
|
|
221
214
|
};
|
|
222
215
|
this.#views.set(normalizedKey, view);
|
|
@@ -260,7 +253,7 @@ export class Dialog extends Modal {
|
|
|
260
253
|
* @param {AlertOptions} [options={}] Alert options.
|
|
261
254
|
* @returns {Promise<void>}
|
|
262
255
|
*/
|
|
263
|
-
static alert({ title = '', message = '', okLabel
|
|
256
|
+
static alert({ title = '', message = '', okLabel } = {}) {
|
|
264
257
|
return new Promise((resolve) => {
|
|
265
258
|
let settled = false;
|
|
266
259
|
const finish = () => {
|
|
@@ -269,11 +262,13 @@ export class Dialog extends Modal {
|
|
|
269
262
|
dialog.close();
|
|
270
263
|
resolve();
|
|
271
264
|
};
|
|
272
|
-
const dialog = new Dialog(null, {
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
265
|
+
const dialog = new Dialog(null, { title, content: message });
|
|
266
|
+
dialog.setButtons([{
|
|
267
|
+
label: okLabel ?? dialog._message('dialog.ok', 'OK'),
|
|
268
|
+
kind: 'primary',
|
|
269
|
+
action: finish,
|
|
270
|
+
autofocus: true
|
|
271
|
+
}]);
|
|
277
272
|
dialog.once('close', () => {
|
|
278
273
|
if (!settled) {
|
|
279
274
|
settled = true;
|
|
@@ -291,7 +286,7 @@ export class Dialog extends Modal {
|
|
|
291
286
|
* @returns {Promise<boolean>}
|
|
292
287
|
*/
|
|
293
288
|
static confirm({
|
|
294
|
-
title = '', message = '', okLabel
|
|
289
|
+
title = '', message = '', okLabel, cancelLabel, danger = false
|
|
295
290
|
} = {}) {
|
|
296
291
|
return new Promise((resolve) => {
|
|
297
292
|
let settled = false;
|
|
@@ -301,14 +296,16 @@ export class Dialog extends Modal {
|
|
|
301
296
|
dialog.close(value);
|
|
302
297
|
resolve(value);
|
|
303
298
|
};
|
|
304
|
-
const dialog = new Dialog(null, {
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
299
|
+
const dialog = new Dialog(null, { title, content: message });
|
|
300
|
+
dialog.setButtons([
|
|
301
|
+
{ label: cancelLabel ?? dialog._message('dialog.cancel', 'Cancel'), action: () => finish(false) },
|
|
302
|
+
{
|
|
303
|
+
label: okLabel ?? dialog._message('dialog.ok', 'OK'),
|
|
304
|
+
kind: danger ? 'danger' : 'primary',
|
|
305
|
+
action: () => finish(true),
|
|
306
|
+
autofocus: true
|
|
307
|
+
}
|
|
308
|
+
]);
|
|
312
309
|
dialog.once('close', () => {
|
|
313
310
|
if (!settled) {
|
|
314
311
|
settled = true;
|
|
@@ -332,8 +329,7 @@ export class Dialog extends Modal {
|
|
|
332
329
|
class: 'zx-dialog__prompt-input',
|
|
333
330
|
value: String(value),
|
|
334
331
|
placeholder: String(placeholder),
|
|
335
|
-
autofocus: true
|
|
336
|
-
ariaLabel: String(title || 'Prompt value')
|
|
332
|
+
autofocus: true
|
|
337
333
|
});
|
|
338
334
|
const content = h('div', { class: 'zx-dialog__prompt' },
|
|
339
335
|
h('div', { class: 'zx-dialog__prompt-message' }, message),
|
|
@@ -345,14 +341,12 @@ export class Dialog extends Modal {
|
|
|
345
341
|
dialog.close(result);
|
|
346
342
|
resolve(result);
|
|
347
343
|
};
|
|
348
|
-
const dialog = new Dialog(null, {
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
]
|
|
355
|
-
});
|
|
344
|
+
const dialog = new Dialog(null, { title, content });
|
|
345
|
+
input.setAttribute('aria-label', String(title || dialog._message('dialog.promptValue', 'Prompt value')));
|
|
346
|
+
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) }
|
|
349
|
+
]);
|
|
356
350
|
dialog.listen(input, 'keydown', (event) => {
|
|
357
351
|
if (event.key !== 'Enter') return;
|
|
358
352
|
event.preventDefault();
|
|
@@ -383,6 +377,17 @@ export class Dialog extends Modal {
|
|
|
383
377
|
this.el.style.inlineSize = `${Math.max(0, width)}px`;
|
|
384
378
|
}
|
|
385
379
|
|
|
380
|
+
/**
|
|
381
|
+
* Resolves a component message, falling back to the built-in English text.
|
|
382
|
+
* @param {string} key Message key.
|
|
383
|
+
* @param {string} fallback English default.
|
|
384
|
+
* @returns {string}
|
|
385
|
+
*/
|
|
386
|
+
_message(key, fallback) {
|
|
387
|
+
const message = this.msg(key);
|
|
388
|
+
return message === key ? fallback : message;
|
|
389
|
+
}
|
|
390
|
+
|
|
386
391
|
/** @param {string} title @returns {void} */
|
|
387
392
|
#applyTitle(title) {
|
|
388
393
|
this.refs.title.textContent = title;
|
|
@@ -390,23 +395,27 @@ export class Dialog extends Modal {
|
|
|
390
395
|
|
|
391
396
|
/** @param {DialogButton[]} list @returns {void} */
|
|
392
397
|
#renderButtons(list) {
|
|
393
|
-
this.#renderedButtons =
|
|
398
|
+
this.#renderedButtons = normalizeDialogButtons(list);
|
|
394
399
|
this.refs.footer.replaceChildren();
|
|
395
400
|
this.refs.footer.hidden = this.#renderedButtons.length === 0;
|
|
396
401
|
this.#renderedButtons.forEach((definition, index) => {
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
402
|
+
const control = button({
|
|
403
|
+
label: definition.label,
|
|
404
|
+
kind: definition.kind,
|
|
405
|
+
shortcut: definition.shortcut,
|
|
406
|
+
disabled: definition.disabled
|
|
407
|
+
});
|
|
408
|
+
control.dataset.dialogButton = String(index);
|
|
409
|
+
control.autofocus = definition.autofocus;
|
|
410
|
+
this.refs.footer.append(control);
|
|
405
411
|
});
|
|
406
412
|
}
|
|
407
413
|
|
|
408
414
|
/** @param {DialogButton} definition @returns {void} */
|
|
409
415
|
#runButton(definition) {
|
|
416
|
+
// A native disabled button swallows its own clicks, but the footer listener is delegated and
|
|
417
|
+
// a host may dispatch one at the row; the descriptor is the authority either way.
|
|
418
|
+
if (definition.disabled) return;
|
|
410
419
|
const action = definition.action ?? 'close';
|
|
411
420
|
if (action === 'close') {
|
|
412
421
|
this.close();
|
|
@@ -417,17 +426,30 @@ export class Dialog extends Modal {
|
|
|
417
426
|
action(this);
|
|
418
427
|
}
|
|
419
428
|
}
|
|
420
|
-
|
|
421
|
-
/** @returns {void} */
|
|
422
|
-
#restoreFocus() {
|
|
423
|
-
if (this.#opener?.isConnected && typeof this.#opener.focus === 'function') this.#opener.focus();
|
|
424
|
-
this.#opener = null;
|
|
425
|
-
}
|
|
426
429
|
}
|
|
427
430
|
|
|
428
|
-
/**
|
|
429
|
-
|
|
430
|
-
|
|
431
|
+
/**
|
|
432
|
+
* Copies footer-button descriptors without mutating the caller's array or objects, and settles
|
|
433
|
+
* the two booleans the footer renders from.
|
|
434
|
+
*
|
|
435
|
+
* `autofocus` and `disabled` are resolved together because they contradict each other: a control
|
|
436
|
+
* that refuses activation must not be the one initial focus lands on, and a host that writes both
|
|
437
|
+
* has said what it wants twice. Deciding it here rather than at render time keeps `Sheet` — which
|
|
438
|
+
* takes the same descriptors through the same footer — on one answer.
|
|
439
|
+
* @param {DialogButton[]|null|undefined} list Button descriptors.
|
|
440
|
+
* @returns {DialogButton[]} Normalized copies.
|
|
441
|
+
*/
|
|
442
|
+
export function normalizeDialogButtons(list) {
|
|
443
|
+
if (!Array.isArray(list)) return [];
|
|
444
|
+
return list.map((button) => {
|
|
445
|
+
const disabled = Boolean(button?.disabled);
|
|
446
|
+
return {
|
|
447
|
+
...button,
|
|
448
|
+
label: String(button?.label ?? ''),
|
|
449
|
+
disabled,
|
|
450
|
+
autofocus: Boolean(button?.autofocus) && !disabled
|
|
451
|
+
};
|
|
452
|
+
});
|
|
431
453
|
}
|
|
432
454
|
|
|
433
455
|
/**
|
|
@@ -182,7 +182,7 @@ export class Dropdown extends Component {
|
|
|
182
182
|
|
|
183
183
|
/** @param {KeyboardEvent} event @returns {void} */
|
|
184
184
|
[HANDLE_ESCAPE_DISMISS](event) {
|
|
185
|
-
if (event.key !== 'Escape' || !this.isOpen()) return;
|
|
185
|
+
if (event.defaultPrevented || event.key !== 'Escape' || !this.isOpen()) return;
|
|
186
186
|
event.preventDefault();
|
|
187
187
|
const restoreFocus = this.el.contains(document.activeElement);
|
|
188
188
|
this.close();
|
|
@@ -205,7 +205,7 @@ class DropdownDismissManager extends Component {
|
|
|
205
205
|
if (component instanceof Dropdown) component[HANDLE_POINTER_DISMISS](event);
|
|
206
206
|
});
|
|
207
207
|
this.listen(document, 'keydown', (event) => {
|
|
208
|
-
if (event.key !== 'Escape') return;
|
|
208
|
+
if (event.defaultPrevented || event.key !== 'Escape') return;
|
|
209
209
|
const component = topmostOverlayComponent();
|
|
210
210
|
if (component instanceof Dropdown) component[HANDLE_ESCAPE_DISMISS](event);
|
|
211
211
|
});
|