material-inspired-component-library 5.0.0 → 5.0.1

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 (98) hide show
  1. package/README.md +10 -0
  2. package/components/alert/index.scss +4 -4
  3. package/components/appbar/index.scss +3 -2
  4. package/components/badge/index.scss +2 -2
  5. package/components/bottomsheet/index.scss +6 -5
  6. package/components/button/index.scss +20 -20
  7. package/components/card/index.scss +10 -9
  8. package/components/checkbox/index.scss +11 -11
  9. package/components/datepicker/index.scss +435 -0
  10. package/components/datepicker/index.ts +600 -0
  11. package/components/dialog/README.md +6 -6
  12. package/components/dialog/index.scss +23 -17
  13. package/components/divider/index.scss +2 -0
  14. package/components/iconbutton/index.scss +18 -17
  15. package/components/list/index.scss +10 -10
  16. package/components/menu/index.scss +2 -1
  17. package/components/navigationrail/index.scss +10 -9
  18. package/components/radio/README.md +0 -1
  19. package/components/radio/index.scss +11 -11
  20. package/components/select/index.scss +2 -1
  21. package/components/sidesheet/index.scss +3 -1
  22. package/components/slider/index.scss +7 -7
  23. package/components/stepper/index.scss +5 -4
  24. package/components/switch/README.md +0 -1
  25. package/components/switch/index.scss +21 -21
  26. package/components/textfield/index.scss +6 -5
  27. package/components/textfield/index.ts +7 -6
  28. package/components/timepicker/index.scss +9 -8
  29. package/components/timepicker/index.ts +12 -12
  30. package/dist/alert.css +1 -1
  31. package/dist/appbar.css +1 -1
  32. package/dist/badge.css +1 -1
  33. package/dist/bottomsheet.css +1 -1
  34. package/dist/button.css +1 -1
  35. package/dist/card.css +1 -1
  36. package/dist/checkbox.css +1 -1
  37. package/dist/components/datepicker/index.d.ts +6 -0
  38. package/dist/datepicker.css +1 -0
  39. package/dist/datepicker.js +1 -0
  40. package/dist/dialog.css +1 -1
  41. package/dist/divider.css +1 -1
  42. package/dist/foundations.css +1 -0
  43. package/dist/foundations.js +1 -0
  44. package/dist/iconbutton.css +1 -1
  45. package/dist/layout.css +1 -1
  46. package/dist/list.css +1 -1
  47. package/dist/menu.css +1 -1
  48. package/dist/micl.css +1 -1
  49. package/dist/micl.js +1 -1
  50. package/dist/navigationrail.css +1 -1
  51. package/dist/radio.css +1 -1
  52. package/dist/scrollbar.css +1 -0
  53. package/dist/scrollbar.js +1 -0
  54. package/dist/select.css +1 -1
  55. package/dist/sidesheet.css +1 -1
  56. package/dist/slider.css +1 -1
  57. package/dist/stepper.css +1 -1
  58. package/dist/switch.css +1 -1
  59. package/dist/textfield.css +1 -1
  60. package/dist/timepicker.css +1 -1
  61. package/docs/accordion.html +3 -1
  62. package/docs/alert.html +3 -1
  63. package/docs/bottomsheet.html +3 -1
  64. package/docs/button.html +3 -1
  65. package/docs/card.html +3 -1
  66. package/docs/checkbox.html +3 -1
  67. package/docs/datepicker.html +151 -0
  68. package/docs/dialog.html +23 -9
  69. package/docs/divider.html +3 -1
  70. package/docs/docs.js +43 -0
  71. package/docs/iconbutton.html +1 -1
  72. package/docs/index.html +3 -1
  73. package/docs/list.html +3 -1
  74. package/docs/menu.html +3 -1
  75. package/docs/micl.css +1 -1
  76. package/docs/micl.js +1 -1
  77. package/docs/navigationrail.html +3 -1
  78. package/docs/radio.html +3 -1
  79. package/docs/select.html +3 -1
  80. package/docs/sidesheet.html +3 -1
  81. package/docs/slider.html +1 -1
  82. package/docs/stepper.html +3 -1
  83. package/docs/switch.html +3 -1
  84. package/docs/textfield.html +3 -1
  85. package/docs/timepicker.html +4 -2
  86. package/foundations/index.scss +102 -0
  87. package/foundations/layout/index.scss +0 -52
  88. package/foundations/scrollbar/index.scss +46 -0
  89. package/intl.d.ts +9 -0
  90. package/micl.ts +18 -8
  91. package/package.json +2 -1
  92. package/styles/README.md +17 -8
  93. package/styles/motion.scss +3 -0
  94. package/styles/shapes.scss +23 -18
  95. package/styles/statelayer.scss +4 -0
  96. package/styles/typography.scss +2 -2
  97. package/styles.scss +3 -26
  98. package/tsconfig.json +2 -2
