@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.
Files changed (160) hide show
  1. package/README.md +11 -5
  2. package/dist/build-audit.json +181 -53
  3. package/dist/checkbox.css +1 -0
  4. package/dist/checkbox.d.ts +2 -0
  5. package/dist/checkbox.js +1 -0
  6. package/dist/chunk-3PNBQVYS.js +1 -0
  7. package/dist/chunk-5ENZVZGF.js +0 -0
  8. package/dist/chunk-AL2ETLJV.js +1 -0
  9. package/dist/chunk-BN3PDE5G.js +0 -0
  10. package/dist/chunk-EOKNLCNS.js +1 -0
  11. package/dist/chunk-EPZGLHPD.js +0 -0
  12. package/dist/chunk-FNX33PDD.js +0 -0
  13. package/dist/chunk-GXHANADL.js +1 -0
  14. package/dist/chunk-HO6CYWED.js +0 -0
  15. package/dist/chunk-IJ62ILKE.js +0 -0
  16. package/dist/chunk-IO22GWON.js +0 -0
  17. package/dist/chunk-IZJN3EF7.js +1 -0
  18. package/dist/chunk-JVE6CXCJ.js +1 -0
  19. package/dist/chunk-MYVLBALF.js +1 -0
  20. package/dist/chunk-OPMD7QNV.js +1 -0
  21. package/dist/chunk-RWBLICYX.js +0 -0
  22. package/dist/chunk-RYD6RHRX.js +0 -0
  23. package/dist/chunk-UN2SLVFG.js +1 -0
  24. package/dist/chunk-VJVFHL6P.js +0 -0
  25. package/dist/chunk-XWXEH3SW.js +1 -0
  26. package/dist/chunk-YCLR3IE3.js +0 -0
  27. package/dist/columns2.css +1 -0
  28. package/dist/columns2.d.ts +2 -0
  29. package/dist/columns2.js +1 -0
  30. package/dist/columns3.css +1 -0
  31. package/dist/columns3.d.ts +2 -0
  32. package/dist/columns3.js +1 -0
  33. package/dist/columns4.css +1 -0
  34. package/dist/columns4.d.ts +2 -0
  35. package/dist/columns4.js +1 -0
  36. package/dist/content.js +1 -1
  37. package/dist/core.js +1 -1
  38. package/dist/data.css +63 -0
  39. package/dist/data.d.ts +1 -0
  40. package/dist/data.js +1 -1
  41. package/dist/date-picker.css +5 -5
  42. package/dist/date-picker.js +1 -1
  43. package/dist/date-range-picker.css +5 -5
  44. package/dist/date-range-picker.js +1 -1
  45. package/dist/date-time-picker.css +5 -5
  46. package/dist/date-time-picker.js +1 -1
  47. package/dist/draft-components.json +77 -0
  48. package/dist/feedback.js +1 -1
  49. package/dist/form-field.css +1 -0
  50. package/dist/form-field.d.ts +2 -0
  51. package/dist/form-field.js +1 -0
  52. package/dist/form-layout/choice.d.ts +15 -0
  53. package/dist/form-layout/layout.d.ts +12 -0
  54. package/dist/form-layout/select.d.ts +3 -0
  55. package/dist/form-layout/shared.d.ts +21 -0
  56. package/dist/form-layout/table.d.ts +20 -0
  57. package/dist/form-layout/text.d.ts +3 -0
  58. package/dist/form-layout/types.d.ts +151 -0
  59. package/dist/form-layout.css +62 -0
  60. package/dist/form-layout.d.ts +12 -0
  61. package/dist/form-layout.js +1 -0
  62. package/dist/index.d.ts +1 -0
  63. package/dist/index.js +1 -1
  64. package/dist/inputs.css +83 -20
  65. package/dist/inputs.d.ts +8 -0
  66. package/dist/inputs.js +1 -1
  67. package/dist/layout.css +63 -0
  68. package/dist/layout.d.ts +3 -0
  69. package/dist/layout.js +1 -1
  70. package/dist/native/date-picker.js +1 -1
  71. package/dist/native/date-range-picker.js +1 -1
  72. package/dist/native/date-time-picker.js +1 -1
  73. package/dist/native/form-layout.js +1 -0
  74. package/dist/native/time-picker.js +1 -1
  75. package/dist/native-components/form-layout.d.ts +1 -0
  76. package/dist/radio-button.css +1 -0
  77. package/dist/radio-button.d.ts +2 -0
  78. package/dist/radio-button.js +1 -0
  79. package/dist/radio-group.css +1 -0
  80. package/dist/radio-group.d.ts +2 -0
  81. package/dist/radio-group.js +1 -0
  82. package/dist/select.css +1 -0
  83. package/dist/select.d.ts +2 -0
  84. package/dist/select.js +1 -0
  85. package/dist/sizes.json +272 -90
  86. package/dist/styles.css +83 -20
  87. package/dist/switch.css +1 -0
  88. package/dist/switch.d.ts +2 -0
  89. package/dist/switch.js +1 -0
  90. package/dist/table.css +1 -0
  91. package/dist/table.d.ts +2 -0
  92. package/dist/table.js +1 -0
  93. package/dist/text-area.css +1 -0
  94. package/dist/text-area.d.ts +2 -0
  95. package/dist/text-area.js +1 -0
  96. package/dist/text-input.css +1 -0
  97. package/dist/text-input.d.ts +2 -0
  98. package/dist/text-input.js +1 -0
  99. package/dist/time-picker.css +5 -5
  100. package/dist/time-picker.js +1 -1
  101. package/docs/FORM-LAYOUT-ESSENTIALS.md +122 -0
  102. package/docs/PUBLISHING.md +129 -25
  103. package/draft-components.json +77 -0
  104. package/edition.json +44 -7
  105. package/npm-shrinkwrap.json +2 -2
  106. package/package.json +76 -10
  107. package/scripts/build.mjs +16 -8
  108. package/src/checkbox.ts +2 -0
  109. package/src/columns2.ts +2 -0
  110. package/src/columns3.ts +2 -0
  111. package/src/columns4.ts +2 -0
  112. package/src/data.ts +1 -0
  113. package/src/form-field.ts +2 -0
  114. package/src/form-layout/choice.ts +66 -0
  115. package/src/form-layout/layout.ts +40 -0
  116. package/src/form-layout/select.ts +97 -0
  117. package/src/form-layout/shared.ts +57 -0
  118. package/src/form-layout/table.ts +86 -0
  119. package/src/form-layout/text.ts +73 -0
  120. package/src/form-layout/types.ts +59 -0
  121. package/src/form-layout.ts +12 -0
  122. package/src/index.ts +2 -0
  123. package/src/inputs.ts +8 -0
  124. package/src/layout.ts +3 -0
  125. package/src/native-components/form-layout.ts +21 -0
  126. package/src/radio-button.ts +2 -0
  127. package/src/radio-group.ts +2 -0
  128. package/src/runtime/date-picker.js +21 -12
  129. package/src/runtime/date-range-picker.js +21 -12
  130. package/src/runtime/date-time-picker.js +21 -12
  131. package/src/runtime/time-picker.js +21 -12
  132. package/src/select.ts +2 -0
  133. package/src/switch.ts +2 -0
  134. package/src/table.ts +2 -0
  135. package/src/text-area.ts +2 -0
  136. package/src/text-input.ts +2 -0
  137. package/src/types.ts +0 -12
  138. package/styles/checkbox.css +1 -0
  139. package/styles/columns2.css +1 -0
  140. package/styles/columns3.css +1 -0
  141. package/styles/columns4.css +1 -0
  142. package/styles/date-picker.css +5 -5
  143. package/styles/date-range-picker.css +5 -5
  144. package/styles/date-time-picker.css +5 -5
  145. package/styles/form-field.css +1 -0
  146. package/styles/form-layout.css +62 -0
  147. package/styles/radio-button.css +1 -0
  148. package/styles/radio-group.css +1 -0
  149. package/styles/select.css +1 -0
  150. package/styles/switch.css +1 -0
  151. package/styles/table.css +1 -0
  152. package/styles/text-area.css +1 -0
  153. package/styles/text-input.css +1 -0
  154. package/styles/time-picker.css +5 -5
  155. package/test/form-layout.test.mjs +28 -0
  156. package/test/isolation.test.mjs +2 -2
  157. package/dist/chunk-2TCNAQC6.js +0 -1
  158. package/dist/chunk-CTLZIWMX.js +0 -1
  159. package/dist/chunk-MHHJZYXW.js +0 -1
  160. 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
