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.
- package/README.md +10 -0
- package/components/alert/index.scss +4 -4
- package/components/appbar/index.scss +3 -2
- package/components/badge/index.scss +2 -2
- package/components/bottomsheet/index.scss +6 -5
- package/components/button/index.scss +20 -20
- package/components/card/index.scss +10 -9
- package/components/checkbox/index.scss +11 -11
- package/components/datepicker/index.scss +435 -0
- package/components/datepicker/index.ts +600 -0
- package/components/dialog/README.md +6 -6
- package/components/dialog/index.scss +23 -17
- package/components/divider/index.scss +2 -0
- package/components/iconbutton/index.scss +18 -17
- package/components/list/index.scss +10 -10
- package/components/menu/index.scss +2 -1
- package/components/navigationrail/index.scss +10 -9
- package/components/radio/README.md +0 -1
- package/components/radio/index.scss +11 -11
- package/components/select/index.scss +2 -1
- package/components/sidesheet/index.scss +3 -1
- package/components/slider/index.scss +7 -7
- package/components/stepper/index.scss +5 -4
- package/components/switch/README.md +0 -1
- package/components/switch/index.scss +21 -21
- package/components/textfield/index.scss +6 -5
- package/components/textfield/index.ts +7 -6
- package/components/timepicker/index.scss +9 -8
- package/components/timepicker/index.ts +12 -12
- package/dist/alert.css +1 -1
- package/dist/appbar.css +1 -1
- package/dist/badge.css +1 -1
- package/dist/bottomsheet.css +1 -1
- package/dist/button.css +1 -1
- package/dist/card.css +1 -1
- package/dist/checkbox.css +1 -1
- package/dist/components/datepicker/index.d.ts +6 -0
- package/dist/datepicker.css +1 -0
- package/dist/datepicker.js +1 -0
- package/dist/dialog.css +1 -1
- package/dist/divider.css +1 -1
- package/dist/foundations.css +1 -0
- package/dist/foundations.js +1 -0
- package/dist/iconbutton.css +1 -1
- package/dist/layout.css +1 -1
- package/dist/list.css +1 -1
- package/dist/menu.css +1 -1
- package/dist/micl.css +1 -1
- package/dist/micl.js +1 -1
- package/dist/navigationrail.css +1 -1
- package/dist/radio.css +1 -1
- package/dist/scrollbar.css +1 -0
- package/dist/scrollbar.js +1 -0
- package/dist/select.css +1 -1
- package/dist/sidesheet.css +1 -1
- package/dist/slider.css +1 -1
- package/dist/stepper.css +1 -1
- package/dist/switch.css +1 -1
- package/dist/textfield.css +1 -1
- package/dist/timepicker.css +1 -1
- package/docs/accordion.html +3 -1
- package/docs/alert.html +3 -1
- package/docs/bottomsheet.html +3 -1
- package/docs/button.html +3 -1
- package/docs/card.html +3 -1
- package/docs/checkbox.html +3 -1
- package/docs/datepicker.html +151 -0
- package/docs/dialog.html +23 -9
- package/docs/divider.html +3 -1
- package/docs/docs.js +43 -0
- package/docs/iconbutton.html +1 -1
- package/docs/index.html +3 -1
- package/docs/list.html +3 -1
- package/docs/menu.html +3 -1
- package/docs/micl.css +1 -1
- package/docs/micl.js +1 -1
- package/docs/navigationrail.html +3 -1
- package/docs/radio.html +3 -1
- package/docs/select.html +3 -1
- package/docs/sidesheet.html +3 -1
- package/docs/slider.html +1 -1
- package/docs/stepper.html +3 -1
- package/docs/switch.html +3 -1
- package/docs/textfield.html +3 -1
- package/docs/timepicker.html +4 -2
- package/foundations/index.scss +102 -0
- package/foundations/layout/index.scss +0 -52
- package/foundations/scrollbar/index.scss +46 -0
- package/intl.d.ts +9 -0
- package/micl.ts +18 -8
- package/package.json +2 -1
- package/styles/README.md +17 -8
- package/styles/motion.scss +3 -0
- package/styles/shapes.scss +23 -18
- package/styles/statelayer.scss +4 -0
- package/styles/typography.scss +2 -2
- package/styles.scss +3 -26
- 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-
|
|
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.
|