@uni-design-system/uni-angular 8.0.0 → 8.2.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,710 @@ 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
|
+
/**
|
|
2689
|
+
* The month on screen: the `month` model, else the value's month, else
|
|
2690
|
+
* today's. Falsy guards on purpose: `''` — the only typeable empty for a
|
|
2691
|
+
* string-typed model — counts as unset, or it would reach the month math
|
|
2692
|
+
* and blow up the grid and heading.
|
|
2693
|
+
*/
|
|
2694
|
+
viewMonth = computed(() => this.month() || monthOf(this.anchorDate() || todayIso()), ...(ngDevMode ? [{ debugName: "viewMonth" }] : /* istanbul ignore next */ []));
|
|
2695
|
+
anchorDate = computed(() => {
|
|
2696
|
+
const value = this.value();
|
|
2697
|
+
return this.mode() === 'range'
|
|
2698
|
+
? value?.start
|
|
2699
|
+
: value;
|
|
2700
|
+
}, ...(ngDevMode ? [{ debugName: "anchorDate" }] : /* istanbul ignore next */ []));
|
|
2701
|
+
/**
|
|
2702
|
+
* The roving-tabindex day. Re-derives when the view or value changes
|
|
2703
|
+
* (selected day in view → today in view → first enabled day); keyboard
|
|
2704
|
+
* navigation writes it directly.
|
|
2705
|
+
*/
|
|
2706
|
+
focusedDate = linkedSignal(() => this.pickFocus(), ...(ngDevMode ? [{ debugName: "focusedDate" }] : /* istanbul ignore next */ []));
|
|
2707
|
+
showError = computed(() => this.invalid() && (this.touched() || this.dirty()), ...(ngDevMode ? [{ debugName: "showError" }] : /* istanbul ignore next */ []));
|
|
2708
|
+
heading = computed(() => formatMonthHeading(this.viewMonth(), this.resolvedLocale()), ...(ngDevMode ? [{ debugName: "heading" }] : /* istanbul ignore next */ []));
|
|
2709
|
+
weekdays = computed(() => weekdayNames(this.resolvedLocale(), this.resolvedWeekStart(), this.componentOptions().weekdayFormat ?? 'short'), ...(ngDevMode ? [{ debugName: "weekdays" }] : /* istanbul ignore next */ []));
|
|
2710
|
+
disabledDateSet = computed(() => {
|
|
2711
|
+
const dates = this.disabledDates();
|
|
2712
|
+
return Array.isArray(dates) ? new Set(dates) : null;
|
|
2713
|
+
}, ...(ngDevMode ? [{ debugName: "disabledDateSet" }] : /* istanbul ignore next */ []));
|
|
2714
|
+
markersByDate = computed(() => {
|
|
2715
|
+
const map = new Map();
|
|
2716
|
+
for (const marker of this.markers()) {
|
|
2717
|
+
const existing = map.get(marker.date);
|
|
2718
|
+
if (existing)
|
|
2719
|
+
existing.push(marker);
|
|
2720
|
+
else
|
|
2721
|
+
map.set(marker.date, [marker]);
|
|
2722
|
+
}
|
|
2723
|
+
return map;
|
|
2724
|
+
}, ...(ngDevMode ? [{ debugName: "markersByDate" }] : /* istanbul ignore next */ []));
|
|
2725
|
+
/** The committed range, or the pending preview band. */
|
|
2726
|
+
rangeBand = computed(() => {
|
|
2727
|
+
if (this.mode() !== 'range')
|
|
2728
|
+
return null;
|
|
2729
|
+
const pending = this.pendingStart();
|
|
2730
|
+
const preview = this.previewDate();
|
|
2731
|
+
if (pending && preview) {
|
|
2732
|
+
const [start, end] = preview < pending ? [preview, pending] : [pending, preview];
|
|
2733
|
+
return { start, end, preview: true };
|
|
2734
|
+
}
|
|
2735
|
+
if (pending)
|
|
2736
|
+
return { start: pending, end: pending, preview: true };
|
|
2737
|
+
const value = this.value();
|
|
2738
|
+
if (value?.start && value?.end)
|
|
2739
|
+
return { start: value.start, end: value.end, preview: false };
|
|
2740
|
+
return null;
|
|
2741
|
+
}, ...(ngDevMode ? [{ debugName: "rangeBand" }] : /* istanbul ignore next */ []));
|
|
2742
|
+
isDayDisabled(date) {
|
|
2743
|
+
const min = this.minDate();
|
|
2744
|
+
const max = this.maxDate();
|
|
2745
|
+
if ((min && date < min) || (max && date > max))
|
|
2746
|
+
return true;
|
|
2747
|
+
const dates = this.disabledDates();
|
|
2748
|
+
if (!dates)
|
|
2749
|
+
return false;
|
|
2750
|
+
const set = this.disabledDateSet();
|
|
2751
|
+
return set ? set.has(date) : dates(date);
|
|
2752
|
+
}
|
|
2753
|
+
isSelected(date) {
|
|
2754
|
+
if (this.mode() === 'single')
|
|
2755
|
+
return this.value() === date;
|
|
2756
|
+
const pending = this.pendingStart();
|
|
2757
|
+
if (pending)
|
|
2758
|
+
return pending === date;
|
|
2759
|
+
const value = this.value();
|
|
2760
|
+
return value?.start === date || value?.end === date;
|
|
2761
|
+
}
|
|
2762
|
+
/** The full render model: one precomputed cell per grid position. */
|
|
2763
|
+
gridWeeks = computed(() => {
|
|
2764
|
+
const locale = this.resolvedLocale();
|
|
2765
|
+
const band = this.rangeBand();
|
|
2766
|
+
const today = todayIso();
|
|
2767
|
+
const focused = this.focusedDate();
|
|
2768
|
+
const markers = this.markersByDate();
|
|
2769
|
+
const cellBase = this.cellClass();
|
|
2770
|
+
const dayBase = this.dayClass();
|
|
2771
|
+
const todayClass = this.todayClass();
|
|
2772
|
+
return buildMonthGrid(this.viewMonth(), this.resolvedWeekStart()).map((week) => week.map((cell) => {
|
|
2773
|
+
const date = cell.date;
|
|
2774
|
+
const selected = !cell.outside && this.isSelected(date);
|
|
2775
|
+
const inBand = !cell.outside && !!band && date >= band.start && date <= band.end;
|
|
2776
|
+
const dayMarkers = cell.outside ? [] : (markers.get(date) ?? []).slice(0, 3);
|
|
2777
|
+
const markerLabel = dayMarkers
|
|
2778
|
+
.filter((marker) => marker.label)
|
|
2779
|
+
.map((marker) => marker.label)
|
|
2780
|
+
.join(', ');
|
|
2781
|
+
const isToday = date === today;
|
|
2782
|
+
return {
|
|
2783
|
+
date,
|
|
2784
|
+
day: Number(date.slice(8, 10)),
|
|
2785
|
+
outside: cell.outside,
|
|
2786
|
+
disabled: this.isDayDisabled(date),
|
|
2787
|
+
today: isToday,
|
|
2788
|
+
selected,
|
|
2789
|
+
inBand,
|
|
2790
|
+
tabIndex: date === focused && !cell.outside ? 0 : -1,
|
|
2791
|
+
ariaLabel: formatDate(date, locale, { dateStyle: 'full' }) + (markerLabel ? `, ${markerLabel}` : ''),
|
|
2792
|
+
markers: dayMarkers,
|
|
2793
|
+
cellClass: [
|
|
2794
|
+
cellBase,
|
|
2795
|
+
inBand && (band.preview ? this.previewClass() : this.inRangeClass()),
|
|
2796
|
+
inBand && date === band.start && this.bandStartClass(),
|
|
2797
|
+
inBand && date === band.end && this.bandEndClass(),
|
|
2798
|
+
]
|
|
2799
|
+
.filter(Boolean)
|
|
2800
|
+
.join(' '),
|
|
2801
|
+
dayClass: [dayBase, isToday && todayClass, selected && this.selectedClass()]
|
|
2802
|
+
.filter(Boolean)
|
|
2803
|
+
.join(' '),
|
|
2804
|
+
};
|
|
2805
|
+
}));
|
|
2806
|
+
}, ...(ngDevMode ? [{ debugName: "gridWeeks" }] : /* istanbul ignore next */ []));
|
|
2807
|
+
// --- Selection -------------------------------------------------------------
|
|
2808
|
+
select(date) {
|
|
2809
|
+
if (this.disabled() || this.isDayDisabled(date))
|
|
2810
|
+
return;
|
|
2811
|
+
this.setViewMonth(monthOf(date));
|
|
2812
|
+
this.focusedDate.set(date);
|
|
2813
|
+
const locale = this.resolvedLocale();
|
|
2814
|
+
const full = (d) => formatDate(d, locale, { dateStyle: 'full' });
|
|
2815
|
+
if (this.mode() === 'single') {
|
|
2816
|
+
this.value.set(date);
|
|
2817
|
+
this.announce(`${full(date)} selected.`);
|
|
2818
|
+
}
|
|
2819
|
+
else if (!this.pendingStart()) {
|
|
2820
|
+
this.pendingStart.set(date);
|
|
2821
|
+
this.previewDate.set(date);
|
|
2822
|
+
this.announce(`Start date ${full(date)}. Choose an end date.`);
|
|
2823
|
+
}
|
|
2824
|
+
else {
|
|
2825
|
+
let [start, end] = [this.pendingStart(), date];
|
|
2826
|
+
if (end < start)
|
|
2827
|
+
[start, end] = [end, start]; // backwards commit swaps
|
|
2828
|
+
this.pendingStart.set(null);
|
|
2829
|
+
this.previewDate.set(null);
|
|
2830
|
+
this.value.set({ start, end });
|
|
2831
|
+
const days = inclusiveDayCount(start, end);
|
|
2832
|
+
this.announce(`Range selected, ${full(start)} to ${full(end)}. ${days} ${days === 1 ? 'day' : 'days'}.`);
|
|
2833
|
+
}
|
|
2834
|
+
this.selected.emit(date);
|
|
2835
|
+
}
|
|
2836
|
+
cancelPending() {
|
|
2837
|
+
this.pendingStart.set(null);
|
|
2838
|
+
this.previewDate.set(null);
|
|
2839
|
+
this.announce('Range selection cancelled.');
|
|
2840
|
+
}
|
|
2841
|
+
// --- Navigation ------------------------------------------------------------
|
|
2842
|
+
onNav(direction) {
|
|
2843
|
+
this.setViewMonth(monthOf(addMonths(`${this.viewMonth()}-01`, direction)));
|
|
2844
|
+
}
|
|
2845
|
+
setViewMonth(month) {
|
|
2846
|
+
if (this.month() !== month)
|
|
2847
|
+
this.month.set(month);
|
|
2848
|
+
}
|
|
2849
|
+
/**
|
|
2850
|
+
* Move the roving focus. A landing on a disabled day keeps going in the
|
|
2851
|
+
* same direction until an enabled day; the min/max fence stops the caret,
|
|
2852
|
+
* it never wraps. Month edges never block — the grid follows.
|
|
2853
|
+
*/
|
|
2854
|
+
moveFocus(target, direction) {
|
|
2855
|
+
const min = this.minDate();
|
|
2856
|
+
const max = this.maxDate();
|
|
2857
|
+
if ((min && target < min) || (max && target > max))
|
|
2858
|
+
return;
|
|
2859
|
+
let date = target;
|
|
2860
|
+
let guard = 0;
|
|
2861
|
+
while (this.isDayDisabled(date)) {
|
|
2862
|
+
date = addDays(date, direction);
|
|
2863
|
+
if ((min && date < min) || (max && date > max) || ++guard > 500)
|
|
2864
|
+
return;
|
|
2865
|
+
}
|
|
2866
|
+
this.setViewMonth(monthOf(date));
|
|
2867
|
+
this.focusedDate.set(date);
|
|
2868
|
+
if (this.pendingStart())
|
|
2869
|
+
this.previewDate.set(date);
|
|
2870
|
+
this.focusDay(date);
|
|
2871
|
+
}
|
|
2872
|
+
onGridKeydown(event) {
|
|
2873
|
+
const focused = this.focusedDate();
|
|
2874
|
+
const week = (dayOfWeek(focused) - this.resolvedWeekStart() + 7) % 7;
|
|
2875
|
+
const handlers = {
|
|
2876
|
+
ArrowLeft: () => this.moveFocus(addDays(focused, -1), -1),
|
|
2877
|
+
ArrowRight: () => this.moveFocus(addDays(focused, 1), 1),
|
|
2878
|
+
ArrowUp: () => this.moveFocus(addDays(focused, -7), -1),
|
|
2879
|
+
ArrowDown: () => this.moveFocus(addDays(focused, 7), 1),
|
|
2880
|
+
Home: () => this.moveFocus(addDays(focused, -week), 1),
|
|
2881
|
+
End: () => this.moveFocus(addDays(focused, 6 - week), -1),
|
|
2882
|
+
PageUp: () => this.moveFocus(addMonths(focused, event.shiftKey ? -12 : -1), -1),
|
|
2883
|
+
PageDown: () => this.moveFocus(addMonths(focused, event.shiftKey ? 12 : 1), 1),
|
|
2884
|
+
Enter: () => this.select(focused),
|
|
2885
|
+
' ': () => this.select(focused),
|
|
2886
|
+
};
|
|
2887
|
+
if (event.key === 'Escape') {
|
|
2888
|
+
// Only a pending range consumes Escape; otherwise it bubbles so a
|
|
2889
|
+
// hosting popover can light-dismiss.
|
|
2890
|
+
if (this.pendingStart()) {
|
|
2891
|
+
event.preventDefault();
|
|
2892
|
+
event.stopPropagation();
|
|
2893
|
+
this.cancelPending();
|
|
2894
|
+
}
|
|
2895
|
+
return;
|
|
2896
|
+
}
|
|
2897
|
+
const handler = handlers[event.key];
|
|
2898
|
+
if (handler) {
|
|
2899
|
+
event.preventDefault();
|
|
2900
|
+
handler();
|
|
2901
|
+
}
|
|
2902
|
+
}
|
|
2903
|
+
onDayHover(date) {
|
|
2904
|
+
if (this.pendingStart() && !this.isDayDisabled(date))
|
|
2905
|
+
this.previewDate.set(date);
|
|
2906
|
+
}
|
|
2907
|
+
/** Focus the roving day — used by popup hosts when the calendar opens. */
|
|
2908
|
+
focusActiveDay() {
|
|
2909
|
+
this.focusDay(this.focusedDate());
|
|
2910
|
+
}
|
|
2911
|
+
onHostFocusOut(event) {
|
|
2912
|
+
const next = event.relatedTarget;
|
|
2913
|
+
if (!next || !this.host.nativeElement.contains(next))
|
|
2914
|
+
this.touched.set(true);
|
|
2915
|
+
}
|
|
2916
|
+
focusDay(date) {
|
|
2917
|
+
queueMicrotask(() => this.host.nativeElement.querySelector(`[data-date="${date}"]`)?.focus());
|
|
2918
|
+
}
|
|
2919
|
+
pickFocus() {
|
|
2920
|
+
const view = this.viewMonth();
|
|
2921
|
+
const anchor = this.anchorDate();
|
|
2922
|
+
if (anchor && monthOf(anchor) === view && !this.isDayDisabled(anchor))
|
|
2923
|
+
return anchor;
|
|
2924
|
+
const today = todayIso();
|
|
2925
|
+
if (monthOf(today) === view && !this.isDayDisabled(today))
|
|
2926
|
+
return today;
|
|
2927
|
+
return this.firstEnabledInView(view);
|
|
2928
|
+
}
|
|
2929
|
+
firstEnabledInView(view) {
|
|
2930
|
+
const first = `${view}-01`;
|
|
2931
|
+
let date = first;
|
|
2932
|
+
for (let i = 0; i < 31 && monthOf(date) === view; i++) {
|
|
2933
|
+
if (!this.isDayDisabled(date))
|
|
2934
|
+
return date;
|
|
2935
|
+
date = addDays(date, 1);
|
|
2936
|
+
}
|
|
2937
|
+
return first;
|
|
2938
|
+
}
|
|
2939
|
+
announce(message) {
|
|
2940
|
+
// Re-announce identical text by breaking the string equality.
|
|
2941
|
+
this.announcement.set(this.announcement() === message ? `${message} ` : message);
|
|
2942
|
+
}
|
|
2943
|
+
// --- Styling ---------------------------------------------------------------
|
|
2944
|
+
daySize = computed(() => (this.componentTheme().sizes?.[this.size()] ?? {}), ...(ngDevMode ? [{ debugName: "daySize" }] : /* istanbul ignore next */ []));
|
|
2945
|
+
className = computed(() => css([(this.componentTheme().fixed ?? { display: 'inline-block' })]), ...(ngDevMode ? [{ debugName: "className" }] : /* istanbul ignore next */ []));
|
|
2946
|
+
navClass = computed(() => css({
|
|
2947
|
+
display: 'flex',
|
|
2948
|
+
alignItems: 'center',
|
|
2949
|
+
justifyContent: 'space-between',
|
|
2950
|
+
...this.theme.gap('xs'),
|
|
2951
|
+
marginBottom: 4,
|
|
2952
|
+
}), ...(ngDevMode ? [{ debugName: "navClass" }] : /* istanbul ignore next */ []));
|
|
2953
|
+
headingClass = computed(() => css({ flex: 1, textAlign: 'center', ...this.theme.typeface('title-small') }), ...(ngDevMode ? [{ debugName: "headingClass" }] : /* istanbul ignore next */ []));
|
|
2954
|
+
gridClass = computed(() => css({ display: 'grid', ...this.theme.gap(this.componentOptions().gap ?? 'xxs') }), ...(ngDevMode ? [{ debugName: "gridClass" }] : /* istanbul ignore next */ []));
|
|
2955
|
+
rowClass = computed(() => css({
|
|
2956
|
+
display: 'grid',
|
|
2957
|
+
gridTemplateColumns: 'repeat(7, 1fr)',
|
|
2958
|
+
justifyItems: 'center',
|
|
2959
|
+
...this.theme.gap(this.componentOptions().gap ?? 'xxs'),
|
|
2960
|
+
}), ...(ngDevMode ? [{ debugName: "rowClass" }] : /* istanbul ignore next */ []));
|
|
2961
|
+
weekdayClass = computed(() => css({
|
|
2962
|
+
display: 'flex',
|
|
2963
|
+
alignItems: 'center',
|
|
2964
|
+
justifyContent: 'center',
|
|
2965
|
+
...this.daySize(),
|
|
2966
|
+
height: 'auto',
|
|
2967
|
+
...this.theme.typeface(this.componentOptions().typeface ?? 'label'),
|
|
2968
|
+
...this.theme.color('on-background-variant'),
|
|
2969
|
+
'& abbr': { textDecoration: 'none' },
|
|
2970
|
+
}), ...(ngDevMode ? [{ debugName: "weekdayClass" }] : /* istanbul ignore next */ []));
|
|
2971
|
+
/** The gridcell — range bands paint here so they read as one bar. */
|
|
2972
|
+
cellClass = computed(() => css({ position: 'relative', display: 'flex' }), ...(ngDevMode ? [{ debugName: "cellClass" }] : /* istanbul ignore next */ []));
|
|
2973
|
+
inRangeClass = computed(() => css({ ...this.theme.backgroundColor('primary-container'), borderRadius: 0 }), ...(ngDevMode ? [{ debugName: "inRangeClass" }] : /* istanbul ignore next */ []));
|
|
2974
|
+
previewClass = computed(() => {
|
|
2975
|
+
const colors = this.theme.colorPalette();
|
|
2976
|
+
return css({
|
|
2977
|
+
backgroundColor: `color-mix(in srgb, ${colors['primary-container']} 55%, transparent)`,
|
|
2978
|
+
outline: `1px dashed ${colors['primary']}`,
|
|
2979
|
+
outlineOffset: -1,
|
|
2980
|
+
});
|
|
2981
|
+
}, ...(ngDevMode ? [{ debugName: "previewClass" }] : /* istanbul ignore next */ []));
|
|
2982
|
+
bandStartClass = computed(() => css({ ...this.theme.getRadiusLeft(this.componentOptions().dayBorderRadius ?? 'max') }), ...(ngDevMode ? [{ debugName: "bandStartClass" }] : /* istanbul ignore next */ []));
|
|
2983
|
+
bandEndClass = computed(() => css({ ...this.theme.getRadiusRight(this.componentOptions().dayBorderRadius ?? 'max') }), ...(ngDevMode ? [{ debugName: "bandEndClass" }] : /* istanbul ignore next */ []));
|
|
2984
|
+
dayClass = computed(() => {
|
|
2985
|
+
const options = this.componentOptions();
|
|
2986
|
+
const colors = this.theme.colorPalette();
|
|
2987
|
+
return css({
|
|
2988
|
+
position: 'relative',
|
|
2989
|
+
display: 'flex',
|
|
2990
|
+
alignItems: 'center',
|
|
2991
|
+
justifyContent: 'center',
|
|
2992
|
+
border: 0,
|
|
2993
|
+
padding: 0,
|
|
2994
|
+
background: 'transparent',
|
|
2995
|
+
color: 'inherit',
|
|
2996
|
+
cursor: 'pointer',
|
|
2997
|
+
...this.theme.typeface(options.typeface ?? 'label'),
|
|
2998
|
+
...this.daySize(),
|
|
2999
|
+
...this.theme.radius(options.dayBorderRadius ?? 'max'),
|
|
3000
|
+
'&:hover:not(:disabled)': { ...this.theme.colorPair('primary-container') },
|
|
3001
|
+
...this.theme.focusRing(),
|
|
3002
|
+
'&:disabled': {
|
|
3003
|
+
color: colors['on-disabled'],
|
|
3004
|
+
cursor: 'default',
|
|
3005
|
+
pointerEvents: 'none',
|
|
3006
|
+
},
|
|
3007
|
+
});
|
|
3008
|
+
}, ...(ngDevMode ? [{ debugName: "dayClass" }] : /* istanbul ignore next */ []));
|
|
3009
|
+
todayClass = computed(() => {
|
|
3010
|
+
const colors = this.theme.colorPalette();
|
|
3011
|
+
// Outline (or dot), never fill — so today and selected can coincide and
|
|
3012
|
+
// both stay legible (WCAG 1.4.1: no state carried by colour alone).
|
|
3013
|
+
return (this.componentOptions().todayStyle ?? 'outline') === 'outline'
|
|
3014
|
+
? css({ boxShadow: `inset 0 0 0 1.5px ${colors['primary']}` })
|
|
3015
|
+
: css({
|
|
3016
|
+
'&::after': {
|
|
3017
|
+
content: '""',
|
|
3018
|
+
position: 'absolute',
|
|
3019
|
+
bottom: 3,
|
|
3020
|
+
left: '50%',
|
|
3021
|
+
transform: 'translateX(-50%)',
|
|
3022
|
+
width: 4,
|
|
3023
|
+
height: 4,
|
|
3024
|
+
borderRadius: 999,
|
|
3025
|
+
backgroundColor: colors['primary'],
|
|
3026
|
+
},
|
|
3027
|
+
});
|
|
3028
|
+
}, ...(ngDevMode ? [{ debugName: "todayClass" }] : /* istanbul ignore next */ []));
|
|
3029
|
+
selectedClass = computed(() => {
|
|
3030
|
+
const colors = this.theme.colorPalette();
|
|
3031
|
+
return css({
|
|
3032
|
+
...this.theme.colorPair('primary'),
|
|
3033
|
+
// A marker dot survives selection by switching to the on-colour.
|
|
3034
|
+
'& [data-dot]': { backgroundColor: colors['on-primary'] },
|
|
3035
|
+
});
|
|
3036
|
+
}, ...(ngDevMode ? [{ debugName: "selectedClass" }] : /* istanbul ignore next */ []));
|
|
3037
|
+
outsideDayClass = computed(() => {
|
|
3038
|
+
const colors = this.theme.colorPalette();
|
|
3039
|
+
return css({
|
|
3040
|
+
display: 'flex',
|
|
3041
|
+
alignItems: 'center',
|
|
3042
|
+
justifyContent: 'center',
|
|
3043
|
+
...this.theme.typeface(this.componentOptions().typeface ?? 'label'),
|
|
3044
|
+
...this.daySize(),
|
|
3045
|
+
color: colors['on-disabled'],
|
|
3046
|
+
});
|
|
3047
|
+
}, ...(ngDevMode ? [{ debugName: "outsideDayClass" }] : /* istanbul ignore next */ []));
|
|
3048
|
+
dotsClass = computed(() => css({
|
|
3049
|
+
position: 'absolute',
|
|
3050
|
+
bottom: 2,
|
|
3051
|
+
left: 0,
|
|
3052
|
+
right: 0,
|
|
3053
|
+
display: 'flex',
|
|
3054
|
+
justifyContent: 'center',
|
|
3055
|
+
gap: 2,
|
|
3056
|
+
pointerEvents: 'none',
|
|
3057
|
+
}), ...(ngDevMode ? [{ debugName: "dotsClass" }] : /* istanbul ignore next */ []));
|
|
3058
|
+
/** One dot class per marker variant present, resolved from the palette. */
|
|
3059
|
+
dotClasses = computed(() => {
|
|
3060
|
+
const colors = this.theme.colorPalette();
|
|
3061
|
+
const classes = new Map();
|
|
3062
|
+
for (const marker of this.markers()) {
|
|
3063
|
+
const variant = marker.variant ?? 'primary';
|
|
3064
|
+
if (!classes.has(variant)) {
|
|
3065
|
+
classes.set(variant, css({ width: 4, height: 4, borderRadius: 999, backgroundColor: colors[variant] }));
|
|
3066
|
+
}
|
|
3067
|
+
}
|
|
3068
|
+
return classes;
|
|
3069
|
+
}, ...(ngDevMode ? [{ debugName: "dotClasses" }] : /* istanbul ignore next */ []));
|
|
3070
|
+
dotClassFor(variant) {
|
|
3071
|
+
return this.dotClasses().get(variant ?? 'primary') ?? '';
|
|
3072
|
+
}
|
|
3073
|
+
showOutside = computed(() => this.componentOptions().showOutsideDays ?? false, ...(ngDevMode ? [{ debugName: "showOutside" }] : /* istanbul ignore next */ []));
|
|
3074
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: UniCalendarComponent, deps: null, target: i0.ɵɵFactoryTarget.Component });
|
|
3075
|
+
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 });
|
|
3076
|
+
}
|
|
3077
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: UniCalendarComponent, decorators: [{
|
|
3078
|
+
type: Component,
|
|
3079
|
+
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" }]
|
|
3080
|
+
}], 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"] }] } });
|
|
3081
|
+
|
|
3082
|
+
class UniCardContentComponent extends BaseComponent {
|
|
3083
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: UniCardContentComponent, deps: null, target: i0.ɵɵFactoryTarget.Component });
|
|
3084
|
+
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 });
|
|
3085
|
+
}
|
|
3086
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: UniCardContentComponent, decorators: [{
|
|
3087
|
+
type: Component,
|
|
3088
|
+
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" }]
|
|
3089
|
+
}] });
|
|
3090
|
+
|
|
3091
|
+
/**
|
|
3092
|
+
* Typeface defaults by host element, so semantic HTML and the type scale
|
|
3093
|
+
* reinforce each other: `<h1 uni-text>` is already headline-large.
|
|
3094
|
+
*/
|
|
3095
|
+
const TagTypefaces = {
|
|
3096
|
+
h1: 'headline-large',
|
|
3097
|
+
h2: 'headline-medium',
|
|
3098
|
+
h3: 'headline-small',
|
|
3099
|
+
h4: 'title-large',
|
|
3100
|
+
h5: 'title-medium',
|
|
3101
|
+
h6: 'title-small',
|
|
3102
|
+
p: 'body-1-long',
|
|
3103
|
+
small: 'caption',
|
|
3104
|
+
figcaption: 'caption',
|
|
3105
|
+
blockquote: 'quote',
|
|
3106
|
+
label: 'label',
|
|
3107
|
+
};
|
|
3108
|
+
/**
|
|
3109
|
+
* The typography primitive, applied as an attribute to any element so
|
|
3110
|
+
* semantics stay yours. The attribute value is the typeface:
|
|
3111
|
+
* `<h1 uni-text="display-small">`, `<span uni-text="caption">`, dynamic via
|
|
3112
|
+
* `[uni-text]="role()"`. With no value, the typeface is inferred from the
|
|
3113
|
+
* host element (h1 → headline-large, p → body-1-long, …), falling back to
|
|
3114
|
+
* `title-small`; the `typeface` input remains as an explicit override.
|
|
3115
|
+
*/
|
|
3116
|
+
class UniTextComponent {
|
|
3117
|
+
theme = inject(ThemeService);
|
|
3118
|
+
tag = inject(ElementRef).nativeElement.tagName.toLowerCase();
|
|
3119
|
+
/** Typeface via the selector attribute: `uni-text="headline-large"`. */
|
|
3120
|
+
uniText = input('', { ...(ngDevMode ? { debugName: "uniText" } : /* istanbul ignore next */ {}), alias: 'uni-text' });
|
|
3121
|
+
/** Explicit typeface; the attribute value wins when both are set. */
|
|
3122
|
+
typeface = input(undefined, ...(ngDevMode ? [{ debugName: "typeface" }] : /* istanbul ignore next */ []));
|
|
3123
|
+
color = input(...(ngDevMode ? [undefined, { debugName: "color" }] : /* istanbul ignore next */ []));
|
|
3124
|
+
display = input(...(ngDevMode ? [undefined, { debugName: "display" }] : /* istanbul ignore next */ []));
|
|
3125
|
+
align = input(...(ngDevMode ? [undefined, { debugName: "align" }] : /* istanbul ignore next */ []));
|
|
3126
|
+
nowrap = input(...(ngDevMode ? [undefined, { debugName: "nowrap" }] : /* istanbul ignore next */ []));
|
|
3127
|
+
maxWidth = input(...(ngDevMode ? [undefined, { debugName: "maxWidth" }] : /* istanbul ignore next */ []));
|
|
3128
|
+
ellipsis = input(false, ...(ngDevMode ? [{ debugName: "ellipsis" }] : /* istanbul ignore next */ []));
|
|
3129
|
+
resolvedTypeface = computed(() => this.uniText() || this.typeface() || TagTypefaces[this.tag] || 'title-small', ...(ngDevMode ? [{ debugName: "resolvedTypeface" }] : /* istanbul ignore next */ []));
|
|
3130
|
+
className = computed(() => {
|
|
3131
|
+
return css([
|
|
3132
|
+
{
|
|
3133
|
+
...this.theme.typeface(this.resolvedTypeface()),
|
|
3134
|
+
...this.theme.color(this.color()),
|
|
3135
|
+
display: this.display(),
|
|
3136
|
+
},
|
|
3137
|
+
this.align() && {
|
|
3138
|
+
textAlign: this.align(),
|
|
3139
|
+
},
|
|
3140
|
+
this.nowrap() && {
|
|
3141
|
+
whiteSpace: 'nowrap',
|
|
3142
|
+
},
|
|
3143
|
+
this.maxWidth() && {
|
|
3144
|
+
maxWidth: this.maxWidth(),
|
|
3145
|
+
overflow: 'hidden',
|
|
3146
|
+
whiteSpace: 'nowrap',
|
|
3147
|
+
textOverflow: 'ellipsis',
|
|
3148
|
+
display: 'inline-block',
|
|
2209
3149
|
},
|
|
2210
3150
|
this.ellipsis() && {
|
|
2211
3151
|
whiteSpace: 'nowrap',
|
|
@@ -2376,19 +3316,13 @@ class UniCheckboxComponent extends BaseComponent {
|
|
|
2376
3316
|
'&:disabled + .checkbox': {
|
|
2377
3317
|
cursor: 'not-allowed',
|
|
2378
3318
|
},
|
|
3319
|
+
// The shared, themable focus indicator, keyed off the hidden input's
|
|
3320
|
+
// focus. The ring sits out from the box, so its radius carries an extra
|
|
3321
|
+
// 4px to round proportionally (as the original hand-drawn ring did) —
|
|
3322
|
+
// without it the corners gap away from the box.
|
|
2379
3323
|
'&: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
|
-
},
|
|
3324
|
+
...this.theme.focusRingStyle(this.getThemeColor(this.variant()), this.componentOptions().focusRingGap),
|
|
3325
|
+
borderRadius: `${(Number(this.componentOptions().borderRadius) || 2) + 2}px`,
|
|
2392
3326
|
},
|
|
2393
3327
|
}), ...(ngDevMode ? [{ debugName: "checkboxInput" }] : /* istanbul ignore next */ []));
|
|
2394
3328
|
getThemeColor(token) {
|
|
@@ -2408,196 +3342,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImpo
|
|
|
2408
3342
|
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
3343
|
}], 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
3344
|
|
|
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
3345
|
class UniDataSearchComponent extends BaseComponent {
|
|
2602
3346
|
datasource = input(...(ngDevMode ? [undefined, { debugName: "datasource" }] : /* istanbul ignore next */ []));
|
|
2603
3347
|
placeholder = input('Search', ...(ngDevMode ? [{ debugName: "placeholder" }] : /* istanbul ignore next */ []));
|
|
@@ -3056,7 +3800,7 @@ class UniDataTableComponent extends BaseComponent {
|
|
|
3056
3800
|
return name ? this.theme.theme().borders[name] : undefined;
|
|
3057
3801
|
}
|
|
3058
3802
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: UniDataTableComponent, deps: null, target: i0.ɵɵFactoryTarget.Component });
|
|
3059
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.12", type: UniDataTableComponent, isStandalone: true, selector: "uni-data-table", inputs: { datasource: { classPropertyName: "datasource", publicName: "datasource", isSignal: true, isRequired: true, transformFunction: null }, columns: { classPropertyName: "columns", publicName: "columns", isSignal: true, isRequired: true, transformFunction: null }, detailRowTemplate: { classPropertyName: "detailRowTemplate", publicName: "detailRowTemplate", isSignal: true, isRequired: false, transformFunction: null }, useMultiSelect: { classPropertyName: "useMultiSelect", publicName: "useMultiSelect", isSignal: true, isRequired: false, transformFunction: null }, useRowClick: { classPropertyName: "useRowClick", publicName: "useRowClick", isSignal: true, isRequired: false, transformFunction: null }, highlight: { classPropertyName: "highlight", publicName: "highlight", isSignal: true, isRequired: false, transformFunction: null }, height: { classPropertyName: "height", publicName: "height", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { rowSelect: "rowSelect", rowClick: "rowClick" }, providers: [{ provide: COMPONENT_NAME, useValue: 'dataTable' }], usesInheritance: true, ngImport: i0, template: "@let ds = datasource();\n@if (ds) {\n <div\n box-layout\n [color]=\"componentOptions().color\"\n [border]=\"componentOptions().border\"\n [borderRadius]=\"componentOptions().borderRadius\"\n [elevation]=\"componentOptions().elevation\"\n overflow=\"hidden\"\n >\n <div\n box-layout\n [padding]=\"componentOptions().headerPadding\"\n [color]=\"componentOptions().headerColor\"\n [class]=\"headerClass()\"\n >\n <ng-content select=\"data-table-header\"></ng-content>\n </div>\n\n <div box-layout [height]=\"height()\" position=\"relative\" [attr.aria-busy]=\"isLoading()\">\n <div scroll-area [verticalScrollPadding]=\"0\" (scrollable)=\"handleScrollable($event)\">\n <table [class]=\"tableClass()\">\n <thead>\n <tr>\n @for (column of columns(); track column) {\n <th\n scope=\"col\"\n [attr.aria-sort]=\"ariaSort(column)\"\n [class]=\"thClass()\"\n [class.scrollable]=\"scrollable()\"\n [class.sticky]=\"column.isSticky\"\n [style.text-align]=\"column.textAlign\"\n >\n <uni-sort-header [datasource]=\"datasource()\" [column]=\"column.columnDef\">\n <span uni-text [typeface]=\"componentOptions().thTextRole\">{{ column.header }}</span>\n </uni-sort-header>\n </th>\n }\n </tr>\n </thead>\n <tbody>\n @for (record of ds.records(); track
|
|
3803
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.12", type: UniDataTableComponent, isStandalone: true, selector: "uni-data-table", inputs: { datasource: { classPropertyName: "datasource", publicName: "datasource", isSignal: true, isRequired: true, transformFunction: null }, columns: { classPropertyName: "columns", publicName: "columns", isSignal: true, isRequired: true, transformFunction: null }, detailRowTemplate: { classPropertyName: "detailRowTemplate", publicName: "detailRowTemplate", isSignal: true, isRequired: false, transformFunction: null }, useMultiSelect: { classPropertyName: "useMultiSelect", publicName: "useMultiSelect", isSignal: true, isRequired: false, transformFunction: null }, useRowClick: { classPropertyName: "useRowClick", publicName: "useRowClick", isSignal: true, isRequired: false, transformFunction: null }, highlight: { classPropertyName: "highlight", publicName: "highlight", isSignal: true, isRequired: false, transformFunction: null }, height: { classPropertyName: "height", publicName: "height", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { rowSelect: "rowSelect", rowClick: "rowClick" }, providers: [{ provide: COMPONENT_NAME, useValue: 'dataTable' }], usesInheritance: true, ngImport: i0, template: "@let ds = datasource();\n@if (ds) {\n <div\n box-layout\n [color]=\"componentOptions().color\"\n [border]=\"componentOptions().border\"\n [borderRadius]=\"componentOptions().borderRadius\"\n [elevation]=\"componentOptions().elevation\"\n overflow=\"hidden\"\n >\n <div\n box-layout\n [padding]=\"componentOptions().headerPadding\"\n [color]=\"componentOptions().headerColor\"\n [class]=\"headerClass()\"\n >\n <ng-content select=\"data-table-header\"></ng-content>\n </div>\n\n <div box-layout [height]=\"height()\" position=\"relative\" [attr.aria-busy]=\"isLoading()\">\n <div scroll-area [verticalScrollPadding]=\"0\" (scrollable)=\"handleScrollable($event)\">\n <table [class]=\"tableClass()\">\n <thead>\n <tr>\n @for (column of columns(); track column.columnDef) {\n <th\n scope=\"col\"\n [attr.aria-sort]=\"ariaSort(column)\"\n [class]=\"thClass()\"\n [class.scrollable]=\"scrollable()\"\n [class.sticky]=\"column.isSticky\"\n [style.text-align]=\"column.textAlign\"\n >\n <uni-sort-header [datasource]=\"datasource()\" [column]=\"column.columnDef\">\n <span uni-text [typeface]=\"componentOptions().thTextRole\">{{ column.header }}</span>\n </uni-sort-header>\n </th>\n }\n </tr>\n </thead>\n <tbody>\n <!-- track $index: records are arbitrary consumer objects, typically\n re-fetched as fresh references \u2014 identity tracking recreated\n every row (NG0956). Row expansion is index-addressed already. -->\n @for (record of ds.records(); track $index; let i = $index) {\n <tr\n [class]=\"trClass()\"\n [attr.tabindex]=\"useRowClick() ? 0 : null\"\n (click)=\"handleRowClick(record, $index)\"\n (keydown.enter)=\"useRowClick() && handleRowClick(record, $index)\"\n (keydown.space)=\"\n useRowClick() && handleRowClick(record, $index);\n useRowClick() && $event.preventDefault()\n \"\n >\n @for (column of columns(); track column.columnDef) {\n <td\n [class]=\"tdClass()\"\n [class.scrollable]=\"scrollable()\"\n [class.sticky]=\"column.isSticky\"\n [class.template]=\"!!column.template\"\n [style.text-align]=\"column.textAlign\"\n >\n @if (column.template) {\n <ng-container\n [ngTemplateOutlet]=\"column.template\"\n [ngTemplateOutletContext]=\"record\"\n ></ng-container>\n } @else {\n <span uni-text [typeface]=\"componentOptions().tdTextRole\">\n @if (!column.cell) {\n {{ record[column.columnDef] }}\n } @else {\n {{ column.cell(record) }}\n }\n </span>\n }\n </td>\n }\n </tr>\n @if (detailRowTemplate()) {\n <!-- Collapsed detail rows are inert: invisible to screen\n readers and unreachable by keyboard until expanded -->\n <tr\n [class]=\"detailRowClass()\"\n [class.expanded]=\"expandedIndex() === $index\"\n [attr.inert]=\"expandedIndex() === $index ? null : ''\"\n >\n <td [attr.colspan]=\"columns().length\">\n <div class=\"detail-content-wrapper\">\n <div class=\"detail-inner\">\n <ng-container\n [ngTemplateOutlet]=\"detailRowTemplate()\"\n [ngTemplateOutletContext]=\"{\n $implicit: record,\n index: i,\n }\"\n ></ng-container>\n </div>\n </div>\n </td>\n </tr>\n }\n }\n </tbody>\n </table>\n </div>\n\n <!-- Loading Overlay -->\n @if (isLoading()) {\n <div\n uni-center-layout\n role=\"status\"\n aria-label=\"Loading\"\n position=\"absolute\"\n [inset]=\"0\"\n [backgroundColor]=\"componentOptions().loadingOverlayColor\"\n [class]=\"loadingOverlayClass\"\n zIndex=\"overlay\"\n >\n <!-- Loading Spinner -->\n <div\n uni-box-layout\n [height]=\"componentOptions().loadingSpinnerSize\"\n [width]=\"componentOptions().loadingSpinnerSize\"\n >\n <uni-icon name=\"spinner\" [color]=\"componentOptions().loadingSpinnerColor\" />\n </div>\n </div>\n }\n </div>\n\n <div\n box-layout\n [padding]=\"componentOptions().footerPadding\"\n [color]=\"componentOptions().footerColor\"\n [class]=\"footerClass()\"\n >\n <ng-content select=\"data-table-footer\"></ng-content>\n </div>\n </div>\n}\n", dependencies: [{ kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { 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"] }, { kind: "component", type: UniScrollAreaComponent, selector: "[uni-scroll-area], [scroll-area]", inputs: ["color", "borderRadius", "padding", "paddingHorizontal", "paddingVertical", "paddingLeft", "paddingRight", "paddingTop", "paddingBottom", "border", "height", "width", "appearance", "autoHeightDisabled", "verticalScrollPadding"], outputs: ["scrollable"] }, { kind: "component", type: UniTextComponent, selector: "[uni-text]", inputs: ["uni-text", "typeface", "color", "display", "align", "nowrap", "maxWidth", "ellipsis"] }, { kind: "component", type: UniSortHeaderComponent, selector: "uni-sort-header", inputs: ["column", "datasource"] }, { kind: "component", type: UniIconComponent, selector: "uni-icon", inputs: ["color", "name", "size"] }, { kind: "component", type: UniCenterComponent, selector: "[uni-center-layout], [center-layout]", inputs: ["display", "justifyContent", "alignItems"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
3060
3804
|
}
|
|
3061
3805
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: UniDataTableComponent, decorators: [{
|
|
3062
3806
|
type: Component,
|
|
@@ -3068,7 +3812,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImpo
|
|
|
3068
3812
|
UniSortHeaderComponent,
|
|
3069
3813
|
UniIconComponent,
|
|
3070
3814
|
UniCenterComponent,
|
|
3071
|
-
], providers: [{ provide: COMPONENT_NAME, useValue: 'dataTable' }], changeDetection: ChangeDetectionStrategy.OnPush, template: "@let ds = datasource();\n@if (ds) {\n <div\n box-layout\n [color]=\"componentOptions().color\"\n [border]=\"componentOptions().border\"\n [borderRadius]=\"componentOptions().borderRadius\"\n [elevation]=\"componentOptions().elevation\"\n overflow=\"hidden\"\n >\n <div\n box-layout\n [padding]=\"componentOptions().headerPadding\"\n [color]=\"componentOptions().headerColor\"\n [class]=\"headerClass()\"\n >\n <ng-content select=\"data-table-header\"></ng-content>\n </div>\n\n <div box-layout [height]=\"height()\" position=\"relative\" [attr.aria-busy]=\"isLoading()\">\n <div scroll-area [verticalScrollPadding]=\"0\" (scrollable)=\"handleScrollable($event)\">\n <table [class]=\"tableClass()\">\n <thead>\n <tr>\n @for (column of columns(); track column) {\n <th\n scope=\"col\"\n [attr.aria-sort]=\"ariaSort(column)\"\n [class]=\"thClass()\"\n [class.scrollable]=\"scrollable()\"\n [class.sticky]=\"column.isSticky\"\n [style.text-align]=\"column.textAlign\"\n >\n <uni-sort-header [datasource]=\"datasource()\" [column]=\"column.columnDef\">\n <span uni-text [typeface]=\"componentOptions().thTextRole\">{{ column.header }}</span>\n </uni-sort-header>\n </th>\n }\n </tr>\n </thead>\n <tbody>\n @for (record of ds.records(); track
|
|
3815
|
+
], providers: [{ provide: COMPONENT_NAME, useValue: 'dataTable' }], changeDetection: ChangeDetectionStrategy.OnPush, template: "@let ds = datasource();\n@if (ds) {\n <div\n box-layout\n [color]=\"componentOptions().color\"\n [border]=\"componentOptions().border\"\n [borderRadius]=\"componentOptions().borderRadius\"\n [elevation]=\"componentOptions().elevation\"\n overflow=\"hidden\"\n >\n <div\n box-layout\n [padding]=\"componentOptions().headerPadding\"\n [color]=\"componentOptions().headerColor\"\n [class]=\"headerClass()\"\n >\n <ng-content select=\"data-table-header\"></ng-content>\n </div>\n\n <div box-layout [height]=\"height()\" position=\"relative\" [attr.aria-busy]=\"isLoading()\">\n <div scroll-area [verticalScrollPadding]=\"0\" (scrollable)=\"handleScrollable($event)\">\n <table [class]=\"tableClass()\">\n <thead>\n <tr>\n @for (column of columns(); track column.columnDef) {\n <th\n scope=\"col\"\n [attr.aria-sort]=\"ariaSort(column)\"\n [class]=\"thClass()\"\n [class.scrollable]=\"scrollable()\"\n [class.sticky]=\"column.isSticky\"\n [style.text-align]=\"column.textAlign\"\n >\n <uni-sort-header [datasource]=\"datasource()\" [column]=\"column.columnDef\">\n <span uni-text [typeface]=\"componentOptions().thTextRole\">{{ column.header }}</span>\n </uni-sort-header>\n </th>\n }\n </tr>\n </thead>\n <tbody>\n <!-- track $index: records are arbitrary consumer objects, typically\n re-fetched as fresh references \u2014 identity tracking recreated\n every row (NG0956). Row expansion is index-addressed already. -->\n @for (record of ds.records(); track $index; let i = $index) {\n <tr\n [class]=\"trClass()\"\n [attr.tabindex]=\"useRowClick() ? 0 : null\"\n (click)=\"handleRowClick(record, $index)\"\n (keydown.enter)=\"useRowClick() && handleRowClick(record, $index)\"\n (keydown.space)=\"\n useRowClick() && handleRowClick(record, $index);\n useRowClick() && $event.preventDefault()\n \"\n >\n @for (column of columns(); track column.columnDef) {\n <td\n [class]=\"tdClass()\"\n [class.scrollable]=\"scrollable()\"\n [class.sticky]=\"column.isSticky\"\n [class.template]=\"!!column.template\"\n [style.text-align]=\"column.textAlign\"\n >\n @if (column.template) {\n <ng-container\n [ngTemplateOutlet]=\"column.template\"\n [ngTemplateOutletContext]=\"record\"\n ></ng-container>\n } @else {\n <span uni-text [typeface]=\"componentOptions().tdTextRole\">\n @if (!column.cell) {\n {{ record[column.columnDef] }}\n } @else {\n {{ column.cell(record) }}\n }\n </span>\n }\n </td>\n }\n </tr>\n @if (detailRowTemplate()) {\n <!-- Collapsed detail rows are inert: invisible to screen\n readers and unreachable by keyboard until expanded -->\n <tr\n [class]=\"detailRowClass()\"\n [class.expanded]=\"expandedIndex() === $index\"\n [attr.inert]=\"expandedIndex() === $index ? null : ''\"\n >\n <td [attr.colspan]=\"columns().length\">\n <div class=\"detail-content-wrapper\">\n <div class=\"detail-inner\">\n <ng-container\n [ngTemplateOutlet]=\"detailRowTemplate()\"\n [ngTemplateOutletContext]=\"{\n $implicit: record,\n index: i,\n }\"\n ></ng-container>\n </div>\n </div>\n </td>\n </tr>\n }\n }\n </tbody>\n </table>\n </div>\n\n <!-- Loading Overlay -->\n @if (isLoading()) {\n <div\n uni-center-layout\n role=\"status\"\n aria-label=\"Loading\"\n position=\"absolute\"\n [inset]=\"0\"\n [backgroundColor]=\"componentOptions().loadingOverlayColor\"\n [class]=\"loadingOverlayClass\"\n zIndex=\"overlay\"\n >\n <!-- Loading Spinner -->\n <div\n uni-box-layout\n [height]=\"componentOptions().loadingSpinnerSize\"\n [width]=\"componentOptions().loadingSpinnerSize\"\n >\n <uni-icon name=\"spinner\" [color]=\"componentOptions().loadingSpinnerColor\" />\n </div>\n </div>\n }\n </div>\n\n <div\n box-layout\n [padding]=\"componentOptions().footerPadding\"\n [color]=\"componentOptions().footerColor\"\n [class]=\"footerClass()\"\n >\n <ng-content select=\"data-table-footer\"></ng-content>\n </div>\n </div>\n}\n" }]
|
|
3072
3816
|
}], propDecorators: { datasource: [{ type: i0.Input, args: [{ isSignal: true, alias: "datasource", required: true }] }], columns: [{ type: i0.Input, args: [{ isSignal: true, alias: "columns", required: true }] }], detailRowTemplate: [{ type: i0.Input, args: [{ isSignal: true, alias: "detailRowTemplate", required: false }] }], useMultiSelect: [{ type: i0.Input, args: [{ isSignal: true, alias: "useMultiSelect", required: false }] }], useRowClick: [{ type: i0.Input, args: [{ isSignal: true, alias: "useRowClick", required: false }] }], highlight: [{ type: i0.Input, args: [{ isSignal: true, alias: "highlight", required: false }] }], height: [{ type: i0.Input, args: [{ isSignal: true, alias: "height", required: false }] }], rowSelect: [{ type: i0.Output, args: ["rowSelect"] }], rowClick: [{ type: i0.Output, args: ["rowClick"] }] } });
|
|
3073
3817
|
|
|
3074
3818
|
/**
|
|
@@ -3077,6 +3821,1024 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImpo
|
|
|
3077
3821
|
* This file exports all public-facing elements of the data-table component.
|
|
3078
3822
|
*/
|
|
3079
3823
|
|
|
3824
|
+
class UniDropdownComponent extends BaseComponent {
|
|
3825
|
+
renderer = inject(Renderer2);
|
|
3826
|
+
delay = 100;
|
|
3827
|
+
// Reactively track visibility status using Signals
|
|
3828
|
+
showing = signal(false, ...(ngDevMode ? [{ debugName: "showing" }] : /* istanbul ignore next */ []));
|
|
3829
|
+
trigger = input.required(...(ngDevMode ? [{ debugName: "trigger" }] : /* istanbul ignore next */ []));
|
|
3830
|
+
placement = input('bottom-start', ...(ngDevMode ? [{ debugName: "placement" }] : /* istanbul ignore next */ []));
|
|
3831
|
+
offset = input({ mainAxis: 4, alignmentAxis: 12 }, ...(ngDevMode ? [{ debugName: "offset" }] : /* istanbul ignore next */ []));
|
|
3832
|
+
/** CSS anchor-name linking the trigger to the popover panel. */
|
|
3833
|
+
anchorName = newAnchorName();
|
|
3834
|
+
/**
|
|
3835
|
+
* Value for aria-haspopup on the trigger, describing what the popover
|
|
3836
|
+
* contains (e.g. 'menu' for Menu, 'dialog' for rich content). When unset,
|
|
3837
|
+
* only aria-expanded/aria-controls are managed.
|
|
3838
|
+
*/
|
|
3839
|
+
ariaHasPopup = input(null, ...(ngDevMode ? [{ debugName: "ariaHasPopup" }] : /* istanbul ignore next */ []));
|
|
3840
|
+
/** Document-unique id of the popover element, for aria-controls wiring. */
|
|
3841
|
+
popoverId = uniqueId('uni-dropdown');
|
|
3842
|
+
paddingVertical = input(...(ngDevMode ? [undefined, { debugName: "paddingVertical" }] : /* istanbul ignore next */ []));
|
|
3843
|
+
paddingHorizontal = input(...(ngDevMode ? [undefined, { debugName: "paddingHorizontal" }] : /* istanbul ignore next */ []));
|
|
3844
|
+
// Per-instance panel-chrome overrides; undefined falls back to the theme's
|
|
3845
|
+
// `dropdown` options, so hosts like uni-menu can restyle their panel
|
|
3846
|
+
// without forking the shared dropdown entry.
|
|
3847
|
+
border = input(...(ngDevMode ? [undefined, { debugName: "border" }] : /* istanbul ignore next */ []));
|
|
3848
|
+
borderRadius = input(...(ngDevMode ? [undefined, { debugName: "borderRadius" }] : /* istanbul ignore next */ []));
|
|
3849
|
+
shadow = input(...(ngDevMode ? [undefined, { debugName: "shadow" }] : /* istanbul ignore next */ []));
|
|
3850
|
+
color = input(...(ngDevMode ? [undefined, { debugName: "color" }] : /* istanbul ignore next */ []));
|
|
3851
|
+
dropdownShowing = output();
|
|
3852
|
+
dropdownHiding = output();
|
|
3853
|
+
dropdownRef;
|
|
3854
|
+
get _trigger() {
|
|
3855
|
+
return this.trigger();
|
|
3856
|
+
}
|
|
3857
|
+
get _dropdown() {
|
|
3858
|
+
return this.dropdownRef.nativeElement;
|
|
3859
|
+
}
|
|
3860
|
+
transformOriginMap = {
|
|
3861
|
+
top: 'bottom center',
|
|
3862
|
+
right: 'center left',
|
|
3863
|
+
bottom: 'top center',
|
|
3864
|
+
left: 'center right',
|
|
3865
|
+
'top-start': 'bottom left',
|
|
3866
|
+
'top-end': 'bottom right',
|
|
3867
|
+
'right-start': 'top left',
|
|
3868
|
+
'right-end': 'bottom left',
|
|
3869
|
+
'bottom-start': 'top left',
|
|
3870
|
+
'bottom-end': 'top right',
|
|
3871
|
+
'left-start': 'top right',
|
|
3872
|
+
'left-end': 'bottom right',
|
|
3873
|
+
};
|
|
3874
|
+
dropdownClass = computed(() => {
|
|
3875
|
+
const currentPlacement = this.placement();
|
|
3876
|
+
return css([
|
|
3877
|
+
{
|
|
3878
|
+
// Reset browser agent default popover styles
|
|
3879
|
+
border: 'none',
|
|
3880
|
+
background: 'transparent',
|
|
3881
|
+
padding: 0,
|
|
3882
|
+
overflow: 'visible',
|
|
3883
|
+
width: 'max-content',
|
|
3884
|
+
// Native anchor positioning: the browser keeps the panel attached to
|
|
3885
|
+
// the trigger (no scroll/resize listeners needed)
|
|
3886
|
+
...anchorStyles(this.anchorName, currentPlacement, this.offset()),
|
|
3887
|
+
// 2. Animate discrete properties across top layer layout contexts
|
|
3888
|
+
transitionProperty: 'transform, opacity, display, overlay',
|
|
3889
|
+
transitionDuration: `${this.delay}ms`,
|
|
3890
|
+
transitionTimingFunction: 'linear',
|
|
3891
|
+
transitionBehavior: 'allow-discrete',
|
|
3892
|
+
// Hidden State (Closed)
|
|
3893
|
+
opacity: 0,
|
|
3894
|
+
transform: 'scale(0.8)',
|
|
3895
|
+
transformOrigin: this.transformOriginMap[currentPlacement],
|
|
3896
|
+
// 3. Active state styling controlled via the native browser pseudo-class
|
|
3897
|
+
['&:popover-open']: {
|
|
3898
|
+
opacity: 1,
|
|
3899
|
+
transform: 'scale(1)',
|
|
3900
|
+
},
|
|
3901
|
+
// 4. Starting-style rules what properties animate *from* when transitioning in
|
|
3902
|
+
['@starting-style']: {
|
|
3903
|
+
['&:popover-open']: {
|
|
3904
|
+
opacity: 0,
|
|
3905
|
+
transform: 'scale(0.8)',
|
|
3906
|
+
},
|
|
3907
|
+
},
|
|
3908
|
+
},
|
|
3909
|
+
]);
|
|
3910
|
+
}, ...(ngDevMode ? [{ debugName: "dropdownClass" }] : /* istanbul ignore next */ []));
|
|
3911
|
+
/** The element that receives focus and carries the ARIA popup state. */
|
|
3912
|
+
get _focusTarget() {
|
|
3913
|
+
return resolveFocusTarget(this._trigger);
|
|
3914
|
+
}
|
|
3915
|
+
ngOnInit() {
|
|
3916
|
+
// Single native click binding to manage open/close commands
|
|
3917
|
+
this.renderer.listen(this._trigger, 'click', (e) => {
|
|
3918
|
+
e.stopPropagation();
|
|
3919
|
+
this.toggleDropdown();
|
|
3920
|
+
});
|
|
3921
|
+
// Anchor the popover panel to the trigger element
|
|
3922
|
+
this.renderer.setStyle(this._trigger, 'anchor-name', this.anchorName);
|
|
3923
|
+
// Wire the ARIA popup contract onto the focusable trigger element
|
|
3924
|
+
const focusTarget = this._focusTarget;
|
|
3925
|
+
this.renderer.setAttribute(focusTarget, 'aria-expanded', 'false');
|
|
3926
|
+
this.renderer.setAttribute(focusTarget, 'aria-controls', this.popoverId);
|
|
3927
|
+
if (this.ariaHasPopup()) {
|
|
3928
|
+
this.renderer.setAttribute(focusTarget, 'aria-haspopup', this.ariaHasPopup());
|
|
3929
|
+
}
|
|
3930
|
+
// Sync state if user invokes light-dismiss via outside click or Escape key
|
|
3931
|
+
this.renderer.listen(this._dropdown, 'toggle', (event) => {
|
|
3932
|
+
const isOpened = event.newState === 'open';
|
|
3933
|
+
this.showing.set(isOpened);
|
|
3934
|
+
this.renderer.setAttribute(this._focusTarget, 'aria-expanded', `${isOpened}`);
|
|
3935
|
+
if (isOpened) {
|
|
3936
|
+
this.dropdownShowing.emit(true);
|
|
3937
|
+
}
|
|
3938
|
+
else {
|
|
3939
|
+
this.dropdownHiding.emit(true);
|
|
3940
|
+
this.restoreFocus();
|
|
3941
|
+
}
|
|
3942
|
+
});
|
|
3943
|
+
}
|
|
3944
|
+
/**
|
|
3945
|
+
* Returns focus to the trigger when the popover closes while focus was
|
|
3946
|
+
* inside it (or was dropped on <body> by the top layer closing), so
|
|
3947
|
+
* keyboard users are never stranded (WCAG 2.4.3).
|
|
3948
|
+
*/
|
|
3949
|
+
restoreFocus() {
|
|
3950
|
+
const active = document.activeElement;
|
|
3951
|
+
if (active === document.body || (active && this._dropdown.contains(active))) {
|
|
3952
|
+
this._focusTarget.focus();
|
|
3953
|
+
}
|
|
3954
|
+
}
|
|
3955
|
+
toggleDropdown() {
|
|
3956
|
+
if (this.showing()) {
|
|
3957
|
+
this._dropdown.hidePopover();
|
|
3958
|
+
}
|
|
3959
|
+
else {
|
|
3960
|
+
this._dropdown.showPopover();
|
|
3961
|
+
}
|
|
3962
|
+
}
|
|
3963
|
+
hideDropdown() {
|
|
3964
|
+
this._dropdown.hidePopover();
|
|
3965
|
+
}
|
|
3966
|
+
ngOnDestroy() {
|
|
3967
|
+
try {
|
|
3968
|
+
this._dropdown.hidePopover();
|
|
3969
|
+
}
|
|
3970
|
+
catch {
|
|
3971
|
+
// popover was already closed or detached
|
|
3972
|
+
}
|
|
3973
|
+
}
|
|
3974
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: UniDropdownComponent, deps: null, target: i0.ɵɵFactoryTarget.Component });
|
|
3975
|
+
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: `
|
|
3976
|
+
<!-- 1. The native 'popover' attribute brings it to the top layer with native light-dismiss -->
|
|
3977
|
+
<div #dropdown popover="auto" [id]="popoverId" [class]="dropdownClass()">
|
|
3978
|
+
<div
|
|
3979
|
+
box-layout
|
|
3980
|
+
[border]="border() ?? componentOptions().border"
|
|
3981
|
+
[borderRadius]="borderRadius() ?? componentOptions().borderRadius"
|
|
3982
|
+
[paddingVertical]="paddingVertical()"
|
|
3983
|
+
[paddingHorizontal]="paddingHorizontal()"
|
|
3984
|
+
[color]="color() ?? componentOptions().color"
|
|
3985
|
+
[shadow]="shadow() ?? componentOptions().shadow"
|
|
3986
|
+
>
|
|
3987
|
+
<ng-content></ng-content>
|
|
3988
|
+
</div>
|
|
3989
|
+
</div>
|
|
3990
|
+
`, 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 });
|
|
3991
|
+
}
|
|
3992
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: UniDropdownComponent, decorators: [{
|
|
3993
|
+
type: Component,
|
|
3994
|
+
args: [{
|
|
3995
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
3996
|
+
selector: 'uni-dropdown',
|
|
3997
|
+
imports: [UniBoxComponent],
|
|
3998
|
+
template: `
|
|
3999
|
+
<!-- 1. The native 'popover' attribute brings it to the top layer with native light-dismiss -->
|
|
4000
|
+
<div #dropdown popover="auto" [id]="popoverId" [class]="dropdownClass()">
|
|
4001
|
+
<div
|
|
4002
|
+
box-layout
|
|
4003
|
+
[border]="border() ?? componentOptions().border"
|
|
4004
|
+
[borderRadius]="borderRadius() ?? componentOptions().borderRadius"
|
|
4005
|
+
[paddingVertical]="paddingVertical()"
|
|
4006
|
+
[paddingHorizontal]="paddingHorizontal()"
|
|
4007
|
+
[color]="color() ?? componentOptions().color"
|
|
4008
|
+
[shadow]="shadow() ?? componentOptions().shadow"
|
|
4009
|
+
>
|
|
4010
|
+
<ng-content></ng-content>
|
|
4011
|
+
</div>
|
|
4012
|
+
</div>
|
|
4013
|
+
`,
|
|
4014
|
+
providers: [{ provide: COMPONENT_NAME, useValue: 'dropdown' }],
|
|
4015
|
+
}]
|
|
4016
|
+
}], 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: [{
|
|
4017
|
+
type: ViewChild,
|
|
4018
|
+
args: ['dropdown', { static: true }]
|
|
4019
|
+
}] } });
|
|
4020
|
+
|
|
4021
|
+
class UniInputBoxComponent extends BaseComponent {
|
|
4022
|
+
className = css({ display: 'contents' });
|
|
4023
|
+
disabled = input(false, ...(ngDevMode ? [{ debugName: "disabled" }] : /* istanbul ignore next */ []));
|
|
4024
|
+
error = input(false, ...(ngDevMode ? [{ debugName: "error" }] : /* istanbul ignore next */ []));
|
|
4025
|
+
minWidth = input('0', ...(ngDevMode ? [{ debugName: "minWidth" }] : /* istanbul ignore next */ []));
|
|
4026
|
+
/** Override the themed field height, e.g. `'auto'` for multi-line fields. */
|
|
4027
|
+
height = input(undefined, ...(ngDevMode ? [{ debugName: "height" }] : /* istanbul ignore next */ []));
|
|
4028
|
+
color = computed(() => this.error() ? this.componentOptions().errorColor : this.componentOptions().color, ...(ngDevMode ? [{ debugName: "color" }] : /* istanbul ignore next */ []));
|
|
4029
|
+
border = computed(() => this.error() ? this.componentOptions().errorBorder : this.componentOptions().border, ...(ngDevMode ? [{ debugName: "border" }] : /* istanbul ignore next */ []));
|
|
4030
|
+
shadow = computed(() => this.error() ? this.componentOptions().errorShadow : this.componentOptions().shadow, ...(ngDevMode ? [{ debugName: "shadow" }] : /* istanbul ignore next */ []));
|
|
4031
|
+
inputBoxClass = computed(() => css([
|
|
4032
|
+
this.disabled() && {
|
|
4033
|
+
...this.theme.color(this.componentOptions().disabledTextColor),
|
|
4034
|
+
...this.theme.backgroundColor(this.componentOptions().disabledColor),
|
|
4035
|
+
cursor: 'not-allowed !important',
|
|
4036
|
+
},
|
|
4037
|
+
{
|
|
4038
|
+
'& input, select, textarea': {
|
|
4039
|
+
...removeInputPlatformStyling,
|
|
4040
|
+
height: '100%',
|
|
4041
|
+
...this.theme.paddingLeft(this.componentOptions().paddingLeft),
|
|
4042
|
+
...this.theme.color(this.componentOptions().textColor),
|
|
4043
|
+
// `typeFace` is the deprecated casing; themes that still set it win
|
|
4044
|
+
// only when the canonical key is absent.
|
|
4045
|
+
...this.theme.typeface(this.componentOptions().typeface ?? this.componentOptions().typeFace),
|
|
4046
|
+
},
|
|
4047
|
+
// Multi-line fields size themselves (rows/resize), not from the box.
|
|
4048
|
+
'& textarea': {
|
|
4049
|
+
height: 'auto',
|
|
4050
|
+
...this.theme.paddingTop('xs'),
|
|
4051
|
+
...this.theme.paddingBottom('xs'),
|
|
4052
|
+
},
|
|
4053
|
+
'&:has(input:disabled, select:disabled, textarea:disabled)': {
|
|
4054
|
+
...this.theme.color(this.componentOptions().disabledTextColor),
|
|
4055
|
+
...this.theme.backgroundColor(this.componentOptions().disabledColor),
|
|
4056
|
+
},
|
|
4057
|
+
'& input:disabled, select:disabled, textarea:disabled': {
|
|
4058
|
+
cursor: 'not-allowed !important',
|
|
4059
|
+
},
|
|
4060
|
+
'&:has(input:focus, select:focus, textarea:focus)': {
|
|
4061
|
+
outline: this.componentOptions().focusOutline,
|
|
4062
|
+
outlineOffset: this.componentOptions().focusOutlineOffset,
|
|
4063
|
+
// Optional focus chrome (border/ring/background). It yields to the
|
|
4064
|
+
// error state, so a flagged field stays visibly flagged while the
|
|
4065
|
+
// user is in it correcting the value.
|
|
4066
|
+
...(this.error()
|
|
4067
|
+
? {}
|
|
4068
|
+
: {
|
|
4069
|
+
...this.theme.border(this.componentOptions().focusBorder),
|
|
4070
|
+
...this.theme.boxShadow(this.componentOptions().focusShadow),
|
|
4071
|
+
...this.theme.backgroundColor(this.componentOptions().focusColor),
|
|
4072
|
+
}),
|
|
4073
|
+
},
|
|
4074
|
+
},
|
|
4075
|
+
]), ...(ngDevMode ? [{ debugName: "inputBoxClass" }] : /* istanbul ignore next */ []));
|
|
4076
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: UniInputBoxComponent, deps: null, target: i0.ɵɵFactoryTarget.Component });
|
|
4077
|
+
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 });
|
|
4078
|
+
}
|
|
4079
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: UniInputBoxComponent, decorators: [{
|
|
4080
|
+
type: Component,
|
|
4081
|
+
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" }]
|
|
4082
|
+
}], 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 }] }] } });
|
|
4083
|
+
|
|
4084
|
+
/**
|
|
4085
|
+
* Date field with free-typed parsing and a popup calendar. Type `aug 20`,
|
|
4086
|
+
* `8/20/2026` or `2026-08-20`, or pick from the grid — the form gets the
|
|
4087
|
+
* same canonical `'YYYY-MM-DD'` string either way. Parsing is `Intl`-driven
|
|
4088
|
+
* (locale digit order and month names, never hardcoded); unreadable text
|
|
4089
|
+
* stays in the field, flagged, with a `rejected` event. The popup is a
|
|
4090
|
+
* native popover hosting the same `uni-calendar` an app could render inline.
|
|
4091
|
+
*/
|
|
4092
|
+
class UniDateInputComponent extends BaseComponent {
|
|
4093
|
+
// --- Signal Forms block (explicit per AGENTS.md, not a base class) --------
|
|
4094
|
+
value = model(...(ngDevMode ? [undefined, { debugName: "value" }] : /* istanbul ignore next */ []));
|
|
4095
|
+
disabled = input(false, ...(ngDevMode ? [{ debugName: "disabled" }] : /* istanbul ignore next */ []));
|
|
4096
|
+
touched = model(false, ...(ngDevMode ? [{ debugName: "touched" }] : /* istanbul ignore next */ []));
|
|
4097
|
+
invalid = input(false, ...(ngDevMode ? [{ debugName: "invalid" }] : /* istanbul ignore next */ []));
|
|
4098
|
+
dirty = input(false, ...(ngDevMode ? [{ debugName: "dirty" }] : /* istanbul ignore next */ []));
|
|
4099
|
+
required = input(false, ...(ngDevMode ? [{ debugName: "required" }] : /* istanbul ignore next */ []));
|
|
4100
|
+
ariaDescribedBy = input(...(ngDevMode ? [undefined, { debugName: "ariaDescribedBy" }] : /* istanbul ignore next */ []));
|
|
4101
|
+
// --- Configuration -------------------------------------------------------
|
|
4102
|
+
/** Accessible name for the field, e.g. "Appointment date". */
|
|
4103
|
+
label = input.required(...(ngDevMode ? [{ debugName: "label" }] : /* istanbul ignore next */ []));
|
|
4104
|
+
/** Defaults to the locale's digit pattern, e.g. `MM/DD/YYYY`. */
|
|
4105
|
+
placeholder = input(...(ngDevMode ? [undefined, { debugName: "placeholder" }] : /* istanbul ignore next */ []));
|
|
4106
|
+
/** How the committed value renders, e.g. `{ dateStyle: 'long' }`. */
|
|
4107
|
+
displayFormat = input({ dateStyle: 'medium' }, ...(ngDevMode ? [{ debugName: "displayFormat" }] : /* istanbul ignore next */ []));
|
|
4108
|
+
/** BCP 47 tag; defaults to the document language, then the browser's. */
|
|
4109
|
+
locale = input(...(ngDevMode ? [undefined, { debugName: "locale" }] : /* istanbul ignore next */ []));
|
|
4110
|
+
commitOnBlur = input(true, ...(ngDevMode ? [{ debugName: "commitOnBlur" }] : /* istanbul ignore next */ []));
|
|
4111
|
+
/** Custom parser, replacing the built-in ISO/locale/month-name parsing. */
|
|
4112
|
+
parse = input(...(ngDevMode ? [undefined, { debugName: "parse" }] : /* istanbul ignore next */ []));
|
|
4113
|
+
/** Renders without its own input-box chrome, for composers like uni-date-time-input. */
|
|
4114
|
+
embedded = input(false, ...(ngDevMode ? [{ debugName: "embedded" }] : /* istanbul ignore next */ []));
|
|
4115
|
+
// --- Forwarded to the popup calendar --------------------------------------
|
|
4116
|
+
minDate = input(...(ngDevMode ? [undefined, { debugName: "minDate" }] : /* istanbul ignore next */ []));
|
|
4117
|
+
maxDate = input(...(ngDevMode ? [undefined, { debugName: "maxDate" }] : /* istanbul ignore next */ []));
|
|
4118
|
+
disabledDates = input(...(ngDevMode ? [undefined, { debugName: "disabledDates" }] : /* istanbul ignore next */ []));
|
|
4119
|
+
markers = input([], ...(ngDevMode ? [{ debugName: "markers" }] : /* istanbul ignore next */ []));
|
|
4120
|
+
weekStart = input(...(ngDevMode ? [undefined, { debugName: "weekStart" }] : /* istanbul ignore next */ []));
|
|
4121
|
+
// --- Events ----------------------------------------------------------------
|
|
4122
|
+
/** Popup shown. */
|
|
4123
|
+
opened = output();
|
|
4124
|
+
/** Popup hidden. */
|
|
4125
|
+
closed = output();
|
|
4126
|
+
/** A typed commit was refused; the raw text stays in the field. */
|
|
4127
|
+
rejected = output();
|
|
4128
|
+
host = inject(ElementRef);
|
|
4129
|
+
inputRef = viewChild('field', ...(ngDevMode ? [{ debugName: "inputRef" }] : /* istanbul ignore next */ []));
|
|
4130
|
+
// #toggle sits on the icon-button component, so read the element explicitly.
|
|
4131
|
+
toggleRef = viewChild('toggle', { ...(ngDevMode ? { debugName: "toggleRef" } : /* istanbul ignore next */ {}), read: ElementRef });
|
|
4132
|
+
popupRef = viewChild('popupDialog', ...(ngDevMode ? [{ debugName: "popupRef" }] : /* istanbul ignore next */ []));
|
|
4133
|
+
dropdown = viewChild(UniDropdownComponent, ...(ngDevMode ? [{ debugName: "dropdown" }] : /* istanbul ignore next */ []));
|
|
4134
|
+
calendar = viewChild(UniCalendarComponent, ...(ngDevMode ? [{ debugName: "calendar" }] : /* istanbul ignore next */ []));
|
|
4135
|
+
srOnly = css(visuallyHidden);
|
|
4136
|
+
/** A refused commit — styles the field and sets aria-invalid until edited. */
|
|
4137
|
+
draftInvalid = signal(false, ...(ngDevMode ? [{ debugName: "draftInvalid" }] : /* istanbul ignore next */ []));
|
|
4138
|
+
announcement = signal('', ...(ngDevMode ? [{ debugName: "announcement" }] : /* istanbul ignore next */ []));
|
|
4139
|
+
toggleElement = computed(() => this.toggleRef()?.nativeElement, ...(ngDevMode ? [{ debugName: "toggleElement" }] : /* istanbul ignore next */ []));
|
|
4140
|
+
popupOpen = computed(() => this.dropdown()?.showing() ?? false, ...(ngDevMode ? [{ debugName: "popupOpen" }] : /* istanbul ignore next */ []));
|
|
4141
|
+
resolvedLocale = computed(() => this.locale() ?? (document.documentElement.lang || navigator.language || 'en-US'), ...(ngDevMode ? [{ debugName: "resolvedLocale" }] : /* istanbul ignore next */ []));
|
|
4142
|
+
displayText = computed(() => {
|
|
4143
|
+
const value = this.value();
|
|
4144
|
+
return value ? formatDate(value, this.resolvedLocale(), this.displayFormat()) : '';
|
|
4145
|
+
}, ...(ngDevMode ? [{ debugName: "displayText" }] : /* istanbul ignore next */ []));
|
|
4146
|
+
resolvedPlaceholder = computed(() => this.placeholder() ?? localeDatePlaceholder(this.resolvedLocale()), ...(ngDevMode ? [{ debugName: "resolvedPlaceholder" }] : /* istanbul ignore next */ []));
|
|
4147
|
+
showError = computed(() => this.invalid() && (this.touched() || this.dirty()), ...(ngDevMode ? [{ debugName: "showError" }] : /* istanbul ignore next */ []));
|
|
4148
|
+
toggleLabel = computed(() => {
|
|
4149
|
+
const value = this.value();
|
|
4150
|
+
return value
|
|
4151
|
+
? `Change date, ${formatDate(value, this.resolvedLocale(), { dateStyle: 'full' })}`
|
|
4152
|
+
: 'Choose date';
|
|
4153
|
+
}, ...(ngDevMode ? [{ debugName: "toggleLabel" }] : /* istanbul ignore next */ []));
|
|
4154
|
+
// --- Committing -------------------------------------------------------------
|
|
4155
|
+
fullDate(date) {
|
|
4156
|
+
return formatDate(date, this.resolvedLocale(), { dateStyle: 'full' });
|
|
4157
|
+
}
|
|
4158
|
+
isDayBlocked(date) {
|
|
4159
|
+
const dates = this.disabledDates();
|
|
4160
|
+
if (!dates)
|
|
4161
|
+
return false;
|
|
4162
|
+
return Array.isArray(dates) ? dates.includes(date) : dates(date);
|
|
4163
|
+
}
|
|
4164
|
+
setValue(date, silent = false) {
|
|
4165
|
+
this.value.set(date);
|
|
4166
|
+
this.draftInvalid.set(false);
|
|
4167
|
+
this.setFieldText(this.displayText());
|
|
4168
|
+
if (!silent)
|
|
4169
|
+
this.announce(date ? `${this.fullDate(date)}.` : 'Date cleared.');
|
|
4170
|
+
}
|
|
4171
|
+
refuse(raw, reason) {
|
|
4172
|
+
this.draftInvalid.set(true);
|
|
4173
|
+
const message = {
|
|
4174
|
+
unparseable: `Couldn't read “${raw}” as a date.`,
|
|
4175
|
+
'out-of-range': `${raw} is outside the allowed dates.`,
|
|
4176
|
+
disabled: `${raw} isn't available.`,
|
|
4177
|
+
}[reason];
|
|
4178
|
+
this.announce(message);
|
|
4179
|
+
this.rejected.emit({ raw, reason });
|
|
4180
|
+
}
|
|
4181
|
+
commit(raw) {
|
|
4182
|
+
const trimmed = raw.trim();
|
|
4183
|
+
if (!trimmed) {
|
|
4184
|
+
this.setValue(undefined);
|
|
4185
|
+
return true;
|
|
4186
|
+
}
|
|
4187
|
+
const custom = this.parse();
|
|
4188
|
+
const parsed = custom
|
|
4189
|
+
? custom(trimmed, this.resolvedLocale())
|
|
4190
|
+
: parseDateText(trimmed, this.resolvedLocale());
|
|
4191
|
+
if (!parsed) {
|
|
4192
|
+
this.refuse(trimmed, 'unparseable');
|
|
4193
|
+
return false;
|
|
4194
|
+
}
|
|
4195
|
+
const min = this.minDate();
|
|
4196
|
+
const max = this.maxDate();
|
|
4197
|
+
if ((min && parsed < min) || (max && parsed > max)) {
|
|
4198
|
+
this.refuse(trimmed, 'out-of-range');
|
|
4199
|
+
return false;
|
|
4200
|
+
}
|
|
4201
|
+
if (this.isDayBlocked(parsed)) {
|
|
4202
|
+
this.refuse(trimmed, 'disabled');
|
|
4203
|
+
return false;
|
|
4204
|
+
}
|
|
4205
|
+
this.setValue(parsed);
|
|
4206
|
+
return true;
|
|
4207
|
+
}
|
|
4208
|
+
/** Step a committed value ±1 day, skipping blocked days, stopping at fences. */
|
|
4209
|
+
step(direction) {
|
|
4210
|
+
const committed = this.value();
|
|
4211
|
+
if (!committed)
|
|
4212
|
+
return;
|
|
4213
|
+
let date = addDays(committed, direction);
|
|
4214
|
+
let guard = 0;
|
|
4215
|
+
while (this.isDayBlocked(date) && guard++ < 400)
|
|
4216
|
+
date = addDays(date, direction);
|
|
4217
|
+
const min = this.minDate();
|
|
4218
|
+
const max = this.maxDate();
|
|
4219
|
+
if ((min && date < min) || (max && date > max))
|
|
4220
|
+
return; // fence
|
|
4221
|
+
this.setValue(date);
|
|
4222
|
+
}
|
|
4223
|
+
// --- Keyboard ----------------------------------------------------------------
|
|
4224
|
+
onInputKeydown(event) {
|
|
4225
|
+
const element = this.inputRef().nativeElement;
|
|
4226
|
+
switch (event.key) {
|
|
4227
|
+
case 'Enter':
|
|
4228
|
+
event.preventDefault();
|
|
4229
|
+
this.commit(element.value);
|
|
4230
|
+
break;
|
|
4231
|
+
case 'Escape':
|
|
4232
|
+
if (this.popupOpen()) {
|
|
4233
|
+
this.closePopup();
|
|
4234
|
+
}
|
|
4235
|
+
else {
|
|
4236
|
+
this.setFieldText(this.displayText());
|
|
4237
|
+
this.draftInvalid.set(false);
|
|
4238
|
+
}
|
|
4239
|
+
break;
|
|
4240
|
+
case 'ArrowDown':
|
|
4241
|
+
event.preventDefault();
|
|
4242
|
+
// Alt or an empty field opens the popup; on a committed value the
|
|
4243
|
+
// caret has nowhere to go, so stepping is what a spinner would do.
|
|
4244
|
+
if (event.altKey || element.value.trim() === '')
|
|
4245
|
+
this.openPopup();
|
|
4246
|
+
else if (this.value() && element.value === this.displayText())
|
|
4247
|
+
this.step(-1);
|
|
4248
|
+
break;
|
|
4249
|
+
case 'ArrowUp':
|
|
4250
|
+
event.preventDefault();
|
|
4251
|
+
if (this.value() && element.value === this.displayText())
|
|
4252
|
+
this.step(1);
|
|
4253
|
+
break;
|
|
4254
|
+
}
|
|
4255
|
+
}
|
|
4256
|
+
onInput() {
|
|
4257
|
+
this.draftInvalid.set(false);
|
|
4258
|
+
}
|
|
4259
|
+
onFocusOut(event) {
|
|
4260
|
+
const next = event.relatedTarget;
|
|
4261
|
+
if (next && this.host.nativeElement.contains(next))
|
|
4262
|
+
return;
|
|
4263
|
+
this.touched.set(true);
|
|
4264
|
+
if (this.popupOpen())
|
|
4265
|
+
return;
|
|
4266
|
+
const element = this.inputRef()?.nativeElement;
|
|
4267
|
+
if (element && this.commitOnBlur() && element.value !== this.displayText())
|
|
4268
|
+
this.commit(element.value);
|
|
4269
|
+
}
|
|
4270
|
+
// --- Popup ---------------------------------------------------------------------
|
|
4271
|
+
openPopup() {
|
|
4272
|
+
if (this.disabled() || this.popupOpen())
|
|
4273
|
+
return;
|
|
4274
|
+
this.dropdown()?.toggleDropdown();
|
|
4275
|
+
}
|
|
4276
|
+
closePopup() {
|
|
4277
|
+
if (this.popupOpen())
|
|
4278
|
+
this.dropdown()?.hideDropdown();
|
|
4279
|
+
}
|
|
4280
|
+
onPopupShowing() {
|
|
4281
|
+
// The grid opens on the committed value's month (or today's). Falsy
|
|
4282
|
+
// guard: a bound '' counts as no value, like everywhere else.
|
|
4283
|
+
this.calendar()?.month.set(monthOf(this.value() || todayIso()));
|
|
4284
|
+
this.calendar()?.focusActiveDay();
|
|
4285
|
+
this.opened.emit();
|
|
4286
|
+
}
|
|
4287
|
+
onPopupHiding() {
|
|
4288
|
+
this.closed.emit();
|
|
4289
|
+
// Focus returns to the field (not the toggle) when it was in the popup —
|
|
4290
|
+
// this runs before the dropdown's own restoreFocus, which then no-ops.
|
|
4291
|
+
const active = document.activeElement;
|
|
4292
|
+
const popup = this.popupRef()?.nativeElement;
|
|
4293
|
+
if (!active || active === document.body || (popup && popup.contains(active)))
|
|
4294
|
+
this.inputRef()?.nativeElement.focus();
|
|
4295
|
+
}
|
|
4296
|
+
onCalendarPick(date) {
|
|
4297
|
+
this.setValue(date);
|
|
4298
|
+
this.closePopup();
|
|
4299
|
+
}
|
|
4300
|
+
/** The popup is a focus-holding dialog: Tab cycles inside it (APG pattern). */
|
|
4301
|
+
onPopupKeydown(event) {
|
|
4302
|
+
if (event.key === 'Escape') {
|
|
4303
|
+
event.preventDefault();
|
|
4304
|
+
this.closePopup();
|
|
4305
|
+
return;
|
|
4306
|
+
}
|
|
4307
|
+
if (event.key !== 'Tab')
|
|
4308
|
+
return;
|
|
4309
|
+
const popup = this.popupRef()?.nativeElement;
|
|
4310
|
+
if (!popup)
|
|
4311
|
+
return;
|
|
4312
|
+
const focusables = Array.from(popup.querySelectorAll('button:not(:disabled)')).filter((button) => button.tabIndex >= 0);
|
|
4313
|
+
if (!focusables.length)
|
|
4314
|
+
return;
|
|
4315
|
+
const index = focusables.indexOf(document.activeElement);
|
|
4316
|
+
const next = focusables[(index + (event.shiftKey ? -1 : 1) + focusables.length) % focusables.length];
|
|
4317
|
+
event.preventDefault();
|
|
4318
|
+
next.focus();
|
|
4319
|
+
}
|
|
4320
|
+
// --- Internals -------------------------------------------------------------------
|
|
4321
|
+
setFieldText(text) {
|
|
4322
|
+
const element = this.inputRef()?.nativeElement;
|
|
4323
|
+
if (element)
|
|
4324
|
+
element.value = text;
|
|
4325
|
+
}
|
|
4326
|
+
announce(message) {
|
|
4327
|
+
this.announcement.set(this.announcement() === message ? `${message} ` : message);
|
|
4328
|
+
}
|
|
4329
|
+
// --- Styling -----------------------------------------------------------------------
|
|
4330
|
+
className = computed(() => css({ display: 'block', position: 'relative' }), ...(ngDevMode ? [{ debugName: "className" }] : /* istanbul ignore next */ []));
|
|
4331
|
+
rowClass = computed(() => css({ display: 'flex', alignItems: 'center', flex: 1, width: '100%', minWidth: 0 }), ...(ngDevMode ? [{ debugName: "rowClass" }] : /* istanbul ignore next */ []));
|
|
4332
|
+
inputClass = computed(() => {
|
|
4333
|
+
const colors = this.theme.colorPalette();
|
|
4334
|
+
return css([
|
|
4335
|
+
{
|
|
4336
|
+
flex: 1,
|
|
4337
|
+
minWidth: 0,
|
|
4338
|
+
border: 0,
|
|
4339
|
+
outline: 'none',
|
|
4340
|
+
background: 'transparent',
|
|
4341
|
+
color: 'inherit',
|
|
4342
|
+
font: 'inherit',
|
|
4343
|
+
},
|
|
4344
|
+
this.draftInvalid() && {
|
|
4345
|
+
color: colors['warn'],
|
|
4346
|
+
// Shape and colour, not colour alone (WCAG 1.4.1).
|
|
4347
|
+
textDecoration: `underline dashed ${colors['warn']} 1.5px`,
|
|
4348
|
+
textUnderlineOffset: 3,
|
|
4349
|
+
},
|
|
4350
|
+
]);
|
|
4351
|
+
}, ...(ngDevMode ? [{ debugName: "inputClass" }] : /* istanbul ignore next */ []));
|
|
4352
|
+
toggleWrapClass = computed(() => css({ display: 'flex', alignItems: 'center', ...this.theme.paddingRight('xxs') }), ...(ngDevMode ? [{ debugName: "toggleWrapClass" }] : /* istanbul ignore next */ []));
|
|
4353
|
+
/** Embedded mode: the composer owns the box; keep only the flex row. */
|
|
4354
|
+
embeddedClass = computed(() => {
|
|
4355
|
+
const colors = this.theme.colorPalette();
|
|
4356
|
+
return css([
|
|
4357
|
+
{ display: 'flex', alignItems: 'center', flex: 1, minWidth: 0 },
|
|
4358
|
+
this.draftInvalid() && { color: colors['warn'] },
|
|
4359
|
+
]);
|
|
4360
|
+
}, ...(ngDevMode ? [{ debugName: "embeddedClass" }] : /* istanbul ignore next */ []));
|
|
4361
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: UniDateInputComponent, deps: null, target: i0.ɵɵFactoryTarget.Component });
|
|
4362
|
+
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 });
|
|
4363
|
+
}
|
|
4364
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: UniDateInputComponent, decorators: [{
|
|
4365
|
+
type: Component,
|
|
4366
|
+
args: [{ changeDetection: ChangeDetectionStrategy.OnPush, selector: 'uni-date-input, DateInput', imports: [
|
|
4367
|
+
NgTemplateOutlet,
|
|
4368
|
+
UniCalendarComponent,
|
|
4369
|
+
UniDropdownComponent,
|
|
4370
|
+
UniIconButtonComponent,
|
|
4371
|
+
UniInputBoxComponent,
|
|
4372
|
+
], 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" }]
|
|
4373
|
+
}], 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 }] }] } });
|
|
4374
|
+
|
|
4375
|
+
const toMinutes = (time) => {
|
|
4376
|
+
const [h, m] = time.split(':').map(Number);
|
|
4377
|
+
return h * 60 + m;
|
|
4378
|
+
};
|
|
4379
|
+
const fromMinutes = (minutes) => `${String(Math.floor(minutes / 60)).padStart(2, '0')}:${String(minutes % 60).padStart(2, '0')}`;
|
|
4380
|
+
/**
|
|
4381
|
+
* Time field: a combobox over time options — the same listbox contract as
|
|
4382
|
+
* uni-search-input and uni-tag-input. Type `3p`, `930` or `15:00`, or pick
|
|
4383
|
+
* `3:00 PM` from the list; the form always gets 24-hour `'HH:mm'` (`hour12`
|
|
4384
|
+
* affects display only). The list is assistive, not exhaustive: any
|
|
4385
|
+
* parseable time commits, unless `slots` pins the choices (a slot picker) —
|
|
4386
|
+
* then a typed time must match one. Unreadable or unavailable text stays in
|
|
4387
|
+
* the field, flagged, with a `rejected` event.
|
|
4388
|
+
*/
|
|
4389
|
+
class UniTimeInputComponent extends BaseComponent {
|
|
4390
|
+
// --- Signal Forms block (explicit per AGENTS.md, not a base class) --------
|
|
4391
|
+
value = model(...(ngDevMode ? [undefined, { debugName: "value" }] : /* istanbul ignore next */ []));
|
|
4392
|
+
disabled = input(false, ...(ngDevMode ? [{ debugName: "disabled" }] : /* istanbul ignore next */ []));
|
|
4393
|
+
touched = model(false, ...(ngDevMode ? [{ debugName: "touched" }] : /* istanbul ignore next */ []));
|
|
4394
|
+
invalid = input(false, ...(ngDevMode ? [{ debugName: "invalid" }] : /* istanbul ignore next */ []));
|
|
4395
|
+
dirty = input(false, ...(ngDevMode ? [{ debugName: "dirty" }] : /* istanbul ignore next */ []));
|
|
4396
|
+
required = input(false, ...(ngDevMode ? [{ debugName: "required" }] : /* istanbul ignore next */ []));
|
|
4397
|
+
ariaDescribedBy = input(...(ngDevMode ? [undefined, { debugName: "ariaDescribedBy" }] : /* istanbul ignore next */ []));
|
|
4398
|
+
// --- Configuration -------------------------------------------------------
|
|
4399
|
+
/** Accessible name for the field, e.g. "Start time". */
|
|
4400
|
+
label = input.required(...(ngDevMode ? [{ debugName: "label" }] : /* istanbul ignore next */ []));
|
|
4401
|
+
placeholder = input(...(ngDevMode ? [undefined, { debugName: "placeholder" }] : /* istanbul ignore next */ []));
|
|
4402
|
+
/** Generated list granularity, in minutes. */
|
|
4403
|
+
minuteStep = input(30, ...(ngDevMode ? [{ debugName: "minuteStep" }] : /* istanbul ignore next */ []));
|
|
4404
|
+
/** Earliest allowed time (inclusive), `'09:00'`. */
|
|
4405
|
+
minTime = input(...(ngDevMode ? [undefined, { debugName: "minTime" }] : /* istanbul ignore next */ []));
|
|
4406
|
+
/** Latest allowed time (inclusive), `'17:00'`. */
|
|
4407
|
+
maxTime = input(...(ngDevMode ? [undefined, { debugName: "maxTime" }] : /* istanbul ignore next */ []));
|
|
4408
|
+
/** Exact allowed times (scheduling). When set, typed entry must match one. */
|
|
4409
|
+
slots = input(...(ngDevMode ? [undefined, { debugName: "slots" }] : /* istanbul ignore next */ []));
|
|
4410
|
+
/** 12-hour display; defaults from the locale. The value stays 24-hour. */
|
|
4411
|
+
hour12 = input(...(ngDevMode ? [undefined, { debugName: "hour12" }] : /* istanbul ignore next */ []));
|
|
4412
|
+
/** BCP 47 tag for the display format; defaults to the document language. */
|
|
4413
|
+
locale = input(...(ngDevMode ? [undefined, { debugName: "locale" }] : /* istanbul ignore next */ []));
|
|
4414
|
+
commitOnBlur = input(true, ...(ngDevMode ? [{ debugName: "commitOnBlur" }] : /* istanbul ignore next */ []));
|
|
4415
|
+
/** Renders without its own input-box chrome, for composers like uni-date-time-input. */
|
|
4416
|
+
embedded = input(false, ...(ngDevMode ? [{ debugName: "embedded" }] : /* istanbul ignore next */ []));
|
|
4417
|
+
// --- Events ----------------------------------------------------------------
|
|
4418
|
+
/** A typed commit was refused; the raw text stays in the field. */
|
|
4419
|
+
rejected = output();
|
|
4420
|
+
host = inject(ElementRef);
|
|
4421
|
+
inputRef = viewChild('field', ...(ngDevMode ? [{ debugName: "inputRef" }] : /* istanbul ignore next */ []));
|
|
4422
|
+
listRef = viewChild('listbox', ...(ngDevMode ? [{ debugName: "listRef" }] : /* istanbul ignore next */ []));
|
|
4423
|
+
srOnly = css(visuallyHidden);
|
|
4424
|
+
/** A refused commit — styles the field and sets aria-invalid until edited. */
|
|
4425
|
+
draftInvalid = signal(false, ...(ngDevMode ? [{ debugName: "draftInvalid" }] : /* istanbul ignore next */ []));
|
|
4426
|
+
announcement = signal('', ...(ngDevMode ? [{ debugName: "announcement" }] : /* istanbul ignore next */ []));
|
|
4427
|
+
resolvedLocale = computed(() => this.locale() ?? (document.documentElement.lang || navigator.language || 'en-US'), ...(ngDevMode ? [{ debugName: "resolvedLocale" }] : /* istanbul ignore next */ []));
|
|
4428
|
+
resolvedHour12 = computed(() => this.hour12() ?? localeDefaultHour12(this.resolvedLocale()), ...(ngDevMode ? [{ debugName: "resolvedHour12" }] : /* istanbul ignore next */ []));
|
|
4429
|
+
/** The listed times: pinned `slots` verbatim, else the generated step grid. */
|
|
4430
|
+
options = computed(() => this.slots() ?? timeSlots(this.minuteStep(), this.minTime(), this.maxTime()), ...(ngDevMode ? [{ debugName: "options" }] : /* istanbul ignore next */ []));
|
|
4431
|
+
optionLabels = computed(() => this.options().map((time) => this.formatValue(time)), ...(ngDevMode ? [{ debugName: "optionLabels" }] : /* istanbul ignore next */ []));
|
|
4432
|
+
/** Shared combobox bookkeeping — identical contract to uni-search-input. */
|
|
4433
|
+
list = createListboxNavigation({
|
|
4434
|
+
count: () => this.options().length,
|
|
4435
|
+
idPrefix: 'uni-time-listbox',
|
|
4436
|
+
});
|
|
4437
|
+
displayText = computed(() => {
|
|
4438
|
+
const value = this.value();
|
|
4439
|
+
return value ? this.formatValue(value) : '';
|
|
4440
|
+
}, ...(ngDevMode ? [{ debugName: "displayText" }] : /* istanbul ignore next */ []));
|
|
4441
|
+
resolvedPlaceholder = computed(() => this.placeholder() ?? this.formatValue('09:00'), ...(ngDevMode ? [{ debugName: "resolvedPlaceholder" }] : /* istanbul ignore next */ []));
|
|
4442
|
+
showError = computed(() => this.invalid() && (this.touched() || this.dirty()), ...(ngDevMode ? [{ debugName: "showError" }] : /* istanbul ignore next */ []));
|
|
4443
|
+
formatValue(time) {
|
|
4444
|
+
return formatTime(time, this.resolvedLocale(), this.resolvedHour12());
|
|
4445
|
+
}
|
|
4446
|
+
// --- Committing -------------------------------------------------------------
|
|
4447
|
+
setValue(time, silent = false) {
|
|
4448
|
+
this.value.set(time);
|
|
4449
|
+
this.draftInvalid.set(false);
|
|
4450
|
+
this.setFieldText(this.displayText());
|
|
4451
|
+
if (!silent)
|
|
4452
|
+
this.announce(time ? `${this.formatValue(time)}.` : 'Time cleared.');
|
|
4453
|
+
}
|
|
4454
|
+
refuse(raw, reason, shown = raw) {
|
|
4455
|
+
this.draftInvalid.set(true);
|
|
4456
|
+
const message = {
|
|
4457
|
+
unparseable: `Couldn't read “${shown}” as a time.`,
|
|
4458
|
+
'out-of-range': `${shown} is outside the allowed times.`,
|
|
4459
|
+
unavailable: `${shown} isn't available.`,
|
|
4460
|
+
}[reason];
|
|
4461
|
+
this.announce(message);
|
|
4462
|
+
this.rejected.emit({ raw, reason });
|
|
4463
|
+
}
|
|
4464
|
+
commit(raw) {
|
|
4465
|
+
const trimmed = raw.trim();
|
|
4466
|
+
if (!trimmed) {
|
|
4467
|
+
this.setValue(undefined);
|
|
4468
|
+
return true;
|
|
4469
|
+
}
|
|
4470
|
+
let parsed = parseTimeText(trimmed, this.resolvedHour12());
|
|
4471
|
+
if (!parsed) {
|
|
4472
|
+
this.refuse(trimmed, 'unparseable');
|
|
4473
|
+
return false;
|
|
4474
|
+
}
|
|
4475
|
+
const min = this.minTime();
|
|
4476
|
+
const max = this.maxTime();
|
|
4477
|
+
const inBounds = (time) => !(min && time < min) && !(max && time > max);
|
|
4478
|
+
const slots = this.slots();
|
|
4479
|
+
if (!slots && !inBounds(parsed)) {
|
|
4480
|
+
// The PM bias yields if it pushed the time out of bounds.
|
|
4481
|
+
const unbiased = parseTimeText(trimmed, false);
|
|
4482
|
+
if (unbiased && inBounds(unbiased))
|
|
4483
|
+
parsed = unbiased;
|
|
4484
|
+
else {
|
|
4485
|
+
this.refuse(trimmed, 'out-of-range');
|
|
4486
|
+
return false;
|
|
4487
|
+
}
|
|
4488
|
+
}
|
|
4489
|
+
if (slots && !slots.includes(parsed)) {
|
|
4490
|
+
// Announce the formatted time — "5:00 PM isn't available."
|
|
4491
|
+
this.refuse(trimmed, 'unavailable', this.formatValue(parsed));
|
|
4492
|
+
return false;
|
|
4493
|
+
}
|
|
4494
|
+
this.setValue(parsed);
|
|
4495
|
+
return true;
|
|
4496
|
+
}
|
|
4497
|
+
/** Step a committed value ±minuteStep (±1 slot when pinned), clamped. */
|
|
4498
|
+
stepValue(direction) {
|
|
4499
|
+
const committed = this.value();
|
|
4500
|
+
if (!committed)
|
|
4501
|
+
return;
|
|
4502
|
+
const slots = this.slots();
|
|
4503
|
+
if (slots?.length) {
|
|
4504
|
+
const sorted = [...slots].sort();
|
|
4505
|
+
const index = sorted.indexOf(committed);
|
|
4506
|
+
const next = index >= 0
|
|
4507
|
+
? sorted[index + direction]
|
|
4508
|
+
: direction > 0
|
|
4509
|
+
? sorted.find((slot) => slot > committed)
|
|
4510
|
+
: [...sorted].reverse().find((slot) => slot < committed);
|
|
4511
|
+
if (next)
|
|
4512
|
+
this.setValue(next);
|
|
4513
|
+
return;
|
|
4514
|
+
}
|
|
4515
|
+
const step = this.minuteStep();
|
|
4516
|
+
const current = toMinutes(committed);
|
|
4517
|
+
const snapped = direction > 0
|
|
4518
|
+
? Math.floor(current / step) * step + step
|
|
4519
|
+
: Math.ceil(current / step) * step - step;
|
|
4520
|
+
if (snapped < 0 || snapped >= 24 * 60)
|
|
4521
|
+
return;
|
|
4522
|
+
const candidate = fromMinutes(snapped);
|
|
4523
|
+
const min = this.minTime();
|
|
4524
|
+
const max = this.maxTime();
|
|
4525
|
+
if ((min && candidate < min) || (max && candidate > max))
|
|
4526
|
+
return; // fence
|
|
4527
|
+
this.setValue(candidate);
|
|
4528
|
+
}
|
|
4529
|
+
// --- Keyboard ----------------------------------------------------------------
|
|
4530
|
+
onInputKeydown(event) {
|
|
4531
|
+
const element = this.inputRef().nativeElement;
|
|
4532
|
+
switch (event.key) {
|
|
4533
|
+
case 'ArrowDown':
|
|
4534
|
+
case 'ArrowUp': {
|
|
4535
|
+
event.preventDefault();
|
|
4536
|
+
const direction = event.key === 'ArrowDown' ? 1 : -1;
|
|
4537
|
+
// A committed value with the list closed steps like a spinner —
|
|
4538
|
+
// ArrowUp means later, like the date field's ArrowUp means tomorrow —
|
|
4539
|
+
// while the list opens from an empty or edited field (or the toggle).
|
|
4540
|
+
if (!this.list.open() && this.value() && element.value === this.displayText()) {
|
|
4541
|
+
this.stepValue(direction === 1 ? -1 : 1);
|
|
4542
|
+
break;
|
|
4543
|
+
}
|
|
4544
|
+
if (!this.list.open()) {
|
|
4545
|
+
this.openList();
|
|
4546
|
+
if (this.list.activeIndex() < 0)
|
|
4547
|
+
this.list.setActive(direction === 1 ? 0 : this.options().length - 1);
|
|
4548
|
+
}
|
|
4549
|
+
else {
|
|
4550
|
+
const count = this.options().length;
|
|
4551
|
+
this.list.setActive((this.list.activeIndex() + direction + count) % count);
|
|
4552
|
+
}
|
|
4553
|
+
this.scrollToActive();
|
|
4554
|
+
break;
|
|
4555
|
+
}
|
|
4556
|
+
case 'Enter': {
|
|
4557
|
+
event.preventDefault();
|
|
4558
|
+
const active = this.list.activeIndex();
|
|
4559
|
+
if (this.list.open() && active >= 0)
|
|
4560
|
+
this.setValue(this.options()[active]);
|
|
4561
|
+
else
|
|
4562
|
+
this.commit(element.value);
|
|
4563
|
+
this.list.hide();
|
|
4564
|
+
break;
|
|
4565
|
+
}
|
|
4566
|
+
case 'Escape':
|
|
4567
|
+
if (this.list.open()) {
|
|
4568
|
+
this.list.hide();
|
|
4569
|
+
}
|
|
4570
|
+
else {
|
|
4571
|
+
this.setFieldText(this.displayText());
|
|
4572
|
+
this.draftInvalid.set(false);
|
|
4573
|
+
}
|
|
4574
|
+
break;
|
|
4575
|
+
case 'Tab':
|
|
4576
|
+
// Never trap: commit what is typed, then let focus move on.
|
|
4577
|
+
if (element.value.trim() && element.value !== this.displayText())
|
|
4578
|
+
this.commit(element.value);
|
|
4579
|
+
this.list.hide();
|
|
4580
|
+
break;
|
|
4581
|
+
}
|
|
4582
|
+
}
|
|
4583
|
+
onInput() {
|
|
4584
|
+
this.draftInvalid.set(false);
|
|
4585
|
+
// Typing never selects an option — Enter must commit the draft, not
|
|
4586
|
+
// whatever happens to sit nearest; the list only scrolls alongside.
|
|
4587
|
+
this.list.show();
|
|
4588
|
+
this.list.setActive(-1);
|
|
4589
|
+
const parsed = parseTimeText(this.inputRef().nativeElement.value, this.resolvedHour12());
|
|
4590
|
+
if (parsed) {
|
|
4591
|
+
const options = this.options();
|
|
4592
|
+
let nearest = options.findIndex((time) => time >= parsed);
|
|
4593
|
+
if (nearest < 0)
|
|
4594
|
+
nearest = options.length - 1;
|
|
4595
|
+
this.scrollToIndex(nearest);
|
|
4596
|
+
}
|
|
4597
|
+
}
|
|
4598
|
+
onToggle() {
|
|
4599
|
+
if (this.list.open()) {
|
|
4600
|
+
this.list.hide();
|
|
4601
|
+
}
|
|
4602
|
+
else {
|
|
4603
|
+
this.openList();
|
|
4604
|
+
this.inputRef()?.nativeElement.focus();
|
|
4605
|
+
}
|
|
4606
|
+
}
|
|
4607
|
+
selectOption(time) {
|
|
4608
|
+
this.setValue(time);
|
|
4609
|
+
this.list.hide();
|
|
4610
|
+
this.inputRef()?.nativeElement.focus();
|
|
4611
|
+
}
|
|
4612
|
+
onFocusOut(event) {
|
|
4613
|
+
const next = event.relatedTarget;
|
|
4614
|
+
if (next && this.host.nativeElement.contains(next))
|
|
4615
|
+
return;
|
|
4616
|
+
this.list.hide();
|
|
4617
|
+
this.touched.set(true);
|
|
4618
|
+
const element = this.inputRef()?.nativeElement;
|
|
4619
|
+
if (element && this.commitOnBlur() && element.value !== this.displayText())
|
|
4620
|
+
this.commit(element.value);
|
|
4621
|
+
}
|
|
4622
|
+
// --- Internals -------------------------------------------------------------------
|
|
4623
|
+
openList() {
|
|
4624
|
+
this.list.show();
|
|
4625
|
+
const committed = this.value();
|
|
4626
|
+
if (committed)
|
|
4627
|
+
this.list.setActive(this.options().indexOf(committed));
|
|
4628
|
+
this.scrollToActive();
|
|
4629
|
+
}
|
|
4630
|
+
scrollToActive() {
|
|
4631
|
+
this.scrollToIndex(this.list.activeIndex());
|
|
4632
|
+
}
|
|
4633
|
+
scrollToIndex(index) {
|
|
4634
|
+
if (index < 0)
|
|
4635
|
+
return;
|
|
4636
|
+
queueMicrotask(() => this.listRef()?.nativeElement.children[index]?.scrollIntoView?.({ block: 'nearest' }));
|
|
4637
|
+
}
|
|
4638
|
+
setFieldText(text) {
|
|
4639
|
+
const element = this.inputRef()?.nativeElement;
|
|
4640
|
+
if (element)
|
|
4641
|
+
element.value = text;
|
|
4642
|
+
}
|
|
4643
|
+
announce(message) {
|
|
4644
|
+
this.announcement.set(this.announcement() === message ? `${message} ` : message);
|
|
4645
|
+
}
|
|
4646
|
+
// --- Styling -----------------------------------------------------------------------
|
|
4647
|
+
className = computed(() => css({ display: 'block', position: 'relative' }), ...(ngDevMode ? [{ debugName: "className" }] : /* istanbul ignore next */ []));
|
|
4648
|
+
rowClass = computed(() => css({ display: 'flex', alignItems: 'center', flex: 1, width: '100%', minWidth: 0 }), ...(ngDevMode ? [{ debugName: "rowClass" }] : /* istanbul ignore next */ []));
|
|
4649
|
+
inputClass = computed(() => {
|
|
4650
|
+
const colors = this.theme.colorPalette();
|
|
4651
|
+
return css([
|
|
4652
|
+
{
|
|
4653
|
+
flex: 1,
|
|
4654
|
+
minWidth: 0,
|
|
4655
|
+
border: 0,
|
|
4656
|
+
outline: 'none',
|
|
4657
|
+
background: 'transparent',
|
|
4658
|
+
color: 'inherit',
|
|
4659
|
+
font: 'inherit',
|
|
4660
|
+
},
|
|
4661
|
+
this.draftInvalid() && {
|
|
4662
|
+
color: colors['warn'],
|
|
4663
|
+
// Shape and colour, not colour alone (WCAG 1.4.1).
|
|
4664
|
+
textDecoration: `underline dashed ${colors['warn']} 1.5px`,
|
|
4665
|
+
textUnderlineOffset: 3,
|
|
4666
|
+
},
|
|
4667
|
+
]);
|
|
4668
|
+
}, ...(ngDevMode ? [{ debugName: "inputClass" }] : /* istanbul ignore next */ []));
|
|
4669
|
+
toggleWrapClass = computed(() => css({ display: 'flex', alignItems: 'center', ...this.theme.paddingRight('xxs') }), ...(ngDevMode ? [{ debugName: "toggleWrapClass" }] : /* istanbul ignore next */ []));
|
|
4670
|
+
/** Embedded mode: the composer owns the box; keep only the flex row. */
|
|
4671
|
+
embeddedClass = computed(() => css({ display: 'flex', alignItems: 'center', flex: 1, minWidth: 0 }), ...(ngDevMode ? [{ debugName: "embeddedClass" }] : /* istanbul ignore next */ []));
|
|
4672
|
+
listClass = computed(() => {
|
|
4673
|
+
const options = this.componentOptions();
|
|
4674
|
+
return css({
|
|
4675
|
+
position: 'absolute',
|
|
4676
|
+
top: '100%',
|
|
4677
|
+
left: 0,
|
|
4678
|
+
right: 0,
|
|
4679
|
+
zIndex: 20,
|
|
4680
|
+
margin: '4px 0 0',
|
|
4681
|
+
padding: 4,
|
|
4682
|
+
listStyle: 'none',
|
|
4683
|
+
maxHeight: (options.maxVisibleOptions ?? 7) * 36,
|
|
4684
|
+
overflowY: 'auto',
|
|
4685
|
+
...this.theme.backgroundColor(options.listColor ?? 'primary-surface'),
|
|
4686
|
+
...this.theme.boxShadow(options.listShadow ?? 'menu'),
|
|
4687
|
+
...this.theme.radius(options.listBorderRadius ?? 'xs'),
|
|
4688
|
+
'& [role="option"]': {
|
|
4689
|
+
padding: '8px 12px',
|
|
4690
|
+
cursor: 'pointer',
|
|
4691
|
+
...this.theme.typeface('label'),
|
|
4692
|
+
...this.theme.color('on-primary-surface'),
|
|
4693
|
+
...this.theme.radius('xxs'),
|
|
4694
|
+
'&.active, &:hover': {
|
|
4695
|
+
...this.theme.backgroundColor('primary-container'),
|
|
4696
|
+
...this.theme.color('on-primary-container'),
|
|
4697
|
+
},
|
|
4698
|
+
},
|
|
4699
|
+
});
|
|
4700
|
+
}, ...(ngDevMode ? [{ debugName: "listClass" }] : /* istanbul ignore next */ []));
|
|
4701
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: UniTimeInputComponent, deps: null, target: i0.ɵɵFactoryTarget.Component });
|
|
4702
|
+
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 });
|
|
4703
|
+
}
|
|
4704
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: UniTimeInputComponent, decorators: [{
|
|
4705
|
+
type: Component,
|
|
4706
|
+
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" }]
|
|
4707
|
+
}], 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 }] }] } });
|
|
4708
|
+
|
|
4709
|
+
/**
|
|
4710
|
+
* One field for a date and a time: a thin composer seating a uni-date-input
|
|
4711
|
+
* and a uni-time-input in one input-box chrome under one label, yielding one
|
|
4712
|
+
* combined `'YYYY-MM-DDTHH:mm'` value. The value emits only when both parts
|
|
4713
|
+
* are set — a time without a day is not an answer — and clearing the date
|
|
4714
|
+
* clears it. With `slotsFor`, the time part stays disabled until a day is
|
|
4715
|
+
* chosen and offers exactly that day's slots: the scheduling flow in one
|
|
4716
|
+
* attribute. Two honest tab stops (it is two questions); apps needing a
|
|
4717
|
+
* different arrangement compose the primitives directly.
|
|
4718
|
+
*/
|
|
4719
|
+
class UniDateTimeInputComponent extends BaseComponent {
|
|
4720
|
+
// --- Signal Forms block (explicit per AGENTS.md, not a base class) --------
|
|
4721
|
+
value = model(...(ngDevMode ? [undefined, { debugName: "value" }] : /* istanbul ignore next */ []));
|
|
4722
|
+
disabled = input(false, ...(ngDevMode ? [{ debugName: "disabled" }] : /* istanbul ignore next */ []));
|
|
4723
|
+
touched = model(false, ...(ngDevMode ? [{ debugName: "touched" }] : /* istanbul ignore next */ []));
|
|
4724
|
+
invalid = input(false, ...(ngDevMode ? [{ debugName: "invalid" }] : /* istanbul ignore next */ []));
|
|
4725
|
+
dirty = input(false, ...(ngDevMode ? [{ debugName: "dirty" }] : /* istanbul ignore next */ []));
|
|
4726
|
+
required = input(false, ...(ngDevMode ? [{ debugName: "required" }] : /* istanbul ignore next */ []));
|
|
4727
|
+
ariaDescribedBy = input(...(ngDevMode ? [undefined, { debugName: "ariaDescribedBy" }] : /* istanbul ignore next */ []));
|
|
4728
|
+
// --- Configuration -------------------------------------------------------
|
|
4729
|
+
/** Names the group; the parts are announced as "Date" and "Time" under it. */
|
|
4730
|
+
label = input.required(...(ngDevMode ? [{ debugName: "label" }] : /* istanbul ignore next */ []));
|
|
4731
|
+
/** Earliest allowed moment; the date and time fences are split from it. */
|
|
4732
|
+
minDateTime = input(...(ngDevMode ? [undefined, { debugName: "minDateTime" }] : /* istanbul ignore next */ []));
|
|
4733
|
+
/** Latest allowed moment; the date and time fences are split from it. */
|
|
4734
|
+
maxDateTime = input(...(ngDevMode ? [undefined, { debugName: "maxDateTime" }] : /* istanbul ignore next */ []));
|
|
4735
|
+
// --- Forwarded wholesale ----------------------------------------------------
|
|
4736
|
+
disabledDates = input(...(ngDevMode ? [undefined, { debugName: "disabledDates" }] : /* istanbul ignore next */ []));
|
|
4737
|
+
markers = input([], ...(ngDevMode ? [{ debugName: "markers" }] : /* istanbul ignore next */ []));
|
|
4738
|
+
/** Fixed time choices; superseded per-day by `slotsFor` when that is set. */
|
|
4739
|
+
slots = input(...(ngDevMode ? [undefined, { debugName: "slots" }] : /* istanbul ignore next */ []));
|
|
4740
|
+
minuteStep = input(30, ...(ngDevMode ? [{ debugName: "minuteStep" }] : /* istanbul ignore next */ []));
|
|
4741
|
+
hour12 = input(...(ngDevMode ? [undefined, { debugName: "hour12" }] : /* istanbul ignore next */ []));
|
|
4742
|
+
weekStart = input(...(ngDevMode ? [undefined, { debugName: "weekStart" }] : /* istanbul ignore next */ []));
|
|
4743
|
+
locale = input(...(ngDevMode ? [undefined, { debugName: "locale" }] : /* istanbul ignore next */ []));
|
|
4744
|
+
/** Scheduling: the day's available times. Gates the time part on a date. */
|
|
4745
|
+
slotsFor = input(...(ngDevMode ? [undefined, { debugName: "slotsFor" }] : /* istanbul ignore next */ []));
|
|
4746
|
+
host = inject(ElementRef);
|
|
4747
|
+
/**
|
|
4748
|
+
* The two part-values. An external `value` write re-derives both; an
|
|
4749
|
+
* internal partial state (a date without a time round-trips through
|
|
4750
|
+
* `undefined`) must not be wiped by its own echo.
|
|
4751
|
+
*/
|
|
4752
|
+
parts = linkedSignal({ ...(ngDevMode ? { debugName: "parts" } : /* istanbul ignore next */ {}), source: this.value,
|
|
4753
|
+
computation: (value, previous) => {
|
|
4754
|
+
if (value)
|
|
4755
|
+
return splitDateTime(value);
|
|
4756
|
+
const kept = previous?.value;
|
|
4757
|
+
if (kept && joinDateTime(kept.date, kept.time) === undefined)
|
|
4758
|
+
return kept;
|
|
4759
|
+
return {};
|
|
4760
|
+
} });
|
|
4761
|
+
dateValue = computed(() => this.parts().date, ...(ngDevMode ? [{ debugName: "dateValue" }] : /* istanbul ignore next */ []));
|
|
4762
|
+
timeValue = computed(() => this.parts().time, ...(ngDevMode ? [{ debugName: "timeValue" }] : /* istanbul ignore next */ []));
|
|
4763
|
+
showError = computed(() => this.invalid() && (this.touched() || this.dirty()), ...(ngDevMode ? [{ debugName: "showError" }] : /* istanbul ignore next */ []));
|
|
4764
|
+
minParts = computed(() => splitDateTime(this.minDateTime()), ...(ngDevMode ? [{ debugName: "minParts" }] : /* istanbul ignore next */ []));
|
|
4765
|
+
maxParts = computed(() => splitDateTime(this.maxDateTime()), ...(ngDevMode ? [{ debugName: "maxParts" }] : /* istanbul ignore next */ []));
|
|
4766
|
+
dateMin = computed(() => this.minParts().date, ...(ngDevMode ? [{ debugName: "dateMin" }] : /* istanbul ignore next */ []));
|
|
4767
|
+
dateMax = computed(() => this.maxParts().date, ...(ngDevMode ? [{ debugName: "dateMax" }] : /* istanbul ignore next */ []));
|
|
4768
|
+
// The time fence applies only on the boundary date itself — 'after 9:00'
|
|
4769
|
+
// on the min date, any time on later days.
|
|
4770
|
+
timeMin = computed(() => {
|
|
4771
|
+
const { date, time } = this.minParts();
|
|
4772
|
+
return time && date && this.dateValue() === date ? time : undefined;
|
|
4773
|
+
}, ...(ngDevMode ? [{ debugName: "timeMin" }] : /* istanbul ignore next */ []));
|
|
4774
|
+
timeMax = computed(() => {
|
|
4775
|
+
const { date, time } = this.maxParts();
|
|
4776
|
+
return time && date && this.dateValue() === date ? time : undefined;
|
|
4777
|
+
}, ...(ngDevMode ? [{ debugName: "timeMax" }] : /* istanbul ignore next */ []));
|
|
4778
|
+
/** The chosen day's slots when `slotsFor` is set, else the fixed list. */
|
|
4779
|
+
effectiveSlots = computed(() => {
|
|
4780
|
+
const slotsFor = this.slotsFor();
|
|
4781
|
+
if (!slotsFor)
|
|
4782
|
+
return this.slots();
|
|
4783
|
+
const date = this.dateValue();
|
|
4784
|
+
return date ? slotsFor(date) : [];
|
|
4785
|
+
}, ...(ngDevMode ? [{ debugName: "effectiveSlots" }] : /* istanbul ignore next */ []));
|
|
4786
|
+
timeDisabled = computed(() => this.disabled() || (!!this.slotsFor() && !this.dateValue()), ...(ngDevMode ? [{ debugName: "timeDisabled" }] : /* istanbul ignore next */ []));
|
|
4787
|
+
// --- Part plumbing -----------------------------------------------------------
|
|
4788
|
+
onDatePartChange(date) {
|
|
4789
|
+
let time = this.parts().time;
|
|
4790
|
+
if (!date) {
|
|
4791
|
+
time = undefined; // clearing the date clears the combined value
|
|
4792
|
+
}
|
|
4793
|
+
else {
|
|
4794
|
+
const slotsFor = this.slotsFor();
|
|
4795
|
+
// Changing the day clears a slot that no longer exists.
|
|
4796
|
+
if (slotsFor && time && !slotsFor(date).includes(time))
|
|
4797
|
+
time = undefined;
|
|
4798
|
+
}
|
|
4799
|
+
this.setParts({ date, time });
|
|
4800
|
+
}
|
|
4801
|
+
onTimePartChange(time) {
|
|
4802
|
+
this.setParts({ date: this.parts().date, time });
|
|
4803
|
+
}
|
|
4804
|
+
setParts(parts) {
|
|
4805
|
+
this.parts.set(parts);
|
|
4806
|
+
this.value.set(joinDateTime(parts.date, parts.time));
|
|
4807
|
+
}
|
|
4808
|
+
onHostFocusOut(event) {
|
|
4809
|
+
const next = event.relatedTarget;
|
|
4810
|
+
if (!next || !this.host.nativeElement.contains(next))
|
|
4811
|
+
this.touched.set(true);
|
|
4812
|
+
}
|
|
4813
|
+
// --- Styling --------------------------------------------------------------------
|
|
4814
|
+
className = computed(() => css({ display: 'block' }), ...(ngDevMode ? [{ debugName: "className" }] : /* istanbul ignore next */ []));
|
|
4815
|
+
groupClass = computed(() => css({
|
|
4816
|
+
display: 'flex',
|
|
4817
|
+
alignItems: 'stretch',
|
|
4818
|
+
flex: 1,
|
|
4819
|
+
width: '100%',
|
|
4820
|
+
minWidth: 0,
|
|
4821
|
+
...this.theme.gap(this.componentOptions().partGap ?? 'sm'),
|
|
4822
|
+
}), ...(ngDevMode ? [{ debugName: "groupClass" }] : /* istanbul ignore next */ []));
|
|
4823
|
+
datePartClass = computed(() => css({ flex: 1.4, minWidth: 0, display: 'flex', '& > *': { flex: 1, minWidth: 0 } }), ...(ngDevMode ? [{ debugName: "datePartClass" }] : /* istanbul ignore next */ []));
|
|
4824
|
+
timePartClass = computed(() => css({ flex: 1, minWidth: 0, display: 'flex', '& > *': { flex: 1, minWidth: 0 } }), ...(ngDevMode ? [{ debugName: "timePartClass" }] : /* istanbul ignore next */ []));
|
|
4825
|
+
dividerClass = computed(() => {
|
|
4826
|
+
const colors = this.theme.colorPalette();
|
|
4827
|
+
return css({
|
|
4828
|
+
width: 1,
|
|
4829
|
+
alignSelf: 'stretch',
|
|
4830
|
+
flex: 'none',
|
|
4831
|
+
backgroundColor: colors[this.componentOptions().dividerColor ?? 'outline'],
|
|
4832
|
+
});
|
|
4833
|
+
}, ...(ngDevMode ? [{ debugName: "dividerClass" }] : /* istanbul ignore next */ []));
|
|
4834
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: UniDateTimeInputComponent, deps: null, target: i0.ɵɵFactoryTarget.Component });
|
|
4835
|
+
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 });
|
|
4836
|
+
}
|
|
4837
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: UniDateTimeInputComponent, decorators: [{
|
|
4838
|
+
type: Component,
|
|
4839
|
+
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" }]
|
|
4840
|
+
}], 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 }] }] } });
|
|
4841
|
+
|
|
3080
4842
|
class UniDialogComponent extends BaseComponent {
|
|
3081
4843
|
elem = inject(ElementRef);
|
|
3082
4844
|
/** Two-way bindable open state: [(show)]. */
|
|
@@ -3482,203 +5244,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImpo
|
|
|
3482
5244
|
}]
|
|
3483
5245
|
}], 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
5246
|
|
|
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
5247
|
class UniExpandComponent extends BaseComponent {
|
|
3683
5248
|
collapsed = model(true, ...(ngDevMode ? [{ debugName: "collapsed" }] : /* istanbul ignore next */ []));
|
|
3684
5249
|
/**
|
|
@@ -4169,57 +5734,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImpo
|
|
|
4169
5734
|
* This file exports all public-facing elements of the file-drop-zone component.
|
|
4170
5735
|
*/
|
|
4171
5736
|
|
|
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
5737
|
/**
|
|
4224
5738
|
* Text input that emits `change` only after the user pauses typing. Wears the
|
|
4225
5739
|
* shared input chrome (themed color, border, typeface, focus ring) via
|
|
@@ -4586,7 +6100,11 @@ class UniMenuComponent {
|
|
|
4586
6100
|
Enter/Space activation is dispatched from onMenuKeydown. -->
|
|
4587
6101
|
<!-- eslint-disable-next-line @angular-eslint/template/interactive-supports-focus -->
|
|
4588
6102
|
<div role="menu" [class]="menuClassName()" (keydown)="onMenuKeydown($event, dropdown)">
|
|
4589
|
-
|
|
6103
|
+
<!-- track $index: items carry no stable key (template items have no
|
|
6104
|
+
label, labels may repeat, dividers have neither) and consumers
|
|
6105
|
+
naturally rebuild the array each CD pass — identity tracking
|
|
6106
|
+
recreated every node and tripped NG0956. -->
|
|
6107
|
+
@for (item of menuItems(); track $index) {
|
|
4590
6108
|
@if (isDivider(item)) {
|
|
4591
6109
|
<div role="separator" [class]="dividerClassName()"></div>
|
|
4592
6110
|
} @else {
|
|
@@ -4642,7 +6160,11 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImpo
|
|
|
4642
6160
|
Enter/Space activation is dispatched from onMenuKeydown. -->
|
|
4643
6161
|
<!-- eslint-disable-next-line @angular-eslint/template/interactive-supports-focus -->
|
|
4644
6162
|
<div role="menu" [class]="menuClassName()" (keydown)="onMenuKeydown($event, dropdown)">
|
|
4645
|
-
|
|
6163
|
+
<!-- track $index: items carry no stable key (template items have no
|
|
6164
|
+
label, labels may repeat, dividers have neither) and consumers
|
|
6165
|
+
naturally rebuild the array each CD pass — identity tracking
|
|
6166
|
+
recreated every node and tripped NG0956. -->
|
|
6167
|
+
@for (item of menuItems(); track $index) {
|
|
4646
6168
|
@if (isDivider(item)) {
|
|
4647
6169
|
<div role="separator" [class]="dividerClassName()"></div>
|
|
4648
6170
|
} @else {
|
|
@@ -4710,11 +6232,11 @@ class UniMultiSelectComponent {
|
|
|
4710
6232
|
width: '100%',
|
|
4711
6233
|
});
|
|
4712
6234
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: UniMultiSelectComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
4713
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.12", type: UniMultiSelectComponent, isStandalone: true, selector: "uni-multi-select", inputs: { options: { classPropertyName: "options", publicName: "options", isSignal: true, isRequired: false, transformFunction: null }, selections: { classPropertyName: "selections", publicName: "selections", isSignal: true, isRequired: false, transformFunction: null }, flexDirection: { classPropertyName: "flexDirection", publicName: "flexDirection", isSignal: true, isRequired: false, transformFunction: null }, checkboxGap: { classPropertyName: "checkboxGap", publicName: "checkboxGap", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { updates: "updates" }, host: { properties: { "class": "className" } }, ngImport: i0, template: "<div box-layout display=\"flex\" [flexDirection]=\"flexDirection()\" [gap]=\"checkboxGap()\">\n @for (option of options(); track
|
|
6235
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.12", type: UniMultiSelectComponent, isStandalone: true, selector: "uni-multi-select", inputs: { options: { classPropertyName: "options", publicName: "options", isSignal: true, isRequired: false, transformFunction: null }, selections: { classPropertyName: "selections", publicName: "selections", isSignal: true, isRequired: false, transformFunction: null }, flexDirection: { classPropertyName: "flexDirection", publicName: "flexDirection", isSignal: true, isRequired: false, transformFunction: null }, checkboxGap: { classPropertyName: "checkboxGap", publicName: "checkboxGap", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { updates: "updates" }, host: { properties: { "class": "className" } }, ngImport: i0, template: "<div box-layout display=\"flex\" [flexDirection]=\"flexDirection()\" [gap]=\"checkboxGap()\">\n <!-- track $index: option values may be objects and consumers rebuild the\n array, so identity tracking recreated every node (NG0956). -->\n @for (option of options(); track $index) {\n <div [class]=\"optionWrapper\">\n <uni-checkbox\n [label]=\"option.label\"\n [checked]=\"values().indexOf(option.value) > -1\"\n (checkedChange)=\"handleCheck($event, option.value)\"\n variant=\"primary\"\n >\n </uni-checkbox>\n </div>\n }\n</div>\n", dependencies: [{ kind: "component", type: UniCheckboxComponent, selector: "uni-checkbox", inputs: ["checked", "disabled", "touched", "invalid", "dirty", "required", "ariaDescribedBy", "label", "indeterminate"], outputs: ["checkedChange", "touchedChange", "indeterminateChange"] }, { 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 });
|
|
4714
6236
|
}
|
|
4715
6237
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: UniMultiSelectComponent, decorators: [{
|
|
4716
6238
|
type: Component,
|
|
4717
|
-
args: [{ selector: 'uni-multi-select', imports: [UniCheckboxComponent, UniBoxComponent], host: { '[class]': 'className' }, changeDetection: ChangeDetectionStrategy.OnPush, template: "<div box-layout display=\"flex\" [flexDirection]=\"flexDirection()\" [gap]=\"checkboxGap()\">\n @for (option of options(); track
|
|
6239
|
+
args: [{ selector: 'uni-multi-select', imports: [UniCheckboxComponent, UniBoxComponent], host: { '[class]': 'className' }, changeDetection: ChangeDetectionStrategy.OnPush, template: "<div box-layout display=\"flex\" [flexDirection]=\"flexDirection()\" [gap]=\"checkboxGap()\">\n <!-- track $index: option values may be objects and consumers rebuild the\n array, so identity tracking recreated every node (NG0956). -->\n @for (option of options(); track $index) {\n <div [class]=\"optionWrapper\">\n <uni-checkbox\n [label]=\"option.label\"\n [checked]=\"values().indexOf(option.value) > -1\"\n (checkedChange)=\"handleCheck($event, option.value)\"\n variant=\"primary\"\n >\n </uni-checkbox>\n </div>\n }\n</div>\n" }]
|
|
4718
6240
|
}], propDecorators: { options: [{ type: i0.Input, args: [{ isSignal: true, alias: "options", required: false }] }], selections: [{ type: i0.Input, args: [{ isSignal: true, alias: "selections", required: false }] }], updates: [{ type: i0.Output, args: ["updates"] }], flexDirection: [{ type: i0.Input, args: [{ isSignal: true, alias: "flexDirection", required: false }] }], checkboxGap: [{ type: i0.Input, args: [{ isSignal: true, alias: "checkboxGap", required: false }] }] } });
|
|
4719
6241
|
|
|
4720
6242
|
class UniMultiSelectDropdownComponent extends BaseComponent {
|
|
@@ -4928,7 +6450,9 @@ class UniNotificationBadgeComponent extends BaseComponent {
|
|
|
4928
6450
|
const variant = this.badgeVariant();
|
|
4929
6451
|
const color = this.color();
|
|
4930
6452
|
const position = this.position();
|
|
4931
|
-
|
|
6453
|
+
// A theme may omit `offset`; without the fallback this concatenates to
|
|
6454
|
+
// the invalid length 'undefinedpx' and the badge loses its position.
|
|
6455
|
+
const offset = (this.componentOptions().offset ?? 0) + 'px';
|
|
4932
6456
|
const positionStyles = {
|
|
4933
6457
|
'top-right': { top: offset, right: offset },
|
|
4934
6458
|
'top-left': { top: offset, left: offset },
|
|
@@ -5533,6 +7057,13 @@ class UniRadioComponent extends BaseComponent {
|
|
|
5533
7057
|
});
|
|
5534
7058
|
radioOptionClass = computed(() => {
|
|
5535
7059
|
const { outerCircleSize, innerCircleSize, innerCircleOffset } = this.metrics();
|
|
7060
|
+
// The dot's grow/retract is a token: 0.3s default, 0 = instant. The
|
|
7061
|
+
// transitions are scoped — never `all` — so the focus ring's outline and
|
|
7062
|
+
// shadow apply instantly instead of interpolating from a stale outline
|
|
7063
|
+
// color, which flashed a dark ring before the themed ring color landed.
|
|
7064
|
+
const speed = this.componentOptions().transitionSpeed ?? 0.3;
|
|
7065
|
+
const ringTransition = `border-color ${speed}s ease, background-color ${speed}s ease`;
|
|
7066
|
+
const dotTransition = `transform ${speed}s ease`;
|
|
5536
7067
|
return css({
|
|
5537
7068
|
userSelect: 'none',
|
|
5538
7069
|
cursor: this.disabled() ? 'not-allowed' : 'pointer',
|
|
@@ -5548,7 +7079,7 @@ class UniRadioComponent extends BaseComponent {
|
|
|
5548
7079
|
? this.getThemeColor('on-disabled')
|
|
5549
7080
|
: this.getThemeColor(this.componentOptions().ringColor ?? 'outline')}`,
|
|
5550
7081
|
position: 'relative',
|
|
5551
|
-
transition:
|
|
7082
|
+
transition: ringTransition,
|
|
5552
7083
|
backgroundColor: this.getThemeColor(this.componentOptions().fillColor ?? 'surface'),
|
|
5553
7084
|
flexShrink: 0,
|
|
5554
7085
|
},
|
|
@@ -5561,7 +7092,7 @@ class UniRadioComponent extends BaseComponent {
|
|
|
5561
7092
|
top: innerCircleOffset,
|
|
5562
7093
|
left: innerCircleOffset,
|
|
5563
7094
|
transform: 'scale(0)',
|
|
5564
|
-
transition:
|
|
7095
|
+
transition: dotTransition,
|
|
5565
7096
|
},
|
|
5566
7097
|
'&:hover .radio-button': this.disabled()
|
|
5567
7098
|
? {}
|
|
@@ -5589,9 +7120,9 @@ class UniRadioComponent extends BaseComponent {
|
|
|
5589
7120
|
'&:checked + .radio-button .radio-inner': {
|
|
5590
7121
|
transform: 'scale(1)',
|
|
5591
7122
|
},
|
|
7123
|
+
// The shared, themable focus indicator, keyed off the hidden input.
|
|
5592
7124
|
'&:focus + .radio-button': {
|
|
5593
|
-
|
|
5594
|
-
outlineOffset: '2px',
|
|
7125
|
+
...this.theme.focusRingStyle(this.getThemeColor(this.variant())),
|
|
5595
7126
|
},
|
|
5596
7127
|
}), ...(ngDevMode ? [{ debugName: "radioInputClass" }] : /* istanbul ignore next */ []));
|
|
5597
7128
|
handleRadioChange(optionValue) {
|
|
@@ -5765,6 +7296,14 @@ class UniSelectComponent {
|
|
|
5765
7296
|
// --- CONFIGURATION ---
|
|
5766
7297
|
options = input([], ...(ngDevMode ? [{ debugName: "options" }] : /* istanbul ignore next */ []));
|
|
5767
7298
|
placeholder = input(...(ngDevMode ? [undefined, { debugName: "placeholder" }] : /* istanbul ignore next */ []));
|
|
7299
|
+
/**
|
|
7300
|
+
* Equality used to match `value` against option values, called as
|
|
7301
|
+
* `compareWith(optionValue, value)`. Defaults to reference equality, which
|
|
7302
|
+
* never matches an object value rebuilt from elsewhere (e.g. a saved record
|
|
7303
|
+
* against options from a fresh fetch) — pass a key comparison like
|
|
7304
|
+
* `(a, b) => a?.id === b?.id` for object values.
|
|
7305
|
+
*/
|
|
7306
|
+
compareWith = input((a, b) => a === b, ...(ngDevMode ? [{ debugName: "compareWith" }] : /* istanbul ignore next */ []));
|
|
5768
7307
|
/** Accessible name for the select; a placeholder is not a label. */
|
|
5769
7308
|
ariaLabel = input(...(ngDevMode ? [undefined, { debugName: "ariaLabel" }] : /* istanbul ignore next */ []));
|
|
5770
7309
|
UNSELECTED = -1;
|
|
@@ -5777,7 +7316,7 @@ class UniSelectComponent {
|
|
|
5777
7316
|
if (currentVal === null || currentVal === undefined)
|
|
5778
7317
|
return this.UNSELECTED;
|
|
5779
7318
|
// Find the index of the option that contains our current value
|
|
5780
|
-
const index = this.options().findIndex((opt) => opt.value
|
|
7319
|
+
const index = this.options().findIndex((opt) => this.compareWith()(opt.value, currentVal));
|
|
5781
7320
|
return index.toString();
|
|
5782
7321
|
}, ...(ngDevMode ? [{ debugName: "currentSelectedIndex" }] : /* istanbul ignore next */ []));
|
|
5783
7322
|
showError = computed(() => this.invalid() && (this.touched() || this.dirty()), ...(ngDevMode ? [{ debugName: "showError" }] : /* istanbul ignore next */ []));
|
|
@@ -5809,12 +7348,12 @@ class UniSelectComponent {
|
|
|
5809
7348
|
pointerEvents: 'none' /* Crucial for clicking through */,
|
|
5810
7349
|
});
|
|
5811
7350
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: UniSelectComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
5812
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.12", type: UniSelectComponent, isStandalone: true, selector: "uni-select", 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 }, options: { classPropertyName: "options", publicName: "options", isSignal: true, isRequired: false, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { value: "valueChange", touched: "touchedChange" }, ngImport: i0, template: "<uni-input-box [error]=\"showError()\" [class]=\"selectClass\">\n <select\n [disabled]=\"disabled()\"\n [value]=\"currentSelectedIndex()\"\n (change)=\"handleSelectChange($event)\"\n (blur)=\"touched.set(true)\"\n [attr.aria-label]=\"ariaLabel()\"\n [attr.aria-invalid]=\"showError() ? true : null\"\n [attr.aria-required]=\"required() ? true : null\"\n [attr.aria-describedby]=\"ariaDescribedBy() || null\"\n >\n @if (placeholder()) {\n <option [value]=\"UNSELECTED\" disabled selected>\n {{ placeholder() }}\n </option>\n }\n\n @for (opt of options(); track opt.label; let i = $index) {\n <!-- [selected] evaluates during the option's own render, so a selection\n pointing at an option added in the same change-detection pass still\n applies (the select-level [value] write lands before new options\n exist and is ignored by the browser). -->\n <option [value]=\"i\" [selected]=\"currentSelectedIndex() === i.toString()\">\n {{ opt.label }}\n </option>\n }\n </select>\n <uni-symbol name=\"keyboard_arrow_down\" [class]=\"arrowClass\" />\n</uni-input-box>\n", dependencies: [{ kind: "component", type: UniInputBoxComponent, selector: "uni-input-box", inputs: ["disabled", "error", "minWidth", "height"] }, { kind: "component", type: UniSymbolComponent, selector: "uni-symbol", inputs: ["name", "fill", "weight", "grade", "opticalSize"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
7351
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.12", type: UniSelectComponent, isStandalone: true, selector: "uni-select", 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 }, options: { classPropertyName: "options", publicName: "options", isSignal: true, isRequired: false, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, compareWith: { classPropertyName: "compareWith", publicName: "compareWith", isSignal: true, isRequired: false, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { value: "valueChange", touched: "touchedChange" }, ngImport: i0, template: "<uni-input-box [error]=\"showError()\" [class]=\"selectClass\">\n <select\n [disabled]=\"disabled()\"\n [value]=\"currentSelectedIndex()\"\n (change)=\"handleSelectChange($event)\"\n (blur)=\"touched.set(true)\"\n [attr.aria-label]=\"ariaLabel()\"\n [attr.aria-invalid]=\"showError() ? true : null\"\n [attr.aria-required]=\"required() ? true : null\"\n [attr.aria-describedby]=\"ariaDescribedBy() || null\"\n >\n @if (placeholder()) {\n <option [value]=\"UNSELECTED\" disabled selected>\n {{ placeholder() }}\n </option>\n }\n\n @for (opt of options(); track opt.label; let i = $index) {\n <!-- [selected] evaluates during the option's own render, so a selection\n pointing at an option added in the same change-detection pass still\n applies (the select-level [value] write lands before new options\n exist and is ignored by the browser). -->\n <option [value]=\"i\" [selected]=\"currentSelectedIndex() === i.toString()\">\n {{ opt.label }}\n </option>\n }\n </select>\n <uni-symbol name=\"keyboard_arrow_down\" [class]=\"arrowClass\" />\n</uni-input-box>\n", dependencies: [{ kind: "component", type: UniInputBoxComponent, selector: "uni-input-box", inputs: ["disabled", "error", "minWidth", "height"] }, { kind: "component", type: UniSymbolComponent, selector: "uni-symbol", inputs: ["name", "fill", "weight", "grade", "opticalSize"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
5813
7352
|
}
|
|
5814
7353
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: UniSelectComponent, decorators: [{
|
|
5815
7354
|
type: Component,
|
|
5816
7355
|
args: [{ changeDetection: ChangeDetectionStrategy.OnPush, selector: 'uni-select', imports: [UniInputBoxComponent, UniSymbolComponent], template: "<uni-input-box [error]=\"showError()\" [class]=\"selectClass\">\n <select\n [disabled]=\"disabled()\"\n [value]=\"currentSelectedIndex()\"\n (change)=\"handleSelectChange($event)\"\n (blur)=\"touched.set(true)\"\n [attr.aria-label]=\"ariaLabel()\"\n [attr.aria-invalid]=\"showError() ? true : null\"\n [attr.aria-required]=\"required() ? true : null\"\n [attr.aria-describedby]=\"ariaDescribedBy() || null\"\n >\n @if (placeholder()) {\n <option [value]=\"UNSELECTED\" disabled selected>\n {{ placeholder() }}\n </option>\n }\n\n @for (opt of options(); track opt.label; let i = $index) {\n <!-- [selected] evaluates during the option's own render, so a selection\n pointing at an option added in the same change-detection pass still\n applies (the select-level [value] write lands before new options\n exist and is ignored by the browser). -->\n <option [value]=\"i\" [selected]=\"currentSelectedIndex() === i.toString()\">\n {{ opt.label }}\n </option>\n }\n </select>\n <uni-symbol name=\"keyboard_arrow_down\" [class]=\"arrowClass\" />\n</uni-input-box>\n" }]
|
|
5817
|
-
}], 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 }] }], options: [{ type: i0.Input, args: [{ isSignal: true, alias: "options", required: false }] }], placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }] } });
|
|
7356
|
+
}], 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 }] }], options: [{ type: i0.Input, args: [{ isSignal: true, alias: "options", required: false }] }], placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], compareWith: [{ type: i0.Input, args: [{ isSignal: true, alias: "compareWith", required: false }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }] } });
|
|
5818
7357
|
|
|
5819
7358
|
/**
|
|
5820
7359
|
* Loading placeholder painted with surface tokens. `text` renders one or more
|
|
@@ -5978,7 +7517,8 @@ class UniSliderComponent extends BaseComponent {
|
|
|
5978
7517
|
'&::-moz-range-track': { height: trackHeight, borderRadius: radius, background: track },
|
|
5979
7518
|
'&::-moz-range-progress': { height: trackHeight, borderRadius: radius, background: fill },
|
|
5980
7519
|
'&::-moz-range-thumb': thumb,
|
|
5981
|
-
|
|
7520
|
+
// The shared, themable focus indicator, in the track's fill color.
|
|
7521
|
+
'&:focus-visible': { ...this.theme.focusRingStyle(fill) },
|
|
5982
7522
|
'&:disabled': {
|
|
5983
7523
|
cursor: 'not-allowed',
|
|
5984
7524
|
opacity: 0.5,
|
|
@@ -7963,7 +9503,7 @@ class UniThemeSwitchComponent {
|
|
|
7963
9503
|
(valueChange)="select($event)"
|
|
7964
9504
|
[ariaLabel]="ariaLabel()"
|
|
7965
9505
|
/>
|
|
7966
|
-
`, isInline: true, dependencies: [{ kind: "component", type: UniSelectComponent, selector: "uni-select", inputs: ["value", "disabled", "touched", "invalid", "dirty", "required", "ariaDescribedBy", "options", "placeholder", "ariaLabel"], outputs: ["valueChange", "touchedChange"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
9506
|
+
`, isInline: true, dependencies: [{ kind: "component", type: UniSelectComponent, selector: "uni-select", inputs: ["value", "disabled", "touched", "invalid", "dirty", "required", "ariaDescribedBy", "options", "placeholder", "compareWith", "ariaLabel"], outputs: ["valueChange", "touchedChange"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
7967
9507
|
}
|
|
7968
9508
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: UniThemeSwitchComponent, decorators: [{
|
|
7969
9509
|
type: Component,
|
|
@@ -8035,7 +9575,9 @@ class UniToggleComponent extends BaseComponent {
|
|
|
8035
9575
|
: this.getThemeColor(this.componentOptions().trackColor ?? 'surface-variant'),
|
|
8036
9576
|
borderRadius: toggleSize / 2,
|
|
8037
9577
|
position: 'relative',
|
|
8038
|
-
|
|
9578
|
+
// Scoped, never `all`: the focus ring must apply instantly rather
|
|
9579
|
+
// than interpolating its outline color from a stale value.
|
|
9580
|
+
transition: 'background-color 0.3s ease, border-color 0.3s ease',
|
|
8039
9581
|
},
|
|
8040
9582
|
'& .toggle-slider': {
|
|
8041
9583
|
width: sliderSize,
|
|
@@ -8045,7 +9587,7 @@ class UniToggleComponent extends BaseComponent {
|
|
|
8045
9587
|
position: 'absolute',
|
|
8046
9588
|
top: sliderOffset,
|
|
8047
9589
|
left: sliderOffset,
|
|
8048
|
-
transition: '
|
|
9590
|
+
transition: 'transform 0.3s ease, background-color 0.3s ease',
|
|
8049
9591
|
...this.theme.boxShadow('raised'),
|
|
8050
9592
|
},
|
|
8051
9593
|
// Hover darkens whatever the token resolves to — the button convention.
|
|
@@ -8074,9 +9616,9 @@ class UniToggleComponent extends BaseComponent {
|
|
|
8074
9616
|
'&:disabled + .toggle-switch': {
|
|
8075
9617
|
cursor: 'not-allowed',
|
|
8076
9618
|
},
|
|
9619
|
+
// The shared, themable focus indicator, keyed off the hidden input.
|
|
8077
9620
|
'&:focus + .toggle-switch': {
|
|
8078
|
-
|
|
8079
|
-
outlineOffset: '2px',
|
|
9621
|
+
...this.theme.focusRingStyle(this.getThemeColor(this.variant())),
|
|
8080
9622
|
},
|
|
8081
9623
|
});
|
|
8082
9624
|
}, ...(ngDevMode ? [{ debugName: "toggleInput" }] : /* istanbul ignore next */ []));
|
|
@@ -8298,5 +9840,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImpo
|
|
|
8298
9840
|
* Generated bundle index. Do not edit.
|
|
8299
9841
|
*/
|
|
8300
9842
|
|
|
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 };
|
|
9843
|
+
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
9844
|
//# sourceMappingURL=uni-design-system-uni-angular.mjs.map
|