@uni-design-system/uni-angular 8.0.0 → 8.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.
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as i0 from '@angular/core';
|
|
2
2
|
import { signal, computed, linkedSignal, resource, Injectable, inject, DestroyRef, InjectionToken, input, ChangeDetectionStrategy, Component, contentChildren, output, Renderer2, ElementRef, Directive, model, effect, viewChild, afterNextRender, ViewChild, afterRenderEffect, booleanAttribute, viewChildren } from '@angular/core';
|
|
3
3
|
import { injectGlobal, css, keyframes } from '@emotion/css';
|
|
4
|
-
import { UniThemes, LightTheme, toTypefaces, parseTheme, formatThemeIssues, hydrateTheme, createThemeFromPalette, Z_INDEX, fadeIn, fadeOut, EXPAND_DEFAULT_SPEED, expandDuration, expandFadeIn, collapseFadeOut,
|
|
4
|
+
import { UniThemes, LightTheme, toTypefaces, parseTheme, formatThemeIssues, hydrateTheme, createThemeFromPalette, Z_INDEX, fadeIn, removeInputPlatformStyling, fadeOut, EXPAND_DEFAULT_SPEED, expandDuration, expandFadeIn, collapseFadeOut, HOVER_OR_KEYBOARD_FOCUS, ShapeRadii, generatePalette, emitThemeFile, emitDtcgTokens } from '@uni-design-system/uni-core';
|
|
5
5
|
import { NgClass, NgTemplateOutlet, CommonModule } from '@angular/common';
|
|
6
6
|
|
|
7
7
|
let nextUniqueId = 0;
|
|
@@ -50,6 +50,294 @@ function motionSafe(styles) {
|
|
|
50
50
|
return { '@media (prefers-reduced-motion: no-preference)': styles };
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
+
/**
|
|
54
|
+
* Canonical date/time value shapes shared by `uni-calendar`,
|
|
55
|
+
* `uni-date-input`, `uni-time-input` and `uni-date-time-input`.
|
|
56
|
+
*
|
|
57
|
+
* Plain ISO strings, never `Date`s: a calendar date is a label on a wall
|
|
58
|
+
* calendar, not an instant. `new Date('2026-08-20')` is midnight UTC — which
|
|
59
|
+
* is 19 Aug in Honolulu — the classic off-by-one every `Date`-valued picker
|
|
60
|
+
* ships. Strings are timezone-free, JSON-serializable and sortable with `<`.
|
|
61
|
+
* Zoned scheduling is the app's concern; these components never touch
|
|
62
|
+
* `Date.getTimezoneOffset()`.
|
|
63
|
+
*/
|
|
64
|
+
|
|
65
|
+
function memoize(fn) {
|
|
66
|
+
const cache = new Map();
|
|
67
|
+
return ((...args) => {
|
|
68
|
+
const key = JSON.stringify(args);
|
|
69
|
+
if (cache.has(key)) {
|
|
70
|
+
return cache.get(key);
|
|
71
|
+
}
|
|
72
|
+
const result = fn(...args);
|
|
73
|
+
cache.set(key, result);
|
|
74
|
+
return result;
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Pure date/time helpers: ISO string math, `Intl`-driven formatting and
|
|
80
|
+
* parsing. No date library — `Date` appears only as UTC arithmetic inside
|
|
81
|
+
* these functions, so no result ever depends on the machine's timezone.
|
|
82
|
+
*/
|
|
83
|
+
const pad = (n) => String(n).padStart(2, '0');
|
|
84
|
+
const toUTC = (date) => {
|
|
85
|
+
const [y, m, d] = date.split('-').map(Number);
|
|
86
|
+
return Date.UTC(y, m - 1, d);
|
|
87
|
+
};
|
|
88
|
+
const fromUTC = (timestamp) => {
|
|
89
|
+
const d = new Date(timestamp);
|
|
90
|
+
return isoDate(d.getUTCFullYear(), d.getUTCMonth() + 1, d.getUTCDate());
|
|
91
|
+
};
|
|
92
|
+
/** `(2026, 8, 5)` → `'2026-08-05'`. Does not validate — see `isValidDate`. */
|
|
93
|
+
const isoDate = (year, month, day) => `${year}-${pad(month)}-${pad(day)}`;
|
|
94
|
+
/** The ISO date when the parts form a real calendar day, else `null`. */
|
|
95
|
+
const isValidDate = (year, month, day) => month >= 1 && month <= 12 && day >= 1 && day <= daysInMonth(year, month)
|
|
96
|
+
? isoDate(year, month, day)
|
|
97
|
+
: null;
|
|
98
|
+
/** Days in a month, leap-year aware. */
|
|
99
|
+
const daysInMonth = (year, month) => new Date(Date.UTC(year, month, 0)).getUTCDate();
|
|
100
|
+
const addDays = (date, days) => fromUTC(toUTC(date) + days * 86_400_000);
|
|
101
|
+
/** Same day ± n months, clamping the day to the target month's length. */
|
|
102
|
+
const addMonths = (date, months) => {
|
|
103
|
+
let [y, m] = date.split('-').map(Number);
|
|
104
|
+
const d = Number(date.slice(8, 10));
|
|
105
|
+
const total = y * 12 + (m - 1) + months;
|
|
106
|
+
y = Math.floor(total / 12);
|
|
107
|
+
m = ((total % 12) + 12) % 12 + 1;
|
|
108
|
+
return isoDate(y, m, Math.min(d, daysInMonth(y, m)));
|
|
109
|
+
};
|
|
110
|
+
/** Day of week, 0 = Sunday … 6 = Saturday. */
|
|
111
|
+
const dayOfWeek = (date) => new Date(toUTC(date)).getUTCDay();
|
|
112
|
+
/** `'2026-08-20'` → `'2026-08'`. */
|
|
113
|
+
const monthOf = (date) => date.slice(0, 7);
|
|
114
|
+
/** Today as a local-wall-clock ISO date (the one place local time matters). */
|
|
115
|
+
const todayIso = () => {
|
|
116
|
+
const d = new Date();
|
|
117
|
+
return isoDate(d.getFullYear(), d.getMonth() + 1, d.getDate());
|
|
118
|
+
};
|
|
119
|
+
/** Inclusive day count of a range: `('-20', '-24')` → 5. */
|
|
120
|
+
const inclusiveDayCount = (start, end) => Math.round((toUTC(end) - toUTC(start)) / 86_400_000) + 1;
|
|
121
|
+
/**
|
|
122
|
+
* The week matrix a calendar renders: full weeks covering `'YYYY-MM'`,
|
|
123
|
+
* starting on `weekStart` (0 = Sunday). Outside cells are always present so
|
|
124
|
+
* showing or hiding them is a pure render decision.
|
|
125
|
+
*/
|
|
126
|
+
const buildMonthGrid = (month, weekStart) => {
|
|
127
|
+
const [y, m] = month.split('-').map(Number);
|
|
128
|
+
const first = isoDate(y, m, 1);
|
|
129
|
+
const offset = (dayOfWeek(first) - weekStart + 7) % 7;
|
|
130
|
+
const total = daysInMonth(y, m);
|
|
131
|
+
const weeks = [];
|
|
132
|
+
let cursor = addDays(first, -offset);
|
|
133
|
+
for (let w = 0; w < Math.ceil((offset + total) / 7); w++) {
|
|
134
|
+
const week = [];
|
|
135
|
+
for (let c = 0; c < 7; c++) {
|
|
136
|
+
week.push({ date: cursor, outside: monthOf(cursor) !== month });
|
|
137
|
+
cursor = addDays(cursor, 1);
|
|
138
|
+
}
|
|
139
|
+
weeks.push(week);
|
|
140
|
+
}
|
|
141
|
+
return weeks;
|
|
142
|
+
};
|
|
143
|
+
// ---- Intl formatting (always UTC — the ISO string is re-hydrated as UTC
|
|
144
|
+
// midnight, so the local zone can never shift the label) ---------------------
|
|
145
|
+
const formatDate = (date, locale, options = { dateStyle: 'medium' }) => new Intl.DateTimeFormat(locale, { ...options, timeZone: 'UTC' }).format(new Date(toUTC(date)));
|
|
146
|
+
/** `'2026-08'` → `'August 2026'` in the locale. */
|
|
147
|
+
const formatMonthHeading = (month, locale) => formatDate(`${month}-01`, locale, { month: 'long', year: 'numeric' });
|
|
148
|
+
/** Weekday header labels starting at `weekStart` (0 = Sunday). */
|
|
149
|
+
const weekdayNames = memoize((locale, weekStart, format) => {
|
|
150
|
+
const labels = [];
|
|
151
|
+
for (let i = 0; i < 7; i++) {
|
|
152
|
+
// 2023-01-01 is a Sunday.
|
|
153
|
+
const day = new Date(Date.UTC(2023, 0, 1 + ((weekStart + i) % 7)));
|
|
154
|
+
labels.push({
|
|
155
|
+
label: new Intl.DateTimeFormat(locale, { weekday: format, timeZone: 'UTC' }).format(day),
|
|
156
|
+
full: new Intl.DateTimeFormat(locale, { weekday: 'long', timeZone: 'UTC' }).format(day),
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
return labels;
|
|
160
|
+
});
|
|
161
|
+
/** `'15:00'` → `'3:00 PM'` (or `'15:00'` when `hour12` is false). */
|
|
162
|
+
const formatTime = (time, locale, hour12) => {
|
|
163
|
+
const [h, m] = time.split(':').map(Number);
|
|
164
|
+
return new Intl.DateTimeFormat(locale, {
|
|
165
|
+
hour: 'numeric',
|
|
166
|
+
minute: '2-digit',
|
|
167
|
+
hour12,
|
|
168
|
+
timeZone: 'UTC',
|
|
169
|
+
}).format(new Date(Date.UTC(2000, 0, 1, h, m)));
|
|
170
|
+
};
|
|
171
|
+
/**
|
|
172
|
+
* The locale's first day of week, 0 = Sunday … 6 = Saturday. Falls back to
|
|
173
|
+
* Sunday where `Intl.Locale` week info is unavailable.
|
|
174
|
+
*/
|
|
175
|
+
const localeWeekStart = memoize((locale) => {
|
|
176
|
+
try {
|
|
177
|
+
const intlLocale = new Intl.Locale(locale);
|
|
178
|
+
const info = intlLocale.getWeekInfo?.() ?? intlLocale.weekInfo;
|
|
179
|
+
// Intl counts 1 = Monday … 7 = Sunday.
|
|
180
|
+
if (info?.firstDay)
|
|
181
|
+
return info.firstDay % 7;
|
|
182
|
+
}
|
|
183
|
+
catch {
|
|
184
|
+
// Unknown locale tag — fall through to the default.
|
|
185
|
+
}
|
|
186
|
+
return 0;
|
|
187
|
+
});
|
|
188
|
+
/** Whether the locale's default clock is 12-hour. */
|
|
189
|
+
const localeDefaultHour12 = memoize((locale) => new Intl.DateTimeFormat(locale, { hour: 'numeric' }).resolvedOptions().hour12 ?? true);
|
|
190
|
+
/** The locale's numeric-date field order, e.g. en-US → month, day, year. */
|
|
191
|
+
const localeFieldOrder = memoize((locale) => {
|
|
192
|
+
const parts = new Intl.DateTimeFormat(locale, { timeZone: 'UTC' }).formatToParts(new Date(Date.UTC(2000, 11, 31)));
|
|
193
|
+
return parts
|
|
194
|
+
.filter((p) => ['year', 'month', 'day'].includes(p.type))
|
|
195
|
+
.map((p) => p.type);
|
|
196
|
+
});
|
|
197
|
+
/** Long + short month names → month number, lowercased, dots stripped. */
|
|
198
|
+
const localeMonthNames = memoize((locale) => {
|
|
199
|
+
const map = new Map();
|
|
200
|
+
for (let i = 0; i < 12; i++) {
|
|
201
|
+
const date = new Date(Date.UTC(2000, i, 15));
|
|
202
|
+
for (const style of ['long', 'short']) {
|
|
203
|
+
const name = new Intl.DateTimeFormat(locale, { month: style, timeZone: 'UTC' })
|
|
204
|
+
.format(date)
|
|
205
|
+
.toLowerCase()
|
|
206
|
+
.replace(/\./g, '');
|
|
207
|
+
map.set(name, i + 1);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
return map;
|
|
211
|
+
});
|
|
212
|
+
/** The locale's digit pattern as a placeholder, e.g. `'MM/DD/YYYY'`. */
|
|
213
|
+
const localeDatePlaceholder = memoize((locale) => new Intl.DateTimeFormat(locale, { timeZone: 'UTC' })
|
|
214
|
+
.format(new Date(Date.UTC(2000, 11, 31)))
|
|
215
|
+
.replace(/2000/, 'YYYY')
|
|
216
|
+
.replace(/12/, 'MM')
|
|
217
|
+
.replace(/31/, 'DD'));
|
|
218
|
+
// ---- Parsing (Intl-driven — digit order and month names come from the
|
|
219
|
+
// locale, never from a hardcoded table) ---------------------------------------
|
|
220
|
+
/** Missing year → the NEXT occurrence: nobody schedules into the past. */
|
|
221
|
+
const nextOccurrence = (month, day, today) => {
|
|
222
|
+
const year = Number(today.slice(0, 4));
|
|
223
|
+
const candidate = isValidDate(year, month, day);
|
|
224
|
+
if (candidate && candidate >= today)
|
|
225
|
+
return candidate;
|
|
226
|
+
return isValidDate(year + 1, month, day);
|
|
227
|
+
};
|
|
228
|
+
/**
|
|
229
|
+
* Free-typed date text → ISO date, or `null` when unreadable. Accepts, in
|
|
230
|
+
* order: ISO (`2026-08-20`), a month name from the locale's own long/short
|
|
231
|
+
* lists (`aug 20`, `20 aug 2026`, `August 20th`), and locale-numeric text
|
|
232
|
+
* (`8/20/2026`, `20.8.2026`, `08-20`) with the digit order taken from
|
|
233
|
+
* `Intl.DateTimeFormat(locale).formatToParts()`. A missing year resolves to
|
|
234
|
+
* the next occurrence; two-digit years are refused rather than guessed.
|
|
235
|
+
*/
|
|
236
|
+
const parseDateText = (raw, locale, options = {}) => {
|
|
237
|
+
const today = options.today ?? todayIso();
|
|
238
|
+
const s = raw
|
|
239
|
+
.trim()
|
|
240
|
+
.toLowerCase()
|
|
241
|
+
.replace(/(\d+)(st|nd|rd|th)\b/g, '$1')
|
|
242
|
+
.replace(/,/g, ' ');
|
|
243
|
+
if (!s)
|
|
244
|
+
return null;
|
|
245
|
+
// 1. ISO — what agents and APIs write.
|
|
246
|
+
const iso = s.match(/^(\d{4})-(\d{1,2})-(\d{1,2})$/);
|
|
247
|
+
if (iso)
|
|
248
|
+
return isValidDate(+iso[1], +iso[2], +iso[3]);
|
|
249
|
+
// 2. Month name, from the locale's own list.
|
|
250
|
+
const monthNames = localeMonthNames(locale);
|
|
251
|
+
const tokens = s.split(/\s+/);
|
|
252
|
+
const monthToken = tokens.find((t) => monthNames.has(t.replace(/\./g, '')));
|
|
253
|
+
if (monthToken) {
|
|
254
|
+
const month = monthNames.get(monthToken.replace(/\./g, ''));
|
|
255
|
+
const numbers = tokens.filter((t) => /^\d+$/.test(t)).map(Number);
|
|
256
|
+
if (numbers.length === 1 && numbers[0] >= 1 && numbers[0] <= 31)
|
|
257
|
+
return nextOccurrence(month, numbers[0], today);
|
|
258
|
+
if (numbers.length === 2) {
|
|
259
|
+
const year = numbers.find((n) => n >= 1000);
|
|
260
|
+
const day = numbers.find((n) => n <= 31 && n !== year);
|
|
261
|
+
if (year && day)
|
|
262
|
+
return isValidDate(year, month, day);
|
|
263
|
+
}
|
|
264
|
+
return null;
|
|
265
|
+
}
|
|
266
|
+
// 3. Locale numeric — digit order from Intl, not hardcoded.
|
|
267
|
+
const numeric = s.match(/^(\d{1,4})[/.\-\s](\d{1,4})(?:[/.\-\s](\d{1,4}))?$/);
|
|
268
|
+
if (!numeric)
|
|
269
|
+
return null;
|
|
270
|
+
const parts = [numeric[1], numeric[2], numeric[3]].filter((p) => !!p);
|
|
271
|
+
if (parts.length === 3) {
|
|
272
|
+
if (parts[0].length === 4)
|
|
273
|
+
return isValidDate(+parts[0], +parts[1], +parts[2]); // 2026/8/20
|
|
274
|
+
const bag = {};
|
|
275
|
+
localeFieldOrder(locale).forEach((field, i) => (bag[field] = parts[i]));
|
|
276
|
+
if (!bag.year || bag.year.length < 4)
|
|
277
|
+
return null; // two-digit years are refused, not guessed
|
|
278
|
+
return isValidDate(+bag.year, +bag.month, +bag.day);
|
|
279
|
+
}
|
|
280
|
+
// Two numbers: month/day in locale order, year = next occurrence.
|
|
281
|
+
const bag = {};
|
|
282
|
+
localeFieldOrder(locale)
|
|
283
|
+
.filter((field) => field !== 'year')
|
|
284
|
+
.forEach((field, i) => (bag[field] = +parts[i]));
|
|
285
|
+
return bag.month && bag.day ? nextOccurrence(bag.month, bag.day, today) : null;
|
|
286
|
+
};
|
|
287
|
+
/**
|
|
288
|
+
* Free-typed time text → `'HH:mm'`, or `null`. Accepts `9`, `930`, `9:30`,
|
|
289
|
+
* `9.30`, `9 30`, `3p`, `3pm`, `3 PM`, `15:00`. Bare hours 1–7 with no
|
|
290
|
+
* meridiem lean PM when `hour12` — typing `3` into an appointment field
|
|
291
|
+
* means 15:00, not 03:00.
|
|
292
|
+
*/
|
|
293
|
+
const parseTimeText = (raw, hour12) => {
|
|
294
|
+
const s = raw.trim().toLowerCase().replace(/\s+/g, '');
|
|
295
|
+
const match = s.match(/^(\d{1,2})(?:[:.h]?(\d{2}))?(a|am|p|pm)?$/);
|
|
296
|
+
if (!match)
|
|
297
|
+
return null;
|
|
298
|
+
let hour = +match[1];
|
|
299
|
+
const minute = match[2] ? +match[2] : 0;
|
|
300
|
+
const meridiem = match[3];
|
|
301
|
+
if (minute > 59)
|
|
302
|
+
return null;
|
|
303
|
+
if (meridiem) {
|
|
304
|
+
if (hour < 1 || hour > 12)
|
|
305
|
+
return null;
|
|
306
|
+
if (meridiem[0] === 'p' && hour !== 12)
|
|
307
|
+
hour += 12;
|
|
308
|
+
if (meridiem[0] === 'a' && hour === 12)
|
|
309
|
+
hour = 0;
|
|
310
|
+
}
|
|
311
|
+
else {
|
|
312
|
+
if (hour > 23)
|
|
313
|
+
return null;
|
|
314
|
+
if (hour12 && hour >= 1 && hour <= 7)
|
|
315
|
+
hour += 12;
|
|
316
|
+
}
|
|
317
|
+
return `${pad(hour)}:${pad(minute)}`;
|
|
318
|
+
};
|
|
319
|
+
/** Every `minuteStep` time between `min` and `max` (inclusive bounds). */
|
|
320
|
+
const timeSlots = (minuteStep, min, max) => {
|
|
321
|
+
const slots = [];
|
|
322
|
+
const lo = min ?? '00:00';
|
|
323
|
+
const hi = max ?? '23:59';
|
|
324
|
+
for (let minutes = 0; minutes < 24 * 60; minutes += minuteStep) {
|
|
325
|
+
const t = `${pad(Math.floor(minutes / 60))}:${pad(minutes % 60)}`;
|
|
326
|
+
if (t >= lo && t <= hi)
|
|
327
|
+
slots.push(t);
|
|
328
|
+
}
|
|
329
|
+
return slots;
|
|
330
|
+
};
|
|
331
|
+
// ---- Combined values --------------------------------------------------------
|
|
332
|
+
const splitDateTime = (value) => {
|
|
333
|
+
if (!value)
|
|
334
|
+
return {};
|
|
335
|
+
const [date, time] = value.split('T');
|
|
336
|
+
return { ...(date ? { date } : {}), ...(time ? { time } : {}) };
|
|
337
|
+
};
|
|
338
|
+
/** One combined value only when both parts are present. */
|
|
339
|
+
const joinDateTime = (date, time) => date && time ? `${date}T${time}` : undefined;
|
|
340
|
+
|
|
53
341
|
/**
|
|
54
342
|
* The keyboard and ARIA bookkeeping shared by every combobox-style popup:
|
|
55
343
|
* open state, the active option index, and the `aria-activedescendant` id
|
|
@@ -519,19 +807,6 @@ class UniServerSideDatasource extends UniBaseDatasource {
|
|
|
519
807
|
}
|
|
520
808
|
}
|
|
521
809
|
|
|
522
|
-
function memoize(fn) {
|
|
523
|
-
const cache = new Map();
|
|
524
|
-
return ((...args) => {
|
|
525
|
-
const key = JSON.stringify(args);
|
|
526
|
-
if (cache.has(key)) {
|
|
527
|
-
return cache.get(key);
|
|
528
|
-
}
|
|
529
|
-
const result = fn(...args);
|
|
530
|
-
cache.set(key, result);
|
|
531
|
-
return result;
|
|
532
|
-
});
|
|
533
|
-
}
|
|
534
|
-
|
|
535
810
|
class LocalStorageService {
|
|
536
811
|
isLocalStorageAvailable = memoize(() => {
|
|
537
812
|
try {
|
|
@@ -958,15 +1233,43 @@ class ThemeService {
|
|
|
958
1233
|
const token = (color + '-container');
|
|
959
1234
|
return this.colorPair(token, useVariant);
|
|
960
1235
|
};
|
|
1236
|
+
/**
|
|
1237
|
+
* The shared keyboard-focus indicator's styles (WCAG 2.4.7), without a
|
|
1238
|
+
* selector — for controls that key the ring off their own state selector
|
|
1239
|
+
* (`&:focus + .checkbox`). Everything else spreads `focusRing()` instead.
|
|
1240
|
+
*
|
|
1241
|
+
* Themable: a theme that defines `focusRing` **border** and/or **shadow**
|
|
1242
|
+
* primitives replaces the default 2px outline with that border (drawn as
|
|
1243
|
+
* an outline hugging the control) plus the ring shadow — one focus
|
|
1244
|
+
* language for every control, from text fields to checkboxes to calendar
|
|
1245
|
+
* days. A `focusRing` **thickness** primitive sets the outline offset
|
|
1246
|
+
* (default 0 when themed, 2px for the classic outline; negative values
|
|
1247
|
+
* overlay the control's own border, reading as a border-color change).
|
|
1248
|
+
* Without those primitives the classic outline renders, in the given
|
|
1249
|
+
* color or `currentColor`.
|
|
1250
|
+
*/
|
|
1251
|
+
focusRingStyle = (color, gap) => {
|
|
1252
|
+
const border = this.borders()['focusRing'];
|
|
1253
|
+
const shadow = this.shadows()['focusRing'];
|
|
1254
|
+
// An explicit per-call gap wins; else the theme's `focusRing` thickness
|
|
1255
|
+
// primitive; else the branch default (hugging when themed, classic 2px).
|
|
1256
|
+
const offset = gap ?? this.thicknesses()['focusRing'];
|
|
1257
|
+
if (border || shadow) {
|
|
1258
|
+
return {
|
|
1259
|
+
outline: border ?? 'none',
|
|
1260
|
+
outlineOffset: offset ?? 0,
|
|
1261
|
+
...(shadow ? { boxShadow: shadow } : {}),
|
|
1262
|
+
};
|
|
1263
|
+
}
|
|
1264
|
+
return { outline: `2px solid ${color ?? 'currentColor'}`, outlineOffset: offset ?? '2px' };
|
|
1265
|
+
};
|
|
961
1266
|
/**
|
|
962
1267
|
* Shared keyboard-focus indicator (WCAG 2.4.7). Spread into a component's
|
|
963
1268
|
* Emotion styles: `...this.theme.focusRing()` or `focusRing('primary')`.
|
|
1269
|
+
* See `focusRingStyle` for how themes restyle it.
|
|
964
1270
|
*/
|
|
965
1271
|
focusRing = (token) => ({
|
|
966
|
-
'&:focus-visible':
|
|
967
|
-
outline: `2px solid ${token ? this.colors()[token] : 'currentColor'}`,
|
|
968
|
-
outlineOffset: '2px',
|
|
969
|
-
},
|
|
1272
|
+
'&:focus-visible': this.focusRingStyle(token ? this.colors()[token] : undefined),
|
|
970
1273
|
});
|
|
971
1274
|
typeface = (typeface) => {
|
|
972
1275
|
const typefaces = this.typeFaces();
|
|
@@ -2139,73 +2442,705 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImpo
|
|
|
2139
2442
|
* This file exports all public-facing elements of the button-group component.
|
|
2140
2443
|
*/
|
|
2141
2444
|
|
|
2142
|
-
class
|
|
2143
|
-
|
|
2144
|
-
|
|
2445
|
+
class BodyRenderDirective {
|
|
2446
|
+
el = inject(ElementRef);
|
|
2447
|
+
renderer = inject(Renderer2);
|
|
2448
|
+
ngOnInit() {
|
|
2449
|
+
const element = this.el.nativeElement;
|
|
2450
|
+
this.renderer.appendChild(document.body, element);
|
|
2451
|
+
}
|
|
2452
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: BodyRenderDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
2453
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.12", type: BodyRenderDirective, isStandalone: true, selector: "[uniBodyRender]", ngImport: i0 });
|
|
2145
2454
|
}
|
|
2146
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type:
|
|
2147
|
-
type:
|
|
2148
|
-
args: [{
|
|
2455
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: BodyRenderDirective, decorators: [{
|
|
2456
|
+
type: Directive,
|
|
2457
|
+
args: [{
|
|
2458
|
+
selector: '[uniBodyRender]',
|
|
2459
|
+
}]
|
|
2149
2460
|
}] });
|
|
2150
2461
|
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
}
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
2462
|
+
class DragAndDropDirective {
|
|
2463
|
+
// TODO(v4): rename to fileDropped — renaming is breaking
|
|
2464
|
+
// eslint-disable-next-line @angular-eslint/no-output-on-prefix
|
|
2465
|
+
onFileDropped = output();
|
|
2466
|
+
workspaceOpacity = signal('1', ...(ngDevMode ? [{ debugName: "workspaceOpacity" }] : /* istanbul ignore next */ []));
|
|
2467
|
+
// Dragover listener, when files are dragged over our host element
|
|
2468
|
+
onDragOver(event) {
|
|
2469
|
+
event.preventDefault();
|
|
2470
|
+
event.stopPropagation();
|
|
2471
|
+
this.workspaceOpacity.set('0.5');
|
|
2472
|
+
}
|
|
2473
|
+
// Dragleave listener, when files are dragged away from our host element
|
|
2474
|
+
onDragLeave(event) {
|
|
2475
|
+
event.preventDefault();
|
|
2476
|
+
event.stopPropagation();
|
|
2477
|
+
this.workspaceOpacity.set('1');
|
|
2478
|
+
}
|
|
2479
|
+
// Drop listener, when files are dropped on our host element
|
|
2480
|
+
onDrop(event) {
|
|
2481
|
+
event.preventDefault();
|
|
2482
|
+
event.stopPropagation();
|
|
2483
|
+
this.workspaceOpacity.set('1');
|
|
2484
|
+
const files = event.dataTransfer?.files;
|
|
2485
|
+
if (files && files.length > 0) {
|
|
2486
|
+
this.onFileDropped.emit(files);
|
|
2487
|
+
}
|
|
2488
|
+
}
|
|
2489
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: DragAndDropDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
2490
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.12", type: DragAndDropDirective, isStandalone: true, selector: "[uni-drag-n-drop], [dragAndDrop]", outputs: { onFileDropped: "onFileDropped" }, host: { listeners: { "dragover": "onDragOver($event)", "dragleave": "onDragLeave($event)", "drop": "onDrop($event)" }, properties: { "style.opacity": "workspaceOpacity()" } }, ngImport: i0 });
|
|
2491
|
+
}
|
|
2492
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: DragAndDropDirective, decorators: [{
|
|
2493
|
+
type: Directive,
|
|
2494
|
+
args: [{
|
|
2495
|
+
selector: '[uni-drag-n-drop], [dragAndDrop]',
|
|
2496
|
+
host: {
|
|
2497
|
+
'[style.opacity]': 'workspaceOpacity()',
|
|
2498
|
+
'(dragover)': 'onDragOver($event)',
|
|
2499
|
+
'(dragleave)': 'onDragLeave($event)',
|
|
2500
|
+
'(drop)': 'onDrop($event)',
|
|
2501
|
+
},
|
|
2502
|
+
}]
|
|
2503
|
+
}], propDecorators: { onFileDropped: [{ type: i0.Output, args: ["onFileDropped"] }] } });
|
|
2504
|
+
|
|
2505
|
+
class UniIconButtonComponent {
|
|
2177
2506
|
theme = inject(ThemeService);
|
|
2178
|
-
|
|
2179
|
-
/**
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
2507
|
+
config = this.theme.component('iconButton');
|
|
2508
|
+
/**
|
|
2509
|
+
* Accessible name for the button. Alternative to projecting text content
|
|
2510
|
+
* (`<button icon-button>Close</button>`); one of the two is required for
|
|
2511
|
+
* an icon-only button to be announced correctly.
|
|
2512
|
+
*/
|
|
2513
|
+
ariaLabel = input(...(ngDevMode ? [undefined, { debugName: "ariaLabel" }] : /* istanbul ignore next */ []));
|
|
2514
|
+
iconName = input(...(ngDevMode ? [undefined, { debugName: "iconName" }] : /* istanbul ignore next */ []));
|
|
2515
|
+
symbolName = input(...(ngDevMode ? [undefined, { debugName: "symbolName" }] : /* istanbul ignore next */ []));
|
|
2516
|
+
variant = input('ghost', ...(ngDevMode ? [{ debugName: "variant" }] : /* istanbul ignore next */ []));
|
|
2517
|
+
size = input('lg', ...(ngDevMode ? [{ debugName: "size" }] : /* istanbul ignore next */ []));
|
|
2518
|
+
disable = input(...(ngDevMode ? [undefined, { debugName: "disable" }] : /* istanbul ignore next */ []));
|
|
2519
|
+
loading = input(...(ngDevMode ? [undefined, { debugName: "loading" }] : /* istanbul ignore next */ []));
|
|
2520
|
+
opticalSize = input(24, ...(ngDevMode ? [{ debugName: "opticalSize" }] : /* istanbul ignore next */ []));
|
|
2521
|
+
srOnlyClass = css(visuallyHidden);
|
|
2522
|
+
/**
|
|
2523
|
+
* Sizes `iconName` from the size token's `fontSize`, the same value that sizes
|
|
2524
|
+
* a `symbolName` ligature — so the two paths render the same glyph size and
|
|
2525
|
+
* `symbolName` → `iconName` is a like-for-like swap. Without it a masked icon
|
|
2526
|
+
* fills the whole button box, since the base size tokens carry no padding.
|
|
2527
|
+
* Themes that do use padding (Carbon) set a matching `fontSize`, so they land
|
|
2528
|
+
* on the same glyph either way.
|
|
2529
|
+
*/
|
|
2530
|
+
glyphSize = computed(() => {
|
|
2531
|
+
// Size tokens are Emotion style objects, so `fontSize` is typed wider than
|
|
2532
|
+
// a CSS length; anything exotic falls back to filling the button as before.
|
|
2533
|
+
const fontSize = this.config().sizes?.[this.size()]?.fontSize;
|
|
2534
|
+
return typeof fontSize === 'number' || typeof fontSize === 'string' ? fontSize : undefined;
|
|
2535
|
+
}, ...(ngDevMode ? [{ debugName: "glyphSize" }] : /* istanbul ignore next */ []));
|
|
2190
2536
|
className = computed(() => {
|
|
2537
|
+
const { sizes, variants } = this.config();
|
|
2538
|
+
const sizeConfig = sizes && sizes[this.size()];
|
|
2539
|
+
const colorConfig = variants && variants[this.variant()];
|
|
2191
2540
|
return css([
|
|
2192
2541
|
{
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
2542
|
+
position: 'relative',
|
|
2543
|
+
overflow: 'hidden',
|
|
2544
|
+
outline: 0,
|
|
2545
|
+
border: 0,
|
|
2546
|
+
cursor: 'pointer',
|
|
2547
|
+
transition: 'all 0.28s ease',
|
|
2548
|
+
// Token-driven radius (`max` = circle) with the legacy 999 fallback
|
|
2549
|
+
// for hand-authored themes that predate iconButton options.
|
|
2550
|
+
...(this.theme.radius(this.config().options?.borderRadius) ?? { borderRadius: 999 }),
|
|
2551
|
+
// Block-level, but centring: the size tokens make the box bigger than
|
|
2552
|
+
// the glyph (an `sm` button is 22px around an 18px icon), so a plain
|
|
2553
|
+
// `display: block` parks the glyph in the top-left corner. Flex is
|
|
2554
|
+
// still block-level, so nothing about the button's own layout changes.
|
|
2555
|
+
// The accessible-name span is absolutely positioned and so stays out
|
|
2556
|
+
// of the flex flow.
|
|
2557
|
+
display: 'flex',
|
|
2558
|
+
alignItems: 'center',
|
|
2559
|
+
justifyContent: 'center',
|
|
2560
|
+
'&:disabled': {
|
|
2561
|
+
cursor: 'not-allowed !important',
|
|
2562
|
+
},
|
|
2563
|
+
'& symbol': {
|
|
2564
|
+
fontSize: 'inherit',
|
|
2565
|
+
lineHeight: 'inherit',
|
|
2566
|
+
},
|
|
2196
2567
|
},
|
|
2197
|
-
|
|
2198
|
-
|
|
2568
|
+
sizeConfig && {
|
|
2569
|
+
...sizeConfig,
|
|
2199
2570
|
},
|
|
2200
|
-
|
|
2201
|
-
|
|
2571
|
+
colorConfig && {
|
|
2572
|
+
...colorConfig,
|
|
2202
2573
|
},
|
|
2203
|
-
this.
|
|
2204
|
-
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
|
|
2574
|
+
this.symbolName() &&
|
|
2575
|
+
!this.loading() && {
|
|
2576
|
+
padding: 0,
|
|
2577
|
+
},
|
|
2578
|
+
this.variant() !== 'ghost' && {
|
|
2579
|
+
'&:hover': {
|
|
2580
|
+
...this.theme.boxShadow('raised'),
|
|
2581
|
+
},
|
|
2582
|
+
},
|
|
2583
|
+
this.variant() === 'ghost' && {
|
|
2584
|
+
'&:hover': {
|
|
2585
|
+
backgroundColor: 'rgba(0,0,0,0.1)',
|
|
2586
|
+
},
|
|
2587
|
+
},
|
|
2588
|
+
!this.loading() && {
|
|
2589
|
+
'&:disabled': {
|
|
2590
|
+
...this.config().variants?.disabled,
|
|
2591
|
+
},
|
|
2592
|
+
},
|
|
2593
|
+
]);
|
|
2594
|
+
}, ...(ngDevMode ? [{ debugName: "className" }] : /* istanbul ignore next */ []));
|
|
2595
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: UniIconButtonComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
2596
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.12", type: UniIconButtonComponent, isStandalone: true, selector: "button[uni-icon-button], button[icon-button]", inputs: { ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", isSignal: true, isRequired: false, transformFunction: null }, iconName: { classPropertyName: "iconName", publicName: "iconName", isSignal: true, isRequired: false, transformFunction: null }, symbolName: { classPropertyName: "symbolName", publicName: "symbolName", isSignal: true, isRequired: false, transformFunction: null }, variant: { classPropertyName: "variant", publicName: "variant", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null }, disable: { classPropertyName: "disable", publicName: "disable", isSignal: true, isRequired: false, transformFunction: null }, loading: { classPropertyName: "loading", publicName: "loading", isSignal: true, isRequired: false, transformFunction: null }, opticalSize: { classPropertyName: "opticalSize", publicName: "opticalSize", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "attr.disabled": "disable() || loading() || null", "attr.aria-busy": "loading() ? 'true' : null", "attr.aria-label": "ariaLabel() || null", "class": "className()" } }, hostDirectives: [{ directive: RippleDirective }], ngImport: i0, template: `
|
|
2597
|
+
@if (loading()) {
|
|
2598
|
+
<uni-icon name="spinner" />
|
|
2599
|
+
} @else if (symbolName()) {
|
|
2600
|
+
<uni-symbol [name]="symbolName()!" [opticalSize]="opticalSize()" />
|
|
2601
|
+
} @else if (iconName()) {
|
|
2602
|
+
<uni-icon [name]="iconName()!" [size]="glyphSize()" />
|
|
2603
|
+
}
|
|
2604
|
+
<!-- Projected text is the button's accessible name (visually hidden) -->
|
|
2605
|
+
<span [class]="srOnlyClass"><ng-content /></span>
|
|
2606
|
+
`, isInline: true, dependencies: [{ kind: "component", type: UniSymbolComponent, selector: "uni-symbol", inputs: ["name", "fill", "weight", "grade", "opticalSize"] }, { kind: "component", type: UniIconComponent, selector: "uni-icon", inputs: ["color", "name", "size"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
2607
|
+
}
|
|
2608
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: UniIconButtonComponent, decorators: [{
|
|
2609
|
+
type: Component,
|
|
2610
|
+
args: [{
|
|
2611
|
+
selector: 'button[uni-icon-button], button[icon-button]',
|
|
2612
|
+
imports: [UniSymbolComponent, UniIconComponent],
|
|
2613
|
+
template: `
|
|
2614
|
+
@if (loading()) {
|
|
2615
|
+
<uni-icon name="spinner" />
|
|
2616
|
+
} @else if (symbolName()) {
|
|
2617
|
+
<uni-symbol [name]="symbolName()!" [opticalSize]="opticalSize()" />
|
|
2618
|
+
} @else if (iconName()) {
|
|
2619
|
+
<uni-icon [name]="iconName()!" [size]="glyphSize()" />
|
|
2620
|
+
}
|
|
2621
|
+
<!-- Projected text is the button's accessible name (visually hidden) -->
|
|
2622
|
+
<span [class]="srOnlyClass"><ng-content /></span>
|
|
2623
|
+
`,
|
|
2624
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
2625
|
+
host: {
|
|
2626
|
+
'[attr.disabled]': 'disable() || loading() || null',
|
|
2627
|
+
'[attr.aria-busy]': "loading() ? 'true' : null",
|
|
2628
|
+
'[attr.aria-label]': 'ariaLabel() || null',
|
|
2629
|
+
'[class]': 'className()',
|
|
2630
|
+
},
|
|
2631
|
+
hostDirectives: [{ directive: RippleDirective }],
|
|
2632
|
+
}]
|
|
2633
|
+
}], propDecorators: { ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }], iconName: [{ type: i0.Input, args: [{ isSignal: true, alias: "iconName", required: false }] }], symbolName: [{ type: i0.Input, args: [{ isSignal: true, alias: "symbolName", required: false }] }], variant: [{ type: i0.Input, args: [{ isSignal: true, alias: "variant", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], disable: [{ type: i0.Input, args: [{ isSignal: true, alias: "disable", required: false }] }], loading: [{ type: i0.Input, args: [{ isSignal: true, alias: "loading", required: false }] }], opticalSize: [{ type: i0.Input, args: [{ isSignal: true, alias: "opticalSize", required: false }] }] } });
|
|
2634
|
+
|
|
2635
|
+
/**
|
|
2636
|
+
* Inline month calendar: single-date or start–end range selection, day
|
|
2637
|
+
* markers (availability dots), min/max fences and disabled dates. One tab
|
|
2638
|
+
* stop with a roving-tabindex `role="grid"`; every day is a real button
|
|
2639
|
+
* named with its full date. Values are plain ISO strings (`'YYYY-MM-DD'`),
|
|
2640
|
+
* never `Date` objects, so bindings are timezone-free and serializable.
|
|
2641
|
+
* Selection and today colours come from the theme's `primary` role pair;
|
|
2642
|
+
* geometry and glyphs come from the `calendar` theme entry.
|
|
2643
|
+
*/
|
|
2644
|
+
class UniCalendarComponent extends BaseComponent {
|
|
2645
|
+
// --- Signal Forms block (explicit per AGENTS.md, not a base class) --------
|
|
2646
|
+
value = model(...(ngDevMode ? [undefined, { debugName: "value" }] : /* istanbul ignore next */ []));
|
|
2647
|
+
disabled = input(false, ...(ngDevMode ? [{ debugName: "disabled" }] : /* istanbul ignore next */ []));
|
|
2648
|
+
touched = model(false, ...(ngDevMode ? [{ debugName: "touched" }] : /* istanbul ignore next */ []));
|
|
2649
|
+
invalid = input(false, ...(ngDevMode ? [{ debugName: "invalid" }] : /* istanbul ignore next */ []));
|
|
2650
|
+
dirty = input(false, ...(ngDevMode ? [{ debugName: "dirty" }] : /* istanbul ignore next */ []));
|
|
2651
|
+
required = input(false, ...(ngDevMode ? [{ debugName: "required" }] : /* istanbul ignore next */ []));
|
|
2652
|
+
ariaDescribedBy = input(...(ngDevMode ? [undefined, { debugName: "ariaDescribedBy" }] : /* istanbul ignore next */ []));
|
|
2653
|
+
// --- Configuration -------------------------------------------------------
|
|
2654
|
+
/** `'single'` reads/writes a `UniDate`; `'range'` a `UniDateRange`. */
|
|
2655
|
+
mode = input('single', ...(ngDevMode ? [{ debugName: "mode" }] : /* istanbul ignore next */ []));
|
|
2656
|
+
/** Shown month, `'YYYY-MM'` — two-way, so an app can drive "jump to June". */
|
|
2657
|
+
month = model(...(ngDevMode ? [undefined, { debugName: "month" }] : /* istanbul ignore next */ []));
|
|
2658
|
+
/** Earliest selectable date (inclusive). */
|
|
2659
|
+
minDate = input(...(ngDevMode ? [undefined, { debugName: "minDate" }] : /* istanbul ignore next */ []));
|
|
2660
|
+
/** Latest selectable date (inclusive). */
|
|
2661
|
+
maxDate = input(...(ngDevMode ? [undefined, { debugName: "maxDate" }] : /* istanbul ignore next */ []));
|
|
2662
|
+
/** Blocked days: a list of dates, or a predicate. */
|
|
2663
|
+
disabledDates = input(...(ngDevMode ? [undefined, { debugName: "disabledDates" }] : /* istanbul ignore next */ []));
|
|
2664
|
+
/** Availability dots; `label` extends the day's accessible name. */
|
|
2665
|
+
markers = input([], ...(ngDevMode ? [{ debugName: "markers" }] : /* istanbul ignore next */ []));
|
|
2666
|
+
/** BCP 47 tag; defaults to the document language, then the browser's. */
|
|
2667
|
+
locale = input(...(ngDevMode ? [undefined, { debugName: "locale" }] : /* istanbul ignore next */ []));
|
|
2668
|
+
/** First day of week, 0 = Sunday; defaults from the locale's week info. */
|
|
2669
|
+
weekStart = input(...(ngDevMode ? [undefined, { debugName: "weekStart" }] : /* istanbul ignore next */ []));
|
|
2670
|
+
/** Names the grid when it stands alone (otherwise the heading names it). */
|
|
2671
|
+
ariaLabel = input(...(ngDevMode ? [undefined, { debugName: "ariaLabel" }] : /* istanbul ignore next */ []));
|
|
2672
|
+
/** Day geometry token; `sm`/`md`/`lg` map to the theme's `calendar` sizes. */
|
|
2673
|
+
size = input('md', ...(ngDevMode ? [{ debugName: "size" }] : /* istanbul ignore next */ []));
|
|
2674
|
+
// --- Events (value/month changes flow through the models) -----------------
|
|
2675
|
+
/** Each committed day, including both ends of a range. */
|
|
2676
|
+
selected = output();
|
|
2677
|
+
host = inject(ElementRef);
|
|
2678
|
+
headingId = uniqueId('uni-calendar-heading');
|
|
2679
|
+
srOnly = css(visuallyHidden);
|
|
2680
|
+
/** Pending range start — set by the first commit, cleared by the second. */
|
|
2681
|
+
pendingStart = signal(null, ...(ngDevMode ? [{ debugName: "pendingStart" }] : /* istanbul ignore next */ []));
|
|
2682
|
+
/** Hover/focus candidate painting the preview band while a range is pending. */
|
|
2683
|
+
previewDate = signal(null, ...(ngDevMode ? [{ debugName: "previewDate" }] : /* istanbul ignore next */ []));
|
|
2684
|
+
/** Live-region text; selections are otherwise silent for a screen reader. */
|
|
2685
|
+
announcement = signal('', ...(ngDevMode ? [{ debugName: "announcement" }] : /* istanbul ignore next */ []));
|
|
2686
|
+
resolvedLocale = computed(() => this.locale() ?? (document.documentElement.lang || navigator.language || 'en-US'), ...(ngDevMode ? [{ debugName: "resolvedLocale" }] : /* istanbul ignore next */ []));
|
|
2687
|
+
resolvedWeekStart = computed(() => this.weekStart() ?? localeWeekStart(this.resolvedLocale()), ...(ngDevMode ? [{ debugName: "resolvedWeekStart" }] : /* istanbul ignore next */ []));
|
|
2688
|
+
/** The month on screen: the `month` model, else the value's month, else today's. */
|
|
2689
|
+
viewMonth = computed(() => this.month() ?? monthOf(this.anchorDate() ?? todayIso()), ...(ngDevMode ? [{ debugName: "viewMonth" }] : /* istanbul ignore next */ []));
|
|
2690
|
+
anchorDate = computed(() => {
|
|
2691
|
+
const value = this.value();
|
|
2692
|
+
return this.mode() === 'range'
|
|
2693
|
+
? value?.start
|
|
2694
|
+
: value;
|
|
2695
|
+
}, ...(ngDevMode ? [{ debugName: "anchorDate" }] : /* istanbul ignore next */ []));
|
|
2696
|
+
/**
|
|
2697
|
+
* The roving-tabindex day. Re-derives when the view or value changes
|
|
2698
|
+
* (selected day in view → today in view → first enabled day); keyboard
|
|
2699
|
+
* navigation writes it directly.
|
|
2700
|
+
*/
|
|
2701
|
+
focusedDate = linkedSignal(() => this.pickFocus(), ...(ngDevMode ? [{ debugName: "focusedDate" }] : /* istanbul ignore next */ []));
|
|
2702
|
+
showError = computed(() => this.invalid() && (this.touched() || this.dirty()), ...(ngDevMode ? [{ debugName: "showError" }] : /* istanbul ignore next */ []));
|
|
2703
|
+
heading = computed(() => formatMonthHeading(this.viewMonth(), this.resolvedLocale()), ...(ngDevMode ? [{ debugName: "heading" }] : /* istanbul ignore next */ []));
|
|
2704
|
+
weekdays = computed(() => weekdayNames(this.resolvedLocale(), this.resolvedWeekStart(), this.componentOptions().weekdayFormat ?? 'short'), ...(ngDevMode ? [{ debugName: "weekdays" }] : /* istanbul ignore next */ []));
|
|
2705
|
+
disabledDateSet = computed(() => {
|
|
2706
|
+
const dates = this.disabledDates();
|
|
2707
|
+
return Array.isArray(dates) ? new Set(dates) : null;
|
|
2708
|
+
}, ...(ngDevMode ? [{ debugName: "disabledDateSet" }] : /* istanbul ignore next */ []));
|
|
2709
|
+
markersByDate = computed(() => {
|
|
2710
|
+
const map = new Map();
|
|
2711
|
+
for (const marker of this.markers()) {
|
|
2712
|
+
const existing = map.get(marker.date);
|
|
2713
|
+
if (existing)
|
|
2714
|
+
existing.push(marker);
|
|
2715
|
+
else
|
|
2716
|
+
map.set(marker.date, [marker]);
|
|
2717
|
+
}
|
|
2718
|
+
return map;
|
|
2719
|
+
}, ...(ngDevMode ? [{ debugName: "markersByDate" }] : /* istanbul ignore next */ []));
|
|
2720
|
+
/** The committed range, or the pending preview band. */
|
|
2721
|
+
rangeBand = computed(() => {
|
|
2722
|
+
if (this.mode() !== 'range')
|
|
2723
|
+
return null;
|
|
2724
|
+
const pending = this.pendingStart();
|
|
2725
|
+
const preview = this.previewDate();
|
|
2726
|
+
if (pending && preview) {
|
|
2727
|
+
const [start, end] = preview < pending ? [preview, pending] : [pending, preview];
|
|
2728
|
+
return { start, end, preview: true };
|
|
2729
|
+
}
|
|
2730
|
+
if (pending)
|
|
2731
|
+
return { start: pending, end: pending, preview: true };
|
|
2732
|
+
const value = this.value();
|
|
2733
|
+
if (value?.start && value?.end)
|
|
2734
|
+
return { start: value.start, end: value.end, preview: false };
|
|
2735
|
+
return null;
|
|
2736
|
+
}, ...(ngDevMode ? [{ debugName: "rangeBand" }] : /* istanbul ignore next */ []));
|
|
2737
|
+
isDayDisabled(date) {
|
|
2738
|
+
const min = this.minDate();
|
|
2739
|
+
const max = this.maxDate();
|
|
2740
|
+
if ((min && date < min) || (max && date > max))
|
|
2741
|
+
return true;
|
|
2742
|
+
const dates = this.disabledDates();
|
|
2743
|
+
if (!dates)
|
|
2744
|
+
return false;
|
|
2745
|
+
const set = this.disabledDateSet();
|
|
2746
|
+
return set ? set.has(date) : dates(date);
|
|
2747
|
+
}
|
|
2748
|
+
isSelected(date) {
|
|
2749
|
+
if (this.mode() === 'single')
|
|
2750
|
+
return this.value() === date;
|
|
2751
|
+
const pending = this.pendingStart();
|
|
2752
|
+
if (pending)
|
|
2753
|
+
return pending === date;
|
|
2754
|
+
const value = this.value();
|
|
2755
|
+
return value?.start === date || value?.end === date;
|
|
2756
|
+
}
|
|
2757
|
+
/** The full render model: one precomputed cell per grid position. */
|
|
2758
|
+
gridWeeks = computed(() => {
|
|
2759
|
+
const locale = this.resolvedLocale();
|
|
2760
|
+
const band = this.rangeBand();
|
|
2761
|
+
const today = todayIso();
|
|
2762
|
+
const focused = this.focusedDate();
|
|
2763
|
+
const markers = this.markersByDate();
|
|
2764
|
+
const cellBase = this.cellClass();
|
|
2765
|
+
const dayBase = this.dayClass();
|
|
2766
|
+
const todayClass = this.todayClass();
|
|
2767
|
+
return buildMonthGrid(this.viewMonth(), this.resolvedWeekStart()).map((week) => week.map((cell) => {
|
|
2768
|
+
const date = cell.date;
|
|
2769
|
+
const selected = !cell.outside && this.isSelected(date);
|
|
2770
|
+
const inBand = !cell.outside && !!band && date >= band.start && date <= band.end;
|
|
2771
|
+
const dayMarkers = cell.outside ? [] : (markers.get(date) ?? []).slice(0, 3);
|
|
2772
|
+
const markerLabel = dayMarkers
|
|
2773
|
+
.filter((marker) => marker.label)
|
|
2774
|
+
.map((marker) => marker.label)
|
|
2775
|
+
.join(', ');
|
|
2776
|
+
const isToday = date === today;
|
|
2777
|
+
return {
|
|
2778
|
+
date,
|
|
2779
|
+
day: Number(date.slice(8, 10)),
|
|
2780
|
+
outside: cell.outside,
|
|
2781
|
+
disabled: this.isDayDisabled(date),
|
|
2782
|
+
today: isToday,
|
|
2783
|
+
selected,
|
|
2784
|
+
inBand,
|
|
2785
|
+
tabIndex: date === focused && !cell.outside ? 0 : -1,
|
|
2786
|
+
ariaLabel: formatDate(date, locale, { dateStyle: 'full' }) + (markerLabel ? `, ${markerLabel}` : ''),
|
|
2787
|
+
markers: dayMarkers,
|
|
2788
|
+
cellClass: [
|
|
2789
|
+
cellBase,
|
|
2790
|
+
inBand && (band.preview ? this.previewClass() : this.inRangeClass()),
|
|
2791
|
+
inBand && date === band.start && this.bandStartClass(),
|
|
2792
|
+
inBand && date === band.end && this.bandEndClass(),
|
|
2793
|
+
]
|
|
2794
|
+
.filter(Boolean)
|
|
2795
|
+
.join(' '),
|
|
2796
|
+
dayClass: [dayBase, isToday && todayClass, selected && this.selectedClass()]
|
|
2797
|
+
.filter(Boolean)
|
|
2798
|
+
.join(' '),
|
|
2799
|
+
};
|
|
2800
|
+
}));
|
|
2801
|
+
}, ...(ngDevMode ? [{ debugName: "gridWeeks" }] : /* istanbul ignore next */ []));
|
|
2802
|
+
// --- Selection -------------------------------------------------------------
|
|
2803
|
+
select(date) {
|
|
2804
|
+
if (this.disabled() || this.isDayDisabled(date))
|
|
2805
|
+
return;
|
|
2806
|
+
this.setViewMonth(monthOf(date));
|
|
2807
|
+
this.focusedDate.set(date);
|
|
2808
|
+
const locale = this.resolvedLocale();
|
|
2809
|
+
const full = (d) => formatDate(d, locale, { dateStyle: 'full' });
|
|
2810
|
+
if (this.mode() === 'single') {
|
|
2811
|
+
this.value.set(date);
|
|
2812
|
+
this.announce(`${full(date)} selected.`);
|
|
2813
|
+
}
|
|
2814
|
+
else if (!this.pendingStart()) {
|
|
2815
|
+
this.pendingStart.set(date);
|
|
2816
|
+
this.previewDate.set(date);
|
|
2817
|
+
this.announce(`Start date ${full(date)}. Choose an end date.`);
|
|
2818
|
+
}
|
|
2819
|
+
else {
|
|
2820
|
+
let [start, end] = [this.pendingStart(), date];
|
|
2821
|
+
if (end < start)
|
|
2822
|
+
[start, end] = [end, start]; // backwards commit swaps
|
|
2823
|
+
this.pendingStart.set(null);
|
|
2824
|
+
this.previewDate.set(null);
|
|
2825
|
+
this.value.set({ start, end });
|
|
2826
|
+
const days = inclusiveDayCount(start, end);
|
|
2827
|
+
this.announce(`Range selected, ${full(start)} to ${full(end)}. ${days} ${days === 1 ? 'day' : 'days'}.`);
|
|
2828
|
+
}
|
|
2829
|
+
this.selected.emit(date);
|
|
2830
|
+
}
|
|
2831
|
+
cancelPending() {
|
|
2832
|
+
this.pendingStart.set(null);
|
|
2833
|
+
this.previewDate.set(null);
|
|
2834
|
+
this.announce('Range selection cancelled.');
|
|
2835
|
+
}
|
|
2836
|
+
// --- Navigation ------------------------------------------------------------
|
|
2837
|
+
onNav(direction) {
|
|
2838
|
+
this.setViewMonth(monthOf(addMonths(`${this.viewMonth()}-01`, direction)));
|
|
2839
|
+
}
|
|
2840
|
+
setViewMonth(month) {
|
|
2841
|
+
if (this.month() !== month)
|
|
2842
|
+
this.month.set(month);
|
|
2843
|
+
}
|
|
2844
|
+
/**
|
|
2845
|
+
* Move the roving focus. A landing on a disabled day keeps going in the
|
|
2846
|
+
* same direction until an enabled day; the min/max fence stops the caret,
|
|
2847
|
+
* it never wraps. Month edges never block — the grid follows.
|
|
2848
|
+
*/
|
|
2849
|
+
moveFocus(target, direction) {
|
|
2850
|
+
const min = this.minDate();
|
|
2851
|
+
const max = this.maxDate();
|
|
2852
|
+
if ((min && target < min) || (max && target > max))
|
|
2853
|
+
return;
|
|
2854
|
+
let date = target;
|
|
2855
|
+
let guard = 0;
|
|
2856
|
+
while (this.isDayDisabled(date)) {
|
|
2857
|
+
date = addDays(date, direction);
|
|
2858
|
+
if ((min && date < min) || (max && date > max) || ++guard > 500)
|
|
2859
|
+
return;
|
|
2860
|
+
}
|
|
2861
|
+
this.setViewMonth(monthOf(date));
|
|
2862
|
+
this.focusedDate.set(date);
|
|
2863
|
+
if (this.pendingStart())
|
|
2864
|
+
this.previewDate.set(date);
|
|
2865
|
+
this.focusDay(date);
|
|
2866
|
+
}
|
|
2867
|
+
onGridKeydown(event) {
|
|
2868
|
+
const focused = this.focusedDate();
|
|
2869
|
+
const week = (dayOfWeek(focused) - this.resolvedWeekStart() + 7) % 7;
|
|
2870
|
+
const handlers = {
|
|
2871
|
+
ArrowLeft: () => this.moveFocus(addDays(focused, -1), -1),
|
|
2872
|
+
ArrowRight: () => this.moveFocus(addDays(focused, 1), 1),
|
|
2873
|
+
ArrowUp: () => this.moveFocus(addDays(focused, -7), -1),
|
|
2874
|
+
ArrowDown: () => this.moveFocus(addDays(focused, 7), 1),
|
|
2875
|
+
Home: () => this.moveFocus(addDays(focused, -week), 1),
|
|
2876
|
+
End: () => this.moveFocus(addDays(focused, 6 - week), -1),
|
|
2877
|
+
PageUp: () => this.moveFocus(addMonths(focused, event.shiftKey ? -12 : -1), -1),
|
|
2878
|
+
PageDown: () => this.moveFocus(addMonths(focused, event.shiftKey ? 12 : 1), 1),
|
|
2879
|
+
Enter: () => this.select(focused),
|
|
2880
|
+
' ': () => this.select(focused),
|
|
2881
|
+
};
|
|
2882
|
+
if (event.key === 'Escape') {
|
|
2883
|
+
// Only a pending range consumes Escape; otherwise it bubbles so a
|
|
2884
|
+
// hosting popover can light-dismiss.
|
|
2885
|
+
if (this.pendingStart()) {
|
|
2886
|
+
event.preventDefault();
|
|
2887
|
+
event.stopPropagation();
|
|
2888
|
+
this.cancelPending();
|
|
2889
|
+
}
|
|
2890
|
+
return;
|
|
2891
|
+
}
|
|
2892
|
+
const handler = handlers[event.key];
|
|
2893
|
+
if (handler) {
|
|
2894
|
+
event.preventDefault();
|
|
2895
|
+
handler();
|
|
2896
|
+
}
|
|
2897
|
+
}
|
|
2898
|
+
onDayHover(date) {
|
|
2899
|
+
if (this.pendingStart() && !this.isDayDisabled(date))
|
|
2900
|
+
this.previewDate.set(date);
|
|
2901
|
+
}
|
|
2902
|
+
/** Focus the roving day — used by popup hosts when the calendar opens. */
|
|
2903
|
+
focusActiveDay() {
|
|
2904
|
+
this.focusDay(this.focusedDate());
|
|
2905
|
+
}
|
|
2906
|
+
onHostFocusOut(event) {
|
|
2907
|
+
const next = event.relatedTarget;
|
|
2908
|
+
if (!next || !this.host.nativeElement.contains(next))
|
|
2909
|
+
this.touched.set(true);
|
|
2910
|
+
}
|
|
2911
|
+
focusDay(date) {
|
|
2912
|
+
queueMicrotask(() => this.host.nativeElement.querySelector(`[data-date="${date}"]`)?.focus());
|
|
2913
|
+
}
|
|
2914
|
+
pickFocus() {
|
|
2915
|
+
const view = this.viewMonth();
|
|
2916
|
+
const anchor = this.anchorDate();
|
|
2917
|
+
if (anchor && monthOf(anchor) === view && !this.isDayDisabled(anchor))
|
|
2918
|
+
return anchor;
|
|
2919
|
+
const today = todayIso();
|
|
2920
|
+
if (monthOf(today) === view && !this.isDayDisabled(today))
|
|
2921
|
+
return today;
|
|
2922
|
+
return this.firstEnabledInView(view);
|
|
2923
|
+
}
|
|
2924
|
+
firstEnabledInView(view) {
|
|
2925
|
+
const first = `${view}-01`;
|
|
2926
|
+
let date = first;
|
|
2927
|
+
for (let i = 0; i < 31 && monthOf(date) === view; i++) {
|
|
2928
|
+
if (!this.isDayDisabled(date))
|
|
2929
|
+
return date;
|
|
2930
|
+
date = addDays(date, 1);
|
|
2931
|
+
}
|
|
2932
|
+
return first;
|
|
2933
|
+
}
|
|
2934
|
+
announce(message) {
|
|
2935
|
+
// Re-announce identical text by breaking the string equality.
|
|
2936
|
+
this.announcement.set(this.announcement() === message ? `${message} ` : message);
|
|
2937
|
+
}
|
|
2938
|
+
// --- Styling ---------------------------------------------------------------
|
|
2939
|
+
daySize = computed(() => (this.componentTheme().sizes?.[this.size()] ?? {}), ...(ngDevMode ? [{ debugName: "daySize" }] : /* istanbul ignore next */ []));
|
|
2940
|
+
className = computed(() => css([(this.componentTheme().fixed ?? { display: 'inline-block' })]), ...(ngDevMode ? [{ debugName: "className" }] : /* istanbul ignore next */ []));
|
|
2941
|
+
navClass = computed(() => css({
|
|
2942
|
+
display: 'flex',
|
|
2943
|
+
alignItems: 'center',
|
|
2944
|
+
justifyContent: 'space-between',
|
|
2945
|
+
...this.theme.gap('xs'),
|
|
2946
|
+
marginBottom: 4,
|
|
2947
|
+
}), ...(ngDevMode ? [{ debugName: "navClass" }] : /* istanbul ignore next */ []));
|
|
2948
|
+
headingClass = computed(() => css({ flex: 1, textAlign: 'center', ...this.theme.typeface('title-small') }), ...(ngDevMode ? [{ debugName: "headingClass" }] : /* istanbul ignore next */ []));
|
|
2949
|
+
gridClass = computed(() => css({ display: 'grid', ...this.theme.gap(this.componentOptions().gap ?? 'xxs') }), ...(ngDevMode ? [{ debugName: "gridClass" }] : /* istanbul ignore next */ []));
|
|
2950
|
+
rowClass = computed(() => css({
|
|
2951
|
+
display: 'grid',
|
|
2952
|
+
gridTemplateColumns: 'repeat(7, 1fr)',
|
|
2953
|
+
justifyItems: 'center',
|
|
2954
|
+
...this.theme.gap(this.componentOptions().gap ?? 'xxs'),
|
|
2955
|
+
}), ...(ngDevMode ? [{ debugName: "rowClass" }] : /* istanbul ignore next */ []));
|
|
2956
|
+
weekdayClass = computed(() => css({
|
|
2957
|
+
display: 'flex',
|
|
2958
|
+
alignItems: 'center',
|
|
2959
|
+
justifyContent: 'center',
|
|
2960
|
+
...this.daySize(),
|
|
2961
|
+
height: 'auto',
|
|
2962
|
+
...this.theme.typeface(this.componentOptions().typeface ?? 'label'),
|
|
2963
|
+
...this.theme.color('on-background-variant'),
|
|
2964
|
+
'& abbr': { textDecoration: 'none' },
|
|
2965
|
+
}), ...(ngDevMode ? [{ debugName: "weekdayClass" }] : /* istanbul ignore next */ []));
|
|
2966
|
+
/** The gridcell — range bands paint here so they read as one bar. */
|
|
2967
|
+
cellClass = computed(() => css({ position: 'relative', display: 'flex' }), ...(ngDevMode ? [{ debugName: "cellClass" }] : /* istanbul ignore next */ []));
|
|
2968
|
+
inRangeClass = computed(() => css({ ...this.theme.backgroundColor('primary-container'), borderRadius: 0 }), ...(ngDevMode ? [{ debugName: "inRangeClass" }] : /* istanbul ignore next */ []));
|
|
2969
|
+
previewClass = computed(() => {
|
|
2970
|
+
const colors = this.theme.colorPalette();
|
|
2971
|
+
return css({
|
|
2972
|
+
backgroundColor: `color-mix(in srgb, ${colors['primary-container']} 55%, transparent)`,
|
|
2973
|
+
outline: `1px dashed ${colors['primary']}`,
|
|
2974
|
+
outlineOffset: -1,
|
|
2975
|
+
});
|
|
2976
|
+
}, ...(ngDevMode ? [{ debugName: "previewClass" }] : /* istanbul ignore next */ []));
|
|
2977
|
+
bandStartClass = computed(() => css({ ...this.theme.getRadiusLeft(this.componentOptions().dayBorderRadius ?? 'max') }), ...(ngDevMode ? [{ debugName: "bandStartClass" }] : /* istanbul ignore next */ []));
|
|
2978
|
+
bandEndClass = computed(() => css({ ...this.theme.getRadiusRight(this.componentOptions().dayBorderRadius ?? 'max') }), ...(ngDevMode ? [{ debugName: "bandEndClass" }] : /* istanbul ignore next */ []));
|
|
2979
|
+
dayClass = computed(() => {
|
|
2980
|
+
const options = this.componentOptions();
|
|
2981
|
+
const colors = this.theme.colorPalette();
|
|
2982
|
+
return css({
|
|
2983
|
+
position: 'relative',
|
|
2984
|
+
display: 'flex',
|
|
2985
|
+
alignItems: 'center',
|
|
2986
|
+
justifyContent: 'center',
|
|
2987
|
+
border: 0,
|
|
2988
|
+
padding: 0,
|
|
2989
|
+
background: 'transparent',
|
|
2990
|
+
color: 'inherit',
|
|
2991
|
+
cursor: 'pointer',
|
|
2992
|
+
...this.theme.typeface(options.typeface ?? 'label'),
|
|
2993
|
+
...this.daySize(),
|
|
2994
|
+
...this.theme.radius(options.dayBorderRadius ?? 'max'),
|
|
2995
|
+
'&:hover:not(:disabled)': { ...this.theme.colorPair('primary-container') },
|
|
2996
|
+
...this.theme.focusRing(),
|
|
2997
|
+
'&:disabled': {
|
|
2998
|
+
color: colors['on-disabled'],
|
|
2999
|
+
cursor: 'default',
|
|
3000
|
+
pointerEvents: 'none',
|
|
3001
|
+
},
|
|
3002
|
+
});
|
|
3003
|
+
}, ...(ngDevMode ? [{ debugName: "dayClass" }] : /* istanbul ignore next */ []));
|
|
3004
|
+
todayClass = computed(() => {
|
|
3005
|
+
const colors = this.theme.colorPalette();
|
|
3006
|
+
// Outline (or dot), never fill — so today and selected can coincide and
|
|
3007
|
+
// both stay legible (WCAG 1.4.1: no state carried by colour alone).
|
|
3008
|
+
return (this.componentOptions().todayStyle ?? 'outline') === 'outline'
|
|
3009
|
+
? css({ boxShadow: `inset 0 0 0 1.5px ${colors['primary']}` })
|
|
3010
|
+
: css({
|
|
3011
|
+
'&::after': {
|
|
3012
|
+
content: '""',
|
|
3013
|
+
position: 'absolute',
|
|
3014
|
+
bottom: 3,
|
|
3015
|
+
left: '50%',
|
|
3016
|
+
transform: 'translateX(-50%)',
|
|
3017
|
+
width: 4,
|
|
3018
|
+
height: 4,
|
|
3019
|
+
borderRadius: 999,
|
|
3020
|
+
backgroundColor: colors['primary'],
|
|
3021
|
+
},
|
|
3022
|
+
});
|
|
3023
|
+
}, ...(ngDevMode ? [{ debugName: "todayClass" }] : /* istanbul ignore next */ []));
|
|
3024
|
+
selectedClass = computed(() => {
|
|
3025
|
+
const colors = this.theme.colorPalette();
|
|
3026
|
+
return css({
|
|
3027
|
+
...this.theme.colorPair('primary'),
|
|
3028
|
+
// A marker dot survives selection by switching to the on-colour.
|
|
3029
|
+
'& [data-dot]': { backgroundColor: colors['on-primary'] },
|
|
3030
|
+
});
|
|
3031
|
+
}, ...(ngDevMode ? [{ debugName: "selectedClass" }] : /* istanbul ignore next */ []));
|
|
3032
|
+
outsideDayClass = computed(() => {
|
|
3033
|
+
const colors = this.theme.colorPalette();
|
|
3034
|
+
return css({
|
|
3035
|
+
display: 'flex',
|
|
3036
|
+
alignItems: 'center',
|
|
3037
|
+
justifyContent: 'center',
|
|
3038
|
+
...this.theme.typeface(this.componentOptions().typeface ?? 'label'),
|
|
3039
|
+
...this.daySize(),
|
|
3040
|
+
color: colors['on-disabled'],
|
|
3041
|
+
});
|
|
3042
|
+
}, ...(ngDevMode ? [{ debugName: "outsideDayClass" }] : /* istanbul ignore next */ []));
|
|
3043
|
+
dotsClass = computed(() => css({
|
|
3044
|
+
position: 'absolute',
|
|
3045
|
+
bottom: 2,
|
|
3046
|
+
left: 0,
|
|
3047
|
+
right: 0,
|
|
3048
|
+
display: 'flex',
|
|
3049
|
+
justifyContent: 'center',
|
|
3050
|
+
gap: 2,
|
|
3051
|
+
pointerEvents: 'none',
|
|
3052
|
+
}), ...(ngDevMode ? [{ debugName: "dotsClass" }] : /* istanbul ignore next */ []));
|
|
3053
|
+
/** One dot class per marker variant present, resolved from the palette. */
|
|
3054
|
+
dotClasses = computed(() => {
|
|
3055
|
+
const colors = this.theme.colorPalette();
|
|
3056
|
+
const classes = new Map();
|
|
3057
|
+
for (const marker of this.markers()) {
|
|
3058
|
+
const variant = marker.variant ?? 'primary';
|
|
3059
|
+
if (!classes.has(variant)) {
|
|
3060
|
+
classes.set(variant, css({ width: 4, height: 4, borderRadius: 999, backgroundColor: colors[variant] }));
|
|
3061
|
+
}
|
|
3062
|
+
}
|
|
3063
|
+
return classes;
|
|
3064
|
+
}, ...(ngDevMode ? [{ debugName: "dotClasses" }] : /* istanbul ignore next */ []));
|
|
3065
|
+
dotClassFor(variant) {
|
|
3066
|
+
return this.dotClasses().get(variant ?? 'primary') ?? '';
|
|
3067
|
+
}
|
|
3068
|
+
showOutside = computed(() => this.componentOptions().showOutsideDays ?? false, ...(ngDevMode ? [{ debugName: "showOutside" }] : /* istanbul ignore next */ []));
|
|
3069
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: UniCalendarComponent, deps: null, target: i0.ɵɵFactoryTarget.Component });
|
|
3070
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.12", type: UniCalendarComponent, isStandalone: true, selector: "uni-calendar, Calendar", inputs: { value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, touched: { classPropertyName: "touched", publicName: "touched", isSignal: true, isRequired: false, transformFunction: null }, invalid: { classPropertyName: "invalid", publicName: "invalid", isSignal: true, isRequired: false, transformFunction: null }, dirty: { classPropertyName: "dirty", publicName: "dirty", isSignal: true, isRequired: false, transformFunction: null }, required: { classPropertyName: "required", publicName: "required", isSignal: true, isRequired: false, transformFunction: null }, ariaDescribedBy: { classPropertyName: "ariaDescribedBy", publicName: "ariaDescribedBy", isSignal: true, isRequired: false, transformFunction: null }, mode: { classPropertyName: "mode", publicName: "mode", isSignal: true, isRequired: false, transformFunction: null }, month: { classPropertyName: "month", publicName: "month", isSignal: true, isRequired: false, transformFunction: null }, minDate: { classPropertyName: "minDate", publicName: "minDate", isSignal: true, isRequired: false, transformFunction: null }, maxDate: { classPropertyName: "maxDate", publicName: "maxDate", isSignal: true, isRequired: false, transformFunction: null }, disabledDates: { classPropertyName: "disabledDates", publicName: "disabledDates", isSignal: true, isRequired: false, transformFunction: null }, markers: { classPropertyName: "markers", publicName: "markers", isSignal: true, isRequired: false, transformFunction: null }, locale: { classPropertyName: "locale", publicName: "locale", isSignal: true, isRequired: false, transformFunction: null }, weekStart: { classPropertyName: "weekStart", publicName: "weekStart", isSignal: true, isRequired: false, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { value: "valueChange", touched: "touchedChange", month: "monthChange", selected: "selected" }, host: { listeners: { "focusout": "onHostFocusOut($event)" }, properties: { "class": "className()" } }, providers: [{ provide: COMPONENT_NAME, useValue: 'calendar' }], usesInheritance: true, ngImport: i0, template: "<div [class]=\"navClass()\">\n <button\n icon-button\n type=\"button\"\n size=\"sm\"\n [symbolName]=\"componentOptions().navPrevSymbol ?? 'chevron_left'\"\n [disable]=\"disabled()\"\n (click)=\"onNav(-1)\"\n >\n Previous month\n </button>\n <!-- aria-live so PageUp/Down narrates the new month without refocusing. -->\n <div [class]=\"headingClass()\" [id]=\"headingId\" aria-live=\"polite\">{{ heading() }}</div>\n <button\n icon-button\n type=\"button\"\n size=\"sm\"\n [symbolName]=\"componentOptions().navNextSymbol ?? 'chevron_right'\"\n [disable]=\"disabled()\"\n (click)=\"onNav(1)\"\n >\n Next month\n </button>\n</div>\n\n<!-- One tab stop: the roving tabindex lives on the day buttons, and the grid\n itself only relays their bubbling keys (same delegation as tag-input). -->\n<!-- eslint-disable-next-line @angular-eslint/template/interactive-supports-focus -->\n<div\n role=\"grid\"\n [class]=\"gridClass()\"\n [attr.aria-label]=\"ariaLabel() ?? null\"\n [attr.aria-labelledby]=\"ariaLabel() ? null : headingId\"\n [attr.aria-describedby]=\"ariaDescribedBy() ?? null\"\n [attr.aria-invalid]=\"showError() ? true : null\"\n [attr.aria-required]=\"required() ? true : null\"\n (keydown)=\"onGridKeydown($event)\"\n>\n <div role=\"row\" [class]=\"rowClass()\">\n @for (weekday of weekdays(); track weekday.full) {\n <div role=\"columnheader\" [class]=\"weekdayClass()\">\n <abbr [attr.title]=\"weekday.full\">{{ weekday.label }}</abbr>\n </div>\n }\n </div>\n\n @for (week of gridWeeks(); track week[0].date) {\n <div role=\"row\" [class]=\"rowClass()\">\n @for (cell of week; track cell.date) {\n @if (cell.outside) {\n <!-- Outside days: hidden placeholders by default (geometry kept),\n muted and non-interactive under the showOutsideDays option. -->\n <div role=\"gridcell\" aria-hidden=\"true\" [class]=\"cell.cellClass\">\n @if (showOutside()) {\n <span [class]=\"outsideDayClass()\">{{ cell.day }}</span>\n }\n </div>\n } @else {\n <div\n role=\"gridcell\"\n [class]=\"cell.cellClass\"\n [attr.aria-selected]=\"cell.selected || cell.inBand ? true : null\"\n >\n <button\n type=\"button\"\n [class]=\"cell.dayClass\"\n [attr.data-date]=\"cell.date\"\n [tabindex]=\"cell.tabIndex\"\n [attr.aria-label]=\"cell.ariaLabel\"\n [attr.aria-current]=\"cell.today ? 'date' : null\"\n [disabled]=\"cell.disabled || disabled()\"\n (click)=\"select(cell.date)\"\n (mouseenter)=\"onDayHover(cell.date)\"\n >\n {{ cell.day }}\n @if (cell.markers.length) {\n <span [class]=\"dotsClass()\">\n @for (marker of cell.markers; track $index) {\n <span data-dot [class]=\"dotClassFor(marker.variant)\"></span>\n }\n </span>\n }\n </button>\n </div>\n }\n }\n </div>\n }\n</div>\n\n<!-- Selections and range progress are otherwise silent for a screen reader. -->\n<span role=\"status\" aria-live=\"polite\" [class]=\"srOnly\">{{ announcement() }}</span>\n", dependencies: [{ kind: "component", type: UniIconButtonComponent, selector: "button[uni-icon-button], button[icon-button]", inputs: ["ariaLabel", "iconName", "symbolName", "variant", "size", "disable", "loading", "opticalSize"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
3071
|
+
}
|
|
3072
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: UniCalendarComponent, decorators: [{
|
|
3073
|
+
type: Component,
|
|
3074
|
+
args: [{ changeDetection: ChangeDetectionStrategy.OnPush, selector: 'uni-calendar, Calendar', imports: [UniIconButtonComponent], providers: [{ provide: COMPONENT_NAME, useValue: 'calendar' }], host: { '[class]': 'className()', '(focusout)': 'onHostFocusOut($event)' }, template: "<div [class]=\"navClass()\">\n <button\n icon-button\n type=\"button\"\n size=\"sm\"\n [symbolName]=\"componentOptions().navPrevSymbol ?? 'chevron_left'\"\n [disable]=\"disabled()\"\n (click)=\"onNav(-1)\"\n >\n Previous month\n </button>\n <!-- aria-live so PageUp/Down narrates the new month without refocusing. -->\n <div [class]=\"headingClass()\" [id]=\"headingId\" aria-live=\"polite\">{{ heading() }}</div>\n <button\n icon-button\n type=\"button\"\n size=\"sm\"\n [symbolName]=\"componentOptions().navNextSymbol ?? 'chevron_right'\"\n [disable]=\"disabled()\"\n (click)=\"onNav(1)\"\n >\n Next month\n </button>\n</div>\n\n<!-- One tab stop: the roving tabindex lives on the day buttons, and the grid\n itself only relays their bubbling keys (same delegation as tag-input). -->\n<!-- eslint-disable-next-line @angular-eslint/template/interactive-supports-focus -->\n<div\n role=\"grid\"\n [class]=\"gridClass()\"\n [attr.aria-label]=\"ariaLabel() ?? null\"\n [attr.aria-labelledby]=\"ariaLabel() ? null : headingId\"\n [attr.aria-describedby]=\"ariaDescribedBy() ?? null\"\n [attr.aria-invalid]=\"showError() ? true : null\"\n [attr.aria-required]=\"required() ? true : null\"\n (keydown)=\"onGridKeydown($event)\"\n>\n <div role=\"row\" [class]=\"rowClass()\">\n @for (weekday of weekdays(); track weekday.full) {\n <div role=\"columnheader\" [class]=\"weekdayClass()\">\n <abbr [attr.title]=\"weekday.full\">{{ weekday.label }}</abbr>\n </div>\n }\n </div>\n\n @for (week of gridWeeks(); track week[0].date) {\n <div role=\"row\" [class]=\"rowClass()\">\n @for (cell of week; track cell.date) {\n @if (cell.outside) {\n <!-- Outside days: hidden placeholders by default (geometry kept),\n muted and non-interactive under the showOutsideDays option. -->\n <div role=\"gridcell\" aria-hidden=\"true\" [class]=\"cell.cellClass\">\n @if (showOutside()) {\n <span [class]=\"outsideDayClass()\">{{ cell.day }}</span>\n }\n </div>\n } @else {\n <div\n role=\"gridcell\"\n [class]=\"cell.cellClass\"\n [attr.aria-selected]=\"cell.selected || cell.inBand ? true : null\"\n >\n <button\n type=\"button\"\n [class]=\"cell.dayClass\"\n [attr.data-date]=\"cell.date\"\n [tabindex]=\"cell.tabIndex\"\n [attr.aria-label]=\"cell.ariaLabel\"\n [attr.aria-current]=\"cell.today ? 'date' : null\"\n [disabled]=\"cell.disabled || disabled()\"\n (click)=\"select(cell.date)\"\n (mouseenter)=\"onDayHover(cell.date)\"\n >\n {{ cell.day }}\n @if (cell.markers.length) {\n <span [class]=\"dotsClass()\">\n @for (marker of cell.markers; track $index) {\n <span data-dot [class]=\"dotClassFor(marker.variant)\"></span>\n }\n </span>\n }\n </button>\n </div>\n }\n }\n </div>\n }\n</div>\n\n<!-- Selections and range progress are otherwise silent for a screen reader. -->\n<span role=\"status\" aria-live=\"polite\" [class]=\"srOnly\">{{ announcement() }}</span>\n" }]
|
|
3075
|
+
}], propDecorators: { value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }, { type: i0.Output, args: ["valueChange"] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], touched: [{ type: i0.Input, args: [{ isSignal: true, alias: "touched", required: false }] }, { type: i0.Output, args: ["touchedChange"] }], invalid: [{ type: i0.Input, args: [{ isSignal: true, alias: "invalid", required: false }] }], dirty: [{ type: i0.Input, args: [{ isSignal: true, alias: "dirty", required: false }] }], required: [{ type: i0.Input, args: [{ isSignal: true, alias: "required", required: false }] }], ariaDescribedBy: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaDescribedBy", required: false }] }], mode: [{ type: i0.Input, args: [{ isSignal: true, alias: "mode", required: false }] }], month: [{ type: i0.Input, args: [{ isSignal: true, alias: "month", required: false }] }, { type: i0.Output, args: ["monthChange"] }], minDate: [{ type: i0.Input, args: [{ isSignal: true, alias: "minDate", required: false }] }], maxDate: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxDate", required: false }] }], disabledDates: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabledDates", required: false }] }], markers: [{ type: i0.Input, args: [{ isSignal: true, alias: "markers", required: false }] }], locale: [{ type: i0.Input, args: [{ isSignal: true, alias: "locale", required: false }] }], weekStart: [{ type: i0.Input, args: [{ isSignal: true, alias: "weekStart", required: false }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], selected: [{ type: i0.Output, args: ["selected"] }] } });
|
|
3076
|
+
|
|
3077
|
+
class UniCardContentComponent extends BaseComponent {
|
|
3078
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: UniCardContentComponent, deps: null, target: i0.ɵɵFactoryTarget.Component });
|
|
3079
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.2.12", type: UniCardContentComponent, isStandalone: true, selector: "uni-card-content", providers: [{ provide: COMPONENT_NAME, useValue: 'cardContent' }], usesInheritance: true, ngImport: i0, template: "<div [style]=\"style()\">\n <ng-content></ng-content>\n</div>\n", styles: [""], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
3080
|
+
}
|
|
3081
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: UniCardContentComponent, decorators: [{
|
|
3082
|
+
type: Component,
|
|
3083
|
+
args: [{ changeDetection: ChangeDetectionStrategy.OnPush, selector: 'uni-card-content', imports: [], providers: [{ provide: COMPONENT_NAME, useValue: 'cardContent' }], template: "<div [style]=\"style()\">\n <ng-content></ng-content>\n</div>\n" }]
|
|
3084
|
+
}] });
|
|
3085
|
+
|
|
3086
|
+
/**
|
|
3087
|
+
* Typeface defaults by host element, so semantic HTML and the type scale
|
|
3088
|
+
* reinforce each other: `<h1 uni-text>` is already headline-large.
|
|
3089
|
+
*/
|
|
3090
|
+
const TagTypefaces = {
|
|
3091
|
+
h1: 'headline-large',
|
|
3092
|
+
h2: 'headline-medium',
|
|
3093
|
+
h3: 'headline-small',
|
|
3094
|
+
h4: 'title-large',
|
|
3095
|
+
h5: 'title-medium',
|
|
3096
|
+
h6: 'title-small',
|
|
3097
|
+
p: 'body-1-long',
|
|
3098
|
+
small: 'caption',
|
|
3099
|
+
figcaption: 'caption',
|
|
3100
|
+
blockquote: 'quote',
|
|
3101
|
+
label: 'label',
|
|
3102
|
+
};
|
|
3103
|
+
/**
|
|
3104
|
+
* The typography primitive, applied as an attribute to any element so
|
|
3105
|
+
* semantics stay yours. The attribute value is the typeface:
|
|
3106
|
+
* `<h1 uni-text="display-small">`, `<span uni-text="caption">`, dynamic via
|
|
3107
|
+
* `[uni-text]="role()"`. With no value, the typeface is inferred from the
|
|
3108
|
+
* host element (h1 → headline-large, p → body-1-long, …), falling back to
|
|
3109
|
+
* `title-small`; the `typeface` input remains as an explicit override.
|
|
3110
|
+
*/
|
|
3111
|
+
class UniTextComponent {
|
|
3112
|
+
theme = inject(ThemeService);
|
|
3113
|
+
tag = inject(ElementRef).nativeElement.tagName.toLowerCase();
|
|
3114
|
+
/** Typeface via the selector attribute: `uni-text="headline-large"`. */
|
|
3115
|
+
uniText = input('', { ...(ngDevMode ? { debugName: "uniText" } : /* istanbul ignore next */ {}), alias: 'uni-text' });
|
|
3116
|
+
/** Explicit typeface; the attribute value wins when both are set. */
|
|
3117
|
+
typeface = input(undefined, ...(ngDevMode ? [{ debugName: "typeface" }] : /* istanbul ignore next */ []));
|
|
3118
|
+
color = input(...(ngDevMode ? [undefined, { debugName: "color" }] : /* istanbul ignore next */ []));
|
|
3119
|
+
display = input(...(ngDevMode ? [undefined, { debugName: "display" }] : /* istanbul ignore next */ []));
|
|
3120
|
+
align = input(...(ngDevMode ? [undefined, { debugName: "align" }] : /* istanbul ignore next */ []));
|
|
3121
|
+
nowrap = input(...(ngDevMode ? [undefined, { debugName: "nowrap" }] : /* istanbul ignore next */ []));
|
|
3122
|
+
maxWidth = input(...(ngDevMode ? [undefined, { debugName: "maxWidth" }] : /* istanbul ignore next */ []));
|
|
3123
|
+
ellipsis = input(false, ...(ngDevMode ? [{ debugName: "ellipsis" }] : /* istanbul ignore next */ []));
|
|
3124
|
+
resolvedTypeface = computed(() => this.uniText() || this.typeface() || TagTypefaces[this.tag] || 'title-small', ...(ngDevMode ? [{ debugName: "resolvedTypeface" }] : /* istanbul ignore next */ []));
|
|
3125
|
+
className = computed(() => {
|
|
3126
|
+
return css([
|
|
3127
|
+
{
|
|
3128
|
+
...this.theme.typeface(this.resolvedTypeface()),
|
|
3129
|
+
...this.theme.color(this.color()),
|
|
3130
|
+
display: this.display(),
|
|
3131
|
+
},
|
|
3132
|
+
this.align() && {
|
|
3133
|
+
textAlign: this.align(),
|
|
3134
|
+
},
|
|
3135
|
+
this.nowrap() && {
|
|
3136
|
+
whiteSpace: 'nowrap',
|
|
3137
|
+
},
|
|
3138
|
+
this.maxWidth() && {
|
|
3139
|
+
maxWidth: this.maxWidth(),
|
|
3140
|
+
overflow: 'hidden',
|
|
3141
|
+
whiteSpace: 'nowrap',
|
|
3142
|
+
textOverflow: 'ellipsis',
|
|
3143
|
+
display: 'inline-block',
|
|
2209
3144
|
},
|
|
2210
3145
|
this.ellipsis() && {
|
|
2211
3146
|
whiteSpace: 'nowrap',
|
|
@@ -2376,19 +3311,13 @@ class UniCheckboxComponent extends BaseComponent {
|
|
|
2376
3311
|
'&:disabled + .checkbox': {
|
|
2377
3312
|
cursor: 'not-allowed',
|
|
2378
3313
|
},
|
|
3314
|
+
// The shared, themable focus indicator, keyed off the hidden input's
|
|
3315
|
+
// focus. The ring sits out from the box, so its radius carries an extra
|
|
3316
|
+
// 4px to round proportionally (as the original hand-drawn ring did) —
|
|
3317
|
+
// without it the corners gap away from the box.
|
|
2379
3318
|
'&:focus + .checkbox': {
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
content: '""',
|
|
2383
|
-
position: 'absolute',
|
|
2384
|
-
top: '-4px',
|
|
2385
|
-
left: '-4px',
|
|
2386
|
-
right: '-4px',
|
|
2387
|
-
bottom: '-4px',
|
|
2388
|
-
border: `2px solid ${this.getThemeColor(this.variant())}`,
|
|
2389
|
-
borderRadius: `${(Number(this.componentOptions().borderRadius) || 2) + 4}px`,
|
|
2390
|
-
pointerEvents: 'none',
|
|
2391
|
-
},
|
|
3319
|
+
...this.theme.focusRingStyle(this.getThemeColor(this.variant()), this.componentOptions().focusRingGap),
|
|
3320
|
+
borderRadius: `${(Number(this.componentOptions().borderRadius) || 2) + 2}px`,
|
|
2392
3321
|
},
|
|
2393
3322
|
}), ...(ngDevMode ? [{ debugName: "checkboxInput" }] : /* istanbul ignore next */ []));
|
|
2394
3323
|
getThemeColor(token) {
|
|
@@ -2408,196 +3337,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImpo
|
|
|
2408
3337
|
args: [{ selector: 'uni-checkbox', imports: [UniTextComponent], providers: [{ provide: COMPONENT_NAME, useValue: 'checkbox' }], changeDetection: ChangeDetectionStrategy.OnPush, template: "<label [class]=\"checkboxLabel()\">\n <input\n type=\"checkbox\"\n [class]=\"checkboxInput()\"\n [checked]=\"checked()\"\n [indeterminate]=\"indeterminate()\"\n (change)=\"handleChange($event)\"\n [disabled]=\"disabled()\"\n [attr.aria-invalid]=\"showError() ? true : null\"\n [attr.aria-required]=\"required() ? true : null\"\n [attr.aria-describedby]=\"ariaDescribedBy() || null\"\n />\n <div class=\"checkbox\">\n <svg viewBox=\"0 0 20 20\" aria-hidden=\"true\">\n <rect class=\"checkbox-box\" x=\"1\" y=\"1\" width=\"18\" height=\"18\"></rect>\n <polyline class=\"checkbox-check\" points=\"4 11 8 15 16 6\"></polyline>\n <line class=\"checkbox-dash\" x1=\"5\" y1=\"10\" x2=\"15\" y2=\"10\"></line>\n </svg>\n </div>\n @if (label()) {\n <span uni-text=\"label\">{{ label() }}</span>\n }\n</label>\n" }]
|
|
2409
3338
|
}], propDecorators: { checked: [{ type: i0.Input, args: [{ isSignal: true, alias: "checked", required: false }] }, { type: i0.Output, args: ["checkedChange"] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], touched: [{ type: i0.Input, args: [{ isSignal: true, alias: "touched", required: false }] }, { type: i0.Output, args: ["touchedChange"] }], invalid: [{ type: i0.Input, args: [{ isSignal: true, alias: "invalid", required: false }] }], dirty: [{ type: i0.Input, args: [{ isSignal: true, alias: "dirty", required: false }] }], required: [{ type: i0.Input, args: [{ isSignal: true, alias: "required", required: false }] }], ariaDescribedBy: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaDescribedBy", required: false }] }], label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], indeterminate: [{ type: i0.Input, args: [{ isSignal: true, alias: "indeterminate", required: false }] }, { type: i0.Output, args: ["indeterminateChange"] }] } });
|
|
2410
3339
|
|
|
2411
|
-
class BodyRenderDirective {
|
|
2412
|
-
el = inject(ElementRef);
|
|
2413
|
-
renderer = inject(Renderer2);
|
|
2414
|
-
ngOnInit() {
|
|
2415
|
-
const element = this.el.nativeElement;
|
|
2416
|
-
this.renderer.appendChild(document.body, element);
|
|
2417
|
-
}
|
|
2418
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: BodyRenderDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
2419
|
-
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.12", type: BodyRenderDirective, isStandalone: true, selector: "[uniBodyRender]", ngImport: i0 });
|
|
2420
|
-
}
|
|
2421
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: BodyRenderDirective, decorators: [{
|
|
2422
|
-
type: Directive,
|
|
2423
|
-
args: [{
|
|
2424
|
-
selector: '[uniBodyRender]',
|
|
2425
|
-
}]
|
|
2426
|
-
}] });
|
|
2427
|
-
|
|
2428
|
-
class DragAndDropDirective {
|
|
2429
|
-
// TODO(v4): rename to fileDropped — renaming is breaking
|
|
2430
|
-
// eslint-disable-next-line @angular-eslint/no-output-on-prefix
|
|
2431
|
-
onFileDropped = output();
|
|
2432
|
-
workspaceOpacity = signal('1', ...(ngDevMode ? [{ debugName: "workspaceOpacity" }] : /* istanbul ignore next */ []));
|
|
2433
|
-
// Dragover listener, when files are dragged over our host element
|
|
2434
|
-
onDragOver(event) {
|
|
2435
|
-
event.preventDefault();
|
|
2436
|
-
event.stopPropagation();
|
|
2437
|
-
this.workspaceOpacity.set('0.5');
|
|
2438
|
-
}
|
|
2439
|
-
// Dragleave listener, when files are dragged away from our host element
|
|
2440
|
-
onDragLeave(event) {
|
|
2441
|
-
event.preventDefault();
|
|
2442
|
-
event.stopPropagation();
|
|
2443
|
-
this.workspaceOpacity.set('1');
|
|
2444
|
-
}
|
|
2445
|
-
// Drop listener, when files are dropped on our host element
|
|
2446
|
-
onDrop(event) {
|
|
2447
|
-
event.preventDefault();
|
|
2448
|
-
event.stopPropagation();
|
|
2449
|
-
this.workspaceOpacity.set('1');
|
|
2450
|
-
const files = event.dataTransfer?.files;
|
|
2451
|
-
if (files && files.length > 0) {
|
|
2452
|
-
this.onFileDropped.emit(files);
|
|
2453
|
-
}
|
|
2454
|
-
}
|
|
2455
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: DragAndDropDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
2456
|
-
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.12", type: DragAndDropDirective, isStandalone: true, selector: "[uni-drag-n-drop], [dragAndDrop]", outputs: { onFileDropped: "onFileDropped" }, host: { listeners: { "dragover": "onDragOver($event)", "dragleave": "onDragLeave($event)", "drop": "onDrop($event)" }, properties: { "style.opacity": "workspaceOpacity()" } }, ngImport: i0 });
|
|
2457
|
-
}
|
|
2458
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: DragAndDropDirective, decorators: [{
|
|
2459
|
-
type: Directive,
|
|
2460
|
-
args: [{
|
|
2461
|
-
selector: '[uni-drag-n-drop], [dragAndDrop]',
|
|
2462
|
-
host: {
|
|
2463
|
-
'[style.opacity]': 'workspaceOpacity()',
|
|
2464
|
-
'(dragover)': 'onDragOver($event)',
|
|
2465
|
-
'(dragleave)': 'onDragLeave($event)',
|
|
2466
|
-
'(drop)': 'onDrop($event)',
|
|
2467
|
-
},
|
|
2468
|
-
}]
|
|
2469
|
-
}], propDecorators: { onFileDropped: [{ type: i0.Output, args: ["onFileDropped"] }] } });
|
|
2470
|
-
|
|
2471
|
-
class UniIconButtonComponent {
|
|
2472
|
-
theme = inject(ThemeService);
|
|
2473
|
-
config = this.theme.component('iconButton');
|
|
2474
|
-
/**
|
|
2475
|
-
* Accessible name for the button. Alternative to projecting text content
|
|
2476
|
-
* (`<button icon-button>Close</button>`); one of the two is required for
|
|
2477
|
-
* an icon-only button to be announced correctly.
|
|
2478
|
-
*/
|
|
2479
|
-
ariaLabel = input(...(ngDevMode ? [undefined, { debugName: "ariaLabel" }] : /* istanbul ignore next */ []));
|
|
2480
|
-
iconName = input(...(ngDevMode ? [undefined, { debugName: "iconName" }] : /* istanbul ignore next */ []));
|
|
2481
|
-
symbolName = input(...(ngDevMode ? [undefined, { debugName: "symbolName" }] : /* istanbul ignore next */ []));
|
|
2482
|
-
variant = input('ghost', ...(ngDevMode ? [{ debugName: "variant" }] : /* istanbul ignore next */ []));
|
|
2483
|
-
size = input('lg', ...(ngDevMode ? [{ debugName: "size" }] : /* istanbul ignore next */ []));
|
|
2484
|
-
disable = input(...(ngDevMode ? [undefined, { debugName: "disable" }] : /* istanbul ignore next */ []));
|
|
2485
|
-
loading = input(...(ngDevMode ? [undefined, { debugName: "loading" }] : /* istanbul ignore next */ []));
|
|
2486
|
-
opticalSize = input(24, ...(ngDevMode ? [{ debugName: "opticalSize" }] : /* istanbul ignore next */ []));
|
|
2487
|
-
srOnlyClass = css(visuallyHidden);
|
|
2488
|
-
/**
|
|
2489
|
-
* Sizes `iconName` from the size token's `fontSize`, the same value that sizes
|
|
2490
|
-
* a `symbolName` ligature — so the two paths render the same glyph size and
|
|
2491
|
-
* `symbolName` → `iconName` is a like-for-like swap. Without it a masked icon
|
|
2492
|
-
* fills the whole button box, since the base size tokens carry no padding.
|
|
2493
|
-
* Themes that do use padding (Carbon) set a matching `fontSize`, so they land
|
|
2494
|
-
* on the same glyph either way.
|
|
2495
|
-
*/
|
|
2496
|
-
glyphSize = computed(() => {
|
|
2497
|
-
// Size tokens are Emotion style objects, so `fontSize` is typed wider than
|
|
2498
|
-
// a CSS length; anything exotic falls back to filling the button as before.
|
|
2499
|
-
const fontSize = this.config().sizes?.[this.size()]?.fontSize;
|
|
2500
|
-
return typeof fontSize === 'number' || typeof fontSize === 'string' ? fontSize : undefined;
|
|
2501
|
-
}, ...(ngDevMode ? [{ debugName: "glyphSize" }] : /* istanbul ignore next */ []));
|
|
2502
|
-
className = computed(() => {
|
|
2503
|
-
const { sizes, variants } = this.config();
|
|
2504
|
-
const sizeConfig = sizes && sizes[this.size()];
|
|
2505
|
-
const colorConfig = variants && variants[this.variant()];
|
|
2506
|
-
return css([
|
|
2507
|
-
{
|
|
2508
|
-
position: 'relative',
|
|
2509
|
-
overflow: 'hidden',
|
|
2510
|
-
outline: 0,
|
|
2511
|
-
border: 0,
|
|
2512
|
-
cursor: 'pointer',
|
|
2513
|
-
transition: 'all 0.28s ease',
|
|
2514
|
-
// Token-driven radius (`max` = circle) with the legacy 999 fallback
|
|
2515
|
-
// for hand-authored themes that predate iconButton options.
|
|
2516
|
-
...(this.theme.radius(this.config().options?.borderRadius) ?? { borderRadius: 999 }),
|
|
2517
|
-
// Block-level, but centring: the size tokens make the box bigger than
|
|
2518
|
-
// the glyph (an `sm` button is 22px around an 18px icon), so a plain
|
|
2519
|
-
// `display: block` parks the glyph in the top-left corner. Flex is
|
|
2520
|
-
// still block-level, so nothing about the button's own layout changes.
|
|
2521
|
-
// The accessible-name span is absolutely positioned and so stays out
|
|
2522
|
-
// of the flex flow.
|
|
2523
|
-
display: 'flex',
|
|
2524
|
-
alignItems: 'center',
|
|
2525
|
-
justifyContent: 'center',
|
|
2526
|
-
'&:disabled': {
|
|
2527
|
-
cursor: 'not-allowed !important',
|
|
2528
|
-
},
|
|
2529
|
-
'& symbol': {
|
|
2530
|
-
fontSize: 'inherit',
|
|
2531
|
-
lineHeight: 'inherit',
|
|
2532
|
-
},
|
|
2533
|
-
},
|
|
2534
|
-
sizeConfig && {
|
|
2535
|
-
...sizeConfig,
|
|
2536
|
-
},
|
|
2537
|
-
colorConfig && {
|
|
2538
|
-
...colorConfig,
|
|
2539
|
-
},
|
|
2540
|
-
this.symbolName() &&
|
|
2541
|
-
!this.loading() && {
|
|
2542
|
-
padding: 0,
|
|
2543
|
-
},
|
|
2544
|
-
this.variant() !== 'ghost' && {
|
|
2545
|
-
'&:hover': {
|
|
2546
|
-
...this.theme.boxShadow('raised'),
|
|
2547
|
-
},
|
|
2548
|
-
},
|
|
2549
|
-
this.variant() === 'ghost' && {
|
|
2550
|
-
'&:hover': {
|
|
2551
|
-
backgroundColor: 'rgba(0,0,0,0.1)',
|
|
2552
|
-
},
|
|
2553
|
-
},
|
|
2554
|
-
!this.loading() && {
|
|
2555
|
-
'&:disabled': {
|
|
2556
|
-
...this.config().variants?.disabled,
|
|
2557
|
-
},
|
|
2558
|
-
},
|
|
2559
|
-
]);
|
|
2560
|
-
}, ...(ngDevMode ? [{ debugName: "className" }] : /* istanbul ignore next */ []));
|
|
2561
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: UniIconButtonComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
2562
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.12", type: UniIconButtonComponent, isStandalone: true, selector: "button[uni-icon-button], button[icon-button]", inputs: { ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", isSignal: true, isRequired: false, transformFunction: null }, iconName: { classPropertyName: "iconName", publicName: "iconName", isSignal: true, isRequired: false, transformFunction: null }, symbolName: { classPropertyName: "symbolName", publicName: "symbolName", isSignal: true, isRequired: false, transformFunction: null }, variant: { classPropertyName: "variant", publicName: "variant", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null }, disable: { classPropertyName: "disable", publicName: "disable", isSignal: true, isRequired: false, transformFunction: null }, loading: { classPropertyName: "loading", publicName: "loading", isSignal: true, isRequired: false, transformFunction: null }, opticalSize: { classPropertyName: "opticalSize", publicName: "opticalSize", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "attr.disabled": "disable() || loading() || null", "attr.aria-busy": "loading() ? 'true' : null", "attr.aria-label": "ariaLabel() || null", "class": "className()" } }, hostDirectives: [{ directive: RippleDirective }], ngImport: i0, template: `
|
|
2563
|
-
@if (loading()) {
|
|
2564
|
-
<uni-icon name="spinner" />
|
|
2565
|
-
} @else if (symbolName()) {
|
|
2566
|
-
<uni-symbol [name]="symbolName()!" [opticalSize]="opticalSize()" />
|
|
2567
|
-
} @else if (iconName()) {
|
|
2568
|
-
<uni-icon [name]="iconName()!" [size]="glyphSize()" />
|
|
2569
|
-
}
|
|
2570
|
-
<!-- Projected text is the button's accessible name (visually hidden) -->
|
|
2571
|
-
<span [class]="srOnlyClass"><ng-content /></span>
|
|
2572
|
-
`, isInline: true, dependencies: [{ kind: "component", type: UniSymbolComponent, selector: "uni-symbol", inputs: ["name", "fill", "weight", "grade", "opticalSize"] }, { kind: "component", type: UniIconComponent, selector: "uni-icon", inputs: ["color", "name", "size"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
2573
|
-
}
|
|
2574
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: UniIconButtonComponent, decorators: [{
|
|
2575
|
-
type: Component,
|
|
2576
|
-
args: [{
|
|
2577
|
-
selector: 'button[uni-icon-button], button[icon-button]',
|
|
2578
|
-
imports: [UniSymbolComponent, UniIconComponent],
|
|
2579
|
-
template: `
|
|
2580
|
-
@if (loading()) {
|
|
2581
|
-
<uni-icon name="spinner" />
|
|
2582
|
-
} @else if (symbolName()) {
|
|
2583
|
-
<uni-symbol [name]="symbolName()!" [opticalSize]="opticalSize()" />
|
|
2584
|
-
} @else if (iconName()) {
|
|
2585
|
-
<uni-icon [name]="iconName()!" [size]="glyphSize()" />
|
|
2586
|
-
}
|
|
2587
|
-
<!-- Projected text is the button's accessible name (visually hidden) -->
|
|
2588
|
-
<span [class]="srOnlyClass"><ng-content /></span>
|
|
2589
|
-
`,
|
|
2590
|
-
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
2591
|
-
host: {
|
|
2592
|
-
'[attr.disabled]': 'disable() || loading() || null',
|
|
2593
|
-
'[attr.aria-busy]': "loading() ? 'true' : null",
|
|
2594
|
-
'[attr.aria-label]': 'ariaLabel() || null',
|
|
2595
|
-
'[class]': 'className()',
|
|
2596
|
-
},
|
|
2597
|
-
hostDirectives: [{ directive: RippleDirective }],
|
|
2598
|
-
}]
|
|
2599
|
-
}], propDecorators: { ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }], iconName: [{ type: i0.Input, args: [{ isSignal: true, alias: "iconName", required: false }] }], symbolName: [{ type: i0.Input, args: [{ isSignal: true, alias: "symbolName", required: false }] }], variant: [{ type: i0.Input, args: [{ isSignal: true, alias: "variant", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], disable: [{ type: i0.Input, args: [{ isSignal: true, alias: "disable", required: false }] }], loading: [{ type: i0.Input, args: [{ isSignal: true, alias: "loading", required: false }] }], opticalSize: [{ type: i0.Input, args: [{ isSignal: true, alias: "opticalSize", required: false }] }] } });
|
|
2600
|
-
|
|
2601
3340
|
class UniDataSearchComponent extends BaseComponent {
|
|
2602
3341
|
datasource = input(...(ngDevMode ? [undefined, { debugName: "datasource" }] : /* istanbul ignore next */ []));
|
|
2603
3342
|
placeholder = input('Search', ...(ngDevMode ? [{ debugName: "placeholder" }] : /* istanbul ignore next */ []));
|
|
@@ -3077,6 +3816,1021 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImpo
|
|
|
3077
3816
|
* This file exports all public-facing elements of the data-table component.
|
|
3078
3817
|
*/
|
|
3079
3818
|
|
|
3819
|
+
class UniDropdownComponent extends BaseComponent {
|
|
3820
|
+
renderer = inject(Renderer2);
|
|
3821
|
+
delay = 100;
|
|
3822
|
+
// Reactively track visibility status using Signals
|
|
3823
|
+
showing = signal(false, ...(ngDevMode ? [{ debugName: "showing" }] : /* istanbul ignore next */ []));
|
|
3824
|
+
trigger = input.required(...(ngDevMode ? [{ debugName: "trigger" }] : /* istanbul ignore next */ []));
|
|
3825
|
+
placement = input('bottom-start', ...(ngDevMode ? [{ debugName: "placement" }] : /* istanbul ignore next */ []));
|
|
3826
|
+
offset = input({ mainAxis: 4, alignmentAxis: 12 }, ...(ngDevMode ? [{ debugName: "offset" }] : /* istanbul ignore next */ []));
|
|
3827
|
+
/** CSS anchor-name linking the trigger to the popover panel. */
|
|
3828
|
+
anchorName = newAnchorName();
|
|
3829
|
+
/**
|
|
3830
|
+
* Value for aria-haspopup on the trigger, describing what the popover
|
|
3831
|
+
* contains (e.g. 'menu' for Menu, 'dialog' for rich content). When unset,
|
|
3832
|
+
* only aria-expanded/aria-controls are managed.
|
|
3833
|
+
*/
|
|
3834
|
+
ariaHasPopup = input(null, ...(ngDevMode ? [{ debugName: "ariaHasPopup" }] : /* istanbul ignore next */ []));
|
|
3835
|
+
/** Document-unique id of the popover element, for aria-controls wiring. */
|
|
3836
|
+
popoverId = uniqueId('uni-dropdown');
|
|
3837
|
+
paddingVertical = input(...(ngDevMode ? [undefined, { debugName: "paddingVertical" }] : /* istanbul ignore next */ []));
|
|
3838
|
+
paddingHorizontal = input(...(ngDevMode ? [undefined, { debugName: "paddingHorizontal" }] : /* istanbul ignore next */ []));
|
|
3839
|
+
// Per-instance panel-chrome overrides; undefined falls back to the theme's
|
|
3840
|
+
// `dropdown` options, so hosts like uni-menu can restyle their panel
|
|
3841
|
+
// without forking the shared dropdown entry.
|
|
3842
|
+
border = input(...(ngDevMode ? [undefined, { debugName: "border" }] : /* istanbul ignore next */ []));
|
|
3843
|
+
borderRadius = input(...(ngDevMode ? [undefined, { debugName: "borderRadius" }] : /* istanbul ignore next */ []));
|
|
3844
|
+
shadow = input(...(ngDevMode ? [undefined, { debugName: "shadow" }] : /* istanbul ignore next */ []));
|
|
3845
|
+
color = input(...(ngDevMode ? [undefined, { debugName: "color" }] : /* istanbul ignore next */ []));
|
|
3846
|
+
dropdownShowing = output();
|
|
3847
|
+
dropdownHiding = output();
|
|
3848
|
+
dropdownRef;
|
|
3849
|
+
get _trigger() {
|
|
3850
|
+
return this.trigger();
|
|
3851
|
+
}
|
|
3852
|
+
get _dropdown() {
|
|
3853
|
+
return this.dropdownRef.nativeElement;
|
|
3854
|
+
}
|
|
3855
|
+
transformOriginMap = {
|
|
3856
|
+
top: 'bottom center',
|
|
3857
|
+
right: 'center left',
|
|
3858
|
+
bottom: 'top center',
|
|
3859
|
+
left: 'center right',
|
|
3860
|
+
'top-start': 'bottom left',
|
|
3861
|
+
'top-end': 'bottom right',
|
|
3862
|
+
'right-start': 'top left',
|
|
3863
|
+
'right-end': 'bottom left',
|
|
3864
|
+
'bottom-start': 'top left',
|
|
3865
|
+
'bottom-end': 'top right',
|
|
3866
|
+
'left-start': 'top right',
|
|
3867
|
+
'left-end': 'bottom right',
|
|
3868
|
+
};
|
|
3869
|
+
dropdownClass = computed(() => {
|
|
3870
|
+
const currentPlacement = this.placement();
|
|
3871
|
+
return css([
|
|
3872
|
+
{
|
|
3873
|
+
// Reset browser agent default popover styles
|
|
3874
|
+
border: 'none',
|
|
3875
|
+
background: 'transparent',
|
|
3876
|
+
padding: 0,
|
|
3877
|
+
overflow: 'visible',
|
|
3878
|
+
width: 'max-content',
|
|
3879
|
+
// Native anchor positioning: the browser keeps the panel attached to
|
|
3880
|
+
// the trigger (no scroll/resize listeners needed)
|
|
3881
|
+
...anchorStyles(this.anchorName, currentPlacement, this.offset()),
|
|
3882
|
+
// 2. Animate discrete properties across top layer layout contexts
|
|
3883
|
+
transitionProperty: 'transform, opacity, display, overlay',
|
|
3884
|
+
transitionDuration: `${this.delay}ms`,
|
|
3885
|
+
transitionTimingFunction: 'linear',
|
|
3886
|
+
transitionBehavior: 'allow-discrete',
|
|
3887
|
+
// Hidden State (Closed)
|
|
3888
|
+
opacity: 0,
|
|
3889
|
+
transform: 'scale(0.8)',
|
|
3890
|
+
transformOrigin: this.transformOriginMap[currentPlacement],
|
|
3891
|
+
// 3. Active state styling controlled via the native browser pseudo-class
|
|
3892
|
+
['&:popover-open']: {
|
|
3893
|
+
opacity: 1,
|
|
3894
|
+
transform: 'scale(1)',
|
|
3895
|
+
},
|
|
3896
|
+
// 4. Starting-style rules what properties animate *from* when transitioning in
|
|
3897
|
+
['@starting-style']: {
|
|
3898
|
+
['&:popover-open']: {
|
|
3899
|
+
opacity: 0,
|
|
3900
|
+
transform: 'scale(0.8)',
|
|
3901
|
+
},
|
|
3902
|
+
},
|
|
3903
|
+
},
|
|
3904
|
+
]);
|
|
3905
|
+
}, ...(ngDevMode ? [{ debugName: "dropdownClass" }] : /* istanbul ignore next */ []));
|
|
3906
|
+
/** The element that receives focus and carries the ARIA popup state. */
|
|
3907
|
+
get _focusTarget() {
|
|
3908
|
+
return resolveFocusTarget(this._trigger);
|
|
3909
|
+
}
|
|
3910
|
+
ngOnInit() {
|
|
3911
|
+
// Single native click binding to manage open/close commands
|
|
3912
|
+
this.renderer.listen(this._trigger, 'click', (e) => {
|
|
3913
|
+
e.stopPropagation();
|
|
3914
|
+
this.toggleDropdown();
|
|
3915
|
+
});
|
|
3916
|
+
// Anchor the popover panel to the trigger element
|
|
3917
|
+
this.renderer.setStyle(this._trigger, 'anchor-name', this.anchorName);
|
|
3918
|
+
// Wire the ARIA popup contract onto the focusable trigger element
|
|
3919
|
+
const focusTarget = this._focusTarget;
|
|
3920
|
+
this.renderer.setAttribute(focusTarget, 'aria-expanded', 'false');
|
|
3921
|
+
this.renderer.setAttribute(focusTarget, 'aria-controls', this.popoverId);
|
|
3922
|
+
if (this.ariaHasPopup()) {
|
|
3923
|
+
this.renderer.setAttribute(focusTarget, 'aria-haspopup', this.ariaHasPopup());
|
|
3924
|
+
}
|
|
3925
|
+
// Sync state if user invokes light-dismiss via outside click or Escape key
|
|
3926
|
+
this.renderer.listen(this._dropdown, 'toggle', (event) => {
|
|
3927
|
+
const isOpened = event.newState === 'open';
|
|
3928
|
+
this.showing.set(isOpened);
|
|
3929
|
+
this.renderer.setAttribute(this._focusTarget, 'aria-expanded', `${isOpened}`);
|
|
3930
|
+
if (isOpened) {
|
|
3931
|
+
this.dropdownShowing.emit(true);
|
|
3932
|
+
}
|
|
3933
|
+
else {
|
|
3934
|
+
this.dropdownHiding.emit(true);
|
|
3935
|
+
this.restoreFocus();
|
|
3936
|
+
}
|
|
3937
|
+
});
|
|
3938
|
+
}
|
|
3939
|
+
/**
|
|
3940
|
+
* Returns focus to the trigger when the popover closes while focus was
|
|
3941
|
+
* inside it (or was dropped on <body> by the top layer closing), so
|
|
3942
|
+
* keyboard users are never stranded (WCAG 2.4.3).
|
|
3943
|
+
*/
|
|
3944
|
+
restoreFocus() {
|
|
3945
|
+
const active = document.activeElement;
|
|
3946
|
+
if (active === document.body || (active && this._dropdown.contains(active))) {
|
|
3947
|
+
this._focusTarget.focus();
|
|
3948
|
+
}
|
|
3949
|
+
}
|
|
3950
|
+
toggleDropdown() {
|
|
3951
|
+
if (this.showing()) {
|
|
3952
|
+
this._dropdown.hidePopover();
|
|
3953
|
+
}
|
|
3954
|
+
else {
|
|
3955
|
+
this._dropdown.showPopover();
|
|
3956
|
+
}
|
|
3957
|
+
}
|
|
3958
|
+
hideDropdown() {
|
|
3959
|
+
this._dropdown.hidePopover();
|
|
3960
|
+
}
|
|
3961
|
+
ngOnDestroy() {
|
|
3962
|
+
try {
|
|
3963
|
+
this._dropdown.hidePopover();
|
|
3964
|
+
}
|
|
3965
|
+
catch {
|
|
3966
|
+
// popover was already closed or detached
|
|
3967
|
+
}
|
|
3968
|
+
}
|
|
3969
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: UniDropdownComponent, deps: null, target: i0.ɵɵFactoryTarget.Component });
|
|
3970
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "21.2.12", type: UniDropdownComponent, isStandalone: true, selector: "uni-dropdown", inputs: { trigger: { classPropertyName: "trigger", publicName: "trigger", isSignal: true, isRequired: true, transformFunction: null }, placement: { classPropertyName: "placement", publicName: "placement", isSignal: true, isRequired: false, transformFunction: null }, offset: { classPropertyName: "offset", publicName: "offset", isSignal: true, isRequired: false, transformFunction: null }, ariaHasPopup: { classPropertyName: "ariaHasPopup", publicName: "ariaHasPopup", isSignal: true, isRequired: false, transformFunction: null }, paddingVertical: { classPropertyName: "paddingVertical", publicName: "paddingVertical", isSignal: true, isRequired: false, transformFunction: null }, paddingHorizontal: { classPropertyName: "paddingHorizontal", publicName: "paddingHorizontal", isSignal: true, isRequired: false, transformFunction: null }, border: { classPropertyName: "border", publicName: "border", isSignal: true, isRequired: false, transformFunction: null }, borderRadius: { classPropertyName: "borderRadius", publicName: "borderRadius", isSignal: true, isRequired: false, transformFunction: null }, shadow: { classPropertyName: "shadow", publicName: "shadow", isSignal: true, isRequired: false, transformFunction: null }, color: { classPropertyName: "color", publicName: "color", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { dropdownShowing: "dropdownShowing", dropdownHiding: "dropdownHiding" }, providers: [{ provide: COMPONENT_NAME, useValue: 'dropdown' }], viewQueries: [{ propertyName: "dropdownRef", first: true, predicate: ["dropdown"], descendants: true, static: true }], usesInheritance: true, ngImport: i0, template: `
|
|
3971
|
+
<!-- 1. The native 'popover' attribute brings it to the top layer with native light-dismiss -->
|
|
3972
|
+
<div #dropdown popover="auto" [id]="popoverId" [class]="dropdownClass()">
|
|
3973
|
+
<div
|
|
3974
|
+
box-layout
|
|
3975
|
+
[border]="border() ?? componentOptions().border"
|
|
3976
|
+
[borderRadius]="borderRadius() ?? componentOptions().borderRadius"
|
|
3977
|
+
[paddingVertical]="paddingVertical()"
|
|
3978
|
+
[paddingHorizontal]="paddingHorizontal()"
|
|
3979
|
+
[color]="color() ?? componentOptions().color"
|
|
3980
|
+
[shadow]="shadow() ?? componentOptions().shadow"
|
|
3981
|
+
>
|
|
3982
|
+
<ng-content></ng-content>
|
|
3983
|
+
</div>
|
|
3984
|
+
</div>
|
|
3985
|
+
`, isInline: true, dependencies: [{ kind: "component", type: UniBoxComponent, selector: "[uni-box-layout], [box-layout]", inputs: ["color", "backgroundColor", "borderRadius", "borderRadiusLeft", "borderRadiusRight", "borderRadiusTop", "borderRadiusBottom", "padding", "paddingHorizontal", "paddingVertical", "paddingLeft", "paddingRight", "paddingTop", "paddingBottom", "border", "borderTop", "borderBottom", "borderLeft", "borderRight", "dashBorder", "alignSelf", "alignItems", "alignContent", "justifyContent", "grow", "display", "position", "inset", "height", "minHeight", "maxHeight", "width", "minWidth", "maxWidth", "ignoreDir", "gridArea", "gridColumn", "gridRow", "overflow", "elevation", "shadow", "gap", "fullWidth", "fullHeight", "flexDirection", "textAlign", "wrapItems", "zIndex"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
3986
|
+
}
|
|
3987
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: UniDropdownComponent, decorators: [{
|
|
3988
|
+
type: Component,
|
|
3989
|
+
args: [{
|
|
3990
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
3991
|
+
selector: 'uni-dropdown',
|
|
3992
|
+
imports: [UniBoxComponent],
|
|
3993
|
+
template: `
|
|
3994
|
+
<!-- 1. The native 'popover' attribute brings it to the top layer with native light-dismiss -->
|
|
3995
|
+
<div #dropdown popover="auto" [id]="popoverId" [class]="dropdownClass()">
|
|
3996
|
+
<div
|
|
3997
|
+
box-layout
|
|
3998
|
+
[border]="border() ?? componentOptions().border"
|
|
3999
|
+
[borderRadius]="borderRadius() ?? componentOptions().borderRadius"
|
|
4000
|
+
[paddingVertical]="paddingVertical()"
|
|
4001
|
+
[paddingHorizontal]="paddingHorizontal()"
|
|
4002
|
+
[color]="color() ?? componentOptions().color"
|
|
4003
|
+
[shadow]="shadow() ?? componentOptions().shadow"
|
|
4004
|
+
>
|
|
4005
|
+
<ng-content></ng-content>
|
|
4006
|
+
</div>
|
|
4007
|
+
</div>
|
|
4008
|
+
`,
|
|
4009
|
+
providers: [{ provide: COMPONENT_NAME, useValue: 'dropdown' }],
|
|
4010
|
+
}]
|
|
4011
|
+
}], propDecorators: { trigger: [{ type: i0.Input, args: [{ isSignal: true, alias: "trigger", required: true }] }], placement: [{ type: i0.Input, args: [{ isSignal: true, alias: "placement", required: false }] }], offset: [{ type: i0.Input, args: [{ isSignal: true, alias: "offset", required: false }] }], ariaHasPopup: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaHasPopup", required: false }] }], paddingVertical: [{ type: i0.Input, args: [{ isSignal: true, alias: "paddingVertical", required: false }] }], paddingHorizontal: [{ type: i0.Input, args: [{ isSignal: true, alias: "paddingHorizontal", required: false }] }], border: [{ type: i0.Input, args: [{ isSignal: true, alias: "border", required: false }] }], borderRadius: [{ type: i0.Input, args: [{ isSignal: true, alias: "borderRadius", required: false }] }], shadow: [{ type: i0.Input, args: [{ isSignal: true, alias: "shadow", required: false }] }], color: [{ type: i0.Input, args: [{ isSignal: true, alias: "color", required: false }] }], dropdownShowing: [{ type: i0.Output, args: ["dropdownShowing"] }], dropdownHiding: [{ type: i0.Output, args: ["dropdownHiding"] }], dropdownRef: [{
|
|
4012
|
+
type: ViewChild,
|
|
4013
|
+
args: ['dropdown', { static: true }]
|
|
4014
|
+
}] } });
|
|
4015
|
+
|
|
4016
|
+
class UniInputBoxComponent extends BaseComponent {
|
|
4017
|
+
className = css({ display: 'contents' });
|
|
4018
|
+
disabled = input(false, ...(ngDevMode ? [{ debugName: "disabled" }] : /* istanbul ignore next */ []));
|
|
4019
|
+
error = input(false, ...(ngDevMode ? [{ debugName: "error" }] : /* istanbul ignore next */ []));
|
|
4020
|
+
minWidth = input('0', ...(ngDevMode ? [{ debugName: "minWidth" }] : /* istanbul ignore next */ []));
|
|
4021
|
+
/** Override the themed field height, e.g. `'auto'` for multi-line fields. */
|
|
4022
|
+
height = input(undefined, ...(ngDevMode ? [{ debugName: "height" }] : /* istanbul ignore next */ []));
|
|
4023
|
+
color = computed(() => this.error() ? this.componentOptions().errorColor : this.componentOptions().color, ...(ngDevMode ? [{ debugName: "color" }] : /* istanbul ignore next */ []));
|
|
4024
|
+
border = computed(() => this.error() ? this.componentOptions().errorBorder : this.componentOptions().border, ...(ngDevMode ? [{ debugName: "border" }] : /* istanbul ignore next */ []));
|
|
4025
|
+
shadow = computed(() => this.error() ? this.componentOptions().errorShadow : this.componentOptions().shadow, ...(ngDevMode ? [{ debugName: "shadow" }] : /* istanbul ignore next */ []));
|
|
4026
|
+
inputBoxClass = computed(() => css([
|
|
4027
|
+
this.disabled() && {
|
|
4028
|
+
...this.theme.color(this.componentOptions().disabledTextColor),
|
|
4029
|
+
...this.theme.backgroundColor(this.componentOptions().disabledColor),
|
|
4030
|
+
cursor: 'not-allowed !important',
|
|
4031
|
+
},
|
|
4032
|
+
{
|
|
4033
|
+
'& input, select, textarea': {
|
|
4034
|
+
...removeInputPlatformStyling,
|
|
4035
|
+
height: '100%',
|
|
4036
|
+
...this.theme.paddingLeft(this.componentOptions().paddingLeft),
|
|
4037
|
+
...this.theme.color(this.componentOptions().textColor),
|
|
4038
|
+
...this.theme.typeface(this.componentOptions().typeFace),
|
|
4039
|
+
},
|
|
4040
|
+
// Multi-line fields size themselves (rows/resize), not from the box.
|
|
4041
|
+
'& textarea': {
|
|
4042
|
+
height: 'auto',
|
|
4043
|
+
...this.theme.paddingTop('xs'),
|
|
4044
|
+
...this.theme.paddingBottom('xs'),
|
|
4045
|
+
},
|
|
4046
|
+
'&:has(input:disabled, select:disabled, textarea:disabled)': {
|
|
4047
|
+
...this.theme.color(this.componentOptions().disabledTextColor),
|
|
4048
|
+
...this.theme.backgroundColor(this.componentOptions().disabledColor),
|
|
4049
|
+
},
|
|
4050
|
+
'& input:disabled, select:disabled, textarea:disabled': {
|
|
4051
|
+
cursor: 'not-allowed !important',
|
|
4052
|
+
},
|
|
4053
|
+
'&:has(input:focus, select:focus, textarea:focus)': {
|
|
4054
|
+
outline: this.componentOptions().focusOutline,
|
|
4055
|
+
outlineOffset: this.componentOptions().focusOutlineOffset,
|
|
4056
|
+
// Optional focus chrome (border/ring/background). It yields to the
|
|
4057
|
+
// error state, so a flagged field stays visibly flagged while the
|
|
4058
|
+
// user is in it correcting the value.
|
|
4059
|
+
...(this.error()
|
|
4060
|
+
? {}
|
|
4061
|
+
: {
|
|
4062
|
+
...this.theme.border(this.componentOptions().focusBorder),
|
|
4063
|
+
...this.theme.boxShadow(this.componentOptions().focusShadow),
|
|
4064
|
+
...this.theme.backgroundColor(this.componentOptions().focusColor),
|
|
4065
|
+
}),
|
|
4066
|
+
},
|
|
4067
|
+
},
|
|
4068
|
+
]), ...(ngDevMode ? [{ debugName: "inputBoxClass" }] : /* istanbul ignore next */ []));
|
|
4069
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: UniInputBoxComponent, deps: null, target: i0.ɵɵFactoryTarget.Component });
|
|
4070
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "21.2.12", type: UniInputBoxComponent, isStandalone: true, selector: "uni-input-box", inputs: { disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, error: { classPropertyName: "error", publicName: "error", isSignal: true, isRequired: false, transformFunction: null }, minWidth: { classPropertyName: "minWidth", publicName: "minWidth", isSignal: true, isRequired: false, transformFunction: null }, height: { classPropertyName: "height", publicName: "height", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class": "className" } }, providers: [{ provide: COMPONENT_NAME, useValue: 'input' }], usesInheritance: true, ngImport: i0, template: "<div\n row-layout\n alignItems=\"center\"\n [height]=\"height() ?? componentOptions().height\"\n [color]=\"color()\"\n [border]=\"border()\"\n [borderRadius]=\"componentOptions().borderRadius\"\n [shadow]=\"shadow()\"\n [minWidth]=\"minWidth()\"\n [class]=\"inputBoxClass()\"\n position=\"relative\"\n>\n <ng-content></ng-content>\n</div>\n", dependencies: [{ kind: "component", type: UniRowComponent, selector: "[uni-row-layout], [row-layout]", inputs: ["display", "flexDirection", "minWidth"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
4071
|
+
}
|
|
4072
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: UniInputBoxComponent, decorators: [{
|
|
4073
|
+
type: Component,
|
|
4074
|
+
args: [{ selector: 'uni-input-box', imports: [UniRowComponent], providers: [{ provide: COMPONENT_NAME, useValue: 'input' }], host: { '[class]': 'className' }, changeDetection: ChangeDetectionStrategy.OnPush, template: "<div\n row-layout\n alignItems=\"center\"\n [height]=\"height() ?? componentOptions().height\"\n [color]=\"color()\"\n [border]=\"border()\"\n [borderRadius]=\"componentOptions().borderRadius\"\n [shadow]=\"shadow()\"\n [minWidth]=\"minWidth()\"\n [class]=\"inputBoxClass()\"\n position=\"relative\"\n>\n <ng-content></ng-content>\n</div>\n" }]
|
|
4075
|
+
}], propDecorators: { disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], error: [{ type: i0.Input, args: [{ isSignal: true, alias: "error", required: false }] }], minWidth: [{ type: i0.Input, args: [{ isSignal: true, alias: "minWidth", required: false }] }], height: [{ type: i0.Input, args: [{ isSignal: true, alias: "height", required: false }] }] } });
|
|
4076
|
+
|
|
4077
|
+
/**
|
|
4078
|
+
* Date field with free-typed parsing and a popup calendar. Type `aug 20`,
|
|
4079
|
+
* `8/20/2026` or `2026-08-20`, or pick from the grid — the form gets the
|
|
4080
|
+
* same canonical `'YYYY-MM-DD'` string either way. Parsing is `Intl`-driven
|
|
4081
|
+
* (locale digit order and month names, never hardcoded); unreadable text
|
|
4082
|
+
* stays in the field, flagged, with a `rejected` event. The popup is a
|
|
4083
|
+
* native popover hosting the same `uni-calendar` an app could render inline.
|
|
4084
|
+
*/
|
|
4085
|
+
class UniDateInputComponent extends BaseComponent {
|
|
4086
|
+
// --- Signal Forms block (explicit per AGENTS.md, not a base class) --------
|
|
4087
|
+
value = model(...(ngDevMode ? [undefined, { debugName: "value" }] : /* istanbul ignore next */ []));
|
|
4088
|
+
disabled = input(false, ...(ngDevMode ? [{ debugName: "disabled" }] : /* istanbul ignore next */ []));
|
|
4089
|
+
touched = model(false, ...(ngDevMode ? [{ debugName: "touched" }] : /* istanbul ignore next */ []));
|
|
4090
|
+
invalid = input(false, ...(ngDevMode ? [{ debugName: "invalid" }] : /* istanbul ignore next */ []));
|
|
4091
|
+
dirty = input(false, ...(ngDevMode ? [{ debugName: "dirty" }] : /* istanbul ignore next */ []));
|
|
4092
|
+
required = input(false, ...(ngDevMode ? [{ debugName: "required" }] : /* istanbul ignore next */ []));
|
|
4093
|
+
ariaDescribedBy = input(...(ngDevMode ? [undefined, { debugName: "ariaDescribedBy" }] : /* istanbul ignore next */ []));
|
|
4094
|
+
// --- Configuration -------------------------------------------------------
|
|
4095
|
+
/** Accessible name for the field, e.g. "Appointment date". */
|
|
4096
|
+
label = input.required(...(ngDevMode ? [{ debugName: "label" }] : /* istanbul ignore next */ []));
|
|
4097
|
+
/** Defaults to the locale's digit pattern, e.g. `MM/DD/YYYY`. */
|
|
4098
|
+
placeholder = input(...(ngDevMode ? [undefined, { debugName: "placeholder" }] : /* istanbul ignore next */ []));
|
|
4099
|
+
/** How the committed value renders, e.g. `{ dateStyle: 'long' }`. */
|
|
4100
|
+
displayFormat = input({ dateStyle: 'medium' }, ...(ngDevMode ? [{ debugName: "displayFormat" }] : /* istanbul ignore next */ []));
|
|
4101
|
+
/** BCP 47 tag; defaults to the document language, then the browser's. */
|
|
4102
|
+
locale = input(...(ngDevMode ? [undefined, { debugName: "locale" }] : /* istanbul ignore next */ []));
|
|
4103
|
+
commitOnBlur = input(true, ...(ngDevMode ? [{ debugName: "commitOnBlur" }] : /* istanbul ignore next */ []));
|
|
4104
|
+
/** Custom parser, replacing the built-in ISO/locale/month-name parsing. */
|
|
4105
|
+
parse = input(...(ngDevMode ? [undefined, { debugName: "parse" }] : /* istanbul ignore next */ []));
|
|
4106
|
+
/** Renders without its own input-box chrome, for composers like uni-date-time-input. */
|
|
4107
|
+
embedded = input(false, ...(ngDevMode ? [{ debugName: "embedded" }] : /* istanbul ignore next */ []));
|
|
4108
|
+
// --- Forwarded to the popup calendar --------------------------------------
|
|
4109
|
+
minDate = input(...(ngDevMode ? [undefined, { debugName: "minDate" }] : /* istanbul ignore next */ []));
|
|
4110
|
+
maxDate = input(...(ngDevMode ? [undefined, { debugName: "maxDate" }] : /* istanbul ignore next */ []));
|
|
4111
|
+
disabledDates = input(...(ngDevMode ? [undefined, { debugName: "disabledDates" }] : /* istanbul ignore next */ []));
|
|
4112
|
+
markers = input([], ...(ngDevMode ? [{ debugName: "markers" }] : /* istanbul ignore next */ []));
|
|
4113
|
+
weekStart = input(...(ngDevMode ? [undefined, { debugName: "weekStart" }] : /* istanbul ignore next */ []));
|
|
4114
|
+
// --- Events ----------------------------------------------------------------
|
|
4115
|
+
/** Popup shown. */
|
|
4116
|
+
opened = output();
|
|
4117
|
+
/** Popup hidden. */
|
|
4118
|
+
closed = output();
|
|
4119
|
+
/** A typed commit was refused; the raw text stays in the field. */
|
|
4120
|
+
rejected = output();
|
|
4121
|
+
host = inject(ElementRef);
|
|
4122
|
+
inputRef = viewChild('field', ...(ngDevMode ? [{ debugName: "inputRef" }] : /* istanbul ignore next */ []));
|
|
4123
|
+
// #toggle sits on the icon-button component, so read the element explicitly.
|
|
4124
|
+
toggleRef = viewChild('toggle', { ...(ngDevMode ? { debugName: "toggleRef" } : /* istanbul ignore next */ {}), read: ElementRef });
|
|
4125
|
+
popupRef = viewChild('popupDialog', ...(ngDevMode ? [{ debugName: "popupRef" }] : /* istanbul ignore next */ []));
|
|
4126
|
+
dropdown = viewChild(UniDropdownComponent, ...(ngDevMode ? [{ debugName: "dropdown" }] : /* istanbul ignore next */ []));
|
|
4127
|
+
calendar = viewChild(UniCalendarComponent, ...(ngDevMode ? [{ debugName: "calendar" }] : /* istanbul ignore next */ []));
|
|
4128
|
+
srOnly = css(visuallyHidden);
|
|
4129
|
+
/** A refused commit — styles the field and sets aria-invalid until edited. */
|
|
4130
|
+
draftInvalid = signal(false, ...(ngDevMode ? [{ debugName: "draftInvalid" }] : /* istanbul ignore next */ []));
|
|
4131
|
+
announcement = signal('', ...(ngDevMode ? [{ debugName: "announcement" }] : /* istanbul ignore next */ []));
|
|
4132
|
+
toggleElement = computed(() => this.toggleRef()?.nativeElement, ...(ngDevMode ? [{ debugName: "toggleElement" }] : /* istanbul ignore next */ []));
|
|
4133
|
+
popupOpen = computed(() => this.dropdown()?.showing() ?? false, ...(ngDevMode ? [{ debugName: "popupOpen" }] : /* istanbul ignore next */ []));
|
|
4134
|
+
resolvedLocale = computed(() => this.locale() ?? (document.documentElement.lang || navigator.language || 'en-US'), ...(ngDevMode ? [{ debugName: "resolvedLocale" }] : /* istanbul ignore next */ []));
|
|
4135
|
+
displayText = computed(() => {
|
|
4136
|
+
const value = this.value();
|
|
4137
|
+
return value ? formatDate(value, this.resolvedLocale(), this.displayFormat()) : '';
|
|
4138
|
+
}, ...(ngDevMode ? [{ debugName: "displayText" }] : /* istanbul ignore next */ []));
|
|
4139
|
+
resolvedPlaceholder = computed(() => this.placeholder() ?? localeDatePlaceholder(this.resolvedLocale()), ...(ngDevMode ? [{ debugName: "resolvedPlaceholder" }] : /* istanbul ignore next */ []));
|
|
4140
|
+
showError = computed(() => this.invalid() && (this.touched() || this.dirty()), ...(ngDevMode ? [{ debugName: "showError" }] : /* istanbul ignore next */ []));
|
|
4141
|
+
toggleLabel = computed(() => {
|
|
4142
|
+
const value = this.value();
|
|
4143
|
+
return value
|
|
4144
|
+
? `Change date, ${formatDate(value, this.resolvedLocale(), { dateStyle: 'full' })}`
|
|
4145
|
+
: 'Choose date';
|
|
4146
|
+
}, ...(ngDevMode ? [{ debugName: "toggleLabel" }] : /* istanbul ignore next */ []));
|
|
4147
|
+
// --- Committing -------------------------------------------------------------
|
|
4148
|
+
fullDate(date) {
|
|
4149
|
+
return formatDate(date, this.resolvedLocale(), { dateStyle: 'full' });
|
|
4150
|
+
}
|
|
4151
|
+
isDayBlocked(date) {
|
|
4152
|
+
const dates = this.disabledDates();
|
|
4153
|
+
if (!dates)
|
|
4154
|
+
return false;
|
|
4155
|
+
return Array.isArray(dates) ? dates.includes(date) : dates(date);
|
|
4156
|
+
}
|
|
4157
|
+
setValue(date, silent = false) {
|
|
4158
|
+
this.value.set(date);
|
|
4159
|
+
this.draftInvalid.set(false);
|
|
4160
|
+
this.setFieldText(this.displayText());
|
|
4161
|
+
if (!silent)
|
|
4162
|
+
this.announce(date ? `${this.fullDate(date)}.` : 'Date cleared.');
|
|
4163
|
+
}
|
|
4164
|
+
refuse(raw, reason) {
|
|
4165
|
+
this.draftInvalid.set(true);
|
|
4166
|
+
const message = {
|
|
4167
|
+
unparseable: `Couldn't read “${raw}” as a date.`,
|
|
4168
|
+
'out-of-range': `${raw} is outside the allowed dates.`,
|
|
4169
|
+
disabled: `${raw} isn't available.`,
|
|
4170
|
+
}[reason];
|
|
4171
|
+
this.announce(message);
|
|
4172
|
+
this.rejected.emit({ raw, reason });
|
|
4173
|
+
}
|
|
4174
|
+
commit(raw) {
|
|
4175
|
+
const trimmed = raw.trim();
|
|
4176
|
+
if (!trimmed) {
|
|
4177
|
+
this.setValue(undefined);
|
|
4178
|
+
return true;
|
|
4179
|
+
}
|
|
4180
|
+
const custom = this.parse();
|
|
4181
|
+
const parsed = custom
|
|
4182
|
+
? custom(trimmed, this.resolvedLocale())
|
|
4183
|
+
: parseDateText(trimmed, this.resolvedLocale());
|
|
4184
|
+
if (!parsed) {
|
|
4185
|
+
this.refuse(trimmed, 'unparseable');
|
|
4186
|
+
return false;
|
|
4187
|
+
}
|
|
4188
|
+
const min = this.minDate();
|
|
4189
|
+
const max = this.maxDate();
|
|
4190
|
+
if ((min && parsed < min) || (max && parsed > max)) {
|
|
4191
|
+
this.refuse(trimmed, 'out-of-range');
|
|
4192
|
+
return false;
|
|
4193
|
+
}
|
|
4194
|
+
if (this.isDayBlocked(parsed)) {
|
|
4195
|
+
this.refuse(trimmed, 'disabled');
|
|
4196
|
+
return false;
|
|
4197
|
+
}
|
|
4198
|
+
this.setValue(parsed);
|
|
4199
|
+
return true;
|
|
4200
|
+
}
|
|
4201
|
+
/** Step a committed value ±1 day, skipping blocked days, stopping at fences. */
|
|
4202
|
+
step(direction) {
|
|
4203
|
+
const committed = this.value();
|
|
4204
|
+
if (!committed)
|
|
4205
|
+
return;
|
|
4206
|
+
let date = addDays(committed, direction);
|
|
4207
|
+
let guard = 0;
|
|
4208
|
+
while (this.isDayBlocked(date) && guard++ < 400)
|
|
4209
|
+
date = addDays(date, direction);
|
|
4210
|
+
const min = this.minDate();
|
|
4211
|
+
const max = this.maxDate();
|
|
4212
|
+
if ((min && date < min) || (max && date > max))
|
|
4213
|
+
return; // fence
|
|
4214
|
+
this.setValue(date);
|
|
4215
|
+
}
|
|
4216
|
+
// --- Keyboard ----------------------------------------------------------------
|
|
4217
|
+
onInputKeydown(event) {
|
|
4218
|
+
const element = this.inputRef().nativeElement;
|
|
4219
|
+
switch (event.key) {
|
|
4220
|
+
case 'Enter':
|
|
4221
|
+
event.preventDefault();
|
|
4222
|
+
this.commit(element.value);
|
|
4223
|
+
break;
|
|
4224
|
+
case 'Escape':
|
|
4225
|
+
if (this.popupOpen()) {
|
|
4226
|
+
this.closePopup();
|
|
4227
|
+
}
|
|
4228
|
+
else {
|
|
4229
|
+
this.setFieldText(this.displayText());
|
|
4230
|
+
this.draftInvalid.set(false);
|
|
4231
|
+
}
|
|
4232
|
+
break;
|
|
4233
|
+
case 'ArrowDown':
|
|
4234
|
+
event.preventDefault();
|
|
4235
|
+
// Alt or an empty field opens the popup; on a committed value the
|
|
4236
|
+
// caret has nowhere to go, so stepping is what a spinner would do.
|
|
4237
|
+
if (event.altKey || element.value.trim() === '')
|
|
4238
|
+
this.openPopup();
|
|
4239
|
+
else if (this.value() && element.value === this.displayText())
|
|
4240
|
+
this.step(-1);
|
|
4241
|
+
break;
|
|
4242
|
+
case 'ArrowUp':
|
|
4243
|
+
event.preventDefault();
|
|
4244
|
+
if (this.value() && element.value === this.displayText())
|
|
4245
|
+
this.step(1);
|
|
4246
|
+
break;
|
|
4247
|
+
}
|
|
4248
|
+
}
|
|
4249
|
+
onInput() {
|
|
4250
|
+
this.draftInvalid.set(false);
|
|
4251
|
+
}
|
|
4252
|
+
onFocusOut(event) {
|
|
4253
|
+
const next = event.relatedTarget;
|
|
4254
|
+
if (next && this.host.nativeElement.contains(next))
|
|
4255
|
+
return;
|
|
4256
|
+
this.touched.set(true);
|
|
4257
|
+
if (this.popupOpen())
|
|
4258
|
+
return;
|
|
4259
|
+
const element = this.inputRef()?.nativeElement;
|
|
4260
|
+
if (element && this.commitOnBlur() && element.value !== this.displayText())
|
|
4261
|
+
this.commit(element.value);
|
|
4262
|
+
}
|
|
4263
|
+
// --- Popup ---------------------------------------------------------------------
|
|
4264
|
+
openPopup() {
|
|
4265
|
+
if (this.disabled() || this.popupOpen())
|
|
4266
|
+
return;
|
|
4267
|
+
this.dropdown()?.toggleDropdown();
|
|
4268
|
+
}
|
|
4269
|
+
closePopup() {
|
|
4270
|
+
if (this.popupOpen())
|
|
4271
|
+
this.dropdown()?.hideDropdown();
|
|
4272
|
+
}
|
|
4273
|
+
onPopupShowing() {
|
|
4274
|
+
// The grid opens on the committed value's month (or today's).
|
|
4275
|
+
this.calendar()?.month.set(monthOf(this.value() ?? todayIso()));
|
|
4276
|
+
this.calendar()?.focusActiveDay();
|
|
4277
|
+
this.opened.emit();
|
|
4278
|
+
}
|
|
4279
|
+
onPopupHiding() {
|
|
4280
|
+
this.closed.emit();
|
|
4281
|
+
// Focus returns to the field (not the toggle) when it was in the popup —
|
|
4282
|
+
// this runs before the dropdown's own restoreFocus, which then no-ops.
|
|
4283
|
+
const active = document.activeElement;
|
|
4284
|
+
const popup = this.popupRef()?.nativeElement;
|
|
4285
|
+
if (!active || active === document.body || (popup && popup.contains(active)))
|
|
4286
|
+
this.inputRef()?.nativeElement.focus();
|
|
4287
|
+
}
|
|
4288
|
+
onCalendarPick(date) {
|
|
4289
|
+
this.setValue(date);
|
|
4290
|
+
this.closePopup();
|
|
4291
|
+
}
|
|
4292
|
+
/** The popup is a focus-holding dialog: Tab cycles inside it (APG pattern). */
|
|
4293
|
+
onPopupKeydown(event) {
|
|
4294
|
+
if (event.key === 'Escape') {
|
|
4295
|
+
event.preventDefault();
|
|
4296
|
+
this.closePopup();
|
|
4297
|
+
return;
|
|
4298
|
+
}
|
|
4299
|
+
if (event.key !== 'Tab')
|
|
4300
|
+
return;
|
|
4301
|
+
const popup = this.popupRef()?.nativeElement;
|
|
4302
|
+
if (!popup)
|
|
4303
|
+
return;
|
|
4304
|
+
const focusables = Array.from(popup.querySelectorAll('button:not(:disabled)')).filter((button) => button.tabIndex >= 0);
|
|
4305
|
+
if (!focusables.length)
|
|
4306
|
+
return;
|
|
4307
|
+
const index = focusables.indexOf(document.activeElement);
|
|
4308
|
+
const next = focusables[(index + (event.shiftKey ? -1 : 1) + focusables.length) % focusables.length];
|
|
4309
|
+
event.preventDefault();
|
|
4310
|
+
next.focus();
|
|
4311
|
+
}
|
|
4312
|
+
// --- Internals -------------------------------------------------------------------
|
|
4313
|
+
setFieldText(text) {
|
|
4314
|
+
const element = this.inputRef()?.nativeElement;
|
|
4315
|
+
if (element)
|
|
4316
|
+
element.value = text;
|
|
4317
|
+
}
|
|
4318
|
+
announce(message) {
|
|
4319
|
+
this.announcement.set(this.announcement() === message ? `${message} ` : message);
|
|
4320
|
+
}
|
|
4321
|
+
// --- Styling -----------------------------------------------------------------------
|
|
4322
|
+
className = computed(() => css({ display: 'block', position: 'relative' }), ...(ngDevMode ? [{ debugName: "className" }] : /* istanbul ignore next */ []));
|
|
4323
|
+
rowClass = computed(() => css({ display: 'flex', alignItems: 'center', flex: 1, width: '100%', minWidth: 0 }), ...(ngDevMode ? [{ debugName: "rowClass" }] : /* istanbul ignore next */ []));
|
|
4324
|
+
inputClass = computed(() => {
|
|
4325
|
+
const colors = this.theme.colorPalette();
|
|
4326
|
+
return css([
|
|
4327
|
+
{
|
|
4328
|
+
flex: 1,
|
|
4329
|
+
minWidth: 0,
|
|
4330
|
+
border: 0,
|
|
4331
|
+
outline: 'none',
|
|
4332
|
+
background: 'transparent',
|
|
4333
|
+
color: 'inherit',
|
|
4334
|
+
font: 'inherit',
|
|
4335
|
+
},
|
|
4336
|
+
this.draftInvalid() && {
|
|
4337
|
+
color: colors['warn'],
|
|
4338
|
+
// Shape and colour, not colour alone (WCAG 1.4.1).
|
|
4339
|
+
textDecoration: `underline dashed ${colors['warn']} 1.5px`,
|
|
4340
|
+
textUnderlineOffset: 3,
|
|
4341
|
+
},
|
|
4342
|
+
]);
|
|
4343
|
+
}, ...(ngDevMode ? [{ debugName: "inputClass" }] : /* istanbul ignore next */ []));
|
|
4344
|
+
toggleWrapClass = computed(() => css({ display: 'flex', alignItems: 'center', ...this.theme.paddingRight('xxs') }), ...(ngDevMode ? [{ debugName: "toggleWrapClass" }] : /* istanbul ignore next */ []));
|
|
4345
|
+
/** Embedded mode: the composer owns the box; keep only the flex row. */
|
|
4346
|
+
embeddedClass = computed(() => {
|
|
4347
|
+
const colors = this.theme.colorPalette();
|
|
4348
|
+
return css([
|
|
4349
|
+
{ display: 'flex', alignItems: 'center', flex: 1, minWidth: 0 },
|
|
4350
|
+
this.draftInvalid() && { color: colors['warn'] },
|
|
4351
|
+
]);
|
|
4352
|
+
}, ...(ngDevMode ? [{ debugName: "embeddedClass" }] : /* istanbul ignore next */ []));
|
|
4353
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: UniDateInputComponent, deps: null, target: i0.ɵɵFactoryTarget.Component });
|
|
4354
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.12", type: UniDateInputComponent, isStandalone: true, selector: "uni-date-input, DateInput", inputs: { value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, touched: { classPropertyName: "touched", publicName: "touched", isSignal: true, isRequired: false, transformFunction: null }, invalid: { classPropertyName: "invalid", publicName: "invalid", isSignal: true, isRequired: false, transformFunction: null }, dirty: { classPropertyName: "dirty", publicName: "dirty", isSignal: true, isRequired: false, transformFunction: null }, required: { classPropertyName: "required", publicName: "required", isSignal: true, isRequired: false, transformFunction: null }, ariaDescribedBy: { classPropertyName: "ariaDescribedBy", publicName: "ariaDescribedBy", isSignal: true, isRequired: false, transformFunction: null }, label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: true, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, displayFormat: { classPropertyName: "displayFormat", publicName: "displayFormat", isSignal: true, isRequired: false, transformFunction: null }, locale: { classPropertyName: "locale", publicName: "locale", isSignal: true, isRequired: false, transformFunction: null }, commitOnBlur: { classPropertyName: "commitOnBlur", publicName: "commitOnBlur", isSignal: true, isRequired: false, transformFunction: null }, parse: { classPropertyName: "parse", publicName: "parse", isSignal: true, isRequired: false, transformFunction: null }, embedded: { classPropertyName: "embedded", publicName: "embedded", isSignal: true, isRequired: false, transformFunction: null }, minDate: { classPropertyName: "minDate", publicName: "minDate", isSignal: true, isRequired: false, transformFunction: null }, maxDate: { classPropertyName: "maxDate", publicName: "maxDate", isSignal: true, isRequired: false, transformFunction: null }, disabledDates: { classPropertyName: "disabledDates", publicName: "disabledDates", isSignal: true, isRequired: false, transformFunction: null }, markers: { classPropertyName: "markers", publicName: "markers", isSignal: true, isRequired: false, transformFunction: null }, weekStart: { classPropertyName: "weekStart", publicName: "weekStart", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { value: "valueChange", touched: "touchedChange", opened: "opened", closed: "closed", rejected: "rejected" }, host: { properties: { "class": "className()" } }, providers: [{ provide: COMPONENT_NAME, useValue: 'dateInput' }], viewQueries: [{ propertyName: "inputRef", first: true, predicate: ["field"], descendants: true, isSignal: true }, { propertyName: "toggleRef", first: true, predicate: ["toggle"], descendants: true, read: ElementRef, isSignal: true }, { propertyName: "popupRef", first: true, predicate: ["popupDialog"], descendants: true, isSignal: true }, { propertyName: "dropdown", first: true, predicate: UniDropdownComponent, descendants: true, isSignal: true }, { propertyName: "calendar", first: true, predicate: UniCalendarComponent, descendants: true, isSignal: true }], usesInheritance: true, ngImport: i0, template: "<!-- Focusout rides the bubbling event so commits happen only when focus\n leaves the whole field, not while moving into the popup. -->\n<div (focusout)=\"onFocusOut($event)\">\n <ng-template #parts>\n <input\n #field\n type=\"text\"\n autocomplete=\"off\"\n spellcheck=\"false\"\n [class]=\"inputClass()\"\n [disabled]=\"disabled()\"\n [placeholder]=\"resolvedPlaceholder()\"\n [value]=\"displayText()\"\n [attr.aria-label]=\"label()\"\n [attr.aria-describedby]=\"ariaDescribedBy() ?? null\"\n [attr.aria-invalid]=\"showError() || draftInvalid() ? true : null\"\n [attr.aria-required]=\"required() ? true : null\"\n (keydown)=\"onInputKeydown($event)\"\n (input)=\"onInput()\"\n />\n <span [class]=\"toggleWrapClass()\">\n <!-- The dropdown wires aria-haspopup/aria-expanded/aria-controls onto\n this button; its name tracks the value (\"Change date, \u2026\"). -->\n <button\n #toggle\n icon-button\n type=\"button\"\n size=\"sm\"\n [symbolName]=\"componentOptions().toggleSymbol ?? 'calendar_month'\"\n [disable]=\"disabled()\"\n >\n {{ toggleLabel() }}\n </button>\n </span>\n </ng-template>\n\n @if (embedded()) {\n <!-- A composer (uni-date-time-input) owns the field chrome. -->\n <div [class]=\"embeddedClass()\"><ng-container [ngTemplateOutlet]=\"parts\" /></div>\n } @else {\n <uni-input-box [error]=\"showError() || draftInvalid()\" [disabled]=\"disabled()\">\n <div [class]=\"rowClass()\"><ng-container [ngTemplateOutlet]=\"parts\" /></div>\n </uni-input-box>\n }\n\n @if (toggleElement()) {\n <uni-dropdown\n [trigger]=\"toggleElement()!\"\n ariaHasPopup=\"dialog\"\n placement=\"bottom-end\"\n [color]=\"componentOptions().popupColor ?? 'primary-surface'\"\n [shadow]=\"componentOptions().popupShadow ?? 'menu'\"\n [borderRadius]=\"componentOptions().popupBorderRadius ?? 'xs'\"\n paddingVertical=\"xs\"\n paddingHorizontal=\"xs\"\n (dropdownShowing)=\"onPopupShowing()\"\n (dropdownHiding)=\"onPopupHiding()\"\n >\n <!-- A focus-holding dialog per the APG date-picker pattern: Tab cycles\n inside, Escape closes and returns focus to the field. -->\n <!-- eslint-disable-next-line @angular-eslint/template/interactive-supports-focus -->\n <div #popupDialog role=\"dialog\" aria-label=\"Choose date\" (keydown)=\"onPopupKeydown($event)\">\n <uni-calendar\n mode=\"single\"\n [value]=\"value()\"\n [minDate]=\"minDate()\"\n [maxDate]=\"maxDate()\"\n [disabledDates]=\"disabledDates()\"\n [markers]=\"markers()\"\n [weekStart]=\"weekStart()\"\n [locale]=\"locale()\"\n (selected)=\"onCalendarPick($event)\"\n />\n </div>\n </uni-dropdown>\n }\n\n <!-- Commits, steps and rejections are otherwise a silent reformat. -->\n <span role=\"status\" aria-live=\"polite\" [class]=\"srOnly\">{{ announcement() }}</span>\n</div>\n", dependencies: [{ kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "component", type: UniCalendarComponent, selector: "uni-calendar, Calendar", inputs: ["value", "disabled", "touched", "invalid", "dirty", "required", "ariaDescribedBy", "mode", "month", "minDate", "maxDate", "disabledDates", "markers", "locale", "weekStart", "ariaLabel", "size"], outputs: ["valueChange", "touchedChange", "monthChange", "selected"] }, { kind: "component", type: UniDropdownComponent, selector: "uni-dropdown", inputs: ["trigger", "placement", "offset", "ariaHasPopup", "paddingVertical", "paddingHorizontal", "border", "borderRadius", "shadow", "color"], outputs: ["dropdownShowing", "dropdownHiding"] }, { kind: "component", type: UniIconButtonComponent, selector: "button[uni-icon-button], button[icon-button]", inputs: ["ariaLabel", "iconName", "symbolName", "variant", "size", "disable", "loading", "opticalSize"] }, { kind: "component", type: UniInputBoxComponent, selector: "uni-input-box", inputs: ["disabled", "error", "minWidth", "height"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
4355
|
+
}
|
|
4356
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: UniDateInputComponent, decorators: [{
|
|
4357
|
+
type: Component,
|
|
4358
|
+
args: [{ changeDetection: ChangeDetectionStrategy.OnPush, selector: 'uni-date-input, DateInput', imports: [
|
|
4359
|
+
NgTemplateOutlet,
|
|
4360
|
+
UniCalendarComponent,
|
|
4361
|
+
UniDropdownComponent,
|
|
4362
|
+
UniIconButtonComponent,
|
|
4363
|
+
UniInputBoxComponent,
|
|
4364
|
+
], providers: [{ provide: COMPONENT_NAME, useValue: 'dateInput' }], host: { '[class]': 'className()' }, template: "<!-- Focusout rides the bubbling event so commits happen only when focus\n leaves the whole field, not while moving into the popup. -->\n<div (focusout)=\"onFocusOut($event)\">\n <ng-template #parts>\n <input\n #field\n type=\"text\"\n autocomplete=\"off\"\n spellcheck=\"false\"\n [class]=\"inputClass()\"\n [disabled]=\"disabled()\"\n [placeholder]=\"resolvedPlaceholder()\"\n [value]=\"displayText()\"\n [attr.aria-label]=\"label()\"\n [attr.aria-describedby]=\"ariaDescribedBy() ?? null\"\n [attr.aria-invalid]=\"showError() || draftInvalid() ? true : null\"\n [attr.aria-required]=\"required() ? true : null\"\n (keydown)=\"onInputKeydown($event)\"\n (input)=\"onInput()\"\n />\n <span [class]=\"toggleWrapClass()\">\n <!-- The dropdown wires aria-haspopup/aria-expanded/aria-controls onto\n this button; its name tracks the value (\"Change date, \u2026\"). -->\n <button\n #toggle\n icon-button\n type=\"button\"\n size=\"sm\"\n [symbolName]=\"componentOptions().toggleSymbol ?? 'calendar_month'\"\n [disable]=\"disabled()\"\n >\n {{ toggleLabel() }}\n </button>\n </span>\n </ng-template>\n\n @if (embedded()) {\n <!-- A composer (uni-date-time-input) owns the field chrome. -->\n <div [class]=\"embeddedClass()\"><ng-container [ngTemplateOutlet]=\"parts\" /></div>\n } @else {\n <uni-input-box [error]=\"showError() || draftInvalid()\" [disabled]=\"disabled()\">\n <div [class]=\"rowClass()\"><ng-container [ngTemplateOutlet]=\"parts\" /></div>\n </uni-input-box>\n }\n\n @if (toggleElement()) {\n <uni-dropdown\n [trigger]=\"toggleElement()!\"\n ariaHasPopup=\"dialog\"\n placement=\"bottom-end\"\n [color]=\"componentOptions().popupColor ?? 'primary-surface'\"\n [shadow]=\"componentOptions().popupShadow ?? 'menu'\"\n [borderRadius]=\"componentOptions().popupBorderRadius ?? 'xs'\"\n paddingVertical=\"xs\"\n paddingHorizontal=\"xs\"\n (dropdownShowing)=\"onPopupShowing()\"\n (dropdownHiding)=\"onPopupHiding()\"\n >\n <!-- A focus-holding dialog per the APG date-picker pattern: Tab cycles\n inside, Escape closes and returns focus to the field. -->\n <!-- eslint-disable-next-line @angular-eslint/template/interactive-supports-focus -->\n <div #popupDialog role=\"dialog\" aria-label=\"Choose date\" (keydown)=\"onPopupKeydown($event)\">\n <uni-calendar\n mode=\"single\"\n [value]=\"value()\"\n [minDate]=\"minDate()\"\n [maxDate]=\"maxDate()\"\n [disabledDates]=\"disabledDates()\"\n [markers]=\"markers()\"\n [weekStart]=\"weekStart()\"\n [locale]=\"locale()\"\n (selected)=\"onCalendarPick($event)\"\n />\n </div>\n </uni-dropdown>\n }\n\n <!-- Commits, steps and rejections are otherwise a silent reformat. -->\n <span role=\"status\" aria-live=\"polite\" [class]=\"srOnly\">{{ announcement() }}</span>\n</div>\n" }]
|
|
4365
|
+
}], propDecorators: { value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }, { type: i0.Output, args: ["valueChange"] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], touched: [{ type: i0.Input, args: [{ isSignal: true, alias: "touched", required: false }] }, { type: i0.Output, args: ["touchedChange"] }], invalid: [{ type: i0.Input, args: [{ isSignal: true, alias: "invalid", required: false }] }], dirty: [{ type: i0.Input, args: [{ isSignal: true, alias: "dirty", required: false }] }], required: [{ type: i0.Input, args: [{ isSignal: true, alias: "required", required: false }] }], ariaDescribedBy: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaDescribedBy", required: false }] }], label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: true }] }], placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], displayFormat: [{ type: i0.Input, args: [{ isSignal: true, alias: "displayFormat", required: false }] }], locale: [{ type: i0.Input, args: [{ isSignal: true, alias: "locale", required: false }] }], commitOnBlur: [{ type: i0.Input, args: [{ isSignal: true, alias: "commitOnBlur", required: false }] }], parse: [{ type: i0.Input, args: [{ isSignal: true, alias: "parse", required: false }] }], embedded: [{ type: i0.Input, args: [{ isSignal: true, alias: "embedded", required: false }] }], minDate: [{ type: i0.Input, args: [{ isSignal: true, alias: "minDate", required: false }] }], maxDate: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxDate", required: false }] }], disabledDates: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabledDates", required: false }] }], markers: [{ type: i0.Input, args: [{ isSignal: true, alias: "markers", required: false }] }], weekStart: [{ type: i0.Input, args: [{ isSignal: true, alias: "weekStart", required: false }] }], opened: [{ type: i0.Output, args: ["opened"] }], closed: [{ type: i0.Output, args: ["closed"] }], rejected: [{ type: i0.Output, args: ["rejected"] }], inputRef: [{ type: i0.ViewChild, args: ['field', { isSignal: true }] }], toggleRef: [{ type: i0.ViewChild, args: ['toggle', { ...{ read: ElementRef }, isSignal: true }] }], popupRef: [{ type: i0.ViewChild, args: ['popupDialog', { isSignal: true }] }], dropdown: [{ type: i0.ViewChild, args: [i0.forwardRef(() => UniDropdownComponent), { isSignal: true }] }], calendar: [{ type: i0.ViewChild, args: [i0.forwardRef(() => UniCalendarComponent), { isSignal: true }] }] } });
|
|
4366
|
+
|
|
4367
|
+
const toMinutes = (time) => {
|
|
4368
|
+
const [h, m] = time.split(':').map(Number);
|
|
4369
|
+
return h * 60 + m;
|
|
4370
|
+
};
|
|
4371
|
+
const fromMinutes = (minutes) => `${String(Math.floor(minutes / 60)).padStart(2, '0')}:${String(minutes % 60).padStart(2, '0')}`;
|
|
4372
|
+
/**
|
|
4373
|
+
* Time field: a combobox over time options — the same listbox contract as
|
|
4374
|
+
* uni-search-input and uni-tag-input. Type `3p`, `930` or `15:00`, or pick
|
|
4375
|
+
* `3:00 PM` from the list; the form always gets 24-hour `'HH:mm'` (`hour12`
|
|
4376
|
+
* affects display only). The list is assistive, not exhaustive: any
|
|
4377
|
+
* parseable time commits, unless `slots` pins the choices (a slot picker) —
|
|
4378
|
+
* then a typed time must match one. Unreadable or unavailable text stays in
|
|
4379
|
+
* the field, flagged, with a `rejected` event.
|
|
4380
|
+
*/
|
|
4381
|
+
class UniTimeInputComponent extends BaseComponent {
|
|
4382
|
+
// --- Signal Forms block (explicit per AGENTS.md, not a base class) --------
|
|
4383
|
+
value = model(...(ngDevMode ? [undefined, { debugName: "value" }] : /* istanbul ignore next */ []));
|
|
4384
|
+
disabled = input(false, ...(ngDevMode ? [{ debugName: "disabled" }] : /* istanbul ignore next */ []));
|
|
4385
|
+
touched = model(false, ...(ngDevMode ? [{ debugName: "touched" }] : /* istanbul ignore next */ []));
|
|
4386
|
+
invalid = input(false, ...(ngDevMode ? [{ debugName: "invalid" }] : /* istanbul ignore next */ []));
|
|
4387
|
+
dirty = input(false, ...(ngDevMode ? [{ debugName: "dirty" }] : /* istanbul ignore next */ []));
|
|
4388
|
+
required = input(false, ...(ngDevMode ? [{ debugName: "required" }] : /* istanbul ignore next */ []));
|
|
4389
|
+
ariaDescribedBy = input(...(ngDevMode ? [undefined, { debugName: "ariaDescribedBy" }] : /* istanbul ignore next */ []));
|
|
4390
|
+
// --- Configuration -------------------------------------------------------
|
|
4391
|
+
/** Accessible name for the field, e.g. "Start time". */
|
|
4392
|
+
label = input.required(...(ngDevMode ? [{ debugName: "label" }] : /* istanbul ignore next */ []));
|
|
4393
|
+
placeholder = input(...(ngDevMode ? [undefined, { debugName: "placeholder" }] : /* istanbul ignore next */ []));
|
|
4394
|
+
/** Generated list granularity, in minutes. */
|
|
4395
|
+
minuteStep = input(30, ...(ngDevMode ? [{ debugName: "minuteStep" }] : /* istanbul ignore next */ []));
|
|
4396
|
+
/** Earliest allowed time (inclusive), `'09:00'`. */
|
|
4397
|
+
minTime = input(...(ngDevMode ? [undefined, { debugName: "minTime" }] : /* istanbul ignore next */ []));
|
|
4398
|
+
/** Latest allowed time (inclusive), `'17:00'`. */
|
|
4399
|
+
maxTime = input(...(ngDevMode ? [undefined, { debugName: "maxTime" }] : /* istanbul ignore next */ []));
|
|
4400
|
+
/** Exact allowed times (scheduling). When set, typed entry must match one. */
|
|
4401
|
+
slots = input(...(ngDevMode ? [undefined, { debugName: "slots" }] : /* istanbul ignore next */ []));
|
|
4402
|
+
/** 12-hour display; defaults from the locale. The value stays 24-hour. */
|
|
4403
|
+
hour12 = input(...(ngDevMode ? [undefined, { debugName: "hour12" }] : /* istanbul ignore next */ []));
|
|
4404
|
+
/** BCP 47 tag for the display format; defaults to the document language. */
|
|
4405
|
+
locale = input(...(ngDevMode ? [undefined, { debugName: "locale" }] : /* istanbul ignore next */ []));
|
|
4406
|
+
commitOnBlur = input(true, ...(ngDevMode ? [{ debugName: "commitOnBlur" }] : /* istanbul ignore next */ []));
|
|
4407
|
+
/** Renders without its own input-box chrome, for composers like uni-date-time-input. */
|
|
4408
|
+
embedded = input(false, ...(ngDevMode ? [{ debugName: "embedded" }] : /* istanbul ignore next */ []));
|
|
4409
|
+
// --- Events ----------------------------------------------------------------
|
|
4410
|
+
/** A typed commit was refused; the raw text stays in the field. */
|
|
4411
|
+
rejected = output();
|
|
4412
|
+
host = inject(ElementRef);
|
|
4413
|
+
inputRef = viewChild('field', ...(ngDevMode ? [{ debugName: "inputRef" }] : /* istanbul ignore next */ []));
|
|
4414
|
+
listRef = viewChild('listbox', ...(ngDevMode ? [{ debugName: "listRef" }] : /* istanbul ignore next */ []));
|
|
4415
|
+
srOnly = css(visuallyHidden);
|
|
4416
|
+
/** A refused commit — styles the field and sets aria-invalid until edited. */
|
|
4417
|
+
draftInvalid = signal(false, ...(ngDevMode ? [{ debugName: "draftInvalid" }] : /* istanbul ignore next */ []));
|
|
4418
|
+
announcement = signal('', ...(ngDevMode ? [{ debugName: "announcement" }] : /* istanbul ignore next */ []));
|
|
4419
|
+
resolvedLocale = computed(() => this.locale() ?? (document.documentElement.lang || navigator.language || 'en-US'), ...(ngDevMode ? [{ debugName: "resolvedLocale" }] : /* istanbul ignore next */ []));
|
|
4420
|
+
resolvedHour12 = computed(() => this.hour12() ?? localeDefaultHour12(this.resolvedLocale()), ...(ngDevMode ? [{ debugName: "resolvedHour12" }] : /* istanbul ignore next */ []));
|
|
4421
|
+
/** The listed times: pinned `slots` verbatim, else the generated step grid. */
|
|
4422
|
+
options = computed(() => this.slots() ?? timeSlots(this.minuteStep(), this.minTime(), this.maxTime()), ...(ngDevMode ? [{ debugName: "options" }] : /* istanbul ignore next */ []));
|
|
4423
|
+
optionLabels = computed(() => this.options().map((time) => this.formatValue(time)), ...(ngDevMode ? [{ debugName: "optionLabels" }] : /* istanbul ignore next */ []));
|
|
4424
|
+
/** Shared combobox bookkeeping — identical contract to uni-search-input. */
|
|
4425
|
+
list = createListboxNavigation({
|
|
4426
|
+
count: () => this.options().length,
|
|
4427
|
+
idPrefix: 'uni-time-listbox',
|
|
4428
|
+
});
|
|
4429
|
+
displayText = computed(() => {
|
|
4430
|
+
const value = this.value();
|
|
4431
|
+
return value ? this.formatValue(value) : '';
|
|
4432
|
+
}, ...(ngDevMode ? [{ debugName: "displayText" }] : /* istanbul ignore next */ []));
|
|
4433
|
+
resolvedPlaceholder = computed(() => this.placeholder() ?? this.formatValue('09:00'), ...(ngDevMode ? [{ debugName: "resolvedPlaceholder" }] : /* istanbul ignore next */ []));
|
|
4434
|
+
showError = computed(() => this.invalid() && (this.touched() || this.dirty()), ...(ngDevMode ? [{ debugName: "showError" }] : /* istanbul ignore next */ []));
|
|
4435
|
+
formatValue(time) {
|
|
4436
|
+
return formatTime(time, this.resolvedLocale(), this.resolvedHour12());
|
|
4437
|
+
}
|
|
4438
|
+
// --- Committing -------------------------------------------------------------
|
|
4439
|
+
setValue(time, silent = false) {
|
|
4440
|
+
this.value.set(time);
|
|
4441
|
+
this.draftInvalid.set(false);
|
|
4442
|
+
this.setFieldText(this.displayText());
|
|
4443
|
+
if (!silent)
|
|
4444
|
+
this.announce(time ? `${this.formatValue(time)}.` : 'Time cleared.');
|
|
4445
|
+
}
|
|
4446
|
+
refuse(raw, reason, shown = raw) {
|
|
4447
|
+
this.draftInvalid.set(true);
|
|
4448
|
+
const message = {
|
|
4449
|
+
unparseable: `Couldn't read “${shown}” as a time.`,
|
|
4450
|
+
'out-of-range': `${shown} is outside the allowed times.`,
|
|
4451
|
+
unavailable: `${shown} isn't available.`,
|
|
4452
|
+
}[reason];
|
|
4453
|
+
this.announce(message);
|
|
4454
|
+
this.rejected.emit({ raw, reason });
|
|
4455
|
+
}
|
|
4456
|
+
commit(raw) {
|
|
4457
|
+
const trimmed = raw.trim();
|
|
4458
|
+
if (!trimmed) {
|
|
4459
|
+
this.setValue(undefined);
|
|
4460
|
+
return true;
|
|
4461
|
+
}
|
|
4462
|
+
let parsed = parseTimeText(trimmed, this.resolvedHour12());
|
|
4463
|
+
if (!parsed) {
|
|
4464
|
+
this.refuse(trimmed, 'unparseable');
|
|
4465
|
+
return false;
|
|
4466
|
+
}
|
|
4467
|
+
const min = this.minTime();
|
|
4468
|
+
const max = this.maxTime();
|
|
4469
|
+
const inBounds = (time) => !(min && time < min) && !(max && time > max);
|
|
4470
|
+
const slots = this.slots();
|
|
4471
|
+
if (!slots && !inBounds(parsed)) {
|
|
4472
|
+
// The PM bias yields if it pushed the time out of bounds.
|
|
4473
|
+
const unbiased = parseTimeText(trimmed, false);
|
|
4474
|
+
if (unbiased && inBounds(unbiased))
|
|
4475
|
+
parsed = unbiased;
|
|
4476
|
+
else {
|
|
4477
|
+
this.refuse(trimmed, 'out-of-range');
|
|
4478
|
+
return false;
|
|
4479
|
+
}
|
|
4480
|
+
}
|
|
4481
|
+
if (slots && !slots.includes(parsed)) {
|
|
4482
|
+
// Announce the formatted time — "5:00 PM isn't available."
|
|
4483
|
+
this.refuse(trimmed, 'unavailable', this.formatValue(parsed));
|
|
4484
|
+
return false;
|
|
4485
|
+
}
|
|
4486
|
+
this.setValue(parsed);
|
|
4487
|
+
return true;
|
|
4488
|
+
}
|
|
4489
|
+
/** Step a committed value ±minuteStep (±1 slot when pinned), clamped. */
|
|
4490
|
+
stepValue(direction) {
|
|
4491
|
+
const committed = this.value();
|
|
4492
|
+
if (!committed)
|
|
4493
|
+
return;
|
|
4494
|
+
const slots = this.slots();
|
|
4495
|
+
if (slots?.length) {
|
|
4496
|
+
const sorted = [...slots].sort();
|
|
4497
|
+
const index = sorted.indexOf(committed);
|
|
4498
|
+
const next = index >= 0
|
|
4499
|
+
? sorted[index + direction]
|
|
4500
|
+
: direction > 0
|
|
4501
|
+
? sorted.find((slot) => slot > committed)
|
|
4502
|
+
: [...sorted].reverse().find((slot) => slot < committed);
|
|
4503
|
+
if (next)
|
|
4504
|
+
this.setValue(next);
|
|
4505
|
+
return;
|
|
4506
|
+
}
|
|
4507
|
+
const step = this.minuteStep();
|
|
4508
|
+
const current = toMinutes(committed);
|
|
4509
|
+
const snapped = direction > 0
|
|
4510
|
+
? Math.floor(current / step) * step + step
|
|
4511
|
+
: Math.ceil(current / step) * step - step;
|
|
4512
|
+
if (snapped < 0 || snapped >= 24 * 60)
|
|
4513
|
+
return;
|
|
4514
|
+
const candidate = fromMinutes(snapped);
|
|
4515
|
+
const min = this.minTime();
|
|
4516
|
+
const max = this.maxTime();
|
|
4517
|
+
if ((min && candidate < min) || (max && candidate > max))
|
|
4518
|
+
return; // fence
|
|
4519
|
+
this.setValue(candidate);
|
|
4520
|
+
}
|
|
4521
|
+
// --- Keyboard ----------------------------------------------------------------
|
|
4522
|
+
onInputKeydown(event) {
|
|
4523
|
+
const element = this.inputRef().nativeElement;
|
|
4524
|
+
switch (event.key) {
|
|
4525
|
+
case 'ArrowDown':
|
|
4526
|
+
case 'ArrowUp': {
|
|
4527
|
+
event.preventDefault();
|
|
4528
|
+
const direction = event.key === 'ArrowDown' ? 1 : -1;
|
|
4529
|
+
// A committed value with the list closed steps like a spinner —
|
|
4530
|
+
// ArrowUp means later, like the date field's ArrowUp means tomorrow —
|
|
4531
|
+
// while the list opens from an empty or edited field (or the toggle).
|
|
4532
|
+
if (!this.list.open() && this.value() && element.value === this.displayText()) {
|
|
4533
|
+
this.stepValue(direction === 1 ? -1 : 1);
|
|
4534
|
+
break;
|
|
4535
|
+
}
|
|
4536
|
+
if (!this.list.open()) {
|
|
4537
|
+
this.openList();
|
|
4538
|
+
if (this.list.activeIndex() < 0)
|
|
4539
|
+
this.list.setActive(direction === 1 ? 0 : this.options().length - 1);
|
|
4540
|
+
}
|
|
4541
|
+
else {
|
|
4542
|
+
const count = this.options().length;
|
|
4543
|
+
this.list.setActive((this.list.activeIndex() + direction + count) % count);
|
|
4544
|
+
}
|
|
4545
|
+
this.scrollToActive();
|
|
4546
|
+
break;
|
|
4547
|
+
}
|
|
4548
|
+
case 'Enter': {
|
|
4549
|
+
event.preventDefault();
|
|
4550
|
+
const active = this.list.activeIndex();
|
|
4551
|
+
if (this.list.open() && active >= 0)
|
|
4552
|
+
this.setValue(this.options()[active]);
|
|
4553
|
+
else
|
|
4554
|
+
this.commit(element.value);
|
|
4555
|
+
this.list.hide();
|
|
4556
|
+
break;
|
|
4557
|
+
}
|
|
4558
|
+
case 'Escape':
|
|
4559
|
+
if (this.list.open()) {
|
|
4560
|
+
this.list.hide();
|
|
4561
|
+
}
|
|
4562
|
+
else {
|
|
4563
|
+
this.setFieldText(this.displayText());
|
|
4564
|
+
this.draftInvalid.set(false);
|
|
4565
|
+
}
|
|
4566
|
+
break;
|
|
4567
|
+
case 'Tab':
|
|
4568
|
+
// Never trap: commit what is typed, then let focus move on.
|
|
4569
|
+
if (element.value.trim() && element.value !== this.displayText())
|
|
4570
|
+
this.commit(element.value);
|
|
4571
|
+
this.list.hide();
|
|
4572
|
+
break;
|
|
4573
|
+
}
|
|
4574
|
+
}
|
|
4575
|
+
onInput() {
|
|
4576
|
+
this.draftInvalid.set(false);
|
|
4577
|
+
// Typing never selects an option — Enter must commit the draft, not
|
|
4578
|
+
// whatever happens to sit nearest; the list only scrolls alongside.
|
|
4579
|
+
this.list.show();
|
|
4580
|
+
this.list.setActive(-1);
|
|
4581
|
+
const parsed = parseTimeText(this.inputRef().nativeElement.value, this.resolvedHour12());
|
|
4582
|
+
if (parsed) {
|
|
4583
|
+
const options = this.options();
|
|
4584
|
+
let nearest = options.findIndex((time) => time >= parsed);
|
|
4585
|
+
if (nearest < 0)
|
|
4586
|
+
nearest = options.length - 1;
|
|
4587
|
+
this.scrollToIndex(nearest);
|
|
4588
|
+
}
|
|
4589
|
+
}
|
|
4590
|
+
onToggle() {
|
|
4591
|
+
if (this.list.open()) {
|
|
4592
|
+
this.list.hide();
|
|
4593
|
+
}
|
|
4594
|
+
else {
|
|
4595
|
+
this.openList();
|
|
4596
|
+
this.inputRef()?.nativeElement.focus();
|
|
4597
|
+
}
|
|
4598
|
+
}
|
|
4599
|
+
selectOption(time) {
|
|
4600
|
+
this.setValue(time);
|
|
4601
|
+
this.list.hide();
|
|
4602
|
+
this.inputRef()?.nativeElement.focus();
|
|
4603
|
+
}
|
|
4604
|
+
onFocusOut(event) {
|
|
4605
|
+
const next = event.relatedTarget;
|
|
4606
|
+
if (next && this.host.nativeElement.contains(next))
|
|
4607
|
+
return;
|
|
4608
|
+
this.list.hide();
|
|
4609
|
+
this.touched.set(true);
|
|
4610
|
+
const element = this.inputRef()?.nativeElement;
|
|
4611
|
+
if (element && this.commitOnBlur() && element.value !== this.displayText())
|
|
4612
|
+
this.commit(element.value);
|
|
4613
|
+
}
|
|
4614
|
+
// --- Internals -------------------------------------------------------------------
|
|
4615
|
+
openList() {
|
|
4616
|
+
this.list.show();
|
|
4617
|
+
const committed = this.value();
|
|
4618
|
+
if (committed)
|
|
4619
|
+
this.list.setActive(this.options().indexOf(committed));
|
|
4620
|
+
this.scrollToActive();
|
|
4621
|
+
}
|
|
4622
|
+
scrollToActive() {
|
|
4623
|
+
this.scrollToIndex(this.list.activeIndex());
|
|
4624
|
+
}
|
|
4625
|
+
scrollToIndex(index) {
|
|
4626
|
+
if (index < 0)
|
|
4627
|
+
return;
|
|
4628
|
+
queueMicrotask(() => this.listRef()?.nativeElement.children[index]?.scrollIntoView?.({ block: 'nearest' }));
|
|
4629
|
+
}
|
|
4630
|
+
setFieldText(text) {
|
|
4631
|
+
const element = this.inputRef()?.nativeElement;
|
|
4632
|
+
if (element)
|
|
4633
|
+
element.value = text;
|
|
4634
|
+
}
|
|
4635
|
+
announce(message) {
|
|
4636
|
+
this.announcement.set(this.announcement() === message ? `${message} ` : message);
|
|
4637
|
+
}
|
|
4638
|
+
// --- Styling -----------------------------------------------------------------------
|
|
4639
|
+
className = computed(() => css({ display: 'block', position: 'relative' }), ...(ngDevMode ? [{ debugName: "className" }] : /* istanbul ignore next */ []));
|
|
4640
|
+
rowClass = computed(() => css({ display: 'flex', alignItems: 'center', flex: 1, width: '100%', minWidth: 0 }), ...(ngDevMode ? [{ debugName: "rowClass" }] : /* istanbul ignore next */ []));
|
|
4641
|
+
inputClass = computed(() => {
|
|
4642
|
+
const colors = this.theme.colorPalette();
|
|
4643
|
+
return css([
|
|
4644
|
+
{
|
|
4645
|
+
flex: 1,
|
|
4646
|
+
minWidth: 0,
|
|
4647
|
+
border: 0,
|
|
4648
|
+
outline: 'none',
|
|
4649
|
+
background: 'transparent',
|
|
4650
|
+
color: 'inherit',
|
|
4651
|
+
font: 'inherit',
|
|
4652
|
+
},
|
|
4653
|
+
this.draftInvalid() && {
|
|
4654
|
+
color: colors['warn'],
|
|
4655
|
+
// Shape and colour, not colour alone (WCAG 1.4.1).
|
|
4656
|
+
textDecoration: `underline dashed ${colors['warn']} 1.5px`,
|
|
4657
|
+
textUnderlineOffset: 3,
|
|
4658
|
+
},
|
|
4659
|
+
]);
|
|
4660
|
+
}, ...(ngDevMode ? [{ debugName: "inputClass" }] : /* istanbul ignore next */ []));
|
|
4661
|
+
toggleWrapClass = computed(() => css({ display: 'flex', alignItems: 'center', ...this.theme.paddingRight('xxs') }), ...(ngDevMode ? [{ debugName: "toggleWrapClass" }] : /* istanbul ignore next */ []));
|
|
4662
|
+
/** Embedded mode: the composer owns the box; keep only the flex row. */
|
|
4663
|
+
embeddedClass = computed(() => css({ display: 'flex', alignItems: 'center', flex: 1, minWidth: 0 }), ...(ngDevMode ? [{ debugName: "embeddedClass" }] : /* istanbul ignore next */ []));
|
|
4664
|
+
listClass = computed(() => {
|
|
4665
|
+
const options = this.componentOptions();
|
|
4666
|
+
return css({
|
|
4667
|
+
position: 'absolute',
|
|
4668
|
+
top: '100%',
|
|
4669
|
+
left: 0,
|
|
4670
|
+
right: 0,
|
|
4671
|
+
zIndex: 20,
|
|
4672
|
+
margin: '4px 0 0',
|
|
4673
|
+
padding: 4,
|
|
4674
|
+
listStyle: 'none',
|
|
4675
|
+
maxHeight: (options.maxVisibleOptions ?? 7) * 36,
|
|
4676
|
+
overflowY: 'auto',
|
|
4677
|
+
...this.theme.backgroundColor(options.listColor ?? 'primary-surface'),
|
|
4678
|
+
...this.theme.boxShadow(options.listShadow ?? 'menu'),
|
|
4679
|
+
...this.theme.radius(options.listBorderRadius ?? 'xs'),
|
|
4680
|
+
'& [role="option"]': {
|
|
4681
|
+
padding: '8px 12px',
|
|
4682
|
+
cursor: 'pointer',
|
|
4683
|
+
...this.theme.typeface('label'),
|
|
4684
|
+
...this.theme.color('on-primary-surface'),
|
|
4685
|
+
...this.theme.radius('xxs'),
|
|
4686
|
+
'&.active, &:hover': {
|
|
4687
|
+
...this.theme.backgroundColor('primary-container'),
|
|
4688
|
+
...this.theme.color('on-primary-container'),
|
|
4689
|
+
},
|
|
4690
|
+
},
|
|
4691
|
+
});
|
|
4692
|
+
}, ...(ngDevMode ? [{ debugName: "listClass" }] : /* istanbul ignore next */ []));
|
|
4693
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: UniTimeInputComponent, deps: null, target: i0.ɵɵFactoryTarget.Component });
|
|
4694
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.12", type: UniTimeInputComponent, isStandalone: true, selector: "uni-time-input, TimeInput", inputs: { value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, touched: { classPropertyName: "touched", publicName: "touched", isSignal: true, isRequired: false, transformFunction: null }, invalid: { classPropertyName: "invalid", publicName: "invalid", isSignal: true, isRequired: false, transformFunction: null }, dirty: { classPropertyName: "dirty", publicName: "dirty", isSignal: true, isRequired: false, transformFunction: null }, required: { classPropertyName: "required", publicName: "required", isSignal: true, isRequired: false, transformFunction: null }, ariaDescribedBy: { classPropertyName: "ariaDescribedBy", publicName: "ariaDescribedBy", isSignal: true, isRequired: false, transformFunction: null }, label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: true, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, minuteStep: { classPropertyName: "minuteStep", publicName: "minuteStep", isSignal: true, isRequired: false, transformFunction: null }, minTime: { classPropertyName: "minTime", publicName: "minTime", isSignal: true, isRequired: false, transformFunction: null }, maxTime: { classPropertyName: "maxTime", publicName: "maxTime", isSignal: true, isRequired: false, transformFunction: null }, slots: { classPropertyName: "slots", publicName: "slots", isSignal: true, isRequired: false, transformFunction: null }, hour12: { classPropertyName: "hour12", publicName: "hour12", isSignal: true, isRequired: false, transformFunction: null }, locale: { classPropertyName: "locale", publicName: "locale", isSignal: true, isRequired: false, transformFunction: null }, commitOnBlur: { classPropertyName: "commitOnBlur", publicName: "commitOnBlur", isSignal: true, isRequired: false, transformFunction: null }, embedded: { classPropertyName: "embedded", publicName: "embedded", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { value: "valueChange", touched: "touchedChange", rejected: "rejected" }, host: { properties: { "class": "className()" } }, providers: [{ provide: COMPONENT_NAME, useValue: 'timeInput' }], viewQueries: [{ propertyName: "inputRef", first: true, predicate: ["field"], descendants: true, isSignal: true }, { propertyName: "listRef", first: true, predicate: ["listbox"], descendants: true, isSignal: true }], usesInheritance: true, ngImport: i0, template: "<!-- Focusout rides the bubbling event so the list closes and the draft\n commits only when focus leaves the whole field. -->\n<div (focusout)=\"onFocusOut($event)\">\n <ng-template #parts>\n <input\n #field\n type=\"text\"\n role=\"combobox\"\n autocomplete=\"off\"\n spellcheck=\"false\"\n aria-autocomplete=\"list\"\n [class]=\"inputClass()\"\n [disabled]=\"disabled()\"\n [placeholder]=\"resolvedPlaceholder()\"\n [value]=\"displayText()\"\n [attr.aria-label]=\"label()\"\n [attr.aria-expanded]=\"list.open()\"\n [attr.aria-controls]=\"list.listboxId\"\n [attr.aria-activedescendant]=\"list.activeDescendantId()\"\n [attr.aria-describedby]=\"ariaDescribedBy() ?? null\"\n [attr.aria-invalid]=\"showError() || draftInvalid() ? true : null\"\n [attr.aria-required]=\"required() ? true : null\"\n (keydown)=\"onInputKeydown($event)\"\n (input)=\"onInput()\"\n />\n <span [class]=\"toggleWrapClass()\">\n <button\n icon-button\n type=\"button\"\n size=\"sm\"\n aria-haspopup=\"listbox\"\n [symbolName]=\"componentOptions().toggleSymbol ?? 'schedule'\"\n [attr.aria-expanded]=\"list.open()\"\n [disable]=\"disabled()\"\n (click)=\"onToggle()\"\n >\n Choose time\n </button>\n </span>\n </ng-template>\n\n @if (embedded()) {\n <!-- A composer (uni-date-time-input) owns the field chrome. -->\n <div [class]=\"embeddedClass()\"><ng-container [ngTemplateOutlet]=\"parts\" /></div>\n } @else {\n <uni-input-box [error]=\"showError() || draftInvalid()\" [disabled]=\"disabled()\">\n <div [class]=\"rowClass()\"><ng-container [ngTemplateOutlet]=\"parts\" /></div>\n </uni-input-box>\n }\n\n @if (list.open()) {\n <ul #listbox [id]=\"list.listboxId\" role=\"listbox\" [attr.aria-label]=\"label()\" [class]=\"listClass()\">\n @for (time of options(); track time; let i = $index) {\n <!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events, @angular-eslint/template/interactive-supports-focus -->\n <li\n role=\"option\"\n [id]=\"list.optionId(i)\"\n [class.active]=\"i === list.activeIndex()\"\n [attr.aria-selected]=\"time === value()\"\n (mousedown)=\"$event.preventDefault()\"\n (click)=\"selectOption(time)\"\n >\n {{ optionLabels()[i] }}\n </li>\n }\n </ul>\n }\n\n <!-- Commits and rejections are otherwise a silent reformat. -->\n <span role=\"status\" aria-live=\"polite\" [class]=\"srOnly\">{{ announcement() }}</span>\n</div>\n", dependencies: [{ kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "component", type: UniIconButtonComponent, selector: "button[uni-icon-button], button[icon-button]", inputs: ["ariaLabel", "iconName", "symbolName", "variant", "size", "disable", "loading", "opticalSize"] }, { kind: "component", type: UniInputBoxComponent, selector: "uni-input-box", inputs: ["disabled", "error", "minWidth", "height"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
4695
|
+
}
|
|
4696
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: UniTimeInputComponent, decorators: [{
|
|
4697
|
+
type: Component,
|
|
4698
|
+
args: [{ changeDetection: ChangeDetectionStrategy.OnPush, selector: 'uni-time-input, TimeInput', imports: [NgTemplateOutlet, UniIconButtonComponent, UniInputBoxComponent], providers: [{ provide: COMPONENT_NAME, useValue: 'timeInput' }], host: { '[class]': 'className()' }, template: "<!-- Focusout rides the bubbling event so the list closes and the draft\n commits only when focus leaves the whole field. -->\n<div (focusout)=\"onFocusOut($event)\">\n <ng-template #parts>\n <input\n #field\n type=\"text\"\n role=\"combobox\"\n autocomplete=\"off\"\n spellcheck=\"false\"\n aria-autocomplete=\"list\"\n [class]=\"inputClass()\"\n [disabled]=\"disabled()\"\n [placeholder]=\"resolvedPlaceholder()\"\n [value]=\"displayText()\"\n [attr.aria-label]=\"label()\"\n [attr.aria-expanded]=\"list.open()\"\n [attr.aria-controls]=\"list.listboxId\"\n [attr.aria-activedescendant]=\"list.activeDescendantId()\"\n [attr.aria-describedby]=\"ariaDescribedBy() ?? null\"\n [attr.aria-invalid]=\"showError() || draftInvalid() ? true : null\"\n [attr.aria-required]=\"required() ? true : null\"\n (keydown)=\"onInputKeydown($event)\"\n (input)=\"onInput()\"\n />\n <span [class]=\"toggleWrapClass()\">\n <button\n icon-button\n type=\"button\"\n size=\"sm\"\n aria-haspopup=\"listbox\"\n [symbolName]=\"componentOptions().toggleSymbol ?? 'schedule'\"\n [attr.aria-expanded]=\"list.open()\"\n [disable]=\"disabled()\"\n (click)=\"onToggle()\"\n >\n Choose time\n </button>\n </span>\n </ng-template>\n\n @if (embedded()) {\n <!-- A composer (uni-date-time-input) owns the field chrome. -->\n <div [class]=\"embeddedClass()\"><ng-container [ngTemplateOutlet]=\"parts\" /></div>\n } @else {\n <uni-input-box [error]=\"showError() || draftInvalid()\" [disabled]=\"disabled()\">\n <div [class]=\"rowClass()\"><ng-container [ngTemplateOutlet]=\"parts\" /></div>\n </uni-input-box>\n }\n\n @if (list.open()) {\n <ul #listbox [id]=\"list.listboxId\" role=\"listbox\" [attr.aria-label]=\"label()\" [class]=\"listClass()\">\n @for (time of options(); track time; let i = $index) {\n <!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events, @angular-eslint/template/interactive-supports-focus -->\n <li\n role=\"option\"\n [id]=\"list.optionId(i)\"\n [class.active]=\"i === list.activeIndex()\"\n [attr.aria-selected]=\"time === value()\"\n (mousedown)=\"$event.preventDefault()\"\n (click)=\"selectOption(time)\"\n >\n {{ optionLabels()[i] }}\n </li>\n }\n </ul>\n }\n\n <!-- Commits and rejections are otherwise a silent reformat. -->\n <span role=\"status\" aria-live=\"polite\" [class]=\"srOnly\">{{ announcement() }}</span>\n</div>\n" }]
|
|
4699
|
+
}], propDecorators: { value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }, { type: i0.Output, args: ["valueChange"] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], touched: [{ type: i0.Input, args: [{ isSignal: true, alias: "touched", required: false }] }, { type: i0.Output, args: ["touchedChange"] }], invalid: [{ type: i0.Input, args: [{ isSignal: true, alias: "invalid", required: false }] }], dirty: [{ type: i0.Input, args: [{ isSignal: true, alias: "dirty", required: false }] }], required: [{ type: i0.Input, args: [{ isSignal: true, alias: "required", required: false }] }], ariaDescribedBy: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaDescribedBy", required: false }] }], label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: true }] }], placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], minuteStep: [{ type: i0.Input, args: [{ isSignal: true, alias: "minuteStep", required: false }] }], minTime: [{ type: i0.Input, args: [{ isSignal: true, alias: "minTime", required: false }] }], maxTime: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxTime", required: false }] }], slots: [{ type: i0.Input, args: [{ isSignal: true, alias: "slots", required: false }] }], hour12: [{ type: i0.Input, args: [{ isSignal: true, alias: "hour12", required: false }] }], locale: [{ type: i0.Input, args: [{ isSignal: true, alias: "locale", required: false }] }], commitOnBlur: [{ type: i0.Input, args: [{ isSignal: true, alias: "commitOnBlur", required: false }] }], embedded: [{ type: i0.Input, args: [{ isSignal: true, alias: "embedded", required: false }] }], rejected: [{ type: i0.Output, args: ["rejected"] }], inputRef: [{ type: i0.ViewChild, args: ['field', { isSignal: true }] }], listRef: [{ type: i0.ViewChild, args: ['listbox', { isSignal: true }] }] } });
|
|
4700
|
+
|
|
4701
|
+
/**
|
|
4702
|
+
* One field for a date and a time: a thin composer seating a uni-date-input
|
|
4703
|
+
* and a uni-time-input in one input-box chrome under one label, yielding one
|
|
4704
|
+
* combined `'YYYY-MM-DDTHH:mm'` value. The value emits only when both parts
|
|
4705
|
+
* are set — a time without a day is not an answer — and clearing the date
|
|
4706
|
+
* clears it. With `slotsFor`, the time part stays disabled until a day is
|
|
4707
|
+
* chosen and offers exactly that day's slots: the scheduling flow in one
|
|
4708
|
+
* attribute. Two honest tab stops (it is two questions); apps needing a
|
|
4709
|
+
* different arrangement compose the primitives directly.
|
|
4710
|
+
*/
|
|
4711
|
+
class UniDateTimeInputComponent extends BaseComponent {
|
|
4712
|
+
// --- Signal Forms block (explicit per AGENTS.md, not a base class) --------
|
|
4713
|
+
value = model(...(ngDevMode ? [undefined, { debugName: "value" }] : /* istanbul ignore next */ []));
|
|
4714
|
+
disabled = input(false, ...(ngDevMode ? [{ debugName: "disabled" }] : /* istanbul ignore next */ []));
|
|
4715
|
+
touched = model(false, ...(ngDevMode ? [{ debugName: "touched" }] : /* istanbul ignore next */ []));
|
|
4716
|
+
invalid = input(false, ...(ngDevMode ? [{ debugName: "invalid" }] : /* istanbul ignore next */ []));
|
|
4717
|
+
dirty = input(false, ...(ngDevMode ? [{ debugName: "dirty" }] : /* istanbul ignore next */ []));
|
|
4718
|
+
required = input(false, ...(ngDevMode ? [{ debugName: "required" }] : /* istanbul ignore next */ []));
|
|
4719
|
+
ariaDescribedBy = input(...(ngDevMode ? [undefined, { debugName: "ariaDescribedBy" }] : /* istanbul ignore next */ []));
|
|
4720
|
+
// --- Configuration -------------------------------------------------------
|
|
4721
|
+
/** Names the group; the parts are announced as "Date" and "Time" under it. */
|
|
4722
|
+
label = input.required(...(ngDevMode ? [{ debugName: "label" }] : /* istanbul ignore next */ []));
|
|
4723
|
+
/** Earliest allowed moment; the date and time fences are split from it. */
|
|
4724
|
+
minDateTime = input(...(ngDevMode ? [undefined, { debugName: "minDateTime" }] : /* istanbul ignore next */ []));
|
|
4725
|
+
/** Latest allowed moment; the date and time fences are split from it. */
|
|
4726
|
+
maxDateTime = input(...(ngDevMode ? [undefined, { debugName: "maxDateTime" }] : /* istanbul ignore next */ []));
|
|
4727
|
+
// --- Forwarded wholesale ----------------------------------------------------
|
|
4728
|
+
disabledDates = input(...(ngDevMode ? [undefined, { debugName: "disabledDates" }] : /* istanbul ignore next */ []));
|
|
4729
|
+
markers = input([], ...(ngDevMode ? [{ debugName: "markers" }] : /* istanbul ignore next */ []));
|
|
4730
|
+
/** Fixed time choices; superseded per-day by `slotsFor` when that is set. */
|
|
4731
|
+
slots = input(...(ngDevMode ? [undefined, { debugName: "slots" }] : /* istanbul ignore next */ []));
|
|
4732
|
+
minuteStep = input(30, ...(ngDevMode ? [{ debugName: "minuteStep" }] : /* istanbul ignore next */ []));
|
|
4733
|
+
hour12 = input(...(ngDevMode ? [undefined, { debugName: "hour12" }] : /* istanbul ignore next */ []));
|
|
4734
|
+
weekStart = input(...(ngDevMode ? [undefined, { debugName: "weekStart" }] : /* istanbul ignore next */ []));
|
|
4735
|
+
locale = input(...(ngDevMode ? [undefined, { debugName: "locale" }] : /* istanbul ignore next */ []));
|
|
4736
|
+
/** Scheduling: the day's available times. Gates the time part on a date. */
|
|
4737
|
+
slotsFor = input(...(ngDevMode ? [undefined, { debugName: "slotsFor" }] : /* istanbul ignore next */ []));
|
|
4738
|
+
host = inject(ElementRef);
|
|
4739
|
+
/**
|
|
4740
|
+
* The two part-values. An external `value` write re-derives both; an
|
|
4741
|
+
* internal partial state (a date without a time round-trips through
|
|
4742
|
+
* `undefined`) must not be wiped by its own echo.
|
|
4743
|
+
*/
|
|
4744
|
+
parts = linkedSignal({ ...(ngDevMode ? { debugName: "parts" } : /* istanbul ignore next */ {}), source: this.value,
|
|
4745
|
+
computation: (value, previous) => {
|
|
4746
|
+
if (value)
|
|
4747
|
+
return splitDateTime(value);
|
|
4748
|
+
const kept = previous?.value;
|
|
4749
|
+
if (kept && joinDateTime(kept.date, kept.time) === undefined)
|
|
4750
|
+
return kept;
|
|
4751
|
+
return {};
|
|
4752
|
+
} });
|
|
4753
|
+
dateValue = computed(() => this.parts().date, ...(ngDevMode ? [{ debugName: "dateValue" }] : /* istanbul ignore next */ []));
|
|
4754
|
+
timeValue = computed(() => this.parts().time, ...(ngDevMode ? [{ debugName: "timeValue" }] : /* istanbul ignore next */ []));
|
|
4755
|
+
showError = computed(() => this.invalid() && (this.touched() || this.dirty()), ...(ngDevMode ? [{ debugName: "showError" }] : /* istanbul ignore next */ []));
|
|
4756
|
+
minParts = computed(() => splitDateTime(this.minDateTime()), ...(ngDevMode ? [{ debugName: "minParts" }] : /* istanbul ignore next */ []));
|
|
4757
|
+
maxParts = computed(() => splitDateTime(this.maxDateTime()), ...(ngDevMode ? [{ debugName: "maxParts" }] : /* istanbul ignore next */ []));
|
|
4758
|
+
dateMin = computed(() => this.minParts().date, ...(ngDevMode ? [{ debugName: "dateMin" }] : /* istanbul ignore next */ []));
|
|
4759
|
+
dateMax = computed(() => this.maxParts().date, ...(ngDevMode ? [{ debugName: "dateMax" }] : /* istanbul ignore next */ []));
|
|
4760
|
+
// The time fence applies only on the boundary date itself — 'after 9:00'
|
|
4761
|
+
// on the min date, any time on later days.
|
|
4762
|
+
timeMin = computed(() => {
|
|
4763
|
+
const { date, time } = this.minParts();
|
|
4764
|
+
return time && date && this.dateValue() === date ? time : undefined;
|
|
4765
|
+
}, ...(ngDevMode ? [{ debugName: "timeMin" }] : /* istanbul ignore next */ []));
|
|
4766
|
+
timeMax = computed(() => {
|
|
4767
|
+
const { date, time } = this.maxParts();
|
|
4768
|
+
return time && date && this.dateValue() === date ? time : undefined;
|
|
4769
|
+
}, ...(ngDevMode ? [{ debugName: "timeMax" }] : /* istanbul ignore next */ []));
|
|
4770
|
+
/** The chosen day's slots when `slotsFor` is set, else the fixed list. */
|
|
4771
|
+
effectiveSlots = computed(() => {
|
|
4772
|
+
const slotsFor = this.slotsFor();
|
|
4773
|
+
if (!slotsFor)
|
|
4774
|
+
return this.slots();
|
|
4775
|
+
const date = this.dateValue();
|
|
4776
|
+
return date ? slotsFor(date) : [];
|
|
4777
|
+
}, ...(ngDevMode ? [{ debugName: "effectiveSlots" }] : /* istanbul ignore next */ []));
|
|
4778
|
+
timeDisabled = computed(() => this.disabled() || (!!this.slotsFor() && !this.dateValue()), ...(ngDevMode ? [{ debugName: "timeDisabled" }] : /* istanbul ignore next */ []));
|
|
4779
|
+
// --- Part plumbing -----------------------------------------------------------
|
|
4780
|
+
onDatePartChange(date) {
|
|
4781
|
+
let time = this.parts().time;
|
|
4782
|
+
if (!date) {
|
|
4783
|
+
time = undefined; // clearing the date clears the combined value
|
|
4784
|
+
}
|
|
4785
|
+
else {
|
|
4786
|
+
const slotsFor = this.slotsFor();
|
|
4787
|
+
// Changing the day clears a slot that no longer exists.
|
|
4788
|
+
if (slotsFor && time && !slotsFor(date).includes(time))
|
|
4789
|
+
time = undefined;
|
|
4790
|
+
}
|
|
4791
|
+
this.setParts({ date, time });
|
|
4792
|
+
}
|
|
4793
|
+
onTimePartChange(time) {
|
|
4794
|
+
this.setParts({ date: this.parts().date, time });
|
|
4795
|
+
}
|
|
4796
|
+
setParts(parts) {
|
|
4797
|
+
this.parts.set(parts);
|
|
4798
|
+
this.value.set(joinDateTime(parts.date, parts.time));
|
|
4799
|
+
}
|
|
4800
|
+
onHostFocusOut(event) {
|
|
4801
|
+
const next = event.relatedTarget;
|
|
4802
|
+
if (!next || !this.host.nativeElement.contains(next))
|
|
4803
|
+
this.touched.set(true);
|
|
4804
|
+
}
|
|
4805
|
+
// --- Styling --------------------------------------------------------------------
|
|
4806
|
+
className = computed(() => css({ display: 'block' }), ...(ngDevMode ? [{ debugName: "className" }] : /* istanbul ignore next */ []));
|
|
4807
|
+
groupClass = computed(() => css({
|
|
4808
|
+
display: 'flex',
|
|
4809
|
+
alignItems: 'stretch',
|
|
4810
|
+
flex: 1,
|
|
4811
|
+
width: '100%',
|
|
4812
|
+
minWidth: 0,
|
|
4813
|
+
...this.theme.gap(this.componentOptions().partGap ?? 'sm'),
|
|
4814
|
+
}), ...(ngDevMode ? [{ debugName: "groupClass" }] : /* istanbul ignore next */ []));
|
|
4815
|
+
datePartClass = computed(() => css({ flex: 1.4, minWidth: 0, display: 'flex', '& > *': { flex: 1, minWidth: 0 } }), ...(ngDevMode ? [{ debugName: "datePartClass" }] : /* istanbul ignore next */ []));
|
|
4816
|
+
timePartClass = computed(() => css({ flex: 1, minWidth: 0, display: 'flex', '& > *': { flex: 1, minWidth: 0 } }), ...(ngDevMode ? [{ debugName: "timePartClass" }] : /* istanbul ignore next */ []));
|
|
4817
|
+
dividerClass = computed(() => {
|
|
4818
|
+
const colors = this.theme.colorPalette();
|
|
4819
|
+
return css({
|
|
4820
|
+
width: 1,
|
|
4821
|
+
alignSelf: 'stretch',
|
|
4822
|
+
flex: 'none',
|
|
4823
|
+
backgroundColor: colors[this.componentOptions().dividerColor ?? 'outline'],
|
|
4824
|
+
});
|
|
4825
|
+
}, ...(ngDevMode ? [{ debugName: "dividerClass" }] : /* istanbul ignore next */ []));
|
|
4826
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: UniDateTimeInputComponent, deps: null, target: i0.ɵɵFactoryTarget.Component });
|
|
4827
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "21.2.12", type: UniDateTimeInputComponent, isStandalone: true, selector: "uni-date-time-input, DateTimeInput", inputs: { value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, touched: { classPropertyName: "touched", publicName: "touched", isSignal: true, isRequired: false, transformFunction: null }, invalid: { classPropertyName: "invalid", publicName: "invalid", isSignal: true, isRequired: false, transformFunction: null }, dirty: { classPropertyName: "dirty", publicName: "dirty", isSignal: true, isRequired: false, transformFunction: null }, required: { classPropertyName: "required", publicName: "required", isSignal: true, isRequired: false, transformFunction: null }, ariaDescribedBy: { classPropertyName: "ariaDescribedBy", publicName: "ariaDescribedBy", isSignal: true, isRequired: false, transformFunction: null }, label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: true, transformFunction: null }, minDateTime: { classPropertyName: "minDateTime", publicName: "minDateTime", isSignal: true, isRequired: false, transformFunction: null }, maxDateTime: { classPropertyName: "maxDateTime", publicName: "maxDateTime", isSignal: true, isRequired: false, transformFunction: null }, disabledDates: { classPropertyName: "disabledDates", publicName: "disabledDates", isSignal: true, isRequired: false, transformFunction: null }, markers: { classPropertyName: "markers", publicName: "markers", isSignal: true, isRequired: false, transformFunction: null }, slots: { classPropertyName: "slots", publicName: "slots", isSignal: true, isRequired: false, transformFunction: null }, minuteStep: { classPropertyName: "minuteStep", publicName: "minuteStep", isSignal: true, isRequired: false, transformFunction: null }, hour12: { classPropertyName: "hour12", publicName: "hour12", isSignal: true, isRequired: false, transformFunction: null }, weekStart: { classPropertyName: "weekStart", publicName: "weekStart", isSignal: true, isRequired: false, transformFunction: null }, locale: { classPropertyName: "locale", publicName: "locale", isSignal: true, isRequired: false, transformFunction: null }, slotsFor: { classPropertyName: "slotsFor", publicName: "slotsFor", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { value: "valueChange", touched: "touchedChange" }, host: { listeners: { "focusout": "onHostFocusOut($event)" }, properties: { "class": "className()" } }, providers: [{ provide: COMPONENT_NAME, useValue: 'dateTimeInput' }], usesInheritance: true, ngImport: i0, template: "<!-- One field chrome for both parts; the parts render embedded (no box of\n their own), so error/disabled/focus states stay consistent with every\n other field. Tab order: date, then time \u2014 two honest tab stops. -->\n<uni-input-box [error]=\"showError()\" [disabled]=\"disabled()\">\n <div role=\"group\" [attr.aria-label]=\"label()\" [class]=\"groupClass()\">\n <div [class]=\"datePartClass()\">\n <uni-date-input\n [embedded]=\"true\"\n label=\"Date\"\n [value]=\"dateValue()\"\n [minDate]=\"dateMin()\"\n [maxDate]=\"dateMax()\"\n [disabledDates]=\"disabledDates()\"\n [markers]=\"markers()\"\n [weekStart]=\"weekStart()\"\n [locale]=\"locale()\"\n [disabled]=\"disabled()\"\n [ariaDescribedBy]=\"ariaDescribedBy()\"\n (valueChange)=\"onDatePartChange($event)\"\n />\n </div>\n <div [class]=\"dividerClass()\"></div>\n <div [class]=\"timePartClass()\">\n <uni-time-input\n [embedded]=\"true\"\n label=\"Time\"\n [value]=\"timeValue()\"\n [minTime]=\"timeMin()\"\n [maxTime]=\"timeMax()\"\n [slots]=\"effectiveSlots()\"\n [minuteStep]=\"minuteStep()\"\n [hour12]=\"hour12()\"\n [locale]=\"locale()\"\n [disabled]=\"timeDisabled()\"\n (valueChange)=\"onTimePartChange($event)\"\n />\n </div>\n </div>\n</uni-input-box>\n", dependencies: [{ kind: "component", type: UniDateInputComponent, selector: "uni-date-input, DateInput", inputs: ["value", "disabled", "touched", "invalid", "dirty", "required", "ariaDescribedBy", "label", "placeholder", "displayFormat", "locale", "commitOnBlur", "parse", "embedded", "minDate", "maxDate", "disabledDates", "markers", "weekStart"], outputs: ["valueChange", "touchedChange", "opened", "closed", "rejected"] }, { kind: "component", type: UniInputBoxComponent, selector: "uni-input-box", inputs: ["disabled", "error", "minWidth", "height"] }, { kind: "component", type: UniTimeInputComponent, selector: "uni-time-input, TimeInput", inputs: ["value", "disabled", "touched", "invalid", "dirty", "required", "ariaDescribedBy", "label", "placeholder", "minuteStep", "minTime", "maxTime", "slots", "hour12", "locale", "commitOnBlur", "embedded"], outputs: ["valueChange", "touchedChange", "rejected"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
4828
|
+
}
|
|
4829
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: UniDateTimeInputComponent, decorators: [{
|
|
4830
|
+
type: Component,
|
|
4831
|
+
args: [{ changeDetection: ChangeDetectionStrategy.OnPush, selector: 'uni-date-time-input, DateTimeInput', imports: [UniDateInputComponent, UniInputBoxComponent, UniTimeInputComponent], providers: [{ provide: COMPONENT_NAME, useValue: 'dateTimeInput' }], host: { '[class]': 'className()', '(focusout)': 'onHostFocusOut($event)' }, template: "<!-- One field chrome for both parts; the parts render embedded (no box of\n their own), so error/disabled/focus states stay consistent with every\n other field. Tab order: date, then time \u2014 two honest tab stops. -->\n<uni-input-box [error]=\"showError()\" [disabled]=\"disabled()\">\n <div role=\"group\" [attr.aria-label]=\"label()\" [class]=\"groupClass()\">\n <div [class]=\"datePartClass()\">\n <uni-date-input\n [embedded]=\"true\"\n label=\"Date\"\n [value]=\"dateValue()\"\n [minDate]=\"dateMin()\"\n [maxDate]=\"dateMax()\"\n [disabledDates]=\"disabledDates()\"\n [markers]=\"markers()\"\n [weekStart]=\"weekStart()\"\n [locale]=\"locale()\"\n [disabled]=\"disabled()\"\n [ariaDescribedBy]=\"ariaDescribedBy()\"\n (valueChange)=\"onDatePartChange($event)\"\n />\n </div>\n <div [class]=\"dividerClass()\"></div>\n <div [class]=\"timePartClass()\">\n <uni-time-input\n [embedded]=\"true\"\n label=\"Time\"\n [value]=\"timeValue()\"\n [minTime]=\"timeMin()\"\n [maxTime]=\"timeMax()\"\n [slots]=\"effectiveSlots()\"\n [minuteStep]=\"minuteStep()\"\n [hour12]=\"hour12()\"\n [locale]=\"locale()\"\n [disabled]=\"timeDisabled()\"\n (valueChange)=\"onTimePartChange($event)\"\n />\n </div>\n </div>\n</uni-input-box>\n" }]
|
|
4832
|
+
}], propDecorators: { value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }, { type: i0.Output, args: ["valueChange"] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], touched: [{ type: i0.Input, args: [{ isSignal: true, alias: "touched", required: false }] }, { type: i0.Output, args: ["touchedChange"] }], invalid: [{ type: i0.Input, args: [{ isSignal: true, alias: "invalid", required: false }] }], dirty: [{ type: i0.Input, args: [{ isSignal: true, alias: "dirty", required: false }] }], required: [{ type: i0.Input, args: [{ isSignal: true, alias: "required", required: false }] }], ariaDescribedBy: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaDescribedBy", required: false }] }], label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: true }] }], minDateTime: [{ type: i0.Input, args: [{ isSignal: true, alias: "minDateTime", required: false }] }], maxDateTime: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxDateTime", required: false }] }], disabledDates: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabledDates", required: false }] }], markers: [{ type: i0.Input, args: [{ isSignal: true, alias: "markers", required: false }] }], slots: [{ type: i0.Input, args: [{ isSignal: true, alias: "slots", required: false }] }], minuteStep: [{ type: i0.Input, args: [{ isSignal: true, alias: "minuteStep", required: false }] }], hour12: [{ type: i0.Input, args: [{ isSignal: true, alias: "hour12", required: false }] }], weekStart: [{ type: i0.Input, args: [{ isSignal: true, alias: "weekStart", required: false }] }], locale: [{ type: i0.Input, args: [{ isSignal: true, alias: "locale", required: false }] }], slotsFor: [{ type: i0.Input, args: [{ isSignal: true, alias: "slotsFor", required: false }] }] } });
|
|
4833
|
+
|
|
3080
4834
|
class UniDialogComponent extends BaseComponent {
|
|
3081
4835
|
elem = inject(ElementRef);
|
|
3082
4836
|
/** Two-way bindable open state: [(show)]. */
|
|
@@ -3482,203 +5236,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImpo
|
|
|
3482
5236
|
}]
|
|
3483
5237
|
}], ctorParameters: () => [], propDecorators: { open: [{ type: i0.Input, args: [{ isSignal: true, alias: "open", required: false }] }, { type: i0.Output, args: ["openChange"] }], mode: [{ type: i0.Input, args: [{ isSignal: true, alias: "mode", required: false }] }], position: [{ type: i0.Input, args: [{ isSignal: true, alias: "position", required: false }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }], contentTemplate: [{ type: i0.ViewChild, args: ['content', { isSignal: true }] }], overlay: [{ type: i0.ViewChild, args: ['overlay', { isSignal: true }] }] } });
|
|
3484
5238
|
|
|
3485
|
-
class UniDropdownComponent extends BaseComponent {
|
|
3486
|
-
renderer = inject(Renderer2);
|
|
3487
|
-
delay = 100;
|
|
3488
|
-
// Reactively track visibility status using Signals
|
|
3489
|
-
showing = signal(false, ...(ngDevMode ? [{ debugName: "showing" }] : /* istanbul ignore next */ []));
|
|
3490
|
-
trigger = input.required(...(ngDevMode ? [{ debugName: "trigger" }] : /* istanbul ignore next */ []));
|
|
3491
|
-
placement = input('bottom-start', ...(ngDevMode ? [{ debugName: "placement" }] : /* istanbul ignore next */ []));
|
|
3492
|
-
offset = input({ mainAxis: 4, alignmentAxis: 12 }, ...(ngDevMode ? [{ debugName: "offset" }] : /* istanbul ignore next */ []));
|
|
3493
|
-
/** CSS anchor-name linking the trigger to the popover panel. */
|
|
3494
|
-
anchorName = newAnchorName();
|
|
3495
|
-
/**
|
|
3496
|
-
* Value for aria-haspopup on the trigger, describing what the popover
|
|
3497
|
-
* contains (e.g. 'menu' for Menu, 'dialog' for rich content). When unset,
|
|
3498
|
-
* only aria-expanded/aria-controls are managed.
|
|
3499
|
-
*/
|
|
3500
|
-
ariaHasPopup = input(null, ...(ngDevMode ? [{ debugName: "ariaHasPopup" }] : /* istanbul ignore next */ []));
|
|
3501
|
-
/** Document-unique id of the popover element, for aria-controls wiring. */
|
|
3502
|
-
popoverId = uniqueId('uni-dropdown');
|
|
3503
|
-
paddingVertical = input(...(ngDevMode ? [undefined, { debugName: "paddingVertical" }] : /* istanbul ignore next */ []));
|
|
3504
|
-
paddingHorizontal = input(...(ngDevMode ? [undefined, { debugName: "paddingHorizontal" }] : /* istanbul ignore next */ []));
|
|
3505
|
-
// Per-instance panel-chrome overrides; undefined falls back to the theme's
|
|
3506
|
-
// `dropdown` options, so hosts like uni-menu can restyle their panel
|
|
3507
|
-
// without forking the shared dropdown entry.
|
|
3508
|
-
border = input(...(ngDevMode ? [undefined, { debugName: "border" }] : /* istanbul ignore next */ []));
|
|
3509
|
-
borderRadius = input(...(ngDevMode ? [undefined, { debugName: "borderRadius" }] : /* istanbul ignore next */ []));
|
|
3510
|
-
shadow = input(...(ngDevMode ? [undefined, { debugName: "shadow" }] : /* istanbul ignore next */ []));
|
|
3511
|
-
color = input(...(ngDevMode ? [undefined, { debugName: "color" }] : /* istanbul ignore next */ []));
|
|
3512
|
-
dropdownShowing = output();
|
|
3513
|
-
dropdownHiding = output();
|
|
3514
|
-
dropdownRef;
|
|
3515
|
-
get _trigger() {
|
|
3516
|
-
return this.trigger();
|
|
3517
|
-
}
|
|
3518
|
-
get _dropdown() {
|
|
3519
|
-
return this.dropdownRef.nativeElement;
|
|
3520
|
-
}
|
|
3521
|
-
transformOriginMap = {
|
|
3522
|
-
top: 'bottom center',
|
|
3523
|
-
right: 'center left',
|
|
3524
|
-
bottom: 'top center',
|
|
3525
|
-
left: 'center right',
|
|
3526
|
-
'top-start': 'bottom left',
|
|
3527
|
-
'top-end': 'bottom right',
|
|
3528
|
-
'right-start': 'top left',
|
|
3529
|
-
'right-end': 'bottom left',
|
|
3530
|
-
'bottom-start': 'top left',
|
|
3531
|
-
'bottom-end': 'top right',
|
|
3532
|
-
'left-start': 'top right',
|
|
3533
|
-
'left-end': 'bottom right',
|
|
3534
|
-
};
|
|
3535
|
-
dropdownClass = computed(() => {
|
|
3536
|
-
const currentPlacement = this.placement();
|
|
3537
|
-
return css([
|
|
3538
|
-
{
|
|
3539
|
-
// Reset browser agent default popover styles
|
|
3540
|
-
border: 'none',
|
|
3541
|
-
background: 'transparent',
|
|
3542
|
-
padding: 0,
|
|
3543
|
-
overflow: 'visible',
|
|
3544
|
-
width: 'max-content',
|
|
3545
|
-
// Native anchor positioning: the browser keeps the panel attached to
|
|
3546
|
-
// the trigger (no scroll/resize listeners needed)
|
|
3547
|
-
...anchorStyles(this.anchorName, currentPlacement, this.offset()),
|
|
3548
|
-
// 2. Animate discrete properties across top layer layout contexts
|
|
3549
|
-
transitionProperty: 'transform, opacity, display, overlay',
|
|
3550
|
-
transitionDuration: `${this.delay}ms`,
|
|
3551
|
-
transitionTimingFunction: 'linear',
|
|
3552
|
-
transitionBehavior: 'allow-discrete',
|
|
3553
|
-
// Hidden State (Closed)
|
|
3554
|
-
opacity: 0,
|
|
3555
|
-
transform: 'scale(0.8)',
|
|
3556
|
-
transformOrigin: this.transformOriginMap[currentPlacement],
|
|
3557
|
-
// 3. Active state styling controlled via the native browser pseudo-class
|
|
3558
|
-
['&:popover-open']: {
|
|
3559
|
-
opacity: 1,
|
|
3560
|
-
transform: 'scale(1)',
|
|
3561
|
-
},
|
|
3562
|
-
// 4. Starting-style rules what properties animate *from* when transitioning in
|
|
3563
|
-
['@starting-style']: {
|
|
3564
|
-
['&:popover-open']: {
|
|
3565
|
-
opacity: 0,
|
|
3566
|
-
transform: 'scale(0.8)',
|
|
3567
|
-
},
|
|
3568
|
-
},
|
|
3569
|
-
},
|
|
3570
|
-
]);
|
|
3571
|
-
}, ...(ngDevMode ? [{ debugName: "dropdownClass" }] : /* istanbul ignore next */ []));
|
|
3572
|
-
/** The element that receives focus and carries the ARIA popup state. */
|
|
3573
|
-
get _focusTarget() {
|
|
3574
|
-
return resolveFocusTarget(this._trigger);
|
|
3575
|
-
}
|
|
3576
|
-
ngOnInit() {
|
|
3577
|
-
// Single native click binding to manage open/close commands
|
|
3578
|
-
this.renderer.listen(this._trigger, 'click', (e) => {
|
|
3579
|
-
e.stopPropagation();
|
|
3580
|
-
this.toggleDropdown();
|
|
3581
|
-
});
|
|
3582
|
-
// Anchor the popover panel to the trigger element
|
|
3583
|
-
this.renderer.setStyle(this._trigger, 'anchor-name', this.anchorName);
|
|
3584
|
-
// Wire the ARIA popup contract onto the focusable trigger element
|
|
3585
|
-
const focusTarget = this._focusTarget;
|
|
3586
|
-
this.renderer.setAttribute(focusTarget, 'aria-expanded', 'false');
|
|
3587
|
-
this.renderer.setAttribute(focusTarget, 'aria-controls', this.popoverId);
|
|
3588
|
-
if (this.ariaHasPopup()) {
|
|
3589
|
-
this.renderer.setAttribute(focusTarget, 'aria-haspopup', this.ariaHasPopup());
|
|
3590
|
-
}
|
|
3591
|
-
// Sync state if user invokes light-dismiss via outside click or Escape key
|
|
3592
|
-
this.renderer.listen(this._dropdown, 'toggle', (event) => {
|
|
3593
|
-
const isOpened = event.newState === 'open';
|
|
3594
|
-
this.showing.set(isOpened);
|
|
3595
|
-
this.renderer.setAttribute(this._focusTarget, 'aria-expanded', `${isOpened}`);
|
|
3596
|
-
if (isOpened) {
|
|
3597
|
-
this.dropdownShowing.emit(true);
|
|
3598
|
-
}
|
|
3599
|
-
else {
|
|
3600
|
-
this.dropdownHiding.emit(true);
|
|
3601
|
-
this.restoreFocus();
|
|
3602
|
-
}
|
|
3603
|
-
});
|
|
3604
|
-
}
|
|
3605
|
-
/**
|
|
3606
|
-
* Returns focus to the trigger when the popover closes while focus was
|
|
3607
|
-
* inside it (or was dropped on <body> by the top layer closing), so
|
|
3608
|
-
* keyboard users are never stranded (WCAG 2.4.3).
|
|
3609
|
-
*/
|
|
3610
|
-
restoreFocus() {
|
|
3611
|
-
const active = document.activeElement;
|
|
3612
|
-
if (active === document.body || (active && this._dropdown.contains(active))) {
|
|
3613
|
-
this._focusTarget.focus();
|
|
3614
|
-
}
|
|
3615
|
-
}
|
|
3616
|
-
toggleDropdown() {
|
|
3617
|
-
if (this.showing()) {
|
|
3618
|
-
this._dropdown.hidePopover();
|
|
3619
|
-
}
|
|
3620
|
-
else {
|
|
3621
|
-
this._dropdown.showPopover();
|
|
3622
|
-
}
|
|
3623
|
-
}
|
|
3624
|
-
hideDropdown() {
|
|
3625
|
-
this._dropdown.hidePopover();
|
|
3626
|
-
}
|
|
3627
|
-
ngOnDestroy() {
|
|
3628
|
-
try {
|
|
3629
|
-
this._dropdown.hidePopover();
|
|
3630
|
-
}
|
|
3631
|
-
catch {
|
|
3632
|
-
// popover was already closed or detached
|
|
3633
|
-
}
|
|
3634
|
-
}
|
|
3635
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: UniDropdownComponent, deps: null, target: i0.ɵɵFactoryTarget.Component });
|
|
3636
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "21.2.12", type: UniDropdownComponent, isStandalone: true, selector: "uni-dropdown", inputs: { trigger: { classPropertyName: "trigger", publicName: "trigger", isSignal: true, isRequired: true, transformFunction: null }, placement: { classPropertyName: "placement", publicName: "placement", isSignal: true, isRequired: false, transformFunction: null }, offset: { classPropertyName: "offset", publicName: "offset", isSignal: true, isRequired: false, transformFunction: null }, ariaHasPopup: { classPropertyName: "ariaHasPopup", publicName: "ariaHasPopup", isSignal: true, isRequired: false, transformFunction: null }, paddingVertical: { classPropertyName: "paddingVertical", publicName: "paddingVertical", isSignal: true, isRequired: false, transformFunction: null }, paddingHorizontal: { classPropertyName: "paddingHorizontal", publicName: "paddingHorizontal", isSignal: true, isRequired: false, transformFunction: null }, border: { classPropertyName: "border", publicName: "border", isSignal: true, isRequired: false, transformFunction: null }, borderRadius: { classPropertyName: "borderRadius", publicName: "borderRadius", isSignal: true, isRequired: false, transformFunction: null }, shadow: { classPropertyName: "shadow", publicName: "shadow", isSignal: true, isRequired: false, transformFunction: null }, color: { classPropertyName: "color", publicName: "color", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { dropdownShowing: "dropdownShowing", dropdownHiding: "dropdownHiding" }, providers: [{ provide: COMPONENT_NAME, useValue: 'dropdown' }], viewQueries: [{ propertyName: "dropdownRef", first: true, predicate: ["dropdown"], descendants: true, static: true }], usesInheritance: true, ngImport: i0, template: `
|
|
3637
|
-
<!-- 1. The native 'popover' attribute brings it to the top layer with native light-dismiss -->
|
|
3638
|
-
<div #dropdown popover="auto" [id]="popoverId" [class]="dropdownClass()">
|
|
3639
|
-
<div
|
|
3640
|
-
box-layout
|
|
3641
|
-
[border]="border() ?? componentOptions().border"
|
|
3642
|
-
[borderRadius]="borderRadius() ?? componentOptions().borderRadius"
|
|
3643
|
-
[paddingVertical]="paddingVertical()"
|
|
3644
|
-
[paddingHorizontal]="paddingHorizontal()"
|
|
3645
|
-
[color]="color() ?? componentOptions().color"
|
|
3646
|
-
[shadow]="shadow() ?? componentOptions().shadow"
|
|
3647
|
-
>
|
|
3648
|
-
<ng-content></ng-content>
|
|
3649
|
-
</div>
|
|
3650
|
-
</div>
|
|
3651
|
-
`, isInline: true, dependencies: [{ kind: "component", type: UniBoxComponent, selector: "[uni-box-layout], [box-layout]", inputs: ["color", "backgroundColor", "borderRadius", "borderRadiusLeft", "borderRadiusRight", "borderRadiusTop", "borderRadiusBottom", "padding", "paddingHorizontal", "paddingVertical", "paddingLeft", "paddingRight", "paddingTop", "paddingBottom", "border", "borderTop", "borderBottom", "borderLeft", "borderRight", "dashBorder", "alignSelf", "alignItems", "alignContent", "justifyContent", "grow", "display", "position", "inset", "height", "minHeight", "maxHeight", "width", "minWidth", "maxWidth", "ignoreDir", "gridArea", "gridColumn", "gridRow", "overflow", "elevation", "shadow", "gap", "fullWidth", "fullHeight", "flexDirection", "textAlign", "wrapItems", "zIndex"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
3652
|
-
}
|
|
3653
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: UniDropdownComponent, decorators: [{
|
|
3654
|
-
type: Component,
|
|
3655
|
-
args: [{
|
|
3656
|
-
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
3657
|
-
selector: 'uni-dropdown',
|
|
3658
|
-
imports: [UniBoxComponent],
|
|
3659
|
-
template: `
|
|
3660
|
-
<!-- 1. The native 'popover' attribute brings it to the top layer with native light-dismiss -->
|
|
3661
|
-
<div #dropdown popover="auto" [id]="popoverId" [class]="dropdownClass()">
|
|
3662
|
-
<div
|
|
3663
|
-
box-layout
|
|
3664
|
-
[border]="border() ?? componentOptions().border"
|
|
3665
|
-
[borderRadius]="borderRadius() ?? componentOptions().borderRadius"
|
|
3666
|
-
[paddingVertical]="paddingVertical()"
|
|
3667
|
-
[paddingHorizontal]="paddingHorizontal()"
|
|
3668
|
-
[color]="color() ?? componentOptions().color"
|
|
3669
|
-
[shadow]="shadow() ?? componentOptions().shadow"
|
|
3670
|
-
>
|
|
3671
|
-
<ng-content></ng-content>
|
|
3672
|
-
</div>
|
|
3673
|
-
</div>
|
|
3674
|
-
`,
|
|
3675
|
-
providers: [{ provide: COMPONENT_NAME, useValue: 'dropdown' }],
|
|
3676
|
-
}]
|
|
3677
|
-
}], propDecorators: { trigger: [{ type: i0.Input, args: [{ isSignal: true, alias: "trigger", required: true }] }], placement: [{ type: i0.Input, args: [{ isSignal: true, alias: "placement", required: false }] }], offset: [{ type: i0.Input, args: [{ isSignal: true, alias: "offset", required: false }] }], ariaHasPopup: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaHasPopup", required: false }] }], paddingVertical: [{ type: i0.Input, args: [{ isSignal: true, alias: "paddingVertical", required: false }] }], paddingHorizontal: [{ type: i0.Input, args: [{ isSignal: true, alias: "paddingHorizontal", required: false }] }], border: [{ type: i0.Input, args: [{ isSignal: true, alias: "border", required: false }] }], borderRadius: [{ type: i0.Input, args: [{ isSignal: true, alias: "borderRadius", required: false }] }], shadow: [{ type: i0.Input, args: [{ isSignal: true, alias: "shadow", required: false }] }], color: [{ type: i0.Input, args: [{ isSignal: true, alias: "color", required: false }] }], dropdownShowing: [{ type: i0.Output, args: ["dropdownShowing"] }], dropdownHiding: [{ type: i0.Output, args: ["dropdownHiding"] }], dropdownRef: [{
|
|
3678
|
-
type: ViewChild,
|
|
3679
|
-
args: ['dropdown', { static: true }]
|
|
3680
|
-
}] } });
|
|
3681
|
-
|
|
3682
5239
|
class UniExpandComponent extends BaseComponent {
|
|
3683
5240
|
collapsed = model(true, ...(ngDevMode ? [{ debugName: "collapsed" }] : /* istanbul ignore next */ []));
|
|
3684
5241
|
/**
|
|
@@ -4169,57 +5726,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImpo
|
|
|
4169
5726
|
* This file exports all public-facing elements of the file-drop-zone component.
|
|
4170
5727
|
*/
|
|
4171
5728
|
|
|
4172
|
-
class UniInputBoxComponent extends BaseComponent {
|
|
4173
|
-
className = css({ display: 'contents' });
|
|
4174
|
-
disabled = input(false, ...(ngDevMode ? [{ debugName: "disabled" }] : /* istanbul ignore next */ []));
|
|
4175
|
-
error = input(false, ...(ngDevMode ? [{ debugName: "error" }] : /* istanbul ignore next */ []));
|
|
4176
|
-
minWidth = input('0', ...(ngDevMode ? [{ debugName: "minWidth" }] : /* istanbul ignore next */ []));
|
|
4177
|
-
/** Override the themed field height, e.g. `'auto'` for multi-line fields. */
|
|
4178
|
-
height = input(undefined, ...(ngDevMode ? [{ debugName: "height" }] : /* istanbul ignore next */ []));
|
|
4179
|
-
color = computed(() => this.error() ? this.componentOptions().errorColor : this.componentOptions().color, ...(ngDevMode ? [{ debugName: "color" }] : /* istanbul ignore next */ []));
|
|
4180
|
-
border = computed(() => this.error() ? this.componentOptions().errorBorder : this.componentOptions().border, ...(ngDevMode ? [{ debugName: "border" }] : /* istanbul ignore next */ []));
|
|
4181
|
-
shadow = computed(() => this.error() ? this.componentOptions().errorShadow : this.componentOptions().shadow, ...(ngDevMode ? [{ debugName: "shadow" }] : /* istanbul ignore next */ []));
|
|
4182
|
-
inputBoxClass = computed(() => css([
|
|
4183
|
-
this.disabled() && {
|
|
4184
|
-
...this.theme.color(this.componentOptions().disabledTextColor),
|
|
4185
|
-
...this.theme.backgroundColor(this.componentOptions().disabledColor),
|
|
4186
|
-
cursor: 'not-allowed !important',
|
|
4187
|
-
},
|
|
4188
|
-
{
|
|
4189
|
-
'& input, select, textarea': {
|
|
4190
|
-
...removeInputPlatformStyling,
|
|
4191
|
-
height: '100%',
|
|
4192
|
-
...this.theme.paddingLeft(this.componentOptions().paddingLeft),
|
|
4193
|
-
...this.theme.color(this.componentOptions().textColor),
|
|
4194
|
-
...this.theme.typeface(this.componentOptions().typeFace),
|
|
4195
|
-
},
|
|
4196
|
-
// Multi-line fields size themselves (rows/resize), not from the box.
|
|
4197
|
-
'& textarea': {
|
|
4198
|
-
height: 'auto',
|
|
4199
|
-
...this.theme.paddingTop('xs'),
|
|
4200
|
-
...this.theme.paddingBottom('xs'),
|
|
4201
|
-
},
|
|
4202
|
-
'&:has(input:disabled, select:disabled, textarea:disabled)': {
|
|
4203
|
-
...this.theme.color(this.componentOptions().disabledTextColor),
|
|
4204
|
-
...this.theme.backgroundColor(this.componentOptions().disabledColor),
|
|
4205
|
-
},
|
|
4206
|
-
'& input:disabled, select:disabled, textarea:disabled': {
|
|
4207
|
-
cursor: 'not-allowed !important',
|
|
4208
|
-
},
|
|
4209
|
-
'&:has(input:focus, select:focus, textarea:focus)': {
|
|
4210
|
-
outline: this.componentOptions().focusOutline,
|
|
4211
|
-
outlineOffset: this.componentOptions().focusOutlineOffset,
|
|
4212
|
-
},
|
|
4213
|
-
},
|
|
4214
|
-
]), ...(ngDevMode ? [{ debugName: "inputBoxClass" }] : /* istanbul ignore next */ []));
|
|
4215
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: UniInputBoxComponent, deps: null, target: i0.ɵɵFactoryTarget.Component });
|
|
4216
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "21.2.12", type: UniInputBoxComponent, isStandalone: true, selector: "uni-input-box", inputs: { disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, error: { classPropertyName: "error", publicName: "error", isSignal: true, isRequired: false, transformFunction: null }, minWidth: { classPropertyName: "minWidth", publicName: "minWidth", isSignal: true, isRequired: false, transformFunction: null }, height: { classPropertyName: "height", publicName: "height", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class": "className" } }, providers: [{ provide: COMPONENT_NAME, useValue: 'input' }], usesInheritance: true, ngImport: i0, template: "<div\n row-layout\n alignItems=\"center\"\n [height]=\"height() ?? componentOptions().height\"\n [color]=\"color()\"\n [border]=\"border()\"\n [borderRadius]=\"componentOptions().borderRadius\"\n [shadow]=\"shadow()\"\n [minWidth]=\"minWidth()\"\n [class]=\"inputBoxClass()\"\n position=\"relative\"\n>\n <ng-content></ng-content>\n</div>\n", dependencies: [{ kind: "component", type: UniRowComponent, selector: "[uni-row-layout], [row-layout]", inputs: ["display", "flexDirection", "minWidth"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
4217
|
-
}
|
|
4218
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: UniInputBoxComponent, decorators: [{
|
|
4219
|
-
type: Component,
|
|
4220
|
-
args: [{ selector: 'uni-input-box', imports: [UniRowComponent], providers: [{ provide: COMPONENT_NAME, useValue: 'input' }], host: { '[class]': 'className' }, changeDetection: ChangeDetectionStrategy.OnPush, template: "<div\n row-layout\n alignItems=\"center\"\n [height]=\"height() ?? componentOptions().height\"\n [color]=\"color()\"\n [border]=\"border()\"\n [borderRadius]=\"componentOptions().borderRadius\"\n [shadow]=\"shadow()\"\n [minWidth]=\"minWidth()\"\n [class]=\"inputBoxClass()\"\n position=\"relative\"\n>\n <ng-content></ng-content>\n</div>\n" }]
|
|
4221
|
-
}], propDecorators: { disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], error: [{ type: i0.Input, args: [{ isSignal: true, alias: "error", required: false }] }], minWidth: [{ type: i0.Input, args: [{ isSignal: true, alias: "minWidth", required: false }] }], height: [{ type: i0.Input, args: [{ isSignal: true, alias: "height", required: false }] }] } });
|
|
4222
|
-
|
|
4223
5729
|
/**
|
|
4224
5730
|
* Text input that emits `change` only after the user pauses typing. Wears the
|
|
4225
5731
|
* shared input chrome (themed color, border, typeface, focus ring) via
|
|
@@ -5533,6 +7039,13 @@ class UniRadioComponent extends BaseComponent {
|
|
|
5533
7039
|
});
|
|
5534
7040
|
radioOptionClass = computed(() => {
|
|
5535
7041
|
const { outerCircleSize, innerCircleSize, innerCircleOffset } = this.metrics();
|
|
7042
|
+
// The dot's grow/retract is a token: 0.3s default, 0 = instant. The
|
|
7043
|
+
// transitions are scoped — never `all` — so the focus ring's outline and
|
|
7044
|
+
// shadow apply instantly instead of interpolating from a stale outline
|
|
7045
|
+
// color, which flashed a dark ring before the themed ring color landed.
|
|
7046
|
+
const speed = this.componentOptions().transitionSpeed ?? 0.3;
|
|
7047
|
+
const ringTransition = `border-color ${speed}s ease, background-color ${speed}s ease`;
|
|
7048
|
+
const dotTransition = `transform ${speed}s ease`;
|
|
5536
7049
|
return css({
|
|
5537
7050
|
userSelect: 'none',
|
|
5538
7051
|
cursor: this.disabled() ? 'not-allowed' : 'pointer',
|
|
@@ -5548,7 +7061,7 @@ class UniRadioComponent extends BaseComponent {
|
|
|
5548
7061
|
? this.getThemeColor('on-disabled')
|
|
5549
7062
|
: this.getThemeColor(this.componentOptions().ringColor ?? 'outline')}`,
|
|
5550
7063
|
position: 'relative',
|
|
5551
|
-
transition:
|
|
7064
|
+
transition: ringTransition,
|
|
5552
7065
|
backgroundColor: this.getThemeColor(this.componentOptions().fillColor ?? 'surface'),
|
|
5553
7066
|
flexShrink: 0,
|
|
5554
7067
|
},
|
|
@@ -5561,7 +7074,7 @@ class UniRadioComponent extends BaseComponent {
|
|
|
5561
7074
|
top: innerCircleOffset,
|
|
5562
7075
|
left: innerCircleOffset,
|
|
5563
7076
|
transform: 'scale(0)',
|
|
5564
|
-
transition:
|
|
7077
|
+
transition: dotTransition,
|
|
5565
7078
|
},
|
|
5566
7079
|
'&:hover .radio-button': this.disabled()
|
|
5567
7080
|
? {}
|
|
@@ -5589,9 +7102,9 @@ class UniRadioComponent extends BaseComponent {
|
|
|
5589
7102
|
'&:checked + .radio-button .radio-inner': {
|
|
5590
7103
|
transform: 'scale(1)',
|
|
5591
7104
|
},
|
|
7105
|
+
// The shared, themable focus indicator, keyed off the hidden input.
|
|
5592
7106
|
'&:focus + .radio-button': {
|
|
5593
|
-
|
|
5594
|
-
outlineOffset: '2px',
|
|
7107
|
+
...this.theme.focusRingStyle(this.getThemeColor(this.variant())),
|
|
5595
7108
|
},
|
|
5596
7109
|
}), ...(ngDevMode ? [{ debugName: "radioInputClass" }] : /* istanbul ignore next */ []));
|
|
5597
7110
|
handleRadioChange(optionValue) {
|
|
@@ -5978,7 +7491,8 @@ class UniSliderComponent extends BaseComponent {
|
|
|
5978
7491
|
'&::-moz-range-track': { height: trackHeight, borderRadius: radius, background: track },
|
|
5979
7492
|
'&::-moz-range-progress': { height: trackHeight, borderRadius: radius, background: fill },
|
|
5980
7493
|
'&::-moz-range-thumb': thumb,
|
|
5981
|
-
|
|
7494
|
+
// The shared, themable focus indicator, in the track's fill color.
|
|
7495
|
+
'&:focus-visible': { ...this.theme.focusRingStyle(fill) },
|
|
5982
7496
|
'&:disabled': {
|
|
5983
7497
|
cursor: 'not-allowed',
|
|
5984
7498
|
opacity: 0.5,
|
|
@@ -8035,7 +9549,9 @@ class UniToggleComponent extends BaseComponent {
|
|
|
8035
9549
|
: this.getThemeColor(this.componentOptions().trackColor ?? 'surface-variant'),
|
|
8036
9550
|
borderRadius: toggleSize / 2,
|
|
8037
9551
|
position: 'relative',
|
|
8038
|
-
|
|
9552
|
+
// Scoped, never `all`: the focus ring must apply instantly rather
|
|
9553
|
+
// than interpolating its outline color from a stale value.
|
|
9554
|
+
transition: 'background-color 0.3s ease, border-color 0.3s ease',
|
|
8039
9555
|
},
|
|
8040
9556
|
'& .toggle-slider': {
|
|
8041
9557
|
width: sliderSize,
|
|
@@ -8045,7 +9561,7 @@ class UniToggleComponent extends BaseComponent {
|
|
|
8045
9561
|
position: 'absolute',
|
|
8046
9562
|
top: sliderOffset,
|
|
8047
9563
|
left: sliderOffset,
|
|
8048
|
-
transition: '
|
|
9564
|
+
transition: 'transform 0.3s ease, background-color 0.3s ease',
|
|
8049
9565
|
...this.theme.boxShadow('raised'),
|
|
8050
9566
|
},
|
|
8051
9567
|
// Hover darkens whatever the token resolves to — the button convention.
|
|
@@ -8074,9 +9590,9 @@ class UniToggleComponent extends BaseComponent {
|
|
|
8074
9590
|
'&:disabled + .toggle-switch': {
|
|
8075
9591
|
cursor: 'not-allowed',
|
|
8076
9592
|
},
|
|
9593
|
+
// The shared, themable focus indicator, keyed off the hidden input.
|
|
8077
9594
|
'&:focus + .toggle-switch': {
|
|
8078
|
-
|
|
8079
|
-
outlineOffset: '2px',
|
|
9595
|
+
...this.theme.focusRingStyle(this.getThemeColor(this.variant())),
|
|
8080
9596
|
},
|
|
8081
9597
|
});
|
|
8082
9598
|
}, ...(ngDevMode ? [{ debugName: "toggleInput" }] : /* istanbul ignore next */ []));
|
|
@@ -8298,5 +9814,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImpo
|
|
|
8298
9814
|
* Generated bundle index. Do not edit.
|
|
8299
9815
|
*/
|
|
8300
9816
|
|
|
8301
|
-
export { BodyRenderDirective, ConfirmationDialogComponent, DragAndDropDirective, FOCUSABLE_SELECTOR, ListboxNavigation, LocalStorageService, NotificationService, NotificationsComponent, RippleDirective, ThemeService, UNI_THEMES, UniAlertComponent, UniAppBarComponent, UniAvatarComponent, UniAvatarGroupComponent, UniBackgroundComponent, UniBadgeComponent, UniBaseDatasource, UniBoxComponent, UniBreadcrumbComponent, UniButtonComponent, UniButtonGroupComponent, UniCardComponent, UniCardContentComponent, UniCardHeaderComponent, UniCenterComponent, UniCheckboxComponent, UniDataSearchComponent, UniDataTableComponent, UniDebounceInputComponent, UniDialogButtonsComponent, UniDialogComponent, UniDialogHeaderComponent, UniDividerComponent, UniDrawerComponent, UniDropdownComponent, UniExpandAreaComponent, UniExpandComponent, UniExpandToggleComponent, UniFileDropZoneComponent, UniGridAreaComponent, UniGridComponent, UniIconButtonComponent, UniIconComponent, UniInputBoxComponent, UniInputComponent, UniJsonViewComponent, UniMenuComponent, UniMultiSelectComponent, UniMultiSelectDropdownComponent, UniNotificationBadgeComponent, UniPaginatorComponent, UniPopoverComponent, UniProgressBarComponent, UniProgressGaugeComponent, UniRadioComponent, UniRecordDatasource, UniRowComponent, UniScrollAreaComponent, UniSearchInputComponent, UniSelectComponent, UniServerSideDatasource, UniSkeletonComponent, UniSliderComponent, UniSnackbarComponent, UniSortHeaderComponent, UniStackComponent, UniStatComponent, UniSymbolComponent, UniTabComponent, UniTabsComponent, UniTagComponent, UniTagInputComponent, UniTextComponent, UniTextareaComponent, UniThemeBuilderComponent, UniThemeSwitchComponent, UniToggleComponent, UniTooltipComponent, UniWrapComponent, acceptableFile, anchorArrowStyles, anchorStyles, createListboxNavigation, getFileExtension, isDivider, motionSafe, newAnchorName, resolveFocusTarget, uniqueId, useTimer, visuallyHidden };
|
|
9817
|
+
export { BodyRenderDirective, ConfirmationDialogComponent, DragAndDropDirective, FOCUSABLE_SELECTOR, ListboxNavigation, LocalStorageService, NotificationService, NotificationsComponent, RippleDirective, ThemeService, UNI_THEMES, UniAlertComponent, UniAppBarComponent, UniAvatarComponent, UniAvatarGroupComponent, UniBackgroundComponent, UniBadgeComponent, UniBaseDatasource, UniBoxComponent, UniBreadcrumbComponent, UniButtonComponent, UniButtonGroupComponent, UniCalendarComponent, UniCardComponent, UniCardContentComponent, UniCardHeaderComponent, UniCenterComponent, UniCheckboxComponent, UniDataSearchComponent, UniDataTableComponent, UniDateInputComponent, UniDateTimeInputComponent, UniDebounceInputComponent, UniDialogButtonsComponent, UniDialogComponent, UniDialogHeaderComponent, UniDividerComponent, UniDrawerComponent, UniDropdownComponent, UniExpandAreaComponent, UniExpandComponent, UniExpandToggleComponent, UniFileDropZoneComponent, UniGridAreaComponent, UniGridComponent, UniIconButtonComponent, UniIconComponent, UniInputBoxComponent, UniInputComponent, UniJsonViewComponent, UniMenuComponent, UniMultiSelectComponent, UniMultiSelectDropdownComponent, UniNotificationBadgeComponent, UniPaginatorComponent, UniPopoverComponent, UniProgressBarComponent, UniProgressGaugeComponent, UniRadioComponent, UniRecordDatasource, UniRowComponent, UniScrollAreaComponent, UniSearchInputComponent, UniSelectComponent, UniServerSideDatasource, UniSkeletonComponent, UniSliderComponent, UniSnackbarComponent, UniSortHeaderComponent, UniStackComponent, UniStatComponent, UniSymbolComponent, UniTabComponent, UniTabsComponent, UniTagComponent, UniTagInputComponent, UniTextComponent, UniTextareaComponent, UniThemeBuilderComponent, UniThemeSwitchComponent, UniTimeInputComponent, UniToggleComponent, UniTooltipComponent, UniWrapComponent, acceptableFile, addDays, addMonths, anchorArrowStyles, anchorStyles, buildMonthGrid, createListboxNavigation, dayOfWeek, daysInMonth, formatDate, formatMonthHeading, formatTime, getFileExtension, inclusiveDayCount, isDivider, isValidDate, isoDate, joinDateTime, localeDatePlaceholder, localeDefaultHour12, localeFieldOrder, localeMonthNames, localeWeekStart, monthOf, motionSafe, newAnchorName, parseDateText, parseTimeText, resolveFocusTarget, splitDateTime, timeSlots, todayIso, uniqueId, useTimer, visuallyHidden, weekdayNames };
|
|
8302
9818
|
//# sourceMappingURL=uni-design-system-uni-angular.mjs.map
|