@@ -54,3 +54,5 @@ export * from './font-provider.js';
54
54
  export * from './shape-mask.js';
55
55
  export * from './chip-group.js';
56
56
  export * from './grid-basic.js';
57
+
58
+ export * from './form-layout.js';
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
@@ -6,3 +6,6 @@ export * from './splitter.js';
6
6
  export * from './app-layout.js';
7
7
  export * from './page-layout.js';
8
8
  export * from './shape-mask.js';
9
+ export * from './columns2.js';
10
+ export * from './columns3.js';
11
+ export * from './columns4.js';
@@ -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
+ });
@@ -0,0 +1,2 @@
1
+ export {createRadioButton} from './form-layout/choice.js';
2
+ export type * from './form-layout/types.js';
@@ -0,0 +1,2 @@
1
+ export {createRadioGroup} from './form-layout/choice.js';
2
+ export type * from './form-layout/types.js';
@@ -1,4 +1,4 @@
1
- // ../blu-grid/docs/scripts/advanced/shared.js
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
- // ../blu-grid/docs/scripts/advanced/date-utils.js
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
- // ../blu-grid/docs/scripts/advanced/pickers.js
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
- panel.style.left = Math.max(left + edge, Math.min(anchor.left, left + width - edge - panelWidth)) + "px";
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
- // ../blu-grid/docs/scripts/advanced/shared.js
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
- // ../blu-grid/docs/scripts/advanced/date-utils.js
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
- // ../blu-grid/docs/scripts/advanced/pickers.js
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
- panel.style.left = Math.max(left + edge, Math.min(anchor.left, left + width - edge - panelWidth)) + "px";
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
  }