@blukavantui/community 1.0.1 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -5
- package/dist/build-audit.json +181 -53
- package/dist/checkbox.css +1 -0
- package/dist/checkbox.d.ts +2 -0
- package/dist/checkbox.js +1 -0
- package/dist/chunk-3PNBQVYS.js +1 -0
- package/dist/chunk-5ENZVZGF.js +0 -0
- package/dist/chunk-AL2ETLJV.js +1 -0
- package/dist/chunk-BN3PDE5G.js +0 -0
- package/dist/chunk-EOKNLCNS.js +1 -0
- package/dist/chunk-EPZGLHPD.js +0 -0
- package/dist/chunk-FNX33PDD.js +0 -0
- package/dist/chunk-GXHANADL.js +1 -0
- package/dist/chunk-HO6CYWED.js +0 -0
- package/dist/chunk-IJ62ILKE.js +0 -0
- package/dist/chunk-IO22GWON.js +0 -0
- package/dist/chunk-IZJN3EF7.js +1 -0
- package/dist/chunk-JVE6CXCJ.js +1 -0
- package/dist/chunk-MYVLBALF.js +1 -0
- package/dist/chunk-OPMD7QNV.js +1 -0
- package/dist/chunk-RWBLICYX.js +0 -0
- package/dist/chunk-RYD6RHRX.js +0 -0
- package/dist/chunk-UN2SLVFG.js +1 -0
- package/dist/chunk-VJVFHL6P.js +0 -0
- package/dist/chunk-XWXEH3SW.js +1 -0
- package/dist/chunk-YCLR3IE3.js +0 -0
- package/dist/columns2.css +1 -0
- package/dist/columns2.d.ts +2 -0
- package/dist/columns2.js +1 -0
- package/dist/columns3.css +1 -0
- package/dist/columns3.d.ts +2 -0
- package/dist/columns3.js +1 -0
- package/dist/columns4.css +1 -0
- package/dist/columns4.d.ts +2 -0
- package/dist/columns4.js +1 -0
- package/dist/content.js +1 -1
- package/dist/core.js +1 -1
- package/dist/data.css +63 -0
- package/dist/data.d.ts +1 -0
- package/dist/data.js +1 -1
- package/dist/date-picker.css +5 -5
- package/dist/date-picker.js +1 -1
- package/dist/date-range-picker.css +5 -5
- package/dist/date-range-picker.js +1 -1
- package/dist/date-time-picker.css +5 -5
- package/dist/date-time-picker.js +1 -1
- package/dist/draft-components.json +77 -0
- package/dist/feedback.js +1 -1
- package/dist/form-field.css +1 -0
- package/dist/form-field.d.ts +2 -0
- package/dist/form-field.js +1 -0
- package/dist/form-layout/choice.d.ts +15 -0
- package/dist/form-layout/layout.d.ts +12 -0
- package/dist/form-layout/select.d.ts +3 -0
- package/dist/form-layout/shared.d.ts +21 -0
- package/dist/form-layout/table.d.ts +20 -0
- package/dist/form-layout/text.d.ts +3 -0
- package/dist/form-layout/types.d.ts +151 -0
- package/dist/form-layout.css +62 -0
- package/dist/form-layout.d.ts +12 -0
- package/dist/form-layout.js +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -1
- package/dist/inputs.css +83 -20
- package/dist/inputs.d.ts +8 -0
- package/dist/inputs.js +1 -1
- package/dist/layout.css +63 -0
- package/dist/layout.d.ts +3 -0
- package/dist/layout.js +1 -1
- package/dist/native/date-picker.js +1 -1
- package/dist/native/date-range-picker.js +1 -1
- package/dist/native/date-time-picker.js +1 -1
- package/dist/native/form-layout.js +1 -0
- package/dist/native/time-picker.js +1 -1
- package/dist/native-components/form-layout.d.ts +1 -0
- package/dist/radio-button.css +1 -0
- package/dist/radio-button.d.ts +2 -0
- package/dist/radio-button.js +1 -0
- package/dist/radio-group.css +1 -0
- package/dist/radio-group.d.ts +2 -0
- package/dist/radio-group.js +1 -0
- package/dist/select.css +1 -0
- package/dist/select.d.ts +2 -0
- package/dist/select.js +1 -0
- package/dist/sizes.json +272 -90
- package/dist/styles.css +83 -20
- package/dist/switch.css +1 -0
- package/dist/switch.d.ts +2 -0
- package/dist/switch.js +1 -0
- package/dist/table.css +1 -0
- package/dist/table.d.ts +2 -0
- package/dist/table.js +1 -0
- package/dist/text-area.css +1 -0
- package/dist/text-area.d.ts +2 -0
- package/dist/text-area.js +1 -0
- package/dist/text-input.css +1 -0
- package/dist/text-input.d.ts +2 -0
- package/dist/text-input.js +1 -0
- package/dist/time-picker.css +5 -5
- package/dist/time-picker.js +1 -1
- package/docs/FORM-LAYOUT-ESSENTIALS.md +122 -0
- package/docs/PUBLISHING.md +129 -25
- package/draft-components.json +77 -0
- package/edition.json +44 -7
- package/npm-shrinkwrap.json +2 -2
- package/package.json +76 -10
- package/scripts/build.mjs +16 -8
- package/src/checkbox.ts +2 -0
- package/src/columns2.ts +2 -0
- package/src/columns3.ts +2 -0
- package/src/columns4.ts +2 -0
- package/src/data.ts +1 -0
- package/src/form-field.ts +2 -0
- package/src/form-layout/choice.ts +66 -0
- package/src/form-layout/layout.ts +40 -0
- package/src/form-layout/select.ts +97 -0
- package/src/form-layout/shared.ts +57 -0
- package/src/form-layout/table.ts +86 -0
- package/src/form-layout/text.ts +73 -0
- package/src/form-layout/types.ts +59 -0
- package/src/form-layout.ts +12 -0
- package/src/index.ts +2 -0
- package/src/inputs.ts +8 -0
- package/src/layout.ts +3 -0
- package/src/native-components/form-layout.ts +21 -0
- package/src/radio-button.ts +2 -0
- package/src/radio-group.ts +2 -0
- package/src/runtime/date-picker.js +21 -12
- package/src/runtime/date-range-picker.js +21 -12
- package/src/runtime/date-time-picker.js +21 -12
- package/src/runtime/time-picker.js +21 -12
- package/src/select.ts +2 -0
- package/src/switch.ts +2 -0
- package/src/table.ts +2 -0
- package/src/text-area.ts +2 -0
- package/src/text-input.ts +2 -0
- package/src/types.ts +0 -12
- package/styles/checkbox.css +1 -0
- package/styles/columns2.css +1 -0
- package/styles/columns3.css +1 -0
- package/styles/columns4.css +1 -0
- package/styles/date-picker.css +5 -5
- package/styles/date-range-picker.css +5 -5
- package/styles/date-time-picker.css +5 -5
- package/styles/form-field.css +1 -0
- package/styles/form-layout.css +62 -0
- package/styles/radio-button.css +1 -0
- package/styles/radio-group.css +1 -0
- package/styles/select.css +1 -0
- package/styles/switch.css +1 -0
- package/styles/table.css +1 -0
- package/styles/text-area.css +1 -0
- package/styles/text-input.css +1 -0
- package/styles/time-picker.css +5 -5
- package/test/form-layout.test.mjs +28 -0
- package/test/isolation.test.mjs +2 -2
- package/dist/chunk-2TCNAQC6.js +0 -1
- package/dist/chunk-CTLZIWMX.js +0 -1
- package/dist/chunk-MHHJZYXW.js +0 -1
- package/dist/chunk-XDYBNO4P.js +0 -1
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import type { FieldOptions } from './types.js';
|
|
2
|
+
export function el<K extends keyof HTMLElementTagNameMap>(tag: K, cls = '', text?: string) {
|
|
3
|
+
const node = document.createElement(tag); node.className = cls;
|
|
4
|
+
if (text !== undefined) node.textContent = text;
|
|
5
|
+
return node;
|
|
6
|
+
}
|
|
7
|
+
let sequence = 0;
|
|
8
|
+
export function uid() { return 'blu-field-' + (++sequence) + '-' + Math.random().toString(36).slice(2, 8); }
|
|
9
|
+
export function attr(node: Element, name: string, value?: string | number | boolean) {
|
|
10
|
+
if (value === undefined || value === false || value === '') node.removeAttribute(name);
|
|
11
|
+
else node.setAttribute(name, String(value));
|
|
12
|
+
}
|
|
13
|
+
export function frame(host: HTMLElement, options: FieldOptions, group = false) {
|
|
14
|
+
const root = el(group ? 'fieldset' : 'div', 'blu-field');
|
|
15
|
+
const label = el(group ? 'legend' : 'label', 'blu-field__label');
|
|
16
|
+
const body = el('div', 'blu-field__body'), description = el('div', 'blu-field__help'), error = el('div', 'blu-field__error');
|
|
17
|
+
const id = options.Id || uid(); description.id = id + '-help'; error.id = id + '-error'; error.setAttribute('aria-live', 'polite');
|
|
18
|
+
root.append(label, body, description, error); host.append(root);
|
|
19
|
+
function paint(p: FieldOptions, controls: HTMLElement[]) {
|
|
20
|
+
root.className = 'blu-field blu-field--' + (p.Size || 'Medium').toLowerCase() + (p.ExtendedClass ? ' ' + p.ExtendedClass : '');
|
|
21
|
+
root.dataset.invalid = String(!!p.Error); root.dataset.disabled = String(!!p.Disabled);
|
|
22
|
+
label.textContent = (p.Label || '') + (p.Required ? ' *' : ''); label.hidden = !p.Label;
|
|
23
|
+
if (!group && controls[0]) { controls[0].id ||= id; label.setAttribute('for', controls[0].id); }
|
|
24
|
+
description.textContent = p.Description || ''; description.hidden = !p.Description;
|
|
25
|
+
error.textContent = p.Error || ''; error.hidden = !p.Error;
|
|
26
|
+
const described = [p.Description ? description.id : '', p.Error ? error.id : ''].filter(Boolean).join(' ');
|
|
27
|
+
for (const control of controls) {
|
|
28
|
+
attr(control, 'aria-describedby', described); attr(control, 'aria-invalid', p.Error ? 'true' : undefined);
|
|
29
|
+
attr(control, 'aria-required', p.Required ? 'true' : undefined);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return { root, label, body, description, error, id, paint };
|
|
33
|
+
}
|
|
34
|
+
export function lifecycle(root: HTMLElement, reset: () => void) {
|
|
35
|
+
const abort = new AbortController(); let disposed = false;
|
|
36
|
+
// Native form reset is cancelable and restores values after event dispatch.
|
|
37
|
+
const form = root.closest('form');
|
|
38
|
+
form?.addEventListener('reset', event => queueMicrotask(() => { if (!disposed && !event.defaultPrevented) reset(); }), { signal: abort.signal });
|
|
39
|
+
return {
|
|
40
|
+
signal: abort.signal,
|
|
41
|
+
check() { if (disposed) throw Error('Component is destroyed.'); },
|
|
42
|
+
destroy() { if (disposed) return; disposed = true; abort.abort(); root.remove(); },
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
export function common(control: HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement, p: FieldOptions) {
|
|
46
|
+
control.name = p.Name || ''; control.disabled = !!p.Disabled; control.required = !!p.Required;
|
|
47
|
+
if ('readOnly' in control) control.readOnly = !!p.ReadOnly;
|
|
48
|
+
control.setCustomValidity(p.Error || '');
|
|
49
|
+
}
|
|
50
|
+
export function bound(value: number | undefined, fallback: number, min: number, max: number) {
|
|
51
|
+
return Number.isFinite(value) ? Math.max(min, Math.min(max, value!)) : fallback;
|
|
52
|
+
}
|
|
53
|
+
export function imageUrl(value: string) {
|
|
54
|
+
const url = new URL(value, document.baseURI);
|
|
55
|
+
if (!['http:', 'https:'].includes(url.protocol)) throw Error('Adornment images must use an HTTP(S) or relative URL.');
|
|
56
|
+
return url.href;
|
|
57
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { el, bound, uid } from './shared.js';
|
|
2
|
+
import type { TableOptions, TableEvents } from './types.js';
|
|
3
|
+
|
|
4
|
+
export function tableRows<Row extends object>(options: TableOptions<Row>) {
|
|
5
|
+
const rows = [...options.Rows], key = options.SortKey;
|
|
6
|
+
if (key && options.Columns.some(c => c.Key === key && c.Sortable)) rows.sort((a, b) => {
|
|
7
|
+
const x = a[key], y = b[key]; if (x == null || y == null) return x == null ? (y == null ? 0 : 1) : -1;
|
|
8
|
+
const difference = typeof x === 'number' && typeof y === 'number' ? x - y : String(x).localeCompare(String(y), undefined, { numeric: true, sensitivity: 'base' });
|
|
9
|
+
return options.SortDirection === 'Descending' ? -difference : difference;
|
|
10
|
+
});
|
|
11
|
+
const size = options.PageSize ? Math.floor(bound(options.PageSize, 10, 1, 10000)) : Math.max(1, rows.length);
|
|
12
|
+
const pages = Math.max(1, Math.ceil(rows.length / size)), page = Math.floor(bound(options.Page, 1, 1, pages));
|
|
13
|
+
return { rows: rows.slice((page - 1) * size, page * size), page, pages, total: rows.length, start: rows.length ? (page - 1) * size + 1 : 0, end: Math.min(page * size, rows.length) };
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Semantic HTML table, client-side sorting/paging, no grid engine or editing model. */
|
|
17
|
+
export function createTable<Row extends object>(host: HTMLElement, initial: TableOptions<Row>, events: TableEvents<Row> = {}) {
|
|
18
|
+
let p = { ...initial }, disposed = false; const root = el('div', 'blu-table'), scroll = el('div', 'blu-table__scroll');
|
|
19
|
+
const table = el('table'), caption = el('caption'), head = el('thead'), body = el('tbody'), footer = el('div', 'blu-table__footer');
|
|
20
|
+
const status = el('span'), previous = el('button', 'blu-table-button', 'Previous'), next = el('button', 'blu-table-button', 'Next');
|
|
21
|
+
let headerSignature = '';
|
|
22
|
+
caption.id = uid(); scroll.tabIndex = 0; scroll.setAttribute('role', 'region'); scroll.setAttribute('aria-labelledby', caption.id);
|
|
23
|
+
previous.type = next.type = 'button'; status.setAttribute('role', 'status'); footer.append(status, previous, next); table.append(caption, head, body); scroll.append(table); root.append(scroll, footer); host.append(root);
|
|
24
|
+
function update(patch: Partial<TableOptions<Row>>) {
|
|
25
|
+
if (disposed) throw Error('Component is destroyed.'); p = { ...p, ...patch };
|
|
26
|
+
if (!p.Columns.length) throw Error('Table needs at least one column.');
|
|
27
|
+
if (new Set(p.Columns.map(c => c.Key)).size !== p.Columns.length) throw Error('Table column keys must be unique.');
|
|
28
|
+
root.className = 'blu-table' + (p.ExtendedClass ? ' ' + p.ExtendedClass : ''); root.dataset.density = p.Density || 'Comfortable'; root.dataset.striped = String(!!p.Striped);
|
|
29
|
+
table.setAttribute('aria-busy', String(!!p.Loading)); caption.textContent = p.Caption;
|
|
30
|
+
const focused = document.activeElement instanceof HTMLElement && root.contains(document.activeElement) ? document.activeElement.dataset.focusKey : undefined;
|
|
31
|
+
const focusedPagination = document.activeElement === next ? next : document.activeElement === previous ? previous : undefined;
|
|
32
|
+
const nextHeaderSignature = JSON.stringify([p.Columns.map(c => [c.Key, c.Header, !!c.Sortable, c.Align || 'Left']), !!p.Actions?.length]);
|
|
33
|
+
// Retain sortable header controls during sorting, paging and row updates.
|
|
34
|
+
// VoiceOver can activate a header without moving DOM keyboard focus to it.
|
|
35
|
+
if (nextHeaderSignature !== headerSignature) {
|
|
36
|
+
headerSignature = nextHeaderSignature;
|
|
37
|
+
head.replaceChildren(); const tr = el('tr'); head.append(tr);
|
|
38
|
+
for (const column of p.Columns) {
|
|
39
|
+
const th = el('th'); th.scope = 'col'; th.style.textAlign = (column.Align || 'Left').toLowerCase();
|
|
40
|
+
if (column.Sortable) {
|
|
41
|
+
const button = el('button', 'blu-table__sort', column.Header); button.type = 'button'; button.dataset.focusKey = 'sort-' + column.Key;
|
|
42
|
+
const glyph = el('span'); glyph.setAttribute('aria-hidden', 'true'); button.append(glyph);
|
|
43
|
+
button.onclick = () => { const direction = p.SortKey === column.Key && p.SortDirection !== 'Descending' ? 'Descending' : 'Ascending'; update({ SortKey: column.Key, SortDirection: direction, Page: 1 } as Partial<TableOptions<Row>>); events.SortChanged?.(column.Key, direction); };
|
|
44
|
+
th.append(button);
|
|
45
|
+
} else th.textContent = column.Header; tr.append(th);
|
|
46
|
+
}
|
|
47
|
+
if (p.Actions?.length) { const th = el('th', '', 'Actions'); th.scope = 'col'; tr.append(th); }
|
|
48
|
+
}
|
|
49
|
+
p.Columns.forEach((column, index) => {
|
|
50
|
+
if (!column.Sortable) return;
|
|
51
|
+
const th = head.rows[0].cells[index];
|
|
52
|
+
th.setAttribute('aria-sort', p.SortKey === column.Key ? (p.SortDirection || 'Ascending').toLowerCase() : 'none');
|
|
53
|
+
th.querySelector('span')!.textContent = p.SortKey === column.Key ? (p.SortDirection === 'Descending' ? '↓' : '↑') : '↕';
|
|
54
|
+
});
|
|
55
|
+
const state = tableRows(p); p.Page = state.page; body.replaceChildren();
|
|
56
|
+
if (p.Loading || !state.rows.length) {
|
|
57
|
+
const row = el('tr'), cell = el('td', 'blu-table__empty', p.Loading ? 'Loading records…' : (p.EmptyMessage || 'No records yet.')); cell.colSpan = p.Columns.length + (p.Actions?.length ? 1 : 0); row.append(cell); body.append(row);
|
|
58
|
+
} else for (const [index, row] of state.rows.entries()) {
|
|
59
|
+
const rowEl = el('tr'); for (const column of p.Columns) {
|
|
60
|
+
const cell = el('td'); cell.style.textAlign = (column.Align || 'Left').toLowerCase();
|
|
61
|
+
cell.textContent = column.Format ? column.Format(row[column.Key], row) : String(row[column.Key] ?? '—'); rowEl.append(cell);
|
|
62
|
+
}
|
|
63
|
+
if (p.Actions?.length) { const cell = el('td'), actions = el('div', 'blu-table__actions');
|
|
64
|
+
for (const action of p.Actions) {
|
|
65
|
+
const button = el('button', 'blu-table-button', action.Label); button.type = 'button'; button.disabled = typeof action.Disabled === 'function' ? action.Disabled(row) : !!action.Disabled;
|
|
66
|
+
button.dataset.focusKey = 'row-' + String(p.RowKey ? row[p.RowKey] : index) + '-' + action.Key;
|
|
67
|
+
if (p.RowKey) button.setAttribute('aria-label', action.Label + ' ' + String(row[p.RowKey]));
|
|
68
|
+
button.onclick = () => events.RowAction?.(action.Key, row); actions.append(button);
|
|
69
|
+
} cell.append(actions); rowEl.append(cell);
|
|
70
|
+
} body.append(rowEl);
|
|
71
|
+
}
|
|
72
|
+
status.textContent = p.Loading ? 'Loading records…' : `${state.start}–${state.end} of ${state.total} records`;
|
|
73
|
+
previous.hidden = next.hidden = !p.PageSize; previous.disabled = !!p.Loading || state.page <= 1; next.disabled = !!p.Loading || state.page >= state.pages;
|
|
74
|
+
previous.onclick = () => { update({ Page: state.page - 1 } as Partial<TableOptions<Row>>); events.PageChanged?.(p.Page!); };
|
|
75
|
+
next.onclick = () => { update({ Page: state.page + 1 } as Partial<TableOptions<Row>>); events.PageChanged?.(p.Page!); };
|
|
76
|
+
if (focused) Array.from(root.querySelectorAll<HTMLElement>('[data-focus-key]')).find(node => node.dataset.focusKey === focused)?.focus();
|
|
77
|
+
// Safari restarts Tab navigation at the page top if the focused pagination
|
|
78
|
+
// button becomes disabled. Keep the user at the available adjacent control.
|
|
79
|
+
if (!p.Loading && p.PageSize) {
|
|
80
|
+
if (focusedPagination === next && next.disabled) (previous.disabled ? scroll : previous).focus();
|
|
81
|
+
else if (focusedPagination === previous && previous.disabled) (next.disabled ? scroll : next).focus();
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
try { update({}); } catch (error) { root.remove(); throw error; }
|
|
85
|
+
return { element: root, update, getState: () => ({ Page: p.Page || 1, SortKey: p.SortKey, SortDirection: p.SortDirection || 'Ascending' }), destroy() { if (disposed) return; disposed = true; root.remove(); } };
|
|
86
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { el, attr, frame, lifecycle, common, bound, imageUrl } from './shared.js';
|
|
2
|
+
import type { Adornment, TextInputOptions, TextInputEvents, TextAreaOptions, FieldEvents, FieldComponent } from './types.js';
|
|
3
|
+
|
|
4
|
+
export function createTextInput(host: HTMLElement, initial: TextInputOptions = {}, events: TextInputEvents = {}): FieldComponent<TextInputOptions, string> {
|
|
5
|
+
let p = { ...initial }, revealed = false;
|
|
6
|
+
const f = frame(host, p), shell = el('div', 'blu-text'), input = el('input', 'blu-text__input');
|
|
7
|
+
const leading = el('span', 'blu-text__adornment'), trailing = el('span', 'blu-text__adornment');
|
|
8
|
+
const clear = el('button', 'blu-text__action', '×'), reveal = el('button', 'blu-text__action', 'Show');
|
|
9
|
+
clear.type = reveal.type = 'button'; clear.setAttribute('aria-label', 'Clear ' + (p.Label || 'text'));
|
|
10
|
+
shell.append(leading, input, trailing, clear, reveal); f.body.append(shell);
|
|
11
|
+
const life = lifecycle(f.root, reset);
|
|
12
|
+
const adornmentKinds = new WeakMap<HTMLElement, Adornment['Kind']>();
|
|
13
|
+
function adorn(target: HTMLElement, value?: Adornment) {
|
|
14
|
+
// Keep an existing action alive while validation/state updates run. Replacing
|
|
15
|
+
// the focused button leaves VoiceOver pointing at a detached control.
|
|
16
|
+
if (adornmentKinds.get(target) !== value?.Kind) target.replaceChildren();
|
|
17
|
+
target.removeAttribute('role'); target.removeAttribute('aria-label'); target.removeAttribute('aria-hidden'); target.hidden = !value;
|
|
18
|
+
if (!value) { adornmentKinds.delete(target); return; }
|
|
19
|
+
adornmentKinds.set(target, value.Kind);
|
|
20
|
+
if (value.Kind === 'Image') { const img = target.querySelector('img') || el('img'); const src = imageUrl(value.Src); if (img.src !== src) img.src = src; img.alt = value.Alt; if (!img.parentNode) target.append(img); }
|
|
21
|
+
else if (value.Kind === 'Action') {
|
|
22
|
+
const button = target.querySelector('button') || el('button', 'blu-text__action'); button.type = 'button'; button.textContent = value.Text; button.setAttribute('aria-label', value.Label);
|
|
23
|
+
button.disabled = !!p.Disabled || !!p.ReadOnly; button.onclick = () => events.Action?.(value.Key, input.value); if (!button.parentNode) target.append(button);
|
|
24
|
+
} else { target.textContent = value.Text; if (value.Kind === 'Icon') { attr(target, 'role', value.Label ? 'img' : undefined); attr(target, 'aria-label', value.Label); attr(target, 'aria-hidden', value.Label ? undefined : 'true'); } }
|
|
25
|
+
}
|
|
26
|
+
function refreshActions() { clear.hidden = !p.Clearable || !input.value || !!p.ReadOnly; reveal.hidden = !p.RevealPassword || p.Type !== 'password'; }
|
|
27
|
+
function update(patch: Partial<TextInputOptions>) {
|
|
28
|
+
life.check(); p = { ...p, ...patch }; f.paint(p, [input]); common(input, p);
|
|
29
|
+
input.type = p.Type === 'password' && revealed ? 'text' : (p.Type || 'text');
|
|
30
|
+
if (input.value !== (p.Value || '')) input.value = p.Value || '';
|
|
31
|
+
input.placeholder = p.Placeholder || ''; attr(input, 'maxlength', p.MaxLength === undefined ? undefined : Math.max(0, Math.floor(p.MaxLength)));
|
|
32
|
+
attr(input, 'autocomplete', p.Autocomplete); adorn(leading, p.Leading); adorn(trailing, p.Trailing);
|
|
33
|
+
clear.disabled = reveal.disabled = !!p.Disabled; clear.setAttribute('aria-label', 'Clear ' + (p.Label || 'text'));
|
|
34
|
+
reveal.textContent = revealed ? 'Hide' : 'Show'; reveal.setAttribute('aria-label', revealed ? 'Hide password' : 'Show password');
|
|
35
|
+
reveal.setAttribute('aria-pressed', String(revealed)); refreshActions();
|
|
36
|
+
}
|
|
37
|
+
function changed() { p.Value = input.value; refreshActions(); events.Change?.(input.value); }
|
|
38
|
+
function reset() { revealed = false; update({ Value: initial.Value || '' }); }
|
|
39
|
+
input.addEventListener('input', changed, { signal: life.signal });
|
|
40
|
+
input.addEventListener('blur', () => events.Blur?.(), { signal: life.signal });
|
|
41
|
+
clear.addEventListener('click', () => { input.value = ''; changed(); input.focus(); }, { signal: life.signal });
|
|
42
|
+
reveal.addEventListener('click', () => { revealed = !revealed; update({}); input.focus(); }, { signal: life.signal });
|
|
43
|
+
update({});
|
|
44
|
+
return { element: f.root, control: input, update, reset, getValue: () => input.value, focus: () => input.focus(), validate: () => input.reportValidity(), destroy: life.destroy };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function createTextArea(host: HTMLElement, initial: TextAreaOptions = {}, events: FieldEvents<string> = {}): FieldComponent<TextAreaOptions, string> {
|
|
48
|
+
let p = { ...initial };
|
|
49
|
+
const f = frame(host, p), input = el('textarea', 'blu-textarea'), count = el('div', 'blu-field__count');
|
|
50
|
+
count.id = f.id + '-count'; f.body.append(input, count); const life = lifecycle(f.root, reset);
|
|
51
|
+
function resize() {
|
|
52
|
+
count.hidden = !p.ShowCount; count.textContent = input.value.length + (p.MaxLength === undefined ? ' characters' : ' / ' + p.MaxLength);
|
|
53
|
+
input.style.maxHeight = p.AutoGrow ? bound(p.MaxHeight, 320, 80, 2000) + 'px' : '';
|
|
54
|
+
input.style.resize = p.AutoGrow ? 'none' : 'vertical';
|
|
55
|
+
input.style.height = ''; if (p.AutoGrow) input.style.height = Math.min(input.scrollHeight + 2, bound(p.MaxHeight, 320, 80, 2000)) + 'px';
|
|
56
|
+
}
|
|
57
|
+
function update(patch: Partial<TextAreaOptions>) {
|
|
58
|
+
life.check(); p = { ...p, ...patch }; f.paint(p, [input]); common(input, p);
|
|
59
|
+
input.rows = Math.floor(bound(p.Rows, 4, 1, 30)); input.placeholder = p.Placeholder || '';
|
|
60
|
+
attr(input, 'maxlength', p.MaxLength === undefined ? undefined : Math.max(0, Math.floor(p.MaxLength)));
|
|
61
|
+
if (input.value !== (p.Value || '')) input.value = p.Value || '';
|
|
62
|
+
if (p.ShowCount) attr(input, 'aria-describedby', [input.getAttribute('aria-describedby'), count.id].filter(Boolean).join(' '));
|
|
63
|
+
resize();
|
|
64
|
+
}
|
|
65
|
+
function reset() { update({ Value: initial.Value || '' }); }
|
|
66
|
+
input.addEventListener('input', () => { p.Value = input.value; resize(); events.Change?.(input.value); }, { signal: life.signal });
|
|
67
|
+
input.addEventListener('blur', () => events.Blur?.(), { signal: life.signal });
|
|
68
|
+
let lastWidth = 0;
|
|
69
|
+
// Observe the host width only; height changes from auto-grow must not feed back.
|
|
70
|
+
const widthObserver = typeof ResizeObserver !== 'undefined' ? new ResizeObserver(entries => { const w = entries[0].contentRect.width; if (w !== lastWidth) { lastWidth = w; resize(); } }) : undefined;
|
|
71
|
+
widthObserver?.observe(f.root); update({});
|
|
72
|
+
return { element: f.root, control: input, update, reset, getValue: () => input.value, focus: () => input.focus(), validate: () => input.reportValidity(), destroy() { widthObserver?.disconnect(); life.destroy(); } };
|
|
73
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/** Local Community additions. Not present in the published 1.0.1 package. */
|
|
2
|
+
export type FieldSize = 'Small' | 'Medium' | 'Large';
|
|
3
|
+
export interface FieldOptions {
|
|
4
|
+
Id?: string; Name?: string; Label?: string; Description?: string; Error?: string;
|
|
5
|
+
Required?: boolean; Disabled?: boolean; ReadOnly?: boolean; Size?: FieldSize;
|
|
6
|
+
ExtendedClass?: string;
|
|
7
|
+
}
|
|
8
|
+
export interface FieldEvents<T> { Change?: (value: T) => void; Blur?: () => void; }
|
|
9
|
+
export interface FieldComponent<P, T> {
|
|
10
|
+
element: HTMLElement; control: HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement;
|
|
11
|
+
update(patch: Partial<P>): void; getValue(): T; reset(): void; focus(): void;
|
|
12
|
+
validate(): boolean; destroy(): void;
|
|
13
|
+
}
|
|
14
|
+
export type Adornment =
|
|
15
|
+
| { Kind: 'Text'; Text: string }
|
|
16
|
+
| { Kind: 'Icon'; Text: string; Label?: string }
|
|
17
|
+
| { Kind: 'Image'; Src: string; Alt: string }
|
|
18
|
+
| { Kind: 'Action'; Text: string; Label: string; Key: string };
|
|
19
|
+
export interface TextInputOptions extends FieldOptions {
|
|
20
|
+
Value?: string; Type?: 'text' | 'email' | 'password' | 'search' | 'tel' | 'url';
|
|
21
|
+
Placeholder?: string; MaxLength?: number; Autocomplete?: string;
|
|
22
|
+
Leading?: Adornment; Trailing?: Adornment; Clearable?: boolean; RevealPassword?: boolean;
|
|
23
|
+
}
|
|
24
|
+
export interface TextInputEvents extends FieldEvents<string> { Action?: (key: string, value: string) => void; }
|
|
25
|
+
export interface TextAreaOptions extends FieldOptions {
|
|
26
|
+
Value?: string; Placeholder?: string; Rows?: number; MaxLength?: number;
|
|
27
|
+
AutoGrow?: boolean; MaxHeight?: number; ShowCount?: boolean;
|
|
28
|
+
}
|
|
29
|
+
export interface CheckboxOptions extends FieldOptions { Checked?: boolean; Indeterminate?: boolean; Value?: string; }
|
|
30
|
+
export interface SwitchOptions extends FieldOptions { Checked?: boolean; OnText?: string; OffText?: string; }
|
|
31
|
+
export interface RadioButtonOptions extends FieldOptions { Checked?: boolean; Value: string; }
|
|
32
|
+
export interface Choice { Value: string; Label: string; Description?: string; Disabled?: boolean; }
|
|
33
|
+
export interface RadioGroupOptions extends FieldOptions { Value?: string; Options: Choice[]; Direction?: 'Horizontal' | 'Vertical'; }
|
|
34
|
+
export interface SelectOptions extends FieldOptions { Value?: string; Options: Choice[]; Placeholder?: string; Clearable?: boolean; Searchable?: boolean; }
|
|
35
|
+
export interface FormFieldOptions extends FieldOptions { Control?: HTMLElement; }
|
|
36
|
+
export interface ColumnsOptions {
|
|
37
|
+
Ratio?: 'Equal' | 'OneThirdTwoThirds' | 'TwoThirdsOneThird' | 'OneQuarterThreeQuarters' | 'ThreeQuartersOneQuarter' | 'WideCenter';
|
|
38
|
+
/** Match column heights within a row and stretch a single content wrapper. Stacked rows keep natural height. */
|
|
39
|
+
EqualHeight?: boolean;
|
|
40
|
+
Gap?: number; Align?: 'Start' | 'Center' | 'End' | 'Stretch';
|
|
41
|
+
StackAt?: 'Small' | 'Medium' | 'Large'; ExtendedClass?: string;
|
|
42
|
+
}
|
|
43
|
+
export interface ColumnsComponent { element: HTMLElement; columns: HTMLElement[]; update(patch: Partial<ColumnsOptions>): void; destroy(): void; }
|
|
44
|
+
export interface TableColumn<Row extends object> {
|
|
45
|
+
Key: Extract<keyof Row, string>; Header: string; Sortable?: boolean; Align?: 'Left' | 'Center' | 'Right';
|
|
46
|
+
Format?: (value: Row[Extract<keyof Row, string>], row: Row) => string;
|
|
47
|
+
}
|
|
48
|
+
export interface TableAction<Row> { Key: string; Label: string; Disabled?: boolean | ((row: Row) => boolean); }
|
|
49
|
+
export interface TableOptions<Row extends object> {
|
|
50
|
+
Columns: TableColumn<Row>[]; Rows: Row[]; Caption: string; RowKey?: Extract<keyof Row, string>;
|
|
51
|
+
Actions?: TableAction<Row>[]; Loading?: boolean; EmptyMessage?: string;
|
|
52
|
+
PageSize?: number; Page?: number; SortKey?: Extract<keyof Row, string>; SortDirection?: 'Ascending' | 'Descending';
|
|
53
|
+
Density?: 'Compact' | 'Comfortable'; Striped?: boolean; ExtendedClass?: string;
|
|
54
|
+
}
|
|
55
|
+
export interface TableEvents<Row> {
|
|
56
|
+
RowAction?: (key: string, row: Row) => void;
|
|
57
|
+
SortChanged?: (key: Extract<keyof Row, string>, direction: 'Ascending' | 'Descending') => void;
|
|
58
|
+
PageChanged?: (page: number) => void;
|
|
59
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export * from './checkbox.js';
|
|
2
|
+
export * from './text-input.js';
|
|
3
|
+
export * from './text-area.js';
|
|
4
|
+
export * from './radio-button.js';
|
|
5
|
+
export * from './radio-group.js';
|
|
6
|
+
export * from './switch.js';
|
|
7
|
+
export * from './select.js';
|
|
8
|
+
export * from './form-field.js';
|
|
9
|
+
export * from './columns2.js';
|
|
10
|
+
export * from './columns3.js';
|
|
11
|
+
export * from './columns4.js';
|
|
12
|
+
export * from './table.js';
|
package/src/index.ts
CHANGED
package/src/inputs.ts
CHANGED
|
@@ -6,3 +6,11 @@ export * from './date-picker.js';
|
|
|
6
6
|
export * from './time-picker.js';
|
|
7
7
|
export * from './date-time-picker.js';
|
|
8
8
|
export * from './date-range-picker.js';
|
|
9
|
+
export * from './checkbox.js';
|
|
10
|
+
export * from './text-input.js';
|
|
11
|
+
export * from './text-area.js';
|
|
12
|
+
export * from './radio-button.js';
|
|
13
|
+
export * from './radio-group.js';
|
|
14
|
+
export * from './switch.js';
|
|
15
|
+
export * from './select.js';
|
|
16
|
+
export * from './form-field.js';
|
package/src/layout.ts
CHANGED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import {createCheckbox} from '../checkbox.js';
|
|
2
|
+
import {createTextInput} from '../text-input.js';
|
|
3
|
+
import {createTextArea} from '../text-area.js';
|
|
4
|
+
import {createRadioButton} from '../radio-button.js';
|
|
5
|
+
import {createRadioGroup} from '../radio-group.js';
|
|
6
|
+
import {createSwitch} from '../switch.js';
|
|
7
|
+
import {createSelect} from '../select.js';
|
|
8
|
+
import {createTable} from '../table.js';
|
|
9
|
+
import {install} from '../native-base.js';
|
|
10
|
+
const wrap = (factory:any) => (host:HTMLElement,p:any,events:any) => { const instance=factory(host,json(p),events); return {...instance,update:(patch:any)=>instance.update(json(patch))}; };
|
|
11
|
+
const json = (p:Record<string,unknown>) => { const result={...p}; for(const key of ['Options','Rows','Columns','Actions','Leading','Trailing']) if(key+'Json' in result) { result[key]=JSON.parse(String(result[key+'Json'] || (['Leading','Trailing'].includes(key)?'null':'[]'))); delete result[key+'Json']; } return result; };
|
|
12
|
+
install({
|
|
13
|
+
Checkbox:wrap(createCheckbox),
|
|
14
|
+
TextInput:wrap(createTextInput),
|
|
15
|
+
TextArea:wrap(createTextArea),
|
|
16
|
+
RadioButton:wrap(createRadioButton),
|
|
17
|
+
RadioGroup:wrap(createRadioGroup),
|
|
18
|
+
Switch:wrap(createSwitch),
|
|
19
|
+
Select:wrap(createSelect),
|
|
20
|
+
Table:wrap(createTable)
|
|
21
|
+
});
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
//
|
|
1
|
+
// docs/scripts/advanced/shared.js
|
|
2
2
|
import { state, register } from "./kernel.js";
|
|
3
3
|
var shared = state("advanced", () => ({ instances: /* @__PURE__ */ new Map(), serial: 0 }));
|
|
4
4
|
var uid = () => `blu-advanced-${++shared.serial}`;
|
|
@@ -91,7 +91,7 @@ function createAPI(kind, init2, update2) {
|
|
|
91
91
|
}, destroy }));
|
|
92
92
|
}
|
|
93
93
|
|
|
94
|
-
//
|
|
94
|
+
// docs/scripts/advanced/date-utils.js
|
|
95
95
|
var pad = (n) => String(n).padStart(2, "0");
|
|
96
96
|
function dateValue(d) {
|
|
97
97
|
return String(d.getFullYear()).padStart(4, "0") + "-" + pad(d.getMonth() + 1) + "-" + pad(d.getDate());
|
|
@@ -172,7 +172,7 @@ function validateRange(start, end, options = {}) {
|
|
|
172
172
|
return "";
|
|
173
173
|
}
|
|
174
174
|
|
|
175
|
-
//
|
|
175
|
+
// docs/scripts/advanced/pickers.js
|
|
176
176
|
var formatOptions = ["YYYY-MM-DD", "MM/DD/YYYY", "DD/MM/YYYY"];
|
|
177
177
|
var today = () => dateValue(/* @__PURE__ */ new Date());
|
|
178
178
|
var datePart = (value) => String(value || "").split("T")[0];
|
|
@@ -256,14 +256,15 @@ function positionPopup(s) {
|
|
|
256
256
|
const below = Math.max(0, top + height - edge - anchor.bottom - gap), above = Math.max(0, anchor.top - gap - top - edge);
|
|
257
257
|
const natural = panel.scrollHeight + 2, up = natural > below && above > below, room = Math.max(0, height - edge * 2);
|
|
258
258
|
panel.style.maxHeight = room + "px";
|
|
259
|
-
|
|
259
|
+
const fields = s.fieldRow.getBoundingClientRect();
|
|
260
|
+
panel.style.left = Math.max(left + edge, Math.min(fields.left + (fields.width - panelWidth) / 2, left + width - edge - panelWidth)) + "px";
|
|
260
261
|
const preferred = up ? anchor.top - gap - natural : anchor.bottom + gap;
|
|
261
262
|
panel.style.top = Math.max(top + edge, Math.min(preferred, top + height - edge - Math.min(natural, room))) + "px";
|
|
262
263
|
panel.dataset.placement = up ? "top" : "bottom";
|
|
263
264
|
}
|
|
264
265
|
function focusPopup(s) {
|
|
265
266
|
const timeField = s.timeOnly || s.dateTime && s.anchor === s.fields[1].input;
|
|
266
|
-
(timeField ? s.hours : s.calendar.querySelector('[data-date="' + s.focusDate + '"]') || s.monthSelect).focus();
|
|
267
|
+
(timeField ? s.hours : s.calendar.querySelector('[data-date="' + s.focusDate + '"]') || s.monthSelect).focus({ preventScroll: true });
|
|
267
268
|
}
|
|
268
269
|
function commit(s, first, end, time, keepOpen = false) {
|
|
269
270
|
const issue = validate(s, first, end, time);
|
|
@@ -287,7 +288,7 @@ function commit(s, first, end, time, keepOpen = false) {
|
|
|
287
288
|
s.inputSignature = JSON.stringify(s.range ? [s.start, s.end] : [s.value]);
|
|
288
289
|
paintInputs(s);
|
|
289
290
|
if (!keepOpen) close(s);
|
|
290
|
-
if (returnToInput) s.fields[0].input.focus();
|
|
291
|
+
if (returnToInput) s.fields[0].input.focus({ preventScroll: true });
|
|
291
292
|
s.notice.textContent = s.range ? first ? labelDate(s, first) + " to " + labelDate(s, end) : "Date range cleared" : value ? "Selected " + (s.timeOnly ? formatTime(value, s.cycle) : labelDate(s, first) + (s.hasTime ? " at " + formatTime(time, s.cycle) : "")) : "Selection cleared";
|
|
292
293
|
if (changed) {
|
|
293
294
|
if (s.range) emit(s, "Change", first, end);
|
|
@@ -391,11 +392,18 @@ function appendOption(select, value, label) {
|
|
|
391
392
|
return option;
|
|
392
393
|
}
|
|
393
394
|
function selectField(s, label, name) {
|
|
394
|
-
const wrap = node("label", "blu-picker__select-field"), caption = node("span", "", label), select = node("select", "blu-picker__input");
|
|
395
|
+
const wrap = node("label", "blu-picker__select-field"), caption = node("span", "blu-picker__select-caption", label), select = node("select", "blu-picker__input");
|
|
395
396
|
select.id = s.uid + "-" + name;
|
|
396
397
|
wrap.append(caption, select);
|
|
397
398
|
return { wrap, select };
|
|
398
399
|
}
|
|
400
|
+
function focusPopupButton(event) {
|
|
401
|
+
if (event.button !== 0) return;
|
|
402
|
+
const target = event.target.closest?.("button");
|
|
403
|
+
if (!target || target.disabled) return;
|
|
404
|
+
event.preventDefault();
|
|
405
|
+
target.focus({ preventScroll: true });
|
|
406
|
+
}
|
|
399
407
|
function focusDay(s, value) {
|
|
400
408
|
const min = datePart(s.options.Min) || "0001-01-01", max = datePart(s.options.Max) || "9999-12-31";
|
|
401
409
|
value = value < min ? min : value > max ? max : value;
|
|
@@ -403,7 +411,7 @@ function focusDay(s, value) {
|
|
|
403
411
|
s.focusDate = value;
|
|
404
412
|
s.month = value.slice(0, 7);
|
|
405
413
|
paintCalendar(s);
|
|
406
|
-
s.calendar.querySelector('[data-date="' + value + '"]')?.focus();
|
|
414
|
+
s.calendar.querySelector('[data-date="' + value + '"]')?.focus({ preventScroll: true });
|
|
407
415
|
}
|
|
408
416
|
function keyDay(s, event, value) {
|
|
409
417
|
if (event.altKey || event.ctrlKey || event.metaKey) return;
|
|
@@ -441,7 +449,7 @@ function chooseDay(s, value) {
|
|
|
441
449
|
return;
|
|
442
450
|
}
|
|
443
451
|
paintCalendar(s);
|
|
444
|
-
s.calendar.querySelector('[data-date="' + value + '"]')?.focus();
|
|
452
|
+
s.calendar.querySelector('[data-date="' + value + '"]')?.focus({ preventScroll: true });
|
|
445
453
|
paintDraft(s);
|
|
446
454
|
}
|
|
447
455
|
function paintDraft(s) {
|
|
@@ -594,6 +602,7 @@ function init(s) {
|
|
|
594
602
|
attr(s.dialog, "aria-modal", "false");
|
|
595
603
|
if (s.dialog.showPopover) attr(s.dialog, "popover", "manual");
|
|
596
604
|
s.root.append(s.dialog);
|
|
605
|
+
s.dialog.addEventListener("mousedown", focusPopupButton);
|
|
597
606
|
s.title = node("h2", "blu-advanced-sr");
|
|
598
607
|
s.title.id = s.uid + "-title";
|
|
599
608
|
s.dialog.append(s.title);
|
|
@@ -606,7 +615,7 @@ function init(s) {
|
|
|
606
615
|
const month = selectField(s, "Month", "month");
|
|
607
616
|
s.monthSelect = month.select;
|
|
608
617
|
const year = node("label", "blu-picker__select-field");
|
|
609
|
-
year.append(node("span", "", "Year"));
|
|
618
|
+
year.append(node("span", "blu-picker__select-caption", "Year"));
|
|
610
619
|
s.yearInput = node("input", "blu-picker__input");
|
|
611
620
|
s.yearInput.type = "number";
|
|
612
621
|
s.yearInput.step = "1";
|
|
@@ -804,8 +813,8 @@ function update(s) {
|
|
|
804
813
|
else {
|
|
805
814
|
paintCalendar(s);
|
|
806
815
|
paintTimes(s);
|
|
807
|
-
if (focusDate) s.calendar.querySelector('[data-date="' + focusDate + '"]')?.focus();
|
|
808
|
-
else if (focusId) document.getElementById(focusId)?.focus();
|
|
816
|
+
if (focusDate) s.calendar.querySelector('[data-date="' + focusDate + '"]')?.focus({ preventScroll: true });
|
|
817
|
+
else if (focusId) document.getElementById(focusId)?.focus({ preventScroll: true });
|
|
809
818
|
positionPopup(s);
|
|
810
819
|
}
|
|
811
820
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
//
|
|
1
|
+
// docs/scripts/advanced/shared.js
|
|
2
2
|
import { state, register } from "./kernel.js";
|
|
3
3
|
var shared = state("advanced", () => ({ instances: /* @__PURE__ */ new Map(), serial: 0 }));
|
|
4
4
|
var uid = () => `blu-advanced-${++shared.serial}`;
|
|
@@ -91,7 +91,7 @@ function createAPI(kind, init2, update2) {
|
|
|
91
91
|
}, destroy }));
|
|
92
92
|
}
|
|
93
93
|
|
|
94
|
-
//
|
|
94
|
+
// docs/scripts/advanced/date-utils.js
|
|
95
95
|
var pad = (n) => String(n).padStart(2, "0");
|
|
96
96
|
function dateValue(d) {
|
|
97
97
|
return String(d.getFullYear()).padStart(4, "0") + "-" + pad(d.getMonth() + 1) + "-" + pad(d.getDate());
|
|
@@ -172,7 +172,7 @@ function validateRange(start, end, options = {}) {
|
|
|
172
172
|
return "";
|
|
173
173
|
}
|
|
174
174
|
|
|
175
|
-
//
|
|
175
|
+
// docs/scripts/advanced/pickers.js
|
|
176
176
|
var formatOptions = ["YYYY-MM-DD", "MM/DD/YYYY", "DD/MM/YYYY"];
|
|
177
177
|
var today = () => dateValue(/* @__PURE__ */ new Date());
|
|
178
178
|
var datePart = (value) => String(value || "").split("T")[0];
|
|
@@ -256,14 +256,15 @@ function positionPopup(s) {
|
|
|
256
256
|
const below = Math.max(0, top + height - edge - anchor.bottom - gap), above = Math.max(0, anchor.top - gap - top - edge);
|
|
257
257
|
const natural = panel.scrollHeight + 2, up = natural > below && above > below, room = Math.max(0, height - edge * 2);
|
|
258
258
|
panel.style.maxHeight = room + "px";
|
|
259
|
-
|
|
259
|
+
const fields = s.fieldRow.getBoundingClientRect();
|
|
260
|
+
panel.style.left = Math.max(left + edge, Math.min(fields.left + (fields.width - panelWidth) / 2, left + width - edge - panelWidth)) + "px";
|
|
260
261
|
const preferred = up ? anchor.top - gap - natural : anchor.bottom + gap;
|
|
261
262
|
panel.style.top = Math.max(top + edge, Math.min(preferred, top + height - edge - Math.min(natural, room))) + "px";
|
|
262
263
|
panel.dataset.placement = up ? "top" : "bottom";
|
|
263
264
|
}
|
|
264
265
|
function focusPopup(s) {
|
|
265
266
|
const timeField = s.timeOnly || s.dateTime && s.anchor === s.fields[1].input;
|
|
266
|
-
(timeField ? s.hours : s.calendar.querySelector('[data-date="' + s.focusDate + '"]') || s.monthSelect).focus();
|
|
267
|
+
(timeField ? s.hours : s.calendar.querySelector('[data-date="' + s.focusDate + '"]') || s.monthSelect).focus({ preventScroll: true });
|
|
267
268
|
}
|
|
268
269
|
function commit(s, first, end, time, keepOpen = false) {
|
|
269
270
|
const issue = validate(s, first, end, time);
|
|
@@ -287,7 +288,7 @@ function commit(s, first, end, time, keepOpen = false) {
|
|
|
287
288
|
s.inputSignature = JSON.stringify(s.range ? [s.start, s.end] : [s.value]);
|
|
288
289
|
paintInputs(s);
|
|
289
290
|
if (!keepOpen) close(s);
|
|
290
|
-
if (returnToInput) s.fields[0].input.focus();
|
|
291
|
+
if (returnToInput) s.fields[0].input.focus({ preventScroll: true });
|
|
291
292
|
s.notice.textContent = s.range ? first ? labelDate(s, first) + " to " + labelDate(s, end) : "Date range cleared" : value ? "Selected " + (s.timeOnly ? formatTime(value, s.cycle) : labelDate(s, first) + (s.hasTime ? " at " + formatTime(time, s.cycle) : "")) : "Selection cleared";
|
|
292
293
|
if (changed) {
|
|
293
294
|
if (s.range) emit(s, "Change", first, end);
|
|
@@ -391,11 +392,18 @@ function appendOption(select, value, label) {
|
|
|
391
392
|
return option;
|
|
392
393
|
}
|
|
393
394
|
function selectField(s, label, name) {
|
|
394
|
-
const wrap = node("label", "blu-picker__select-field"), caption = node("span", "", label), select = node("select", "blu-picker__input");
|
|
395
|
+
const wrap = node("label", "blu-picker__select-field"), caption = node("span", "blu-picker__select-caption", label), select = node("select", "blu-picker__input");
|
|
395
396
|
select.id = s.uid + "-" + name;
|
|
396
397
|
wrap.append(caption, select);
|
|
397
398
|
return { wrap, select };
|
|
398
399
|
}
|
|
400
|
+
function focusPopupButton(event) {
|
|
401
|
+
if (event.button !== 0) return;
|
|
402
|
+
const target = event.target.closest?.("button");
|
|
403
|
+
if (!target || target.disabled) return;
|
|
404
|
+
event.preventDefault();
|
|
405
|
+
target.focus({ preventScroll: true });
|
|
406
|
+
}
|
|
399
407
|
function focusDay(s, value) {
|
|
400
408
|
const min = datePart(s.options.Min) || "0001-01-01", max = datePart(s.options.Max) || "9999-12-31";
|
|
401
409
|
value = value < min ? min : value > max ? max : value;
|
|
@@ -403,7 +411,7 @@ function focusDay(s, value) {
|
|
|
403
411
|
s.focusDate = value;
|
|
404
412
|
s.month = value.slice(0, 7);
|
|
405
413
|
paintCalendar(s);
|
|
406
|
-
s.calendar.querySelector('[data-date="' + value + '"]')?.focus();
|
|
414
|
+
s.calendar.querySelector('[data-date="' + value + '"]')?.focus({ preventScroll: true });
|
|
407
415
|
}
|
|
408
416
|
function keyDay(s, event, value) {
|
|
409
417
|
if (event.altKey || event.ctrlKey || event.metaKey) return;
|
|
@@ -441,7 +449,7 @@ function chooseDay(s, value) {
|
|
|
441
449
|
return;
|
|
442
450
|
}
|
|
443
451
|
paintCalendar(s);
|
|
444
|
-
s.calendar.querySelector('[data-date="' + value + '"]')?.focus();
|
|
452
|
+
s.calendar.querySelector('[data-date="' + value + '"]')?.focus({ preventScroll: true });
|
|
445
453
|
paintDraft(s);
|
|
446
454
|
}
|
|
447
455
|
function paintDraft(s) {
|
|
@@ -594,6 +602,7 @@ function init(s) {
|
|
|
594
602
|
attr(s.dialog, "aria-modal", "false");
|
|
595
603
|
if (s.dialog.showPopover) attr(s.dialog, "popover", "manual");
|
|
596
604
|
s.root.append(s.dialog);
|
|
605
|
+
s.dialog.addEventListener("mousedown", focusPopupButton);
|
|
597
606
|
s.title = node("h2", "blu-advanced-sr");
|
|
598
607
|
s.title.id = s.uid + "-title";
|
|
599
608
|
s.dialog.append(s.title);
|
|
@@ -606,7 +615,7 @@ function init(s) {
|
|
|
606
615
|
const month = selectField(s, "Month", "month");
|
|
607
616
|
s.monthSelect = month.select;
|
|
608
617
|
const year = node("label", "blu-picker__select-field");
|
|
609
|
-
year.append(node("span", "", "Year"));
|
|
618
|
+
year.append(node("span", "blu-picker__select-caption", "Year"));
|
|
610
619
|
s.yearInput = node("input", "blu-picker__input");
|
|
611
620
|
s.yearInput.type = "number";
|
|
612
621
|
s.yearInput.step = "1";
|
|
@@ -804,8 +813,8 @@ function update(s) {
|
|
|
804
813
|
else {
|
|
805
814
|
paintCalendar(s);
|
|
806
815
|
paintTimes(s);
|
|
807
|
-
if (focusDate) s.calendar.querySelector('[data-date="' + focusDate + '"]')?.focus();
|
|
808
|
-
else if (focusId) document.getElementById(focusId)?.focus();
|
|
816
|
+
if (focusDate) s.calendar.querySelector('[data-date="' + focusDate + '"]')?.focus({ preventScroll: true });
|
|
817
|
+
else if (focusId) document.getElementById(focusId)?.focus({ preventScroll: true });
|
|
809
818
|
positionPopup(s);
|
|
810
819
|
}
|
|
811
820
|
}
|