@@ -0,0 +1,600 @@
1
+ //
2
+ // Copyright © 2025 Hermana AS
3
+ //
4
+ // Permission is hereby granted, free of charge, to any person obtaining a copy
5
+ // of this software and associated documentation files (the "Software"), to deal
6
+ // in the Software without restriction, including without limitation the rights
7
+ // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8
+ // copies of the Software, and to permit persons to whom the Software is
9
+ // furnished to do so, subject to the following conditions:
10
+ //
11
+ // The above copyright notice and this permission notice shall be included in all
12
+ // copies or substantial portions of the Software.
13
+ //
14
+ // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15
+ // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16
+ // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17
+ // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18
+ // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19
+ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20
+ // SOFTWARE.
21
+
22
+ export const datepickerSelector = 'dialog.micl-dialog.micl-datepicker';
23
+
24
+ type ValueElement = HTMLInputElement | HTMLButtonElement;
25
+
26
+ interface DatePickerState {
27
+ invoker : ValueElement | null;
28
+ selected: Date;
29
+ viewDate: Date; // the month/year currently being viewed
30
+ min : Date | null;
31
+ max : Date | null;
32
+ }
33
+
34
+ const stateMap = new WeakMap<HTMLDialogElement, DatePickerState>();
35
+
36
+ const locale = new Intl.DateTimeFormat().resolvedOptions().locale;
37
+
38
+ const isValueElement = (element: Element | null): element is ValueElement =>
39
+ {
40
+ return element instanceof HTMLInputElement || element instanceof HTMLButtonElement;
41
+ };
42
+
43
+ const getFirstDayOfWeek = (): number =>
44
+ {
45
+ try {
46
+ const info = (new Intl.Locale(locale) as any).getWeekInfo?.();
47
+ if (info) {
48
+ return info.firstDay === 7 ? 0 : info.firstDay;
49
+ }
50
+ }
51
+ catch {}
52
+
53
+ return /US|CA|MX/i.test(locale) ? 0 : 1; // Sunday for USA, Mexico and Canada, Monday as default
54
+ };
55
+
56
+ const firstDayOfWeek = getFirstDayOfWeek();
57
+
58
+ const toLocalMidnight = (date: Date): Date =>
59
+ {
60
+ const d = new Date(date);
61
+ d.setHours(0, 0, 0, 0);
62
+ return d;
63
+ };
64
+
65
+ const isValidDate = (d: Date): boolean => !isNaN(d.getTime());
66
+
67
+ const setText = (parent: Element | null, text: string): void =>
68
+ {
69
+ if (!parent) {
70
+ return;
71
+ }
72
+ if (parent.firstElementChild) {
73
+ let node = parent.firstChild;
74
+ while (node) {
75
+ if (node.nodeType === Node.TEXT_NODE) {
76
+ node.nodeValue = text;
77
+ return;
78
+ }
79
+ node = node.nextSibling;
80
+ }
81
+ parent.appendChild(document.createTextNode(text));
82
+ }
83
+ else {
84
+ parent.textContent = text;
85
+ }
86
+ };
87
+
88
+ const getCalendarDays = (
89
+ year : number,
90
+ month: number
91
+ ): Array<{ date: Date, val: string, isCurrentMonth: boolean }> => {
92
+
93
+ const results = [];
94
+ const firstOfMonth = new Date(year, month, 1);
95
+ const dayOfWeek = firstOfMonth.getDay();
96
+ const offset = (dayOfWeek - firstDayOfWeek + 7) % 7;
97
+ const current = new Date(year, month, 1 - offset);
98
+ const pad = (n: number): string => n.toString().padStart(2, '0');
99
+
100
+ // 6 weeks * 7 days
101
+ for (let i = 0; i < 42; i++) {
102
+ results.push({
103
+ date: new Date(current),
104
+ val: `${current.getFullYear()}-${pad(current.getMonth() + 1)}-${pad(current.getDate())}`,
105
+ isCurrentMonth: current.getMonth() === month
106
+ });
107
+ current.setDate(current.getDate() + 1);
108
+ }
109
+ return results;
110
+ };
111
+
112
+ const populateContainerWithDays = (
113
+ container: HTMLElement,
114
+ days : Array<{ date: Date, val: string, isCurrentMonth: boolean }>,
115
+ state : DatePickerState,
116
+ isEmpty : boolean = false
117
+ ): void => {
118
+ if (isEmpty) {
119
+ const fragment = document.createDocumentFragment();
120
+ const tempDate = new Date();
121
+ const startOffset = tempDate.getDay() - firstDayOfWeek;
122
+ tempDate.setDate(tempDate.getDate() - startOffset);
123
+
124
+ for (let i = 0; i < 7; i++) {
125
+ const span = document.createElement('span');
126
+ span.style.gridArea = `1 / ${i + 1}`;
127
+ span.textContent = tempDate.toLocaleDateString(locale, { weekday: 'narrow' });
128
+ span.title = tempDate.toLocaleDateString(locale, { weekday: 'long' });
129
+ fragment.appendChild(span);
130
+ tempDate.setDate(tempDate.getDate() + 1);
131
+ }
132
+
133
+ days.forEach((_, index) => {
134
+ const time = document.createElement('time');
135
+ const row = Math.floor(index / 7) + 2;
136
+ const col = (index % 7) + 1;
137
+ time.style.gridArea = `${row} / ${col}`;
138
+ fragment.appendChild(time);
139
+ });
140
+
141
+ container.appendChild(fragment);
142
+ }
143
+
144
+ const today = toLocalMidnight(new Date());
145
+ container.querySelectorAll('time').forEach((el, index) =>
146
+ {
147
+ const day = days[index];
148
+ el.dateTime = day.val;
149
+ el.textContent = day.date.getDate().toString();
150
+
151
+ const isSelected = day.date.getTime() === state.selected.getTime();
152
+ const isToday = day.date.getTime() === today.getTime();
153
+
154
+ el.className = '';
155
+ if (!day.isCurrentMonth) el.classList.add('micl-datepicker__outside');
156
+ if (isSelected) el.classList.add('micl-datepicker__selected');
157
+ if (isToday) el.classList.add('micl-datepicker__today');
158
+ });
159
+ };
160
+
161
+ const renderCalendar = (
162
+ dialog: HTMLDialogElement,
163
+ state : DatePickerState,
164
+ amount: number = 0
165
+ ): void => {
166
+
167
+ const content = dialog.querySelector<HTMLElement>('.micl-dialog__content');
168
+ const calendars = content?.querySelector<HTMLElement>('.micl-datepicker__calendars');
169
+ if (!calendars) {
170
+ return;
171
+ }
172
+
173
+ const startClass = 'micl-startleft';
174
+ const endClass = 'micl-startright';
175
+ const moveLeftClass = 'micl-moveleft';
176
+ const moveRightClass = 'micl-moveright';
177
+
178
+ calendars.classList.remove(moveLeftClass, moveRightClass, startClass, endClass);
179
+ void calendars.offsetWidth;
180
+
181
+ if (amount !== 0) {
182
+ const oldCalendar = calendars.querySelector<HTMLElement>('.micl-datepicker__calendar');
183
+ if (!oldCalendar) {
184
+ return;
185
+ }
186
+
187
+ const newCalendar = document.createElement('div');
188
+ const newCalendarInner = document.createElement('div');
189
+ newCalendar.classList.add('micl-datepicker__calendar');
190
+ newCalendarInner.classList.add('micl-datepicker__calendar-inner');
191
+
192
+ const days = getCalendarDays(state.viewDate.getFullYear(), state.viewDate.getMonth());
193
+ populateContainerWithDays(newCalendarInner, days, state, true);
194
+
195
+ const isNextMonth = amount > 0;
196
+ const startPositionClass = isNextMonth ? startClass : endClass;
197
+ const endTransformClass = isNextMonth ? moveLeftClass : moveRightClass;
198
+
199
+ newCalendar.appendChild(newCalendarInner);
200
+ if (isNextMonth) {
201
+ calendars.appendChild(newCalendar);
202
+ }
203
+ else {
204
+ calendars.prepend(newCalendar);
205
+ }
206
+
207
+ calendars.classList.add('micl-no-transition', startPositionClass);
208
+ void calendars.offsetWidth;
209
+
210
+ requestAnimationFrame(() => {
211
+ calendars.classList.remove('micl-no-transition', startPositionClass);
212
+ calendars.classList.add(endTransformClass);
213
+ });
214
+
215
+ const onTransitionEnd = () =>
216
+ {
217
+ calendars.removeEventListener('transitionend', onTransitionEnd);
218
+
219
+ setTimeout(() =>
220
+ {
221
+ calendars.classList.remove(endTransformClass);
222
+ if (oldCalendar.parentElement === calendars) {
223
+ oldCalendar.remove();
224
+ }
225
+ calendars.classList.add('micl-no-transition', startClass);
226
+ void calendars.offsetWidth;
227
+
228
+ calendars.classList.remove('micl-no-transition', startClass);
229
+ }, 0);
230
+ };
231
+ calendars.addEventListener('transitionend', onTransitionEnd);
232
+ }
233
+ else {
234
+ let calendar = calendars.querySelector<HTMLElement>('.micl-datepicker__calendar');
235
+ if (!calendar) {
236
+ calendar = document.createElement('div');
237
+ calendar.classList.add('micl-datepicker__calendar');
238
+ calendar = calendars.appendChild(calendar);
239
+ }
240
+ let inner = calendar.querySelector<HTMLElement>('.micl-datepicker__calendar-inner');
241
+ if (!inner) {
242
+ inner = document.createElement('div');
243
+ inner.classList.add('micl-datepicker__calendar-inner');
244
+ calendar.appendChild(inner);
245
+ }
246
+ const days = getCalendarDays(state.viewDate.getFullYear(), state.viewDate.getMonth());
247
+ populateContainerWithDays(inner, days, state, inner.querySelectorAll('time').length === 0);
248
+ }
249
+
250
+ const input = content?.querySelector<HTMLInputElement>('.micl-datepicker__input input');
251
+ if (input) {
252
+ const pad = (n: number): string => n.toString().padStart(2, '0');
253
+ input.value = `${state.selected.getFullYear()}-${pad(state.selected.getMonth() + 1)}-${pad(state.selected.getDate())}`;
254
+ }
255
+
256
+ setText(
257
+ dialog.querySelector('h1, h2, h3, h4, h5, h6, .micl-heading'),
258
+ state.selected.toLocaleDateString(locale, { weekday: 'short', day: 'numeric', month: 'short' })
259
+ );
260
+ setText(
261
+ dialog.querySelector('.micl-datepicker__month'),
262
+ state.viewDate.toLocaleDateString(locale, { month: 'short' })
263
+ );
264
+ setText(
265
+ dialog.querySelector('.micl-datepicker__year'),
266
+ state.viewDate.toLocaleDateString(locale, dialog.classList.contains('micl-dialog--docked') ?
267
+ { year: 'numeric' } : { month: 'long', year: 'numeric' })
268
+ );
269
+
270
+ ['months', 'years'].forEach(period => {
271
+ const value = period === 'months' ? state.viewDate.getMonth() : state.viewDate.getFullYear();
272
+ const input = dialog.querySelector<HTMLInputElement>(`.micl-datepicker__${period} input[value="${value}"]`);
273
+ if (input) {
274
+ input.checked = true;
275
+ }
276
+ });
277
+ };
278
+
279
+ const initPeriodPickers = (dialog: HTMLDialogElement, minYear: number, maxYear: number): void =>
280
+ {
281
+ ['months', 'years'].forEach(period => {
282
+ const container = dialog.querySelector(`.micl-datepicker__${period}`);
283
+ if (container) {
284
+ container.innerHTML = '';
285
+ const frag = document.createDocumentFragment();
286
+
287
+ if (period === 'months') {
288
+ const fmt = new Intl.DateTimeFormat(undefined, { month: 'long' });
289
+
290
+ for (let m = 0; m < 12; m++) {
291
+ const label = document.createElement('label');
292
+ label.innerHTML = `<span class="material-symbols-outlined">check</span><input type="radio" name="miclmonth" value="${m}"> ${fmt.format(new Date(2000, m, 1))}`;
293
+ frag.appendChild(label);
294
+ }
295
+ }
296
+ else {
297
+ for (let y = minYear; y <= maxYear; y++) {
298
+ const label = document.createElement('label');
299
+ label.innerHTML = `<input type="radio" name="miclyear" value="${y}"> ${y}`;
300
+ frag.appendChild(label);
301
+ }
302
+ }
303
+
304
+ const inner = document.createElement('div');
305
+ inner.classList.add(`micl-datepicker__${period}-inner`);
306
+ container.appendChild(inner).appendChild(frag);
307
+ }
308
+ });
309
+ };
310
+
311
+ const toggleView = (dialog: HTMLDialogElement, view: 'calendars' | 'months' | 'years' | 'input'): void =>
312
+ {
313
+ if (view === 'months' || view === 'years') {
314
+ if (!dialog.querySelector(`.micl-datepicker__${view}.micl-datepicker__view-hidden`)) {
315
+ view = 'calendars';
316
+ }
317
+ }
318
+
319
+ ['calendars', 'input', 'month-selector', 'year-selector'].forEach(name =>
320
+ {
321
+ let doHide = view === 'input';
322
+ if (name === 'calendars' || name === 'input') {
323
+ doHide = view !== name;
324
+ }
325
+ dialog.querySelector(`.micl-datepicker__${name}`)?.classList.toggle(
326
+ 'micl-datepicker__view-hidden',
327
+ doHide
328
+ );
329
+ });
330
+
331
+ const content = dialog.querySelector<HTMLElement>('.micl-dialog__content');
332
+ if (!content) {
333
+ return;
334
+ }
335
+ const contentHeight = parseInt(window.getComputedStyle(content).getPropertyValue('max-block-size'), 10);
336
+
337
+ ['.micl-datepicker__months', '.micl-datepicker__years'].forEach(selector =>
338
+ {
339
+ const period = content.querySelector<HTMLElement>(selector);
340
+ if (!period) {
341
+ return;
342
+ }
343
+ const selected = period.querySelector<HTMLInputElement>('input:checked');
344
+ const height = 48;
345
+ let doHide: boolean | null = false;
346
+
347
+ if (selected && (selector.substring(18) === view)) {
348
+ const property = window.getComputedStyle(period).getPropertyValue('transition-duration');
349
+ const duration = parseFloat(property) * (property.includes('ms') ? 1 : 1000);
350
+ const maxScrollDistance = period.scrollHeight - contentHeight;
351
+ const centerTop = (contentHeight - height) / 2;
352
+
353
+ if (selected.offsetTop > centerTop) {
354
+ let scrollDistance = selected.offsetTop - centerTop - (height / 2);
355
+ if (scrollDistance > maxScrollDistance) {
356
+ scrollDistance = maxScrollDistance;
357
+ }
358
+
359
+ const startTime = performance.now();
360
+ const animateScroll = (currentTime: number) => {
361
+ const progress = Math.min((currentTime - startTime) / duration, 1);
362
+ content.scrollTop = scrollDistance * progress;
363
+ if (progress < 1) {
364
+ requestAnimationFrame(animateScroll);
365
+ }
366
+ };
367
+ period.classList.remove('micl-datepicker__view-hidden');
368
+ requestAnimationFrame(animateScroll);
369
+ doHide = null;
370
+
371
+ period.addEventListener('transitionend', function handler(event)
372
+ {
373
+ if (event.propertyName === 'height' || event.propertyName === 'block-size') {
374
+ content.scrollTop = scrollDistance;
375
+ period.removeEventListener('transitionend', handler);
376
+ }
377
+ });
378
+ }
379
+ }
380
+ else {
381
+ doHide = true;
382
+ }
383
+ if (doHide !== null) {
384
+ period.classList.toggle('micl-datepicker__view-hidden', doHide);
385
+ }
386
+ });
387
+ };
388
+
389
+ const changePeriod = (dialog: HTMLDialogElement, amount: number, unit: 'month' | 'year'): void =>
390
+ {
391
+ const state = stateMap.get(dialog);
392
+ if (!state) {
393
+ return;
394
+ }
395
+
396
+ const newDate = new Date(state.viewDate);
397
+ if (unit === 'month') {
398
+ newDate.setMonth(newDate.getMonth() + amount);
399
+ }
400
+ else {
401
+ newDate.setFullYear(newDate.getFullYear() + amount);
402
+ }
403
+
404
+ if (state.min && newDate < state.min) return;
405
+ if (state.max && newDate > state.max) return;
406
+
407
+ state.viewDate = newDate;
408
+ renderCalendar(dialog, state, unit === 'month' ? amount : 0);
409
+ };
410
+
411
+ const selectDate = (dialog: HTMLDialogElement, dateStr: string): void =>
412
+ {
413
+ const state = stateMap.get(dialog);
414
+ if (!state) {
415
+ return;
416
+ }
417
+
418
+ const parts = dateStr.split('-').map(Number);
419
+ state.selected = new Date(parts[0], parts[1] - 1, parts[2]);
420
+ state.viewDate = new Date(state.selected);
421
+
422
+ renderCalendar(dialog, state);
423
+ };
424
+
425
+ export default (() =>
426
+ {
427
+ return {
428
+ keydown: (event: Event): void =>
429
+ {
430
+ if (
431
+ !(event instanceof KeyboardEvent)
432
+ || !(event.target instanceof Element)
433
+ ) {
434
+ return;
435
+ }
436
+ const dialog = event.target.closest(datepickerSelector) as HTMLDialogElement;
437
+ if (!dialog) {
438
+ return;
439
+ }
440
+
441
+ switch (event.key) {
442
+ case 'Enter':
443
+ case ' ':
444
+ if (event.target instanceof HTMLInputElement && event.target.type === 'date') {
445
+ event.preventDefault();
446
+ }
447
+ break;
448
+ case 'M':
449
+ case 'Y':
450
+ toggleView(dialog, event.key === 'M' ? 'months' : 'years');
451
+ break;
452
+ case 'PageUp':
453
+ case 'PageDown':
454
+ changePeriod(dialog, event.key === 'PageUp' ? 1 : -1, event.shiftKey ? 'year' : 'month');
455
+ break;
456
+ default:
457
+ }
458
+ },
459
+
460
+ initialize: (dialog: HTMLDialogElement): void =>
461
+ {
462
+ if (dialog.dataset.miclinitialized) {
463
+ return;
464
+ }
465
+
466
+ const form = dialog.querySelector('form');
467
+ const content = dialog.querySelector('.micl-dialog__content');
468
+ if (!form || !content) {
469
+ return;
470
+ }
471
+ dialog.dataset.miclinitialized = '1';
472
+
473
+ dialog.addEventListener('click', event =>
474
+ {
475
+ const target = event.target as HTMLElement;
476
+ const btn = target.closest('button');
477
+
478
+ if (btn) {
479
+ const forMonth = btn.parentElement?.classList.contains('micl-datepicker__month-selector');
480
+ const isNext = btn.classList.contains('micl-datepicker__next');
481
+ const isPrev = btn.classList.contains('micl-datepicker__previous');
482
+
483
+ if (isNext || isPrev) {
484
+ changePeriod(dialog, isNext ? 1 : -1, forMonth ? 'month' : 'year');
485
+ return;
486
+ }
487
+ }
488
+
489
+ if (target.closest('.micl-datepicker__month')) toggleView(dialog, 'months');
490
+ if (target.closest('.micl-datepicker__year')) toggleView(dialog, 'years');
491
+
492
+ const mode = target.closest('.micl-datepicker__inputmode') as HTMLElement;
493
+ if (mode) {
494
+ const icon = mode.textContent;
495
+ mode.textContent = mode.dataset.alticon || icon;
496
+ mode.dataset.alticon = icon;
497
+ const inputHidden = !!dialog.querySelector('.micl-datepicker__input.micl-datepicker__view-hidden');
498
+ toggleView(dialog, inputHidden ? 'input' : 'calendars');
499
+ }
500
+
501
+ const time = target.closest('time');
502
+ if (time && time.dateTime) {
503
+ selectDate(dialog, time.dateTime);
504
+ }
505
+
506
+ if (
507
+ target instanceof HTMLInputElement
508
+ && (target.name === 'miclmonth' || target.name === 'miclyear')
509
+ ) {
510
+ const state = stateMap.get(dialog);
511
+ if (state) {
512
+ const value = parseInt(target.value, 10);
513
+ if (target.name === 'miclmonth') {
514
+ state.viewDate.setMonth(value);
515
+ }
516
+ else {
517
+ state.viewDate.setFullYear(value);
518
+ }
519
+ renderCalendar(dialog, state);
520
+ toggleView(dialog, 'calendars');
521
+ }
522
+ }
523
+ });
524
+
525
+ dialog.addEventListener('beforetoggle', (event: any): void =>
526
+ {
527
+ if (event.newState !== 'open') {
528
+ return;
529
+ }
530
+
531
+ let invoker = document.activeElement;
532
+ if (
533
+ !isValueElement(invoker)
534
+ || (!invoker.dataset.datepicker && !invoker.getAttribute('popovertarget'))
535
+ ) {
536
+ invoker = document.querySelector(
537
+ `[data-datepicker="${dialog.id}"],[popovertarget="${dialog.id}"]`
538
+ );
539
+ }
540
+ if (!isValueElement(invoker)) {
541
+ return;
542
+ }
543
+
544
+ let initialDate = new Date();
545
+ let min: Date | null = null;
546
+ let max: Date | null = null;
547
+
548
+ if (invoker instanceof HTMLInputElement) {
549
+ if (invoker.type === 'date' && invoker.valueAsDate) {
550
+ initialDate = invoker.valueAsDate;
551
+ }
552
+ else if (invoker.value) {
553
+ initialDate = new Date(invoker.value);
554
+ }
555
+ if (invoker.min) min = new Date(invoker.min);
556
+ if (invoker.max) max = new Date(invoker.max);
557
+ }
558
+ else {
559
+ const parsed = new Date(invoker.value || invoker.textContent);
560
+ if (isValidDate(parsed)) {
561
+ initialDate = parsed;
562
+ }
563
+ }
564
+ if (!isValidDate(initialDate)) initialDate = new Date();
565
+ initialDate = toLocalMidnight(initialDate);
566
+
567
+ stateMap.set(dialog, {
568
+ invoker: invoker as ValueElement,
569
+ selected: initialDate,
570
+ viewDate: new Date(initialDate),
571
+ min,
572
+ max
573
+ });
574
+
575
+ initPeriodPickers(dialog, min ? min.getFullYear() : 1900, max ? max.getFullYear() : 2099);
576
+ toggleView(dialog, 'calendars');
577
+ renderCalendar(dialog, stateMap.get(dialog)!);
578
+ });
579
+
580
+ dialog.addEventListener('close', (): void =>
581
+ {
582
+ const state = stateMap.get(dialog);
583
+ if (!state || !state.invoker || dialog.returnValue === '') {
584
+ return;
585
+ }
586
+ const pad = (n: number): string => n.toString().padStart(2, '0');
587
+ const date = `${state.selected.getFullYear()}-${pad(state.selected.getMonth() + 1)}-${pad(state.selected.getDate())}`;
588
+ state.invoker.value = date;
589
+
590
+ if (state.invoker instanceof HTMLInputElement) {
591
+ state.invoker.dispatchEvent(new Event('change', { bubbles: true }));
592
+ state.invoker.dispatchEvent(new Event('input', { bubbles: true }));
593
+ }
594
+ else {
595
+ state.invoker.textContent = state.selected.toLocaleDateString();
596
+ }
597
+ });
598
+ }
599
+ };
600
+ })();
@@ -64,10 +64,10 @@ Removing the `popover` attribute creates a more intrusive **modal** dialog. This
64
64
 
