@api-client/ui 0.5.5 → 0.5.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.cursor/rules/html-and-css-best-practices.mdc +63 -0
- package/.cursor/rules/lit-best-practices.mdc +78 -0
- package/.github/instructions/html-and-css-best-practices.instructions.md +70 -0
- package/.github/instructions/lit-best-practices.instructions.md +86 -0
- package/build/src/elements/currency/currency-picker.d.ts +10 -0
- package/build/src/elements/currency/currency-picker.d.ts.map +1 -0
- package/build/src/elements/currency/currency-picker.js +27 -0
- package/build/src/elements/currency/currency-picker.js.map +1 -0
- package/build/src/elements/currency/internals/Picker.d.ts +311 -0
- package/build/src/elements/currency/internals/Picker.d.ts.map +1 -0
- package/build/src/elements/currency/internals/Picker.js +857 -0
- package/build/src/elements/currency/internals/Picker.js.map +1 -0
- package/build/src/elements/currency/internals/Picker.styles.d.ts +3 -0
- package/build/src/elements/currency/internals/Picker.styles.d.ts.map +1 -0
- package/build/src/elements/currency/internals/Picker.styles.js +58 -0
- package/build/src/elements/currency/internals/Picker.styles.js.map +1 -0
- package/build/src/elements/highlight/MarkdownStyles.d.ts.map +1 -1
- package/build/src/elements/highlight/MarkdownStyles.js +0 -13
- package/build/src/elements/highlight/MarkdownStyles.js.map +1 -1
- package/build/src/elements/http/BodyEditor.d.ts +0 -13
- package/build/src/elements/http/BodyEditor.d.ts.map +1 -1
- package/build/src/elements/http/BodyEditor.js +0 -13
- package/build/src/elements/http/BodyEditor.js.map +1 -1
- package/build/src/elements/http/BodyTextEditor.d.ts +0 -13
- package/build/src/elements/http/BodyTextEditor.d.ts.map +1 -1
- package/build/src/elements/http/BodyTextEditor.js +0 -13
- package/build/src/elements/http/BodyTextEditor.js.map +1 -1
- package/build/src/elements/http/BodyUrlEncodedEditor.d.ts +0 -13
- package/build/src/elements/http/BodyUrlEncodedEditor.d.ts.map +1 -1
- package/build/src/elements/http/BodyUrlEncodedEditor.js +0 -13
- package/build/src/elements/http/BodyUrlEncodedEditor.js.map +1 -1
- package/build/src/elements/http/UrlInput.d.ts +0 -13
- package/build/src/elements/http/UrlInput.d.ts.map +1 -1
- package/build/src/elements/http/UrlInput.js +0 -13
- package/build/src/elements/http/UrlInput.js.map +1 -1
- package/build/src/index.d.ts +2 -0
- package/build/src/index.d.ts.map +1 -1
- package/build/src/index.js +2 -0
- package/build/src/index.js.map +1 -1
- package/build/src/md/button/internals/base.d.ts +1 -0
- package/build/src/md/button/internals/base.d.ts.map +1 -1
- package/build/src/md/button/internals/base.js +7 -0
- package/build/src/md/button/internals/base.js.map +1 -1
- package/build/src/md/button/internals/button.styles.js +1 -1
- package/build/src/md/button/internals/button.styles.js.map +1 -1
- package/build/src/md/date/internals/DateTime.d.ts +0 -13
- package/build/src/md/date/internals/DateTime.d.ts.map +1 -1
- package/build/src/md/date/internals/DateTime.js +0 -13
- package/build/src/md/date/internals/DateTime.js.map +1 -1
- package/build/src/md/date-picker/index.d.ts +13 -0
- package/build/src/md/date-picker/index.d.ts.map +1 -0
- package/build/src/md/date-picker/index.js +13 -0
- package/build/src/md/date-picker/index.js.map +1 -0
- package/build/src/md/date-picker/internals/DatePicker.styles.d.ts +4 -0
- package/build/src/md/date-picker/internals/DatePicker.styles.d.ts.map +1 -0
- package/build/src/md/date-picker/internals/DatePicker.styles.js +409 -0
- package/build/src/md/date-picker/internals/DatePicker.styles.js.map +1 -0
- package/build/src/md/date-picker/internals/DatePickerCalendar.d.ts +272 -0
- package/build/src/md/date-picker/internals/DatePickerCalendar.d.ts.map +1 -0
- package/build/src/md/date-picker/internals/DatePickerCalendar.js +1062 -0
- package/build/src/md/date-picker/internals/DatePickerCalendar.js.map +1 -0
- package/build/src/md/date-picker/internals/DatePickerUtils.d.ts +93 -0
- package/build/src/md/date-picker/internals/DatePickerUtils.d.ts.map +1 -0
- package/build/src/md/date-picker/internals/DatePickerUtils.js +221 -0
- package/build/src/md/date-picker/internals/DatePickerUtils.js.map +1 -0
- package/build/src/md/date-picker/ui-date-picker-input.d.ts +160 -0
- package/build/src/md/date-picker/ui-date-picker-input.d.ts.map +1 -0
- package/build/src/md/date-picker/ui-date-picker-input.js +464 -0
- package/build/src/md/date-picker/ui-date-picker-input.js.map +1 -0
- package/build/src/md/date-picker/ui-date-picker-modal-input.d.ts +178 -0
- package/build/src/md/date-picker/ui-date-picker-modal-input.d.ts.map +1 -0
- package/build/src/md/date-picker/ui-date-picker-modal-input.js +538 -0
- package/build/src/md/date-picker/ui-date-picker-modal-input.js.map +1 -0
- package/build/src/md/date-picker/ui-date-picker-modal.d.ts +156 -0
- package/build/src/md/date-picker/ui-date-picker-modal.d.ts.map +1 -0
- package/build/src/md/date-picker/ui-date-picker-modal.js +423 -0
- package/build/src/md/date-picker/ui-date-picker-modal.js.map +1 -0
- package/build/src/md/dialog/internals/Dialog.styles.d.ts.map +1 -1
- package/build/src/md/dialog/internals/Dialog.styles.js +1 -0
- package/build/src/md/dialog/internals/Dialog.styles.js.map +1 -1
- package/demo/elements/currency/index.html +91 -0
- package/demo/elements/currency/index.ts +272 -0
- package/demo/elements/har/har2.json +1 -1
- package/demo/elements/index.html +3 -0
- package/demo/md/date-picker/date-picker.ts +336 -0
- package/demo/md/date-picker/index.html +171 -0
- package/demo/md/index.html +2 -0
- package/package.json +1 -1
- package/src/elements/currency/currency-picker.ts +14 -0
- package/src/elements/currency/internals/Picker.styles.ts +58 -0
- package/src/elements/currency/internals/Picker.ts +846 -0
- package/src/elements/highlight/MarkdownStyles.ts +0 -13
- package/src/elements/http/BodyEditor.ts +0 -13
- package/src/elements/http/BodyTextEditor.ts +0 -13
- package/src/elements/http/BodyUrlEncodedEditor.ts +0 -13
- package/src/elements/http/UrlInput.ts +0 -13
- package/src/index.ts +17 -0
- package/src/md/button/internals/base.ts +7 -0
- package/src/md/button/internals/button.styles.ts +1 -1
- package/src/md/date/internals/DateTime.ts +0 -14
- package/src/md/date-picker/README.md +184 -0
- package/src/md/date-picker/index.ts +17 -0
- package/src/md/date-picker/internals/DatePicker.styles.ts +411 -0
- package/src/md/date-picker/internals/DatePickerCalendar.ts +1031 -0
- package/src/md/date-picker/internals/DatePickerUtils.ts +288 -0
- package/src/md/date-picker/ui-date-picker-input.ts +333 -0
- package/src/md/date-picker/ui-date-picker-modal-input.ts +440 -0
- package/src/md/date-picker/ui-date-picker-modal.ts +346 -0
- package/src/md/dialog/internals/Dialog.styles.ts +1 -0
- package/test/README.md +3 -2
- package/test/elements/currency/CurrencyPicker.accessibility.test.ts +328 -0
- package/test/elements/currency/CurrencyPicker.core.test.ts +318 -0
- package/test/elements/currency/CurrencyPicker.integration.test.ts +482 -0
- package/test/elements/currency/CurrencyPicker.test.ts +486 -0
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
export interface DateRange {
|
|
2
|
+
start: Date | null
|
|
3
|
+
end: Date | null
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export interface CalendarDay {
|
|
7
|
+
date: Date
|
|
8
|
+
isCurrentMonth: boolean
|
|
9
|
+
isToday: boolean
|
|
10
|
+
isSelected: boolean
|
|
11
|
+
isInRange: boolean
|
|
12
|
+
isRangeStart: boolean
|
|
13
|
+
isRangeEnd: boolean
|
|
14
|
+
isDisabled: boolean
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface CalendarMonth {
|
|
18
|
+
year: number
|
|
19
|
+
month: number // 0-indexed (0 = January)
|
|
20
|
+
days: CalendarDay[]
|
|
21
|
+
weekdays: string[]
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Get the current date with time set to midnight
|
|
26
|
+
*/
|
|
27
|
+
export function today(): Date {
|
|
28
|
+
const now = new Date()
|
|
29
|
+
return new Date(now.getFullYear(), now.getMonth(), now.getDate())
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Create a new date with time set to midnight
|
|
34
|
+
*/
|
|
35
|
+
export function normalizeDate(date: Date): Date {
|
|
36
|
+
return new Date(date.getFullYear(), date.getMonth(), date.getDate())
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Check if two dates are the same day
|
|
41
|
+
*/
|
|
42
|
+
export function isSameDay(date1: Date, date2: Date): boolean {
|
|
43
|
+
return (
|
|
44
|
+
date1.getFullYear() === date2.getFullYear() &&
|
|
45
|
+
date1.getMonth() === date2.getMonth() &&
|
|
46
|
+
date1.getDate() === date2.getDate()
|
|
47
|
+
)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Check if a date is between two other dates (inclusive)
|
|
52
|
+
*/
|
|
53
|
+
export function isBetween(date: Date, start: Date, end: Date): boolean {
|
|
54
|
+
const normalizedDate = normalizeDate(date)
|
|
55
|
+
const normalizedStart = normalizeDate(start)
|
|
56
|
+
const normalizedEnd = normalizeDate(end)
|
|
57
|
+
return normalizedDate >= normalizedStart && normalizedDate <= normalizedEnd
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Add months to a date
|
|
62
|
+
*/
|
|
63
|
+
export function addMonths(date: Date, months: number): Date {
|
|
64
|
+
const result = new Date(date)
|
|
65
|
+
result.setMonth(result.getMonth() + months)
|
|
66
|
+
return result
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Add days to a date
|
|
71
|
+
*/
|
|
72
|
+
export function addDays(date: Date, days: number): Date {
|
|
73
|
+
const result = new Date(date)
|
|
74
|
+
result.setDate(result.getDate() + days)
|
|
75
|
+
return result
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Get the first day of the month
|
|
80
|
+
*/
|
|
81
|
+
export function getFirstDayOfMonth(year: number, month: number): Date {
|
|
82
|
+
return new Date(year, month, 1)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Get the last day of the month
|
|
87
|
+
*/
|
|
88
|
+
export function getLastDayOfMonth(year: number, month: number): Date {
|
|
89
|
+
return new Date(year, month + 1, 0)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Get the number of days in a month
|
|
94
|
+
*/
|
|
95
|
+
export function getDaysInMonth(year: number, month: number): number {
|
|
96
|
+
return new Date(year, month + 1, 0).getDate()
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Get weekday names for the current locale
|
|
101
|
+
*/
|
|
102
|
+
export function getWeekdayNames(locale?: string): string[] {
|
|
103
|
+
const formatter = new Intl.DateTimeFormat(locale, { weekday: 'short' })
|
|
104
|
+
const days: string[] = []
|
|
105
|
+
|
|
106
|
+
// Start from Sunday and get all 7 days
|
|
107
|
+
const date = new Date(2024, 0, 7) // January 7, 2024 is a Sunday
|
|
108
|
+
for (let i = 0; i < 7; i++) {
|
|
109
|
+
days.push(formatter.format(new Date(date.getTime() + i * 24 * 60 * 60 * 1000)))
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return days
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Get month names for the current locale
|
|
117
|
+
*/
|
|
118
|
+
export function getMonthNames(locale?: string): string[] {
|
|
119
|
+
const formatter = new Intl.DateTimeFormat(locale, { month: 'short' })
|
|
120
|
+
const months: string[] = []
|
|
121
|
+
|
|
122
|
+
for (let i = 0; i < 12; i++) {
|
|
123
|
+
months.push(formatter.format(new Date(2024, i, 1)))
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return months
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Format a date according to locale
|
|
131
|
+
*/
|
|
132
|
+
export function formatDate(date: Date, locale?: string, options?: Intl.DateTimeFormatOptions): string {
|
|
133
|
+
const defaultOptions: Intl.DateTimeFormatOptions = {
|
|
134
|
+
year: 'numeric',
|
|
135
|
+
month: 'short',
|
|
136
|
+
day: 'numeric',
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return new Intl.DateTimeFormat(locale, { ...defaultOptions, ...options }).format(date)
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Parse a date string in various formats
|
|
144
|
+
*/
|
|
145
|
+
export function parseDate(dateString: string): Date | null {
|
|
146
|
+
if (!dateString) return null
|
|
147
|
+
|
|
148
|
+
// Try ISO format first
|
|
149
|
+
const isoDate = new Date(dateString)
|
|
150
|
+
if (!isNaN(isoDate.getTime())) {
|
|
151
|
+
return normalizeDate(isoDate)
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Try other common formats
|
|
155
|
+
const formats = [
|
|
156
|
+
/^(\d{1,2})\/(\d{1,2})\/(\d{4})$/, // MM/DD/YYYY or M/D/YYYY
|
|
157
|
+
/^(\d{1,2})-(\d{1,2})-(\d{4})$/, // MM-DD-YYYY or M-D-YYYY
|
|
158
|
+
/^(\d{4})-(\d{1,2})-(\d{1,2})$/, // YYYY-MM-DD or YYYY-M-D
|
|
159
|
+
]
|
|
160
|
+
|
|
161
|
+
for (const format of formats) {
|
|
162
|
+
const match = dateString.match(format)
|
|
163
|
+
if (match) {
|
|
164
|
+
const [, part1, part2, part3] = match
|
|
165
|
+
let year: number, month: number, day: number
|
|
166
|
+
|
|
167
|
+
if (format === formats[2]) {
|
|
168
|
+
// YYYY-MM-DD
|
|
169
|
+
year = parseInt(part1, 10)
|
|
170
|
+
month = parseInt(part2, 10) - 1 // Convert to 0-indexed
|
|
171
|
+
day = parseInt(part3, 10)
|
|
172
|
+
} else {
|
|
173
|
+
// MM/DD/YYYY or MM-DD-YYYY
|
|
174
|
+
year = parseInt(part3, 10)
|
|
175
|
+
month = parseInt(part1, 10) - 1 // Convert to 0-indexed
|
|
176
|
+
day = parseInt(part2, 10)
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const date = new Date(year, month, day)
|
|
180
|
+
if (!isNaN(date.getTime())) {
|
|
181
|
+
return date
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return null
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Generate calendar data for a specific month
|
|
191
|
+
*/
|
|
192
|
+
export function generateCalendarMonth(
|
|
193
|
+
year: number,
|
|
194
|
+
month: number,
|
|
195
|
+
selectedDate?: Date | null,
|
|
196
|
+
selectedRange?: DateRange | null,
|
|
197
|
+
disabledDates?: Date[],
|
|
198
|
+
locale?: string
|
|
199
|
+
): CalendarMonth {
|
|
200
|
+
const firstDay = getFirstDayOfMonth(year, month)
|
|
201
|
+
const todayDate = today()
|
|
202
|
+
|
|
203
|
+
// Get the first day of the week (0 = Sunday, 1 = Monday)
|
|
204
|
+
const firstDayOfWeek = firstDay.getDay()
|
|
205
|
+
|
|
206
|
+
// Calculate start date (might be from previous month)
|
|
207
|
+
const startDate = addDays(firstDay, -firstDayOfWeek)
|
|
208
|
+
|
|
209
|
+
// Calculate end date (might be from next month)
|
|
210
|
+
const daysToShow = 42 // 6 weeks * 7 days
|
|
211
|
+
const endDate = addDays(startDate, daysToShow - 1)
|
|
212
|
+
|
|
213
|
+
const days: CalendarDay[] = []
|
|
214
|
+
const currentDate = new Date(startDate)
|
|
215
|
+
|
|
216
|
+
while (currentDate <= endDate) {
|
|
217
|
+
const isCurrentMonth = currentDate.getMonth() === month
|
|
218
|
+
const isToday = isSameDay(currentDate, todayDate)
|
|
219
|
+
const isSelected = selectedDate ? isSameDay(currentDate, selectedDate) : false
|
|
220
|
+
|
|
221
|
+
let isInRange = false
|
|
222
|
+
let isRangeStart = false
|
|
223
|
+
let isRangeEnd = false
|
|
224
|
+
|
|
225
|
+
if (selectedRange?.start && selectedRange?.end) {
|
|
226
|
+
isRangeStart = isSameDay(currentDate, selectedRange.start)
|
|
227
|
+
isRangeEnd = isSameDay(currentDate, selectedRange.end)
|
|
228
|
+
isInRange = isBetween(currentDate, selectedRange.start, selectedRange.end)
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const isDisabled = disabledDates?.some((disabledDate) => isSameDay(currentDate, disabledDate)) || false
|
|
232
|
+
|
|
233
|
+
days.push({
|
|
234
|
+
date: new Date(currentDate),
|
|
235
|
+
isCurrentMonth,
|
|
236
|
+
isToday,
|
|
237
|
+
isSelected,
|
|
238
|
+
isInRange,
|
|
239
|
+
isRangeStart,
|
|
240
|
+
isRangeEnd,
|
|
241
|
+
isDisabled,
|
|
242
|
+
})
|
|
243
|
+
|
|
244
|
+
currentDate.setDate(currentDate.getDate() + 1)
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
return {
|
|
248
|
+
year,
|
|
249
|
+
month,
|
|
250
|
+
days,
|
|
251
|
+
weekdays: getWeekdayNames(locale),
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Get the ISO string for a date (YYYY-MM-DD format)
|
|
257
|
+
*/
|
|
258
|
+
export function toISODateString(date: Date): string {
|
|
259
|
+
return date.toISOString().split('T')[0]
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Create a date from ISO string (YYYY-MM-DD format)
|
|
264
|
+
*/
|
|
265
|
+
export function fromISODateString(isoString: string): Date | null {
|
|
266
|
+
const match = isoString.match(/^(\d{4})-(\d{2})-(\d{2})$/)
|
|
267
|
+
if (!match) return null
|
|
268
|
+
|
|
269
|
+
const [, year, month, day] = match
|
|
270
|
+
return new Date(parseInt(year, 10), parseInt(month, 10) - 1, parseInt(day, 10))
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Validate if a date range is valid (start <= end)
|
|
275
|
+
*/
|
|
276
|
+
export function isValidRange(range: DateRange): boolean {
|
|
277
|
+
if (!range.start || !range.end) return true
|
|
278
|
+
return range.start <= range.end
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Get the number of days between two dates
|
|
283
|
+
*/
|
|
284
|
+
export function getDaysBetween(start: Date, end: Date): number {
|
|
285
|
+
const startTime = normalizeDate(start).getTime()
|
|
286
|
+
const endTime = normalizeDate(end).getTime()
|
|
287
|
+
return Math.abs(Math.floor((endTime - startTime) / (24 * 60 * 60 * 1000)))
|
|
288
|
+
}
|
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
import { LitElement, html, TemplateResult } from 'lit'
|
|
2
|
+
import { customElement, property, state, query } from 'lit/decorators.js'
|
|
3
|
+
import { inputStyles } from './internals/DatePicker.styles.js'
|
|
4
|
+
import { formatDate, parseDate } from './internals/DatePickerUtils.js'
|
|
5
|
+
import type { DateSelectEvent } from './internals/DatePickerCalendar.js'
|
|
6
|
+
import './internals/DatePickerCalendar.js'
|
|
7
|
+
import '../../md/text-field/ui-outlined-text-field.js'
|
|
8
|
+
import '../../md/icons/ui-icon.js'
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* A docked date picker that opens from a text field input.
|
|
12
|
+
* Ideal for forms and date selection in medium to large layouts.
|
|
13
|
+
*
|
|
14
|
+
* ## Features
|
|
15
|
+
* - Text field input with calendar dropdown
|
|
16
|
+
* - Keyboard navigation support (Arrow Down, Enter, Escape)
|
|
17
|
+
* - Custom date formatting
|
|
18
|
+
* - Input validation and error handling
|
|
19
|
+
* - Min/max date constraints
|
|
20
|
+
* - Disabled dates support
|
|
21
|
+
* - Accessible design with proper ARIA attributes
|
|
22
|
+
* - CSS Anchor Positioning API for dropdown placement
|
|
23
|
+
*
|
|
24
|
+
* ## Events
|
|
25
|
+
*
|
|
26
|
+
* ### `change`
|
|
27
|
+
* Fired when the selected date changes.
|
|
28
|
+
*
|
|
29
|
+
* **Detail:**
|
|
30
|
+
* ```typescript
|
|
31
|
+
* {
|
|
32
|
+
* value: Date | null,
|
|
33
|
+
* formattedValue: string
|
|
34
|
+
* }
|
|
35
|
+
* ```
|
|
36
|
+
*
|
|
37
|
+
* ## Usage
|
|
38
|
+
*
|
|
39
|
+
* ### Basic usage
|
|
40
|
+
* ```html
|
|
41
|
+
* <ui-date-picker-input
|
|
42
|
+
* label="Select date"
|
|
43
|
+
* name="birthDate"
|
|
44
|
+
* placeholder="MM/DD/YYYY"
|
|
45
|
+
* .value=${new Date()}
|
|
46
|
+
* @change=${this.handleDateChange}
|
|
47
|
+
* ></ui-date-picker-input>
|
|
48
|
+
* ```
|
|
49
|
+
*
|
|
50
|
+
* ### With validation
|
|
51
|
+
* ```html
|
|
52
|
+
* <ui-date-picker-input
|
|
53
|
+
* label="Birth date"
|
|
54
|
+
* name="birthDate"
|
|
55
|
+
* required
|
|
56
|
+
* .error=${this.hasError}
|
|
57
|
+
* .errorMessage=${"Please select a valid date"}
|
|
58
|
+
* .minDate=${new Date('1900-01-01')}
|
|
59
|
+
* .maxDate=${new Date()}
|
|
60
|
+
* ></ui-date-picker-input>
|
|
61
|
+
* ```
|
|
62
|
+
*
|
|
63
|
+
* ### Custom date format
|
|
64
|
+
* ```html
|
|
65
|
+
* <ui-date-picker-input
|
|
66
|
+
* label="Birth date"
|
|
67
|
+
* .dateFormat=${date => date.toLocaleDateString('en-GB')}
|
|
68
|
+
* ></ui-date-picker-input>
|
|
69
|
+
* ```
|
|
70
|
+
*
|
|
71
|
+
* ### With disabled dates
|
|
72
|
+
* ```html
|
|
73
|
+
* <ui-date-picker-input
|
|
74
|
+
* label="Appointment date"
|
|
75
|
+
* .disabledDates=${[new Date('2024-12-25'), new Date('2024-01-01')]}
|
|
76
|
+
* ></ui-date-picker-input>
|
|
77
|
+
* ```
|
|
78
|
+
*/
|
|
79
|
+
@customElement('ui-date-picker-input')
|
|
80
|
+
export class UiDatePickerInput extends LitElement {
|
|
81
|
+
static override styles = inputStyles
|
|
82
|
+
static override shadowRootOptions: ShadowRootInit = {
|
|
83
|
+
mode: 'open',
|
|
84
|
+
delegatesFocus: true,
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* The label for the input field
|
|
89
|
+
*/
|
|
90
|
+
@property({ type: String }) accessor label = ''
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* The name attribute for the input field (for form handling)
|
|
94
|
+
*/
|
|
95
|
+
@property({ type: String }) accessor name = ''
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Placeholder text for the input
|
|
99
|
+
*/
|
|
100
|
+
@property({ type: String }) accessor placeholder = 'MM/DD/YYYY'
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* The currently selected date
|
|
104
|
+
*/
|
|
105
|
+
@property({ type: Object }) accessor value: Date | null = null
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Whether the date picker is disabled
|
|
109
|
+
*/
|
|
110
|
+
@property({ type: Boolean }) accessor disabled = false
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Whether the input is required
|
|
114
|
+
*/
|
|
115
|
+
@property({ type: Boolean }) accessor required = false
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Error message to display
|
|
119
|
+
*/
|
|
120
|
+
@property({ type: String }) accessor errorMessage = ''
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Whether the input has an error state
|
|
124
|
+
*/
|
|
125
|
+
@property({ type: Boolean }) accessor error = false
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Minimum selectable date
|
|
129
|
+
*/
|
|
130
|
+
@property({ type: Object }) accessor minDate: Date | undefined = undefined
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Maximum selectable date
|
|
134
|
+
*/
|
|
135
|
+
@property({ type: Object }) accessor maxDate: Date | undefined = undefined
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Array of disabled dates
|
|
139
|
+
*/
|
|
140
|
+
@property({ type: Array }) accessor disabledDates: Date[] | undefined = undefined
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Locale for date formatting
|
|
144
|
+
*/
|
|
145
|
+
@property({ type: String }) accessor locale: string | undefined = undefined
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Custom date format function
|
|
149
|
+
*/
|
|
150
|
+
@property({ type: Object }) accessor dateFormat: ((date: Date) => string) | undefined = undefined
|
|
151
|
+
|
|
152
|
+
@state() private accessor isOpen = false
|
|
153
|
+
|
|
154
|
+
@state() private accessor inputValue = ''
|
|
155
|
+
|
|
156
|
+
@query('ui-outlined-text-field') private accessor textField!: HTMLElement
|
|
157
|
+
|
|
158
|
+
constructor() {
|
|
159
|
+
super()
|
|
160
|
+
// Initialize boolean properties to false as per Lit best practices
|
|
161
|
+
this.disabled = false
|
|
162
|
+
this.required = false
|
|
163
|
+
this.error = false
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
override connectedCallback(): void {
|
|
167
|
+
super.connectedCallback()
|
|
168
|
+
this.updateInputValue()
|
|
169
|
+
document.addEventListener('click', this.handleDocumentClick.bind(this))
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
override disconnectedCallback(): void {
|
|
173
|
+
super.disconnectedCallback()
|
|
174
|
+
document.removeEventListener('click', this.handleDocumentClick.bind(this))
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
override willUpdate(changedProperties: Map<string | number | symbol, unknown>): void {
|
|
178
|
+
if (changedProperties.has('value')) {
|
|
179
|
+
this.updateInputValue()
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
override updated(changedProperties: Map<string | number | symbol, unknown>): void {
|
|
184
|
+
if (changedProperties.has('isOpen') && this.isOpen) {
|
|
185
|
+
// Set anchor name on text field for CSS Anchor Positioning API
|
|
186
|
+
if (this.textField) {
|
|
187
|
+
// Using setProperty since anchorName is not in TypeScript types yet
|
|
188
|
+
this.textField.style.setProperty('anchor-name', '--ui-date-picker-anchor')
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
private updateInputValue(): void {
|
|
194
|
+
if (this.value) {
|
|
195
|
+
this.inputValue = this.dateFormat ? this.dateFormat(this.value) : formatDate(this.value, this.locale)
|
|
196
|
+
} else {
|
|
197
|
+
this.inputValue = ''
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
private handleDocumentClick(event: Event): void {
|
|
202
|
+
if (!this.contains(event.target as Node)) {
|
|
203
|
+
this.isOpen = false
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
private handleInputClick(): void {
|
|
208
|
+
if (!this.disabled) {
|
|
209
|
+
this.isOpen = !this.isOpen
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
private handleInputChange(event: Event): void {
|
|
214
|
+
const target = event.target as HTMLInputElement
|
|
215
|
+
this.inputValue = target.value
|
|
216
|
+
|
|
217
|
+
const parsedDate = parseDate(this.inputValue)
|
|
218
|
+
if (parsedDate) {
|
|
219
|
+
this.value = parsedDate
|
|
220
|
+
this.dispatchChangeEvent()
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
private handleCalendarDateSelect(event: CustomEvent<DateSelectEvent>): void {
|
|
225
|
+
this.value = event.detail.date
|
|
226
|
+
this.isOpen = false
|
|
227
|
+
this.dispatchChangeEvent()
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
private handleCalendarDateCancel(): void {
|
|
231
|
+
this.isOpen = false
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
private handleKeyDown(event: KeyboardEvent): void {
|
|
235
|
+
switch (event.key) {
|
|
236
|
+
case 'Escape':
|
|
237
|
+
this.isOpen = false
|
|
238
|
+
break
|
|
239
|
+
case 'ArrowDown':
|
|
240
|
+
if (!this.isOpen) {
|
|
241
|
+
event.preventDefault()
|
|
242
|
+
this.isOpen = true
|
|
243
|
+
}
|
|
244
|
+
break
|
|
245
|
+
case 'Enter':
|
|
246
|
+
if (this.isOpen) {
|
|
247
|
+
event.preventDefault()
|
|
248
|
+
this.isOpen = false
|
|
249
|
+
}
|
|
250
|
+
break
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
private dispatchChangeEvent(): void {
|
|
255
|
+
this.dispatchEvent(
|
|
256
|
+
new CustomEvent('change', {
|
|
257
|
+
detail: {
|
|
258
|
+
value: this.value,
|
|
259
|
+
formattedValue: this.inputValue,
|
|
260
|
+
},
|
|
261
|
+
bubbles: true,
|
|
262
|
+
composed: true,
|
|
263
|
+
})
|
|
264
|
+
)
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
private renderCalendarIcon(): TemplateResult {
|
|
268
|
+
return html`
|
|
269
|
+
<ui-icon slot="suffix" icon="calendarToday" @click=${this.handleInputClick} class="calendar-icon"></ui-icon>
|
|
270
|
+
`
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
private renderDropdown(): TemplateResult | null {
|
|
274
|
+
if (!this.isOpen) return null
|
|
275
|
+
|
|
276
|
+
const currentDate = this.value || new Date()
|
|
277
|
+
|
|
278
|
+
return html`
|
|
279
|
+
<div class="dropdown-container">
|
|
280
|
+
<ui-date-picker-calendar
|
|
281
|
+
.year=${currentDate.getFullYear()}
|
|
282
|
+
.month=${currentDate.getMonth()}
|
|
283
|
+
.selectedDate=${this.value}
|
|
284
|
+
.minDate=${this.minDate}
|
|
285
|
+
.maxDate=${this.maxDate}
|
|
286
|
+
.disabledDates=${this.disabledDates}
|
|
287
|
+
.locale=${this.locale}
|
|
288
|
+
showActions
|
|
289
|
+
@date-select=${this.handleCalendarDateSelect}
|
|
290
|
+
@date-cancel=${this.handleCalendarDateCancel}
|
|
291
|
+
></ui-date-picker-calendar>
|
|
292
|
+
</div>
|
|
293
|
+
`
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
override render(): TemplateResult {
|
|
297
|
+
return html`
|
|
298
|
+
<div class="input-container">
|
|
299
|
+
${this.isOpen ? html`<div class="backdrop" @click=${() => (this.isOpen = false)}></div>` : ''}
|
|
300
|
+
<ui-outlined-text-field
|
|
301
|
+
.label=${this.label}
|
|
302
|
+
.name=${this.name}
|
|
303
|
+
.placeholder=${this.placeholder}
|
|
304
|
+
.value=${this.inputValue}
|
|
305
|
+
.disabled=${this.disabled}
|
|
306
|
+
.required=${this.required}
|
|
307
|
+
?invalid=${this.error}
|
|
308
|
+
.invalidText=${this.errorMessage}
|
|
309
|
+
readonly
|
|
310
|
+
@click=${this.handleInputClick}
|
|
311
|
+
@input=${this.handleInputChange}
|
|
312
|
+
@keydown=${this.handleKeyDown}
|
|
313
|
+
>
|
|
314
|
+
${this.renderCalendarIcon()}
|
|
315
|
+
</ui-outlined-text-field>
|
|
316
|
+
${this.renderDropdown()}
|
|
317
|
+
</div>
|
|
318
|
+
`
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
declare global {
|
|
323
|
+
interface HTMLElementTagNameMap {
|
|
324
|
+
'ui-date-picker-input': UiDatePickerInput
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
interface HTMLElementEventMap {
|
|
328
|
+
change: CustomEvent<{
|
|
329
|
+
value: Date | null
|
|
330
|
+
formattedValue: string
|
|
331
|
+
}>
|
|
332
|
+
}
|
|
333
|
+
}
|