@api-client/ui 0.5.6 → 0.5.8
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/mention-textarea/internals/MentionTextArea.d.ts +216 -0
- package/build/src/elements/mention-textarea/internals/MentionTextArea.d.ts.map +1 -0
- package/build/src/elements/mention-textarea/internals/MentionTextArea.js +1037 -0
- package/build/src/elements/mention-textarea/internals/MentionTextArea.js.map +1 -0
- package/build/src/elements/mention-textarea/internals/MentionTextArea.styles.d.ts +3 -0
- package/build/src/elements/mention-textarea/internals/MentionTextArea.styles.d.ts.map +1 -0
- package/build/src/elements/mention-textarea/internals/MentionTextArea.styles.js +274 -0
- package/build/src/elements/mention-textarea/internals/MentionTextArea.styles.js.map +1 -0
- package/build/src/elements/mention-textarea/ui-mention-textarea.d.ts +13 -0
- package/build/src/elements/mention-textarea/ui-mention-textarea.d.ts.map +1 -0
- package/build/src/elements/mention-textarea/ui-mention-textarea.js +28 -0
- package/build/src/elements/mention-textarea/ui-mention-textarea.js.map +1 -0
- 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/chip/internals/Chip.styles.d.ts.map +1 -1
- package/build/src/md/chip/internals/Chip.styles.js +2 -0
- package/build/src/md/chip/internals/Chip.styles.js.map +1 -1
- package/build/src/md/date-picker/internals/DatePicker.styles.d.ts.map +1 -1
- package/build/src/md/date-picker/internals/DatePicker.styles.js +73 -0
- package/build/src/md/date-picker/internals/DatePicker.styles.js.map +1 -1
- package/build/src/md/date-picker/internals/DatePickerCalendar.d.ts +164 -51
- package/build/src/md/date-picker/internals/DatePickerCalendar.d.ts.map +1 -1
- package/build/src/md/date-picker/internals/DatePickerCalendar.js +660 -368
- package/build/src/md/date-picker/internals/DatePickerCalendar.js.map +1 -1
- package/build/src/md/date-picker/ui-date-picker-input.d.ts +65 -13
- package/build/src/md/date-picker/ui-date-picker-input.d.ts.map +1 -1
- package/build/src/md/date-picker/ui-date-picker-input.js +143 -76
- package/build/src/md/date-picker/ui-date-picker-input.js.map +1 -1
- package/build/src/md/date-picker/ui-date-picker-modal-input.d.ts +76 -17
- package/build/src/md/date-picker/ui-date-picker-modal-input.d.ts.map +1 -1
- package/build/src/md/date-picker/ui-date-picker-modal-input.js +192 -127
- package/build/src/md/date-picker/ui-date-picker-modal-input.js.map +1 -1
- package/build/src/md/date-picker/ui-date-picker-modal.d.ts +63 -15
- package/build/src/md/date-picker/ui-date-picker-modal.d.ts.map +1 -1
- package/build/src/md/date-picker/ui-date-picker-modal.js +143 -64
- package/build/src/md/date-picker/ui-date-picker-modal.js.map +1 -1
- package/demo/elements/currency/index.html +91 -0
- package/demo/elements/currency/index.ts +272 -0
- package/demo/elements/index.html +6 -0
- package/demo/elements/mention-textarea/index.html +19 -0
- package/demo/elements/mention-textarea/index.ts +205 -0
- package/demo/md/date-picker/date-picker.ts +138 -103
- package/package.json +2 -2
- 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/mention-textarea/internals/MentionTextArea.styles.ts +274 -0
- package/src/elements/mention-textarea/internals/MentionTextArea.ts +1036 -0
- package/src/elements/mention-textarea/ui-mention-textarea.ts +18 -0
- package/src/md/button/internals/base.ts +7 -0
- package/src/md/chip/internals/Chip.styles.ts +2 -0
- package/src/md/date-picker/internals/DatePicker.styles.ts +73 -0
- package/src/md/date-picker/internals/DatePickerCalendar.ts +643 -309
- package/src/md/date-picker/ui-date-picker-input.ts +110 -49
- package/src/md/date-picker/ui-date-picker-modal-input.ts +168 -99
- package/src/md/date-picker/ui-date-picker-modal.ts +136 -53
- 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
- package/test/elements/mention-textarea/MentionTextArea.basic.test.ts +63 -0
- package/test/elements/mention-textarea/MentionTextArea.test.ts +321 -0
- package/tsconfig.json +1 -1
|
@@ -11,16 +11,25 @@ import {
|
|
|
11
11
|
getMonthNames,
|
|
12
12
|
isSameDay,
|
|
13
13
|
formatDate,
|
|
14
|
+
addDays,
|
|
14
15
|
} from './DatePickerUtils.js'
|
|
15
16
|
import '../../../md/icons/ui-icon.js'
|
|
16
17
|
import '../../../md/button/ui-button.js'
|
|
17
18
|
import '../../../md/icon-button/ui-icon-button.js'
|
|
18
19
|
|
|
20
|
+
/**
|
|
21
|
+
* Event dispatched when a single date is selected in immediate mode
|
|
22
|
+
* or confirmed in pending mode.
|
|
23
|
+
*/
|
|
19
24
|
export interface DateSelectEvent {
|
|
20
25
|
date: Date
|
|
21
26
|
formattedDate: string
|
|
22
27
|
}
|
|
23
28
|
|
|
29
|
+
/**
|
|
30
|
+
* Event dispatched when a date range is completed in immediate mode.
|
|
31
|
+
* Only fired when both start and end dates are selected.
|
|
32
|
+
*/
|
|
24
33
|
export interface DateRangeSelectEvent {
|
|
25
34
|
range: DateRange
|
|
26
35
|
formattedRange: {
|
|
@@ -29,6 +38,10 @@ export interface DateRangeSelectEvent {
|
|
|
29
38
|
}
|
|
30
39
|
}
|
|
31
40
|
|
|
41
|
+
/**
|
|
42
|
+
* Event dispatched when a date range selection is confirmed
|
|
43
|
+
* via the OK button in pending mode.
|
|
44
|
+
*/
|
|
32
45
|
export interface DateRangeConfirmEvent {
|
|
33
46
|
range: DateRange | null
|
|
34
47
|
formattedRange: {
|
|
@@ -37,13 +50,31 @@ export interface DateRangeConfirmEvent {
|
|
|
37
50
|
}
|
|
38
51
|
}
|
|
39
52
|
|
|
53
|
+
/**
|
|
54
|
+
* Event dispatched when user cancels a pending selection
|
|
55
|
+
* via the Cancel button or Escape key.
|
|
56
|
+
*/
|
|
40
57
|
export interface DateCancelEvent {
|
|
41
58
|
reason?: string
|
|
42
59
|
}
|
|
43
60
|
|
|
44
61
|
/**
|
|
45
62
|
* A calendar grid component for date selection.
|
|
46
|
-
* Supports single date selection and date range selection.
|
|
63
|
+
* Supports single date selection and date range selection with full keyboard navigation.
|
|
64
|
+
*
|
|
65
|
+
* ## Features
|
|
66
|
+
* - Single date and date range selection
|
|
67
|
+
* - Keyboard navigation support (arrow keys, home, end, page up/down)
|
|
68
|
+
* - Configurable date restrictions (min/max dates, disabled dates)
|
|
69
|
+
* - Localization support for date formatting and month/day names
|
|
70
|
+
* - Optional action buttons for pending selections
|
|
71
|
+
* - Accessible design with proper ARIA attributes
|
|
72
|
+
*
|
|
73
|
+
* ## Events
|
|
74
|
+
* - `date-select`: Fired when a single date is selected/confirmed
|
|
75
|
+
* - `date-range-select`: Fired when a date range is completed (immediate mode)
|
|
76
|
+
* - `date-range-confirm`: Fired when a date range is confirmed (pending mode)
|
|
77
|
+
* - `date-cancel`: Fired when a pending selection is cancelled
|
|
47
78
|
*
|
|
48
79
|
* ## Usage
|
|
49
80
|
*
|
|
@@ -62,11 +93,24 @@ export interface DateCancelEvent {
|
|
|
62
93
|
* ### Date range selection
|
|
63
94
|
* ```html
|
|
64
95
|
* <ui-date-picker-calendar
|
|
65
|
-
*
|
|
66
|
-
* .
|
|
96
|
+
* rangeSelection
|
|
97
|
+
* .rangeStart=${new Date()}
|
|
98
|
+
* .rangeEnd=${null}
|
|
67
99
|
* @date-range-select=${this.handleRangeSelect}
|
|
68
100
|
* ></ui-date-picker-calendar>
|
|
69
101
|
* ```
|
|
102
|
+
*
|
|
103
|
+
* ### With action buttons and restrictions
|
|
104
|
+
* ```html
|
|
105
|
+
* <ui-date-picker-calendar
|
|
106
|
+
* rangeSelection
|
|
107
|
+
* showActions
|
|
108
|
+
* .minDate=${new Date()}
|
|
109
|
+
* .maxDate=${new Date(Date.now() + 365 * 24 * 60 * 60 * 1000)}
|
|
110
|
+
* @date-range-confirm=${this.handleRangeConfirm}
|
|
111
|
+
* @date-cancel=${this.handleCancel}
|
|
112
|
+
* ></ui-date-picker-calendar>
|
|
113
|
+
* ```
|
|
70
114
|
*/
|
|
71
115
|
@customElement('ui-date-picker-calendar')
|
|
72
116
|
export class UiDatePickerCalendar extends LitElement {
|
|
@@ -78,99 +122,161 @@ export class UiDatePickerCalendar extends LitElement {
|
|
|
78
122
|
@property({ type: Number }) accessor year = new Date().getFullYear()
|
|
79
123
|
|
|
80
124
|
/**
|
|
81
|
-
* The currently displayed month (0-indexed)
|
|
125
|
+
* The currently displayed month (0-indexed, where 0 = January)
|
|
82
126
|
*/
|
|
83
127
|
@property({ type: Number }) accessor month = new Date().getMonth()
|
|
84
128
|
|
|
85
129
|
/**
|
|
86
|
-
* The currently selected date for single selection mode
|
|
130
|
+
* The currently selected date for single selection mode.
|
|
131
|
+
* Set to null for no selection.
|
|
87
132
|
*/
|
|
88
133
|
@property({ type: Object }) accessor selectedDate: Date | null = null
|
|
89
134
|
|
|
90
135
|
/**
|
|
91
|
-
* The
|
|
136
|
+
* The start date of the selected range for range selection mode.
|
|
137
|
+
* Used in combination with rangeEnd to define a date range.
|
|
92
138
|
*/
|
|
93
|
-
@property({ type: Object }) accessor
|
|
139
|
+
@property({ type: Object }) accessor rangeStart: Date | null = null
|
|
94
140
|
|
|
95
141
|
/**
|
|
96
|
-
*
|
|
142
|
+
* The end date of the selected range for range selection mode.
|
|
143
|
+
* Used in combination with rangeStart to define a date range.
|
|
144
|
+
*/
|
|
145
|
+
@property({ type: Object }) accessor rangeEnd: Date | null = null
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Enable range selection mode. When true, users can select date ranges
|
|
149
|
+
* instead of single dates. Affects event dispatching and UI behavior.
|
|
97
150
|
*/
|
|
98
151
|
@property({ type: Boolean }) accessor rangeSelection = false
|
|
99
152
|
|
|
100
153
|
/**
|
|
101
|
-
* Minimum selectable date
|
|
154
|
+
* Minimum selectable date. Dates before this will be disabled.
|
|
155
|
+
* Set to undefined for no minimum restriction.
|
|
102
156
|
*/
|
|
103
157
|
@property({ type: Object }) accessor minDate: Date | undefined = undefined
|
|
104
158
|
|
|
105
159
|
/**
|
|
106
|
-
* Maximum selectable date
|
|
160
|
+
* Maximum selectable date. Dates after this will be disabled.
|
|
161
|
+
* Set to undefined for no maximum restriction.
|
|
107
162
|
*/
|
|
108
163
|
@property({ type: Object }) accessor maxDate: Date | undefined = undefined
|
|
109
164
|
|
|
110
165
|
/**
|
|
111
|
-
* Array of
|
|
166
|
+
* Array of specific dates to disable. These dates will not be selectable
|
|
167
|
+
* regardless of minDate and maxDate settings.
|
|
112
168
|
*/
|
|
113
169
|
@property({ type: Array }) accessor disabledDates: Date[] | undefined = undefined
|
|
114
170
|
|
|
115
171
|
/**
|
|
116
|
-
* Locale for date formatting and month/day names
|
|
172
|
+
* Locale for date formatting and month/day names (e.g., 'en-US', 'fr-FR').
|
|
173
|
+
* Defaults to browser locale if not specified.
|
|
117
174
|
*/
|
|
118
175
|
@property({ type: String }) accessor locale: string | undefined = undefined
|
|
119
176
|
|
|
120
177
|
/**
|
|
121
|
-
* Whether to show navigation controls
|
|
178
|
+
* Whether to show navigation controls (previous/next month and year buttons).
|
|
179
|
+
* When false, users can only navigate using keyboard or programmatically.
|
|
122
180
|
*/
|
|
123
181
|
@property({ type: Boolean }) accessor showNavigation = true
|
|
124
182
|
|
|
125
183
|
/**
|
|
126
|
-
* Whether to show action buttons (OK/Cancel)
|
|
184
|
+
* Whether to show action buttons (OK/Cancel). When true, selections are pending
|
|
185
|
+
* until confirmed with the OK button. When false, selections are immediate.
|
|
127
186
|
*/
|
|
128
187
|
@property({ type: Boolean }) accessor showActions = false
|
|
129
188
|
|
|
130
189
|
/**
|
|
131
|
-
* Text for the OK button
|
|
190
|
+
* Text label for the OK/confirm button. Only visible when showActions is true.
|
|
132
191
|
*/
|
|
133
192
|
@property({ type: String }) accessor okButtonText = 'OK'
|
|
134
193
|
|
|
135
194
|
/**
|
|
136
|
-
* Text for the Cancel button
|
|
195
|
+
* Text label for the Cancel button. Only visible when showActions is true.
|
|
137
196
|
*/
|
|
138
197
|
@property({ type: String }) accessor cancelButtonText = 'Cancel'
|
|
139
198
|
|
|
140
|
-
@state() private accessor
|
|
199
|
+
@state() private accessor calendarData: CalendarMonth | undefined = undefined
|
|
200
|
+
|
|
201
|
+
@state() private accessor monthNames: string[] = []
|
|
141
202
|
|
|
142
|
-
@state() private accessor
|
|
203
|
+
@state() private accessor showMonthDropdown = false
|
|
143
204
|
|
|
144
|
-
@state() private accessor
|
|
205
|
+
@state() private accessor showYearDropdown = false
|
|
145
206
|
|
|
146
|
-
@state() private accessor
|
|
207
|
+
@state() private accessor pendingDate: Date | null = null
|
|
147
208
|
|
|
148
|
-
@state() private accessor
|
|
209
|
+
@state() private accessor pendingRangeStart: Date | null = null
|
|
149
210
|
|
|
150
|
-
@state() private accessor
|
|
211
|
+
@state() private accessor pendingRangeEnd: Date | null = null
|
|
151
212
|
|
|
152
|
-
@state() private accessor
|
|
213
|
+
@state() private accessor focusedDate: Date | null = null
|
|
153
214
|
|
|
154
215
|
override connectedCallback(): void {
|
|
155
216
|
super.connectedCallback()
|
|
156
|
-
this.
|
|
157
|
-
this.
|
|
158
|
-
|
|
217
|
+
this.updateCalendar()
|
|
218
|
+
this.updateMonthNames()
|
|
219
|
+
this.initializeFocusedDate()
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
override firstUpdated(): void {
|
|
223
|
+
// Set initial focus to the calendar container
|
|
224
|
+
this.updateFocus()
|
|
159
225
|
}
|
|
160
226
|
|
|
161
227
|
override disconnectedCallback(): void {
|
|
162
228
|
super.disconnectedCallback()
|
|
163
|
-
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
override willUpdate(changedProperties: Map<string | number | symbol, unknown>): void {
|
|
232
|
+
if (
|
|
233
|
+
changedProperties.has('year') ||
|
|
234
|
+
changedProperties.has('month') ||
|
|
235
|
+
changedProperties.has('selectedDate') ||
|
|
236
|
+
changedProperties.has('rangeStart') ||
|
|
237
|
+
changedProperties.has('rangeEnd') ||
|
|
238
|
+
changedProperties.has('disabledDates') ||
|
|
239
|
+
changedProperties.has('locale')
|
|
240
|
+
) {
|
|
241
|
+
this.updateCalendar()
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
if (changedProperties.has('locale')) {
|
|
245
|
+
this.updateMonthNames()
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Update focused date when month/year changes via navigation
|
|
249
|
+
if (changedProperties.has('year') || changedProperties.has('month')) {
|
|
250
|
+
this.updateFocusedDateForMonthChange()
|
|
251
|
+
}
|
|
164
252
|
}
|
|
165
253
|
|
|
166
254
|
override updated(changedProperties: Map<string | number | symbol, unknown>): void {
|
|
167
|
-
if (changedProperties.has('
|
|
255
|
+
if (changedProperties.has('showYearDropdown') && this.showYearDropdown) {
|
|
168
256
|
// Scroll selected year into view
|
|
169
|
-
this.
|
|
257
|
+
this.scrollSelectedYearIntoView()
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
if (changedProperties.has('focusedDate')) {
|
|
261
|
+
this.updateFocus()
|
|
170
262
|
}
|
|
171
263
|
}
|
|
172
264
|
|
|
173
|
-
private
|
|
265
|
+
private updateFocus(): void {
|
|
266
|
+
if (!this.focusedDate) return
|
|
267
|
+
|
|
268
|
+
// Find the button for the focused date and set focus
|
|
269
|
+
const dateString = this.focusedDate.toISOString().split('T')[0]
|
|
270
|
+
const focusedButton = this.shadowRoot?.querySelector(`[data-date="${dateString}"]`) as HTMLElement
|
|
271
|
+
if (focusedButton && !focusedButton.hasAttribute('disabled')) {
|
|
272
|
+
// Use requestAnimationFrame to ensure DOM is updated
|
|
273
|
+
requestAnimationFrame(() => {
|
|
274
|
+
focusedButton.focus()
|
|
275
|
+
})
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
private scrollSelectedYearIntoView(): void {
|
|
174
280
|
// Wait for next frame to ensure DOM is updated
|
|
175
281
|
requestAnimationFrame(() => {
|
|
176
282
|
const selectedYearButton = this.shadowRoot?.querySelector('.year-option.selected') as HTMLElement
|
|
@@ -183,398 +289,526 @@ export class UiDatePickerCalendar extends LitElement {
|
|
|
183
289
|
})
|
|
184
290
|
}
|
|
185
291
|
|
|
186
|
-
|
|
187
|
-
if (
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
changedProperties.has('selectedDate') ||
|
|
191
|
-
changedProperties.has('selectedRange') ||
|
|
192
|
-
changedProperties.has('disabledDates') ||
|
|
193
|
-
changedProperties.has('locale')
|
|
194
|
-
) {
|
|
195
|
-
this._updateCalendar()
|
|
292
|
+
private updateFocusedDateForMonthChange(): void {
|
|
293
|
+
if (!this.focusedDate) {
|
|
294
|
+
this.initializeFocusedDate()
|
|
295
|
+
return
|
|
196
296
|
}
|
|
197
297
|
|
|
198
|
-
if
|
|
199
|
-
|
|
298
|
+
// Keep the same day of month if possible
|
|
299
|
+
const targetDay = this.focusedDate.getDate()
|
|
300
|
+
const newDate = new Date(this.year, this.month, targetDay)
|
|
301
|
+
|
|
302
|
+
// Check if the target date exists in the new month and is not disabled
|
|
303
|
+
if (newDate.getMonth() === this.month && !this.isDateDisabled(newDate)) {
|
|
304
|
+
this.focusedDate = newDate
|
|
305
|
+
} else {
|
|
306
|
+
// Find the closest available date
|
|
307
|
+
this.focusedDate = this.findFirstAvailableDate()
|
|
200
308
|
}
|
|
201
309
|
}
|
|
202
310
|
|
|
203
|
-
private
|
|
204
|
-
this.
|
|
311
|
+
private updateCalendar(): void {
|
|
312
|
+
const selectedRange = this.rangeStart || this.rangeEnd ? { start: this.rangeStart, end: this.rangeEnd } : null
|
|
313
|
+
this.calendarData = generateCalendarMonth(
|
|
205
314
|
this.year,
|
|
206
315
|
this.month,
|
|
207
316
|
this.selectedDate,
|
|
208
|
-
|
|
317
|
+
selectedRange,
|
|
209
318
|
this.disabledDates,
|
|
210
319
|
this.locale
|
|
211
320
|
)
|
|
212
321
|
}
|
|
213
322
|
|
|
214
|
-
private
|
|
215
|
-
this.
|
|
323
|
+
private updateMonthNames(): void {
|
|
324
|
+
this.monthNames = getMonthNames(this.locale)
|
|
216
325
|
}
|
|
217
326
|
|
|
218
|
-
private
|
|
327
|
+
private navigateMonth(delta: number): void {
|
|
219
328
|
const newDate = addMonths(new Date(this.year, this.month), delta)
|
|
220
329
|
this.year = newDate.getFullYear()
|
|
221
330
|
this.month = newDate.getMonth()
|
|
222
331
|
}
|
|
223
332
|
|
|
224
|
-
private
|
|
225
|
-
this.
|
|
333
|
+
private handlePrevMonth(): void {
|
|
334
|
+
this.navigateMonth(-1)
|
|
226
335
|
}
|
|
227
336
|
|
|
228
|
-
private
|
|
229
|
-
this.
|
|
337
|
+
private handleNextMonth(): void {
|
|
338
|
+
this.navigateMonth(1)
|
|
230
339
|
}
|
|
231
340
|
|
|
232
|
-
private
|
|
341
|
+
private handlePrevYear(): void {
|
|
233
342
|
this.year = this.year - 1
|
|
234
343
|
}
|
|
235
344
|
|
|
236
|
-
private
|
|
345
|
+
private handleNextYear(): void {
|
|
237
346
|
this.year = this.year + 1
|
|
238
347
|
}
|
|
239
348
|
|
|
240
|
-
private
|
|
241
|
-
this.
|
|
242
|
-
this.
|
|
349
|
+
private handleMonthClick(): void {
|
|
350
|
+
this.showMonthDropdown = !this.showMonthDropdown
|
|
351
|
+
this.showYearDropdown = false
|
|
243
352
|
}
|
|
244
353
|
|
|
245
|
-
private
|
|
246
|
-
this.
|
|
247
|
-
this.
|
|
354
|
+
private handleYearClick(): void {
|
|
355
|
+
this.showYearDropdown = !this.showYearDropdown
|
|
356
|
+
this.showMonthDropdown = false
|
|
248
357
|
}
|
|
249
358
|
|
|
250
|
-
private
|
|
359
|
+
private handleMonthSelect(selectedMonth: number): void {
|
|
251
360
|
this.month = selectedMonth
|
|
252
|
-
this.
|
|
361
|
+
this.showMonthDropdown = false
|
|
253
362
|
}
|
|
254
363
|
|
|
255
|
-
private
|
|
364
|
+
private handleYearSelect(selectedYear: number): void {
|
|
256
365
|
this.year = selectedYear
|
|
257
|
-
this.
|
|
366
|
+
this.showYearDropdown = false
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
private closeDropdowns(): void {
|
|
370
|
+
this.showMonthDropdown = false
|
|
371
|
+
this.showYearDropdown = false
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
private navigateDate(delta: number): void {
|
|
375
|
+
if (!this.focusedDate) return
|
|
376
|
+
|
|
377
|
+
const newDate = addDays(this.focusedDate, delta)
|
|
378
|
+
|
|
379
|
+
// Check if new date is in current month or if we should navigate to next/previous month
|
|
380
|
+
if (newDate.getMonth() !== this.month || newDate.getFullYear() !== this.year) {
|
|
381
|
+
// Navigate to next/previous month
|
|
382
|
+
if (delta > 0) {
|
|
383
|
+
this.navigateMonth(1)
|
|
384
|
+
} else {
|
|
385
|
+
this.navigateMonth(-1)
|
|
386
|
+
}
|
|
387
|
+
// Set focused date to the target date in the new month
|
|
388
|
+
this.focusedDate = newDate
|
|
389
|
+
return
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// Check if new date is disabled
|
|
393
|
+
if (this.isDateDisabled(newDate)) {
|
|
394
|
+
// Try to find next available date
|
|
395
|
+
const availableDate = this.findNextAvailableDate(newDate, delta > 0)
|
|
396
|
+
if (availableDate) {
|
|
397
|
+
this.focusedDate = availableDate
|
|
398
|
+
}
|
|
399
|
+
} else {
|
|
400
|
+
this.focusedDate = newDate
|
|
401
|
+
}
|
|
258
402
|
}
|
|
259
403
|
|
|
260
|
-
private
|
|
261
|
-
|
|
262
|
-
|
|
404
|
+
private findNextAvailableDate(startDate: Date, forward: boolean): Date | null {
|
|
405
|
+
const direction = forward ? 1 : -1
|
|
406
|
+
for (let i = 1; i <= 31; i++) {
|
|
407
|
+
const date = addDays(startDate, i * direction)
|
|
408
|
+
if (date.getMonth() !== this.month) break // Out of current month
|
|
409
|
+
if (!this.isDateDisabled(date)) {
|
|
410
|
+
return date
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
return null
|
|
263
414
|
}
|
|
264
415
|
|
|
265
|
-
private
|
|
266
|
-
|
|
267
|
-
|
|
416
|
+
private focusFirstDayOfMonth(): void {
|
|
417
|
+
const firstDay = new Date(this.year, this.month, 1)
|
|
418
|
+
if (!this.isDateDisabled(firstDay)) {
|
|
419
|
+
this.focusedDate = firstDay
|
|
420
|
+
} else {
|
|
421
|
+
this.focusedDate = this.findFirstAvailableDate()
|
|
268
422
|
}
|
|
269
423
|
}
|
|
270
424
|
|
|
271
|
-
private
|
|
425
|
+
private focusLastDayOfMonth(): void {
|
|
426
|
+
const lastDay = new Date(this.year, this.month + 1, 0)
|
|
427
|
+
if (!this.isDateDisabled(lastDay)) {
|
|
428
|
+
this.focusedDate = lastDay
|
|
429
|
+
} else {
|
|
430
|
+
// Find last available date in month
|
|
431
|
+
for (let i = lastDay.getDate(); i >= 1; i--) {
|
|
432
|
+
const date = new Date(this.year, this.month, i)
|
|
433
|
+
if (!this.isDateDisabled(date)) {
|
|
434
|
+
this.focusedDate = date
|
|
435
|
+
break
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
private selectFocusedDate(): void {
|
|
442
|
+
if (!this.focusedDate || this.isDateDisabled(this.focusedDate)) return
|
|
443
|
+
|
|
444
|
+
if (this.rangeSelection) {
|
|
445
|
+
this.handleRangeSelection(this.focusedDate)
|
|
446
|
+
} else {
|
|
447
|
+
this.handleSingleSelection(this.focusedDate)
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
private handleDayClick(day: CalendarDay): void {
|
|
272
452
|
if (day.isDisabled) return
|
|
273
453
|
|
|
454
|
+
// Update focused date to clicked date
|
|
455
|
+
this.focusedDate = day.date
|
|
456
|
+
|
|
274
457
|
if (this.rangeSelection) {
|
|
275
|
-
this.
|
|
458
|
+
this.handleRangeSelection(day.date)
|
|
276
459
|
} else {
|
|
277
|
-
this.
|
|
460
|
+
this.handleSingleSelection(day.date)
|
|
278
461
|
}
|
|
279
462
|
}
|
|
280
463
|
|
|
281
|
-
private
|
|
464
|
+
private handleSingleSelection(date: Date): void {
|
|
282
465
|
if (this.showActions) {
|
|
283
466
|
// Use pending state when actions are enabled
|
|
284
|
-
this.
|
|
467
|
+
this.pendingDate = date
|
|
285
468
|
} else {
|
|
286
469
|
// Immediate selection when no actions
|
|
287
470
|
this.selectedDate = date
|
|
471
|
+
this.dispatchDateEvent(date)
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
private handleRangeSelection(date: Date): void {
|
|
476
|
+
const isImmediate = !this.showActions
|
|
477
|
+
const { start, end } = this.getCurrentRange()
|
|
478
|
+
|
|
479
|
+
// If we have a complete range, start a new one
|
|
480
|
+
if (start && end) {
|
|
481
|
+
this.setRangeValues(date, null, isImmediate)
|
|
482
|
+
return
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
// If we have a start but no end, complete the range
|
|
486
|
+
if (start && !end) {
|
|
487
|
+
const sortedRange = start <= date ? { start, end: date } : { start: date, end: start }
|
|
488
|
+
this.setRangeValues(sortedRange.start, sortedRange.end, isImmediate)
|
|
288
489
|
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
formattedDate: formatDate(date, this.locale),
|
|
490
|
+
if (isImmediate) {
|
|
491
|
+
this.dispatchRangeEvent(sortedRange)
|
|
292
492
|
}
|
|
493
|
+
return
|
|
494
|
+
}
|
|
293
495
|
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
496
|
+
// Start new range
|
|
497
|
+
this.setRangeValues(date, null, isImmediate)
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
/**
|
|
501
|
+
* Helper to get the current range state (either immediate or pending)
|
|
502
|
+
*/
|
|
503
|
+
private getCurrentRange(): { start: Date | null; end: Date | null } {
|
|
504
|
+
const isImmediate = !this.showActions
|
|
505
|
+
return {
|
|
506
|
+
start: isImmediate ? this.rangeStart : this.pendingRangeStart,
|
|
507
|
+
end: isImmediate ? this.rangeEnd : this.pendingRangeEnd,
|
|
301
508
|
}
|
|
302
509
|
}
|
|
303
510
|
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
511
|
+
/**
|
|
512
|
+
* Helper to check if we have a complete range in the current mode
|
|
513
|
+
*/
|
|
514
|
+
private hasCompleteRange(): boolean {
|
|
515
|
+
const { start, end } = this.getCurrentRange()
|
|
516
|
+
return !!(start && end)
|
|
517
|
+
}
|
|
309
518
|
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
this.selectedRange = newRange
|
|
315
|
-
}
|
|
519
|
+
private setRangeValues(start: Date | null, end: Date | null, isImmediate: boolean): void {
|
|
520
|
+
if (isImmediate) {
|
|
521
|
+
this.rangeStart = start
|
|
522
|
+
this.rangeEnd = end
|
|
316
523
|
} else {
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
524
|
+
this.pendingRangeStart = start
|
|
525
|
+
this.pendingRangeEnd = end
|
|
526
|
+
}
|
|
527
|
+
}
|
|
320
528
|
|
|
321
|
-
|
|
322
|
-
|
|
529
|
+
/**
|
|
530
|
+
* Helper to dispatch date selection events
|
|
531
|
+
*/
|
|
532
|
+
private dispatchDateEvent(date: Date): void {
|
|
533
|
+
const event: DateSelectEvent = {
|
|
534
|
+
date,
|
|
535
|
+
formattedDate: formatDate(date, this.locale),
|
|
536
|
+
}
|
|
323
537
|
|
|
324
|
-
|
|
538
|
+
this.dispatchEvent(
|
|
539
|
+
new CustomEvent('date-select', {
|
|
540
|
+
detail: event,
|
|
541
|
+
bubbles: true,
|
|
542
|
+
composed: true,
|
|
543
|
+
})
|
|
544
|
+
)
|
|
545
|
+
}
|
|
325
546
|
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
end: sortedRange.end ? formatDate(sortedRange.end, this.locale) : null,
|
|
338
|
-
},
|
|
339
|
-
}
|
|
547
|
+
/**
|
|
548
|
+
* Helper to dispatch range selection events
|
|
549
|
+
*/
|
|
550
|
+
private dispatchRangeEvent(range: { start: Date; end: Date }): void {
|
|
551
|
+
const event: DateRangeSelectEvent = {
|
|
552
|
+
range,
|
|
553
|
+
formattedRange: {
|
|
554
|
+
start: formatDate(range.start, this.locale),
|
|
555
|
+
end: formatDate(range.end, this.locale),
|
|
556
|
+
},
|
|
557
|
+
}
|
|
340
558
|
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
559
|
+
this.dispatchEvent(
|
|
560
|
+
new CustomEvent('date-range-select', {
|
|
561
|
+
detail: event,
|
|
562
|
+
bubbles: true,
|
|
563
|
+
composed: true,
|
|
564
|
+
})
|
|
565
|
+
)
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
/**
|
|
569
|
+
* Helper to dispatch range confirmation events
|
|
570
|
+
*/
|
|
571
|
+
private dispatchRangeConfirmEvent(range: DateRange | null): void {
|
|
572
|
+
const event: DateRangeConfirmEvent = {
|
|
573
|
+
range,
|
|
574
|
+
formattedRange: {
|
|
575
|
+
start: range?.start ? formatDate(range.start, this.locale) : null,
|
|
576
|
+
end: range?.end ? formatDate(range.end, this.locale) : null,
|
|
577
|
+
},
|
|
349
578
|
}
|
|
579
|
+
|
|
580
|
+
this.dispatchEvent(
|
|
581
|
+
new CustomEvent('date-range-confirm', {
|
|
582
|
+
detail: event,
|
|
583
|
+
bubbles: true,
|
|
584
|
+
composed: true,
|
|
585
|
+
})
|
|
586
|
+
)
|
|
350
587
|
}
|
|
351
588
|
|
|
352
|
-
|
|
589
|
+
/**
|
|
590
|
+
* Helper to dispatch cancel events
|
|
591
|
+
*/
|
|
592
|
+
private dispatchCancelEvent(reason = 'user_cancelled'): void {
|
|
593
|
+
const event: DateCancelEvent = { reason }
|
|
594
|
+
|
|
595
|
+
this.dispatchEvent(
|
|
596
|
+
new CustomEvent('date-cancel', {
|
|
597
|
+
detail: event,
|
|
598
|
+
bubbles: true,
|
|
599
|
+
composed: true,
|
|
600
|
+
})
|
|
601
|
+
)
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
private isDateDisabled(date: Date): boolean {
|
|
353
605
|
if (this.minDate && date < this.minDate) return true
|
|
354
606
|
if (this.maxDate && date > this.maxDate) return true
|
|
355
607
|
if (this.disabledDates?.some((disabledDate) => isSameDay(date, disabledDate))) return true
|
|
356
608
|
return false
|
|
357
609
|
}
|
|
358
610
|
|
|
359
|
-
private
|
|
611
|
+
private handleConfirm(): void {
|
|
360
612
|
if (this.rangeSelection) {
|
|
361
|
-
if (this.
|
|
362
|
-
this.
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
}
|
|
613
|
+
if (this.pendingRangeStart || this.pendingRangeEnd) {
|
|
614
|
+
this.rangeStart = this.pendingRangeStart
|
|
615
|
+
this.rangeEnd = this.pendingRangeEnd
|
|
616
|
+
|
|
617
|
+
const range = this.rangeStart || this.rangeEnd ? { start: this.rangeStart, end: this.rangeEnd } : null
|
|
618
|
+
|
|
619
|
+
// Reset pending state after confirmation
|
|
620
|
+
this.pendingRangeStart = null
|
|
621
|
+
this.pendingRangeEnd = null
|
|
371
622
|
|
|
372
|
-
this.
|
|
373
|
-
new CustomEvent('date-range-confirm', {
|
|
374
|
-
detail: event,
|
|
375
|
-
bubbles: true,
|
|
376
|
-
composed: true,
|
|
377
|
-
})
|
|
378
|
-
)
|
|
623
|
+
this.dispatchRangeConfirmEvent(range)
|
|
379
624
|
}
|
|
380
625
|
} else {
|
|
381
|
-
if (this.
|
|
382
|
-
this.selectedDate = this.
|
|
626
|
+
if (this.pendingDate) {
|
|
627
|
+
this.selectedDate = this.pendingDate
|
|
383
628
|
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
formattedDate: formatDate(this._pendingDate, this.locale),
|
|
387
|
-
}
|
|
629
|
+
// Reset pending state after confirmation
|
|
630
|
+
this.pendingDate = null
|
|
388
631
|
|
|
389
|
-
this.
|
|
390
|
-
new CustomEvent('date-select', {
|
|
391
|
-
detail: event,
|
|
392
|
-
bubbles: true,
|
|
393
|
-
composed: true,
|
|
394
|
-
})
|
|
395
|
-
)
|
|
632
|
+
this.dispatchDateEvent(this.selectedDate)
|
|
396
633
|
}
|
|
397
634
|
}
|
|
398
635
|
}
|
|
399
636
|
|
|
400
|
-
private
|
|
637
|
+
private handleCancel(): void {
|
|
401
638
|
// Reset pending state
|
|
402
|
-
this.
|
|
403
|
-
this.
|
|
404
|
-
this.
|
|
639
|
+
this.pendingDate = null
|
|
640
|
+
this.pendingRangeStart = null
|
|
641
|
+
this.pendingRangeEnd = null
|
|
642
|
+
|
|
643
|
+
this.dispatchCancelEvent()
|
|
644
|
+
}
|
|
405
645
|
|
|
406
|
-
|
|
407
|
-
|
|
646
|
+
/**
|
|
647
|
+
* Helper to render navigation buttons
|
|
648
|
+
*/
|
|
649
|
+
private renderNavButton(
|
|
650
|
+
direction: 'prev' | 'next',
|
|
651
|
+
onClick: () => void,
|
|
652
|
+
ariaLabel: string,
|
|
653
|
+
icon: 'chevronLeft' | 'chevronRight'
|
|
654
|
+
): TemplateResult | typeof nothing {
|
|
655
|
+
if (!this.showNavigation || this.showMonthDropdown || this.showYearDropdown) {
|
|
656
|
+
return nothing
|
|
408
657
|
}
|
|
409
658
|
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
}
|
|
416
|
-
|
|
659
|
+
return html`<ui-icon-button
|
|
660
|
+
class="nav-button"
|
|
661
|
+
size="xs"
|
|
662
|
+
@click=${onClick}
|
|
663
|
+
aria-label=${ariaLabel}
|
|
664
|
+
title=${ariaLabel}
|
|
665
|
+
>
|
|
666
|
+
<ui-icon icon=${icon}></ui-icon>
|
|
667
|
+
</ui-icon-button>`
|
|
417
668
|
}
|
|
418
669
|
|
|
419
|
-
private
|
|
420
|
-
const monthName = this.
|
|
670
|
+
private renderNavigation(): TemplateResult {
|
|
671
|
+
const monthName = this.monthNames[this.month] || ''
|
|
421
672
|
|
|
422
673
|
return html`
|
|
423
674
|
<div class="header">
|
|
424
675
|
<div class="month-year">
|
|
425
676
|
<div class="month-selector">
|
|
426
|
-
${this.
|
|
427
|
-
? html`
|
|
428
|
-
<ui-icon-button
|
|
429
|
-
class="nav-button month-nav"
|
|
430
|
-
size="xs"
|
|
431
|
-
@click=${this._handlePrevMonth}
|
|
432
|
-
aria-label="Previous month"
|
|
433
|
-
title="Previous month"
|
|
434
|
-
>
|
|
435
|
-
<ui-icon icon="chevronLeft"></ui-icon>
|
|
436
|
-
</ui-icon-button>
|
|
437
|
-
`
|
|
438
|
-
: ''}
|
|
677
|
+
${this.renderNavButton('prev', this.handlePrevMonth, 'Previous month', 'chevronLeft')}
|
|
439
678
|
<ui-button
|
|
440
679
|
class="month-button"
|
|
441
680
|
color="text"
|
|
442
681
|
size="xs"
|
|
443
|
-
@click=${this.
|
|
682
|
+
@click=${this.handleMonthClick}
|
|
444
683
|
aria-label="Select month"
|
|
445
|
-
aria-expanded=${this.
|
|
684
|
+
aria-expanded=${this.showMonthDropdown}
|
|
446
685
|
trailingIcon
|
|
447
686
|
>
|
|
448
687
|
${monthName}
|
|
449
688
|
<ui-icon icon="arrowDropDown" slot="icon"></ui-icon>
|
|
450
689
|
</ui-button>
|
|
451
|
-
${this.
|
|
452
|
-
? html`
|
|
453
|
-
<ui-icon-button
|
|
454
|
-
class="nav-button month-nav"
|
|
455
|
-
size="xs"
|
|
456
|
-
@click=${this._handleNextMonth}
|
|
457
|
-
aria-label="Next month"
|
|
458
|
-
title="Next month"
|
|
459
|
-
>
|
|
460
|
-
<ui-icon icon="chevronRight"></ui-icon>
|
|
461
|
-
</ui-icon-button>
|
|
462
|
-
`
|
|
463
|
-
: ''}
|
|
690
|
+
${this.renderNavButton('next', this.handleNextMonth, 'Next month', 'chevronRight')}
|
|
464
691
|
</div>
|
|
465
692
|
<div class="year-selector">
|
|
466
|
-
${this.
|
|
467
|
-
? html`
|
|
468
|
-
<ui-icon-button
|
|
469
|
-
class="nav-button year-nav"
|
|
470
|
-
size="xs"
|
|
471
|
-
@click=${this._handlePrevYear}
|
|
472
|
-
aria-label="Previous year"
|
|
473
|
-
title="Previous year"
|
|
474
|
-
>
|
|
475
|
-
<ui-icon icon="chevronLeft"></ui-icon>
|
|
476
|
-
</ui-icon-button>
|
|
477
|
-
`
|
|
478
|
-
: ''}
|
|
693
|
+
${this.renderNavButton('prev', this.handlePrevYear, 'Previous year', 'chevronLeft')}
|
|
479
694
|
<ui-button
|
|
480
695
|
class="year-button"
|
|
481
696
|
color="text"
|
|
482
697
|
size="xs"
|
|
483
|
-
@click=${this.
|
|
698
|
+
@click=${this.handleYearClick}
|
|
484
699
|
aria-label="Select year"
|
|
485
|
-
aria-expanded=${this.
|
|
700
|
+
aria-expanded=${this.showYearDropdown}
|
|
486
701
|
trailingIcon
|
|
487
702
|
>
|
|
488
703
|
${this.year}
|
|
489
704
|
<ui-icon icon="arrowDropDown" slot="icon"></ui-icon>
|
|
490
705
|
</ui-button>
|
|
491
|
-
${this.
|
|
492
|
-
? html`
|
|
493
|
-
<ui-icon-button
|
|
494
|
-
class="nav-button year-nav"
|
|
495
|
-
size="xs"
|
|
496
|
-
@click=${this._handleNextYear}
|
|
497
|
-
aria-label="Next year"
|
|
498
|
-
title="Next year"
|
|
499
|
-
>
|
|
500
|
-
<ui-icon icon="chevronRight"></ui-icon>
|
|
501
|
-
</ui-icon-button>
|
|
502
|
-
`
|
|
503
|
-
: ''}
|
|
706
|
+
${this.renderNavButton('next', this.handleNextYear, 'Next year', 'chevronRight')}
|
|
504
707
|
</div>
|
|
505
708
|
</div>
|
|
506
709
|
</div>
|
|
507
710
|
`
|
|
508
711
|
}
|
|
509
712
|
|
|
510
|
-
private
|
|
511
|
-
if (!this.
|
|
713
|
+
private renderWeekdays(): TemplateResult {
|
|
714
|
+
if (!this.calendarData) return html``
|
|
512
715
|
|
|
513
716
|
return html`
|
|
514
|
-
<div class="weekdays">
|
|
515
|
-
${this.
|
|
717
|
+
<div class="weekdays" role="row">
|
|
718
|
+
${this.calendarData.weekdays.map((weekday) => html`<div class="weekday" role="columnheader">${weekday}</div>`)}
|
|
516
719
|
</div>
|
|
517
720
|
`
|
|
518
721
|
}
|
|
519
722
|
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
day.date >= this._pendingRange.start &&
|
|
530
|
-
day.date <= this._pendingRange.end &&
|
|
531
|
-
!isPendingRangeStart &&
|
|
532
|
-
!isPendingRangeEnd
|
|
533
|
-
|
|
534
|
-
// Determine button color based on selection state
|
|
535
|
-
let color: 'elevated' | 'filled' | 'outlined' | 'text' | 'tonal' = 'text'
|
|
536
|
-
|
|
723
|
+
/**
|
|
724
|
+
* Helper to determine day selection state for rendering
|
|
725
|
+
*/
|
|
726
|
+
private getDaySelectionState(day: CalendarDay): {
|
|
727
|
+
isSelected: boolean
|
|
728
|
+
isRangeStart: boolean
|
|
729
|
+
isRangeEnd: boolean
|
|
730
|
+
isInRange: boolean
|
|
731
|
+
} {
|
|
537
732
|
if (this.showActions) {
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
733
|
+
// Use pending state
|
|
734
|
+
const isPendingSelected = this.pendingDate && isSameDay(day.date, this.pendingDate)
|
|
735
|
+
const isPendingRangeStart = this.pendingRangeStart && isSameDay(day.date, this.pendingRangeStart)
|
|
736
|
+
const isPendingRangeEnd = this.pendingRangeEnd && isSameDay(day.date, this.pendingRangeEnd)
|
|
737
|
+
const isPendingInRange =
|
|
738
|
+
this.pendingRangeStart &&
|
|
739
|
+
this.pendingRangeEnd &&
|
|
740
|
+
day.date >= this.pendingRangeStart &&
|
|
741
|
+
day.date <= this.pendingRangeEnd &&
|
|
742
|
+
!isPendingRangeStart &&
|
|
743
|
+
!isPendingRangeEnd
|
|
744
|
+
|
|
745
|
+
return {
|
|
746
|
+
isSelected: !!isPendingSelected,
|
|
747
|
+
isRangeStart: !!isPendingRangeStart,
|
|
748
|
+
isRangeEnd: !!isPendingRangeEnd,
|
|
749
|
+
isInRange: !!isPendingInRange,
|
|
542
750
|
}
|
|
543
751
|
} else {
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
752
|
+
// Use immediate state
|
|
753
|
+
return {
|
|
754
|
+
isSelected: day.isSelected,
|
|
755
|
+
isRangeStart: day.isRangeStart,
|
|
756
|
+
isRangeEnd: day.isRangeEnd,
|
|
757
|
+
isInRange: day.isInRange,
|
|
548
758
|
}
|
|
549
759
|
}
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
/**
|
|
763
|
+
* Helper to determine button color for a day
|
|
764
|
+
*/
|
|
765
|
+
private getDayButtonColor(
|
|
766
|
+
day: CalendarDay,
|
|
767
|
+
selectionState: ReturnType<typeof this.getDaySelectionState>
|
|
768
|
+
): 'elevated' | 'filled' | 'outlined' | 'text' | 'tonal' {
|
|
769
|
+
if (selectionState.isRangeStart || selectionState.isRangeEnd || selectionState.isSelected) {
|
|
770
|
+
return 'filled'
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
if (selectionState.isInRange) {
|
|
774
|
+
return 'text'
|
|
775
|
+
}
|
|
550
776
|
|
|
551
|
-
if (day.isToday
|
|
552
|
-
|
|
777
|
+
if (day.isToday) {
|
|
778
|
+
return 'outlined'
|
|
553
779
|
}
|
|
554
780
|
|
|
781
|
+
return 'text'
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
private renderDay(day: CalendarDay): TemplateResult {
|
|
785
|
+
const selectionState = this.getDaySelectionState(day)
|
|
786
|
+
const color = this.getDayButtonColor(day, selectionState)
|
|
787
|
+
|
|
555
788
|
const classes = {
|
|
556
789
|
'day-cell': true,
|
|
557
790
|
'other-month': !day.isCurrentMonth,
|
|
558
791
|
'today': day.isToday,
|
|
559
|
-
'in-range':
|
|
560
|
-
'range-start':
|
|
561
|
-
'range-end':
|
|
562
|
-
'has-complete-range': this.
|
|
563
|
-
? !!(this._pendingRange?.start && this._pendingRange?.end)
|
|
564
|
-
: !!(this.selectedRange?.start && this.selectedRange?.end),
|
|
792
|
+
'in-range': selectionState.isInRange,
|
|
793
|
+
'range-start': selectionState.isRangeStart,
|
|
794
|
+
'range-end': selectionState.isRangeEnd,
|
|
795
|
+
'has-complete-range': this.hasCompleteRange(),
|
|
565
796
|
}
|
|
566
797
|
|
|
798
|
+
const isFocused = this.focusedDate && isSameDay(day.date, this.focusedDate)
|
|
799
|
+
|
|
567
800
|
return html`
|
|
568
|
-
<div class=${classMap(classes)}>
|
|
801
|
+
<div class=${classMap(classes)} role="gridcell">
|
|
569
802
|
<ui-button
|
|
570
803
|
class="day-button"
|
|
571
804
|
color=${color}
|
|
572
805
|
size="s"
|
|
573
806
|
data-date=${day.date.toISOString().split('T')[0]}
|
|
574
|
-
tabindex=${
|
|
807
|
+
tabindex=${isFocused && !(day.isDisabled || this.isDateDisabled(day.date)) ? '0' : '-1'}
|
|
575
808
|
aria-label=${formatDate(day.date, this.locale)}
|
|
576
|
-
|
|
577
|
-
|
|
809
|
+
aria-selected=${selectionState.isSelected || selectionState.isRangeStart || selectionState.isRangeEnd}
|
|
810
|
+
@click=${() => this.handleDayClick(day)}
|
|
811
|
+
?disabled=${day.isDisabled || this.isDateDisabled(day.date)}
|
|
578
812
|
>
|
|
579
813
|
${day.date.getDate()}
|
|
580
814
|
</ui-button>
|
|
@@ -582,50 +816,63 @@ export class UiDatePickerCalendar extends LitElement {
|
|
|
582
816
|
`
|
|
583
817
|
}
|
|
584
818
|
|
|
585
|
-
private
|
|
586
|
-
if (!this.
|
|
819
|
+
private renderDays(): TemplateResult {
|
|
820
|
+
if (!this.calendarData) return html``
|
|
587
821
|
|
|
588
|
-
return html`<div class="days">${this.
|
|
822
|
+
return html`<div class="days">${this.calendarData.days.map((day) => this.renderDay(day))}</div>`
|
|
589
823
|
}
|
|
590
824
|
|
|
591
|
-
private
|
|
825
|
+
private renderActions(): TemplateResult | typeof nothing {
|
|
592
826
|
if (!this.showActions) return nothing
|
|
593
827
|
|
|
594
|
-
const hasSelection = this.rangeSelection ? this.
|
|
828
|
+
const hasSelection = this.rangeSelection ? this.pendingRangeStart : this.pendingDate
|
|
595
829
|
|
|
596
830
|
return html`
|
|
597
831
|
<div class="actions">
|
|
598
|
-
<ui-button size="s" color="text" @click=${this.
|
|
599
|
-
<ui-button size="s" color="text" @click=${this.
|
|
832
|
+
<ui-button size="s" color="text" @click=${this.handleCancel}>${this.cancelButtonText}</ui-button>
|
|
833
|
+
<ui-button size="s" color="text" @click=${this.handleConfirm} ?disabled=${!hasSelection}>
|
|
600
834
|
${this.okButtonText}
|
|
601
835
|
</ui-button>
|
|
602
836
|
</div>
|
|
603
837
|
`
|
|
604
838
|
}
|
|
605
839
|
|
|
606
|
-
|
|
840
|
+
/**
|
|
841
|
+
* Helper to render dropdown option buttons
|
|
842
|
+
*/
|
|
843
|
+
private renderDropdownOption(
|
|
844
|
+
value: string | number,
|
|
845
|
+
label: string,
|
|
846
|
+
isSelected: boolean,
|
|
847
|
+
onClick: () => void,
|
|
848
|
+
className = ''
|
|
849
|
+
): TemplateResult {
|
|
850
|
+
return html`
|
|
851
|
+
<ui-button
|
|
852
|
+
class="dropdown-option ${className} ${isSelected ? 'selected' : ''}"
|
|
853
|
+
color=${isSelected ? 'filled' : 'text'}
|
|
854
|
+
size="s"
|
|
855
|
+
@click=${onClick}
|
|
856
|
+
aria-label=${label}
|
|
857
|
+
>
|
|
858
|
+
${label}
|
|
859
|
+
</ui-button>
|
|
860
|
+
`
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
private renderMonthDropdown(): TemplateResult {
|
|
607
864
|
return html`
|
|
608
865
|
<div class="dropdown-view">
|
|
609
866
|
<div class="month-list">
|
|
610
|
-
${this.
|
|
611
|
-
(monthName, index) =>
|
|
612
|
-
<ui-button
|
|
613
|
-
class="month-option ${index === this.month ? 'selected' : ''}"
|
|
614
|
-
color=${index === this.month ? 'filled' : 'text'}
|
|
615
|
-
size="s"
|
|
616
|
-
@click=${() => this._handleMonthSelect(index)}
|
|
617
|
-
aria-label=${monthName}
|
|
618
|
-
>
|
|
619
|
-
${monthName}
|
|
620
|
-
</ui-button>
|
|
621
|
-
`
|
|
867
|
+
${this.monthNames.map((monthName, index) =>
|
|
868
|
+
this.renderDropdownOption(index, monthName, index === this.month, () => this.handleMonthSelect(index))
|
|
622
869
|
)}
|
|
623
870
|
</div>
|
|
624
871
|
</div>
|
|
625
872
|
`
|
|
626
873
|
}
|
|
627
874
|
|
|
628
|
-
private
|
|
875
|
+
private renderYearDropdown(): TemplateResult {
|
|
629
876
|
const currentYear = this.year
|
|
630
877
|
const startYear = currentYear - 50
|
|
631
878
|
const endYear = currentYear + 50
|
|
@@ -638,46 +885,133 @@ export class UiDatePickerCalendar extends LitElement {
|
|
|
638
885
|
return html`
|
|
639
886
|
<div class="dropdown-view">
|
|
640
887
|
<div class="year-grid">
|
|
641
|
-
${years.map(
|
|
642
|
-
(
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
>
|
|
650
|
-
${year}
|
|
651
|
-
</ui-button>
|
|
652
|
-
`
|
|
888
|
+
${years.map((year) =>
|
|
889
|
+
this.renderDropdownOption(
|
|
890
|
+
year,
|
|
891
|
+
year.toString(),
|
|
892
|
+
year === this.year,
|
|
893
|
+
() => this.handleYearSelect(year),
|
|
894
|
+
'year-option'
|
|
895
|
+
)
|
|
653
896
|
)}
|
|
654
897
|
</div>
|
|
655
898
|
</div>
|
|
656
899
|
`
|
|
657
900
|
}
|
|
658
901
|
|
|
902
|
+
private findFirstAvailableDate(): Date {
|
|
903
|
+
const firstDayOfMonth = new Date(this.year, this.month, 1)
|
|
904
|
+
for (let i = 0; i < 31; i++) {
|
|
905
|
+
const date = addDays(firstDayOfMonth, i)
|
|
906
|
+
if (date.getMonth() !== this.month) break // Next month
|
|
907
|
+
if (!this.isDateDisabled(date)) {
|
|
908
|
+
return date
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
return firstDayOfMonth // Fallback
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
private handleKeyDown(event: KeyboardEvent): void {
|
|
915
|
+
// Prevent default behavior for navigation keys
|
|
916
|
+
const navigationKeys = ['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown', 'Home', 'End', 'PageUp', 'PageDown', ' ']
|
|
917
|
+
if (navigationKeys.includes(event.key)) {
|
|
918
|
+
event.preventDefault()
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
switch (event.key) {
|
|
922
|
+
case 'ArrowLeft':
|
|
923
|
+
this.navigateDate(-1)
|
|
924
|
+
break
|
|
925
|
+
case 'ArrowRight':
|
|
926
|
+
this.navigateDate(1)
|
|
927
|
+
break
|
|
928
|
+
case 'ArrowUp':
|
|
929
|
+
this.navigateDate(-7)
|
|
930
|
+
break
|
|
931
|
+
case 'ArrowDown':
|
|
932
|
+
this.navigateDate(7)
|
|
933
|
+
break
|
|
934
|
+
case 'Enter':
|
|
935
|
+
case ' ':
|
|
936
|
+
this.selectFocusedDate()
|
|
937
|
+
break
|
|
938
|
+
case 'Home':
|
|
939
|
+
this.focusFirstDayOfMonth()
|
|
940
|
+
break
|
|
941
|
+
case 'End':
|
|
942
|
+
this.focusLastDayOfMonth()
|
|
943
|
+
break
|
|
944
|
+
case 'PageUp':
|
|
945
|
+
if (event.shiftKey) {
|
|
946
|
+
this.handlePrevYear()
|
|
947
|
+
} else {
|
|
948
|
+
this.navigateMonth(-1)
|
|
949
|
+
}
|
|
950
|
+
break
|
|
951
|
+
case 'PageDown':
|
|
952
|
+
if (event.shiftKey) {
|
|
953
|
+
this.handleNextYear()
|
|
954
|
+
} else {
|
|
955
|
+
this.navigateMonth(1)
|
|
956
|
+
}
|
|
957
|
+
break
|
|
958
|
+
case 'Escape':
|
|
959
|
+
this.closeDropdowns()
|
|
960
|
+
break
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
/**
|
|
965
|
+
* Helper to check if a date is in the current month and not disabled
|
|
966
|
+
*/
|
|
967
|
+
private isDateAvailable(date: Date): boolean {
|
|
968
|
+
return date.getMonth() === this.month && date.getFullYear() === this.year && !this.isDateDisabled(date)
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
private initializeFocusedDate(): void {
|
|
972
|
+
// Priority: selectedDate, rangeStart, today, first available date
|
|
973
|
+
const candidates = [this.selectedDate, this.rangeStart, new Date()].filter(Boolean) as Date[]
|
|
974
|
+
|
|
975
|
+
for (const candidate of candidates) {
|
|
976
|
+
if (this.isDateAvailable(candidate)) {
|
|
977
|
+
this.focusedDate = candidate
|
|
978
|
+
return
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
// Fallback to first available date in current month
|
|
983
|
+
this.focusedDate = this.findFirstAvailableDate()
|
|
984
|
+
}
|
|
985
|
+
|
|
659
986
|
override render(): TemplateResult {
|
|
660
987
|
// Show dropdown views instead of calendar when dropdowns are open
|
|
661
|
-
if (this.
|
|
988
|
+
if (this.showMonthDropdown) {
|
|
662
989
|
return html`
|
|
663
|
-
<div class="calendar" role="grid" aria-label="
|
|
664
|
-
${this.
|
|
990
|
+
<div class="calendar" role="grid" aria-label="Month selection for ${this.year}">
|
|
991
|
+
${this.renderNavigation()} ${this.renderMonthDropdown()}
|
|
665
992
|
</div>
|
|
666
993
|
`
|
|
667
994
|
}
|
|
668
995
|
|
|
669
|
-
if (this.
|
|
996
|
+
if (this.showYearDropdown) {
|
|
670
997
|
return html`
|
|
671
|
-
<div class="calendar" role="grid" aria-label="
|
|
672
|
-
${this.
|
|
998
|
+
<div class="calendar" role="grid" aria-label="Year selection">
|
|
999
|
+
${this.renderNavigation()} ${this.renderYearDropdown()}
|
|
673
1000
|
</div>
|
|
674
1001
|
`
|
|
675
1002
|
}
|
|
676
1003
|
|
|
677
1004
|
// Default calendar view
|
|
678
1005
|
return html`
|
|
679
|
-
<div
|
|
680
|
-
|
|
1006
|
+
<div
|
|
1007
|
+
class="calendar"
|
|
1008
|
+
role="grid"
|
|
1009
|
+
aria-label="Calendar for ${this.monthNames[this.month]} ${this.year}"
|
|
1010
|
+
aria-roledescription="Calendar grid"
|
|
1011
|
+
tabindex="0"
|
|
1012
|
+
@keydown=${this.handleKeyDown}
|
|
1013
|
+
>
|
|
1014
|
+
${this.renderNavigation()} ${this.renderWeekdays()} ${this.renderDays()} ${this.renderActions()}
|
|
681
1015
|
</div>
|
|
682
1016
|
`
|
|
683
1017
|
}
|