65
65
  - The `closedby="closerequest"` attribute restricts closing methods, typically requiring an explicit action within the dialog.
66
66
 
67
- By default, modal dialogs open in the center of the screen. You can anchor a modal dialog to a control element, causing it to open relative to that element:
67
+ By default, modal dialogs open in the center of the screen. You can anchor a modal dialog to a control element using the `micl-dialog--docked` class and CSS Anchor settings, causing it to open relative to that element:
68
68
 
69
69
  ```HTML
70
- <dialog id="mydialog" class="micl-dialog" style="position-anchor:--myanchor">
70
+ <dialog id="mydialog" class="micl-dialog micl-dialog--docked" style="position-anchor:--myanchor">
71
71
  </dialog>
72
72
 
73
73
  <button type="button" popovertarget="mydialog" style="anchor-name:--myanchor">Open Modal Dialog</button>
@@ -96,10 +96,10 @@ A full-screen dialog covers the entire viewport, primarily on smaller screens. O
96
96
  ```HTML
97
97
  <dialog id="mydialog" class="micl-dialog micl-dialog--fullscreen" closedby="none" popover aria-labelledby="mytitle" aria-describedby="mydesc">
98
98
  <form method="dialog" class="micl-dialog__headline">
99
- <button type="button" class="micl-iconbutton-s material-symbols-outlined" popovertarget="mydialog" aria-label="Close">close</button>
99
+ <button type="button" class="micl-dialog__fullscreen micl-iconbutton-s material-symbols-outlined" popovertarget="mydialog" aria-label="Close">close</button>
100
100
  <span class="micl-dialog__icon material-symbols-outlined" aria-hidden="true">person</span>
101
101
  <h2 id="mytitle">Full-screen dialog</h2>
102
- <button class="micl-button-text-s" value="dosave">Save</button>
102
+ <button class="micl-dialog__fullscreen micl-button-text-s" value="dosave">Save</button>
103
103
  </form>
104
104
  <div class="micl-dialog__content">
105
105
  <span id="mydesc" class="micl-dialog__supporting-text">This dialog covers the whole screen.</span>
@@ -113,9 +113,9 @@ A full-screen dialog covers the entire viewport, primarily on smaller screens. O
113
113
  <button type="button" popovertarget="mydialog">Open Full-Screen Dialog</button>
114
114
  ```
115
115
 
116
- - In full-screen mode, buttons placed directly within the `micl-dialog__headline` become visible, while the `micl-dialog__icon` and `micl-dialog__actions` at the bottom are hidden.
116
+ - In full-screen mode, `micl-dialog__fullscreen` buttons placed directly within the `micl-dialog__headline` become visible, while the `micl-dialog__icon` and `micl-dialog__actions` at the bottom are hidden.
117
117
 
118
- - When not in full-screen mode (e.g., on wider screens), the `micl-dialog__headline` buttons are hidden, and the standard dialog actions (`micl-dialog__actions`) are visible.
118
+ - When not in full-screen mode (e.g., on wider screens), the `micl-dialog__fullscreen` buttons are hidden, and the standard dialog actions (`micl-dialog__actions`) are visible.
119
119
 
120
120
  ## Customizations
121
121
  You can customize the appearance of the Dialog component by overriding its global CSS variables. These variables are declared on the `:root` pseudo-class and can be changed on any appropriate parent element to affect its child dialogs.