noph-ui 0.52.0 → 0.53.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.
Files changed (103) hide show
  1. package/dist/animation.d.ts +1 -13
  2. package/dist/animation.js +18 -13
  3. package/dist/button/Button.svelte +2 -14
  4. package/dist/button/IconButton.svelte +5 -18
  5. package/dist/button/SegmentedButton.svelte +8 -11
  6. package/dist/card/Card.svelte +3 -19
  7. package/dist/card/types.d.ts +12 -2
  8. package/dist/carousel/Carousel.svelte +77 -14
  9. package/dist/carousel/CarouselItem.svelte +26 -14
  10. package/dist/carousel/carouselMorph.js +1 -1
  11. package/dist/carousel/carouselStrategy.js +25 -27
  12. package/dist/checkbox/Checkbox.svelte +1 -8
  13. package/dist/chip/FilterChip.svelte +1 -13
  14. package/dist/date-picker/Calendar.svelte +31 -30
  15. package/dist/date-picker/Calendar.svelte.d.ts +2 -0
  16. package/dist/date-picker/CalendarHeader.svelte +84 -62
  17. package/dist/date-picker/CalendarHeader.svelte.d.ts +3 -0
  18. package/dist/date-picker/DatePickerDialog.svelte +134 -75
  19. package/dist/date-picker/DatePickerDialog.svelte.d.ts +4 -3
  20. package/dist/date-picker/DateRangePicker.svelte +226 -78
  21. package/dist/date-picker/DateRangePicker.svelte.d.ts +3 -3
  22. package/dist/date-picker/DockedDatePicker.svelte +76 -464
  23. package/dist/date-picker/DockedDatePicker.svelte.d.ts +2 -2
  24. package/dist/date-picker/DockedDateTimePicker.svelte +128 -525
  25. package/dist/date-picker/DockedDateTimePicker.svelte.d.ts +2 -2
  26. package/dist/date-picker/DockedPickerField.svelte +221 -0
  27. package/dist/date-picker/DockedPickerField.svelte.d.ts +9 -0
  28. package/dist/date-picker/MonthPanel.svelte +291 -0
  29. package/dist/date-picker/MonthPanel.svelte.d.ts +12 -0
  30. package/dist/date-picker/SelectionList.svelte +7 -6
  31. package/dist/date-picker/SelectionList.svelte.d.ts +1 -0
  32. package/dist/date-picker/TimeColumn.svelte +2 -6
  33. package/dist/date-picker/TimeColumn.svelte.d.ts +1 -0
  34. package/dist/date-picker/YearGrid.svelte +2 -11
  35. package/dist/date-picker/YearGrid.svelte.d.ts +1 -0
  36. package/dist/date-picker/dateUtils.d.ts +4 -0
  37. package/dist/date-picker/dateUtils.js +15 -88
  38. package/dist/date-picker/exitVisibility.svelte.js +13 -6
  39. package/dist/date-picker/formatters.d.ts +16 -0
  40. package/dist/date-picker/formatters.js +48 -15
  41. package/dist/date-picker/internalTypes.d.ts +59 -1
  42. package/dist/date-picker/shortcuts.d.ts +11 -0
  43. package/dist/date-picker/shortcuts.js +40 -0
  44. package/dist/date-picker/timeUtils.d.ts +8 -3
  45. package/dist/date-picker/timeUtils.js +33 -51
  46. package/dist/date-picker/types.d.ts +22 -0
  47. package/dist/icons/Icon.svelte +1 -4
  48. package/dist/internal/exit.css +0 -14
  49. package/dist/internal/field.css +337 -0
  50. package/dist/internal/focus-ring.css +23 -8
  51. package/dist/internal/pop-in.css +19 -0
  52. package/dist/internal/visually-hidden.css +11 -0
  53. package/dist/list/Item.svelte +32 -18
  54. package/dist/menu/Menu.svelte +6 -25
  55. package/dist/menu/Menu.svelte.d.ts +1 -0
  56. package/dist/navigation-bar/NavigationBarItem.svelte +6 -30
  57. package/dist/navigation-drawer/NavigationDrawerItem.svelte +4 -11
  58. package/dist/navigation-rail/NavigationRailItem.svelte +2 -13
  59. package/dist/press.svelte.d.ts +0 -1
  60. package/dist/press.svelte.js +0 -11
  61. package/dist/progress/CircularProgress.svelte +26 -81
  62. package/dist/progress/CircularProgress.svelte.d.ts +1 -0
  63. package/dist/progress/LinearProgress.svelte +19 -151
  64. package/dist/progress/LinearProgress.svelte.d.ts +1 -0
  65. package/dist/progress/four-color.css +34 -0
  66. package/dist/radio/Radio.svelte +6 -9
  67. package/dist/ripple/Ripple.svelte +8 -16
  68. package/dist/search/Search.svelte +2 -9
  69. package/dist/search/Search.svelte.d.ts +1 -0
  70. package/dist/select/Check.svelte +8 -31
  71. package/dist/select/Check.svelte.d.ts +1 -1
  72. package/dist/select/NativeSelect.svelte +97 -366
  73. package/dist/select/NativeSelect.svelte.d.ts +1 -0
  74. package/dist/select/Option.svelte +1 -9
  75. package/dist/select/Select.svelte +139 -746
  76. package/dist/select/Select.svelte.d.ts +1 -0
  77. package/dist/select/VirtualList.svelte +0 -1
  78. package/dist/sheet/Sheet.svelte +11 -22
  79. package/dist/slider/Slider.svelte +13 -37
  80. package/dist/slider/Slider.svelte.d.ts +1 -0
  81. package/dist/switch/Switch.svelte +2 -8
  82. package/dist/tabs/Tab.svelte +2 -4
  83. package/dist/text-field/TextField.svelte +81 -609
  84. package/dist/text-field/TextField.svelte.d.ts +1 -0
  85. package/dist/time-picker/ClockDial.svelte +8 -25
  86. package/dist/time-picker/ClockDial.svelte.d.ts +2 -0
  87. package/dist/time-picker/DockedTimePicker.svelte +102 -244
  88. package/dist/time-picker/DockedTimePicker.svelte.d.ts +2 -2
  89. package/dist/time-picker/PeriodSelector.svelte +8 -7
  90. package/dist/time-picker/PeriodSelector.svelte.d.ts +1 -0
  91. package/dist/time-picker/TimeInputs.svelte +13 -2
  92. package/dist/time-picker/TimePickerDialog.svelte +32 -26
  93. package/dist/time-picker/TimePickerDialog.svelte.d.ts +2 -2
  94. package/dist/time-picker/TimePickerPanel.svelte +26 -4
  95. package/dist/time-picker/TimeSelectors.svelte +4 -9
  96. package/dist/time-picker/TimeSelectors.svelte.d.ts +1 -0
  97. package/dist/time-picker/internalTypes.d.ts +2 -0
  98. package/dist/time-picker/timePickerState.svelte.js +3 -9
  99. package/dist/tooltip/RichTooltip.svelte +2 -23
  100. package/dist/tooltip/RichTooltip.svelte.d.ts +1 -0
  101. package/dist/tooltip/Tooltip.svelte +2 -24
  102. package/dist/tooltip/Tooltip.svelte.d.ts +1 -0
  103. package/package.json +12 -12
@@ -1,23 +1,13 @@
1
1
  <script lang="ts">
2
- import { exitVisibility } from './exitVisibility.svelte.js'
3
2
  import Button from '../button/Button.svelte'
4
- import IconButton from '../button/IconButton.svelte'
5
3
  import Divider from '../divider/Divider.svelte'
6
4
  import CalendarToday from '../icons/CalendarToday.svelte'
7
- import Menu from '../menu/Menu.svelte'
8
- import { formReset } from '../form-reset.js'
9
- import { syncOpenEffect } from '../popover.svelte.js'
10
- import TextField from '../text-field/TextField.svelte'
11
5
  import { onMount, tick } from 'svelte'
12
- import Calendar from './Calendar.svelte'
13
- import CalendarHeader from './CalendarHeader.svelte'
14
- import SelectionList from './SelectionList.svelte'
6
+ import DockedPickerField from './DockedPickerField.svelte'
7
+ import MonthPanel from './MonthPanel.svelte'
15
8
  import TimeColumn from './TimeColumn.svelte'
9
+ import { clampMinutes, isHourReachable, isMinuteWithin, stepOf } from './timeUtils.js'
16
10
  import {
17
- addMonths,
18
- clampMonthInYear,
19
- compareDays,
20
- compareTimes,
21
11
  DEFAULT_YEAR_RANGE,
22
12
  formatDateTime,
23
13
  formatTwoDigits,
@@ -25,19 +15,16 @@
25
15
  getDayPeriodLabels,
26
16
  getFirstDayOfWeek,
27
17
  getHourLabels,
28
- getMonthNames,
29
- getWeekRowCount,
30
18
  HOURS_IN_DAY,
31
- isMonthWithin,
32
19
  isSameDay,
33
20
  isTimeWithin,
34
- isYearWithin,
35
21
  MINUTES_IN_DAY,
36
22
  MINUTES_IN_HOUR,
37
23
  minutesOfDay,
38
24
  parseDateTimeInput,
39
25
  parseISODate,
40
26
  parseISODateTime,
27
+ parseISOTime,
41
28
  startOfMonth,
42
29
  toISODate,
43
30
  toISODateTime,
@@ -52,8 +39,6 @@
52
39
  defaultValue,
53
40
  // eslint-disable-next-line @typescript-eslint/no-unused-vars -- pulled out of the rest props on purpose
54
41
  type,
55
- 'aria-invalid': ariaInvalid,
56
- style,
57
42
  displayMonth = $bindable(),
58
43
  open = $bindable(false),
59
44
  element = $bindable(),
@@ -65,15 +50,7 @@
65
50
  isDateEnabled,
66
51
  adjacentMonthDays = false,
67
52
  label = 'Date and time',
68
- supportingText,
69
- issues,
70
53
  variant = 'outlined',
71
- name,
72
- form,
73
- required,
74
- disabled,
75
- readonly,
76
- noAsterisk,
77
54
  autocomplete = 'off',
78
55
  minuteStep = 5,
79
56
  hour12: hourCycle,
@@ -89,37 +66,23 @@
89
66
  openCalendarLabel = 'Show date and time picker',
90
67
  invalidDateMessage = 'Enter a valid date and time.',
91
68
  selectedDateLabel = 'selected',
69
+ shortcutLabels,
92
70
  hourLabel = 'Hour',
93
71
  minuteLabel = 'Minute',
94
72
  dayPeriodLabel = 'AM or PM',
95
- onchange,
96
- oninput,
97
- onblur,
98
73
  ...attributes
99
74
  }: DockedDateTimePickerProps = $props()
100
75
 
101
76
  const ISO_PATTERN = 'YYYY-MM-DDTHH:mm'
102
77
  const HALF_DAY = (HOURS_IN_DAY / 2) * MINUTES_IN_HOUR
103
78
 
104
- const uid = $props.id()
105
-
106
- let menuElement = $state<HTMLDivElement>()
107
- // Keeps the content while the menu animates out, so it fades as a whole, not as an empty box.
108
- const exit = exitVisibility()
109
- let mode = $state<'days' | 'months' | 'years'>('days')
110
- let listMode = $state<'months' | 'years' | undefined>(undefined)
79
+ let field = $state<DockedPickerField>()
80
+ let panel = $state<MonthPanel>()
111
81
  let pendingDay = $state<ISODate | undefined>(undefined)
112
82
  let pendingMinutes = $state(0)
113
- let focusedDay = $state<Date | undefined>(undefined)
114
- let inputElement = $state<HTMLInputElement | HTMLTextAreaElement>()
115
- let valueInput = $state<HTMLInputElement>()
116
-
117
- let typed = $state.raw<{ source: string | null | undefined; text: string } | undefined>()
118
- let invalidText = $state<string | undefined>()
119
83
  let monthBeforeOpen: ISODate | undefined
120
84
 
121
85
  let todayValue = $derived(getToday())
122
- let week = $derived(firstDayOfWeek ?? getFirstDayOfWeek(locale))
123
86
  let minDate = $derived(parseISODateTime(min))
124
87
  let maxDate = $derived(parseISODateTime(max, MINUTES_IN_DAY - 1))
125
88
  let current = $derived<string | undefined>(
@@ -143,109 +106,37 @@
143
106
  let monthDate = $derived(
144
107
  parseISODate(displayMonth) ?? startOfMonth(pendingDate ?? committed ?? todayValue),
145
108
  )
146
- let monthNames = $derived(getMonthNames(locale, 'long'))
147
- let shortMonthNames = $derived(getMonthNames(locale, 'short'))
148
- let rowCount = $derived(getWeekRowCount(monthDate, week))
149
-
150
- let defaultMinutes = $derived.by(() => {
151
- const parsed = parseISODateTime(`2000-01-01T${defaultTime}`)
152
- return parsed ? minutesOfDay(parsed) : 0
153
- })
154
-
155
- const setMode = (next: 'days' | 'months' | 'years') => {
156
- if (mode === next) return
157
- mode = next
158
- if (next !== 'days') listMode = next
159
- }
160
-
161
- const leaveList = async () => {
162
- setMode('days')
163
- await tick()
164
- focusCalendar()
165
- }
166
-
167
- const formatValue = () => {
168
- if (!committed) return ''
169
- return localeKnown ? formatDateTime(committed, locale, hour12) : toISODateTime(committed)
170
- }
171
-
172
- let text = $derived(typed && typed.source === current ? typed.text : formatValue())
173
-
174
- let inputInvalid = $derived(invalidText !== undefined && invalidText === text)
175
-
176
- let monthOptions = $derived(
177
- monthNames.map((monthName, index) => ({
178
- value: index,
179
- label: monthName,
180
- disabled: !isMonthWithin(monthDate.getFullYear(), index, minDate, maxDate),
181
- })),
182
- )
183
-
184
- let yearOptions = $derived(
185
- Array.from({ length: yearRange[1] - yearRange[0] + 1 }, (_, index) => {
186
- const year = yearRange[0] + index
187
- return {
188
- value: year,
189
- label: `${year}`,
190
- disabled: !isYearWithin(year, minDate, maxDate),
191
- }
192
- }),
193
- )
194
-
195
- let canPreviousMonth = $derived(
196
- !minDate ||
197
- compareDays(new Date(monthDate.getFullYear(), monthDate.getMonth(), 0), minDate) >= 0,
198
- )
199
- let canNextMonth = $derived(
200
- !maxDate ||
201
- compareDays(new Date(monthDate.getFullYear(), monthDate.getMonth() + 1, 1), maxDate) <= 0,
202
- )
203
- let canPreviousYear = $derived(
204
- monthDate.getFullYear() > yearRange[0] &&
205
- (!minDate || compareDays(addMonths(monthDate, -12), startOfMonth(minDate)) >= 0),
206
- )
207
- let canNextYear = $derived(
208
- monthDate.getFullYear() < yearRange[1] &&
209
- (!maxDate || compareDays(addMonths(monthDate, 12), startOfMonth(maxDate)) <= 0),
210
- )
109
+ let defaultMinutes = $derived(parseISOTime(defaultTime) ?? 0)
211
110
 
212
111
  let pendingHour = $derived(Math.floor(pendingMinutes / MINUTES_IN_HOUR))
213
112
  let pendingPeriod = $derived(pendingMinutes < HALF_DAY ? 0 : 1)
214
113
  let periodLabels = $derived(getDayPeriodLabels(locale))
215
114
  let hourLabels = $derived(getHourLabels(locale, hour12))
216
115
 
217
- const spanOffered = (from: number, to: number) => {
218
- if (!pendingDayDate) return true
219
- const start = withMinutes(pendingDayDate, from)
220
- const end = withMinutes(pendingDayDate, to)
221
- return (
222
- (!minDate || compareTimes(end, minDate) >= 0) &&
223
- (!maxDate || compareTimes(start, maxDate) <= 0)
224
- )
225
- }
116
+ // The bounds hold the time only on their own days.
117
+ const boundsOn = (day: Date | undefined) => ({
118
+ min: day && minDate && isSameDay(day, minDate) ? minutesOfDay(minDate) : undefined,
119
+ max: day && maxDate && isSameDay(day, maxDate) ? minutesOfDay(maxDate) : undefined,
120
+ })
121
+ let bounds = $derived(boundsOn(pendingDayDate))
122
+ let step = $derived(stepOf(minuteStep))
123
+ const usable = (minutes: number) => isMinuteWithin(minutes, bounds.min, bounds.max)
124
+ const reachable = (hour: number) => isHourReachable(hour, step, usable)
226
125
 
227
126
  let hourOptions = $derived(
228
127
  hourLabels.map((hourText, index) => {
229
128
  const hour = hour12 ? pendingPeriod * 12 + index : index
230
- return {
231
- value: hour,
232
- label: hourText,
233
- disabled: !spanOffered(
234
- hour * MINUTES_IN_HOUR,
235
- hour * MINUTES_IN_HOUR + MINUTES_IN_HOUR - 1,
236
- ),
237
- }
129
+ return { value: hour, label: hourText, disabled: !reachable(hour) }
238
130
  }),
239
131
  )
240
132
 
241
133
  let minuteOptions = $derived(
242
- Array.from({ length: Math.ceil(MINUTES_IN_HOUR / minuteStep) }, (_, index) => {
243
- const minute = index * minuteStep
244
- const at = pendingHour * MINUTES_IN_HOUR + minute
134
+ Array.from({ length: Math.ceil(MINUTES_IN_HOUR / step) }, (_, index) => {
135
+ const minute = index * step
245
136
  return {
246
137
  value: minute,
247
138
  label: formatTwoDigits(minute, locale),
248
- disabled: !spanOffered(at, at),
139
+ disabled: !usable(pendingHour * MINUTES_IN_HOUR + minute),
249
140
  }
250
141
  }),
251
142
  )
@@ -254,7 +145,7 @@
254
145
  periodLabels.map((periodText, index) => ({
255
146
  value: index,
256
147
  label: periodText,
257
- disabled: !spanOffered(index * HALF_DAY, index * HALF_DAY + HALF_DAY - 1),
148
+ disabled: !Array.from({ length: 12 }, (_, hour) => index * 12 + hour).some(reachable),
258
149
  })),
259
150
  )
260
151
 
@@ -262,354 +153,141 @@
262
153
  displayMonth = toISODate(startOfMonth(date))
263
154
  }
264
155
 
265
- const commit = (next: string | undefined) => {
266
- if (current === next) return
267
- value = next
268
- onchange?.(next)
269
- }
270
-
271
- const restoreDefault = () => {
272
- const next = typeof defaultValue === 'string' ? defaultValue : undefined
273
- if (valueInput) valueInput.value = next ?? ''
274
- queueMicrotask(() => {
275
- typed = undefined
276
- invalidText = undefined
277
- pendingDay = undefined
278
- pendingMinutes = 0
279
- value = next
280
- text = formatValue()
281
- })
282
- }
283
-
284
- const isShowing = () => !!menuElement?.matches(':popover-open')
285
-
286
- const openPicker = () => {
287
- if (isShowing()) return
288
- if (disabled || readonly) {
289
- open = false
290
- return
291
- }
292
- menuElement?.showPopover()
293
- }
294
-
295
- const focusCalendarWhenReady = async () => {
296
- await tick()
297
- focusCalendar()
298
- }
299
-
300
- export const show = () => openPicker()
301
-
302
- export const close = () => {
303
- if (isShowing()) menuElement?.hidePopover()
304
- }
305
-
306
- syncOpenEffect(
307
- () => menuElement,
308
- () => open,
309
- show,
310
- close,
311
- )
312
-
313
- const focusCalendar = () => {
314
- menuElement?.querySelector<HTMLElement>('.np-calendar-day[tabindex="0"]')?.focus()
315
- }
316
-
317
- const closePicker = (restoreFocus = true) => {
318
- menuElement?.hidePopover()
319
- if (restoreFocus) {
320
- element?.querySelector<HTMLElement>('input')?.focus()
321
- }
322
- }
323
-
324
- const clampMinutesToDay = (day: Date, minutes: number) => {
325
- let next = minutes
326
- if (minDate && isSameDay(day, minDate)) next = Math.max(next, minutesOfDay(minDate))
327
- if (maxDate && isSameDay(day, maxDate)) next = Math.min(next, minutesOfDay(maxDate))
328
- return next
329
- }
330
-
331
156
  const selectDay = (date: Date) => {
157
+ const { min, max } = boundsOn(date)
332
158
  pendingDay = toISODate(date)
333
- pendingMinutes = clampMinutesToDay(date, pendingMinutes)
159
+ pendingMinutes = clampMinutes(pendingMinutes, min, max)
334
160
  setMonth(date)
335
- focusedDay = date
336
161
  }
337
162
 
338
163
  const setTime = (minutes: number) => {
339
- pendingMinutes = pendingDayDate ? clampMinutesToDay(pendingDayDate, minutes) : minutes
164
+ pendingMinutes = clampMinutes(minutes, bounds.min, bounds.max)
340
165
  }
341
166
 
342
- const confirm = () => {
343
- typed = undefined
344
- invalidText = undefined
345
- commit(pendingDate ? toISODateTime(pendingDate) : undefined)
346
- closePicker()
167
+ const usableDate = (text: string) => {
168
+ const parsed = parseDateTimeInput(text, locale, hour12)
169
+ if (!parsed || !isTimeWithin(parsed, minDate, maxDate)) return undefined
170
+ return (isDateEnabled?.(parsed) ?? true) ? toISODateTime(parsed) : undefined
347
171
  }
348
172
 
349
- const usableDate = (typed: string) => {
350
- const parsed = parseDateTimeInput(typed, locale, hour12)
351
- if (!parsed) return undefined
352
- if (!isTimeWithin(parsed, minDate, maxDate)) return undefined
353
- return (isDateEnabled?.(parsed) ?? true) ? parsed : undefined
354
- }
355
-
356
- const handleInput = (event: Event) => {
357
- const next = (event.currentTarget as HTMLInputElement).value
358
- const parsed = usableDate(next)
359
- commit(parsed ? toISODateTime(parsed) : undefined)
360
- if (parsed) setMonth(parsed)
361
- typed = { source: current, text: next }
362
- invalidText = undefined
363
- }
364
-
365
- const handleBlur = () => {
366
- if (!text.trim()) {
367
- invalidText = undefined
368
- return
369
- }
370
- const parsed = usableDate(text)
371
- invalidText = parsed ? undefined : text
372
- if (parsed) typed = undefined
373
- }
374
-
375
- $effect(() => {
376
- const input = inputElement
377
- if (!input) return
378
- const unusable = text.trim() !== '' && !usableDate(text)
379
- input.setCustomValidity(unusable ? invalidDateMessage : '')
380
- })
381
-
382
- const chooseMonth = (month: number) => {
383
- setMonth(new Date(monthDate.getFullYear(), month, 1))
384
- leaveList()
385
- }
386
-
387
- const chooseYear = (year: number) => {
388
- setMonth(new Date(year, clampMonthInYear(year, monthDate.getMonth(), minDate, maxDate), 1))
389
- leaveList()
390
- }
173
+ export const show = () => field?.show()
174
+ export const close = () => field?.close()
391
175
  </script>
392
176
 
393
- <TextField
177
+ <DockedPickerField
394
178
  {...attributes}
395
- class={['np-docked-date-time-picker', attributes.class]}
396
- style={`anchor-name:--${uid};${style ?? ''}`}
179
+ bind:this={field}
180
+ kind="date-time-picker"
181
+ bind:value
182
+ bind:open
183
+ bind:element
184
+ {current}
185
+ resetValue={typeof defaultValue === 'string' ? defaultValue : undefined}
397
186
  {label}
398
187
  {variant}
399
- {issues}
400
- {disabled}
401
- {readonly}
402
- {required}
403
- {noAsterisk}
404
188
  {autocomplete}
405
- supportingText={supportingText ?? pattern}
406
- bind:value={text}
407
- bind:element
408
- bind:inputElement
409
- aria-invalid={inputInvalid || issues?.length || ariaInvalid === true || ariaInvalid === 'true'
410
- ? 'true'
411
- : undefined}
412
- oninput={(event) => {
413
- handleInput(event)
414
- oninput?.(event)
189
+ {pattern}
190
+ openLabel={openCalendarLabel}
191
+ invalidMessage={invalidDateMessage}
192
+ format={() =>
193
+ committed
194
+ ? localeKnown
195
+ ? formatDateTime(committed, locale, hour12)
196
+ : toISODateTime(committed)
197
+ : ''}
198
+ parse={usableDate}
199
+ ontyped={(moment) => setMonth(parseISODateTime(moment)!)}
200
+ onrestore={() => {
201
+ pendingDay = undefined
202
+ pendingMinutes = 0
415
203
  }}
416
- onblur={(event) => {
417
- handleBlur()
418
- onblur?.(event)
204
+ onmenukeydown={(event) => panel?.handleKeydown(event)}
205
+ onopen={async () => {
206
+ monthBeforeOpen = displayMonth
207
+ pendingDay = committed ? toISODate(committed) : undefined
208
+ pendingMinutes = committed ? minutesOfDay(committed) : defaultMinutes
209
+ await tick()
210
+ panel?.focus()
419
211
  }}
420
- >
421
- {#snippet end()}
422
- <IconButton
423
- type="button"
424
- aria-label={openCalendarLabel}
425
- aria-expanded={open}
426
- aria-haspopup="dialog"
427
- aria-controls={open ? `${uid}-calendar` : undefined}
428
- {disabled}
429
- onclick={() => {
430
- if (open) closePicker()
431
- else openPicker()
432
- }}
433
- >
434
- <CalendarToday />
435
- </IconButton>
436
- {/snippet}
437
- </TextField>
438
- <input
439
- bind:this={valueInput}
440
- {@attach formReset(restoreDefault)}
441
- class="np-docked-date-time-picker-value"
442
- type="hidden"
443
- value={current ?? ''}
444
- {name}
445
- {form}
446
- {disabled}
447
- />
448
-
449
- <Menu
450
- id="{uid}-calendar"
451
- role="dialog"
452
- aria-label={label}
453
- aria-modal="false"
454
- class="np-docked-date-time-picker-menu"
455
- style={`position-anchor:--${uid};`}
456
- --np-menu-justify-self="none"
457
- --np-menu-position-area="bottom span-right"
458
- --np-menu-over-anchor-position-area="span-all span-right"
459
- --np-menu-margin="0"
460
- --np-menu-container-color="var(--np-docked-date-time-picker-container-color, var(--np-color-surface-container-high))"
461
- --np-menu-container-shape="var(--np-docked-date-time-picker-container-shape, var(--np-shape-corner-large))"
462
- anchor={element}
463
- bind:element={menuElement}
464
- bind:open
465
- ontoggle={({ newState }) => {
466
- if (newState === 'open') {
467
- exit.show()
468
- monthBeforeOpen = displayMonth
469
- pendingDay = committed ? toISODate(committed) : undefined
470
- pendingMinutes = committed ? minutesOfDay(committed) : defaultMinutes
471
- focusCalendarWhenReady()
472
- }
473
- if (newState === 'closed') {
474
- // Reset once the menu is gone, not while it still shows.
475
- exit.scheduleExit(menuElement, () => {
476
- setMode('days')
477
- listMode = undefined
478
- focusedDay = undefined
479
- displayMonth = monthBeforeOpen
480
- })
481
- }
212
+ onclosed={() => {
213
+ panel?.reset()
214
+ displayMonth = monthBeforeOpen
482
215
  }}
483
216
  >
484
- {#if open || exit.visible}
485
- <div
486
- class="np-docked-date-time-picker-container"
487
- style:--np-calendar-rows={rowCount}
488
- style:--np-time-columns={hour12 ? 3 : 2}
489
- >
490
- <div class="np-docked-date-time-picker-panels">
491
- <div class="np-docked-date-time-picker-date">
492
- <CalendarHeader
493
- {mode}
494
- monthLabel={shortMonthNames[monthDate.getMonth()]}
495
- yearLabel={`${monthDate.getFullYear()}`}
496
- {canPreviousMonth}
497
- {canNextMonth}
498
- {canPreviousYear}
499
- {canNextYear}
500
- {previousMonthLabel}
501
- {nextMonthLabel}
502
- {previousYearLabel}
503
- {nextYearLabel}
504
- {selectMonthLabel}
505
- {selectYearLabel}
506
- monthListId="{uid}-months"
507
- yearListId="{uid}-years"
508
- onpreviousmonth={() => setMonth(addMonths(monthDate, -1))}
509
- onnextmonth={() => setMonth(addMonths(monthDate, 1))}
510
- onpreviousyear={() => setMonth(addMonths(monthDate, -12))}
511
- onnextyear={() => setMonth(addMonths(monthDate, 12))}
512
- ontogglemonths={() => setMode(mode === 'months' ? 'days' : 'months')}
513
- ontoggleyears={() => setMode(mode === 'years' ? 'days' : 'years')}
514
- />
515
-
516
- <div class="np-docked-date-time-picker-views">
517
- <div class="np-docked-date-time-picker-body" inert={mode !== 'days'}>
518
- <Calendar
519
- month={monthDate}
520
- selected={pendingDate}
521
- min={minDate}
522
- max={maxDate}
523
- {locale}
524
- firstDayOfWeek={week}
525
- {isDateEnabled}
526
- {adjacentMonthDays}
527
- dynamicRows
528
- todayDate={todayValue}
529
- focusedDate={focusedDay}
530
- selectedLabel={selectedDateLabel}
531
- onselect={selectDay}
532
- onmonthstep={(delta) => setMonth(addMonths(monthDate, delta))}
533
- onfocusday={(date) => {
534
- focusedDay = date
535
- if (
536
- date.getMonth() !== monthDate.getMonth() ||
537
- date.getFullYear() !== monthDate.getFullYear()
538
- ) {
539
- setMonth(date)
540
- }
541
- }}
542
- />
543
- </div>
544
-
545
- {#if listMode}
546
- <div class={['np-docked-date-time-picker-menu-view', mode !== 'days' && 'open']}>
547
- <div class="np-docked-date-time-picker-menu-shade">
548
- <Divider --np-divider-color="var(--np-color-outline-variant)" />
549
- {#if listMode === 'months'}
550
- <SelectionList
551
- id="{uid}-months"
552
- aria-label={selectMonthLabel}
553
- options={monthOptions}
554
- value={monthDate.getMonth()}
555
- onselect={chooseMonth}
556
- />
557
- {:else}
558
- <SelectionList
559
- id="{uid}-years"
560
- aria-label={selectYearLabel}
561
- options={yearOptions}
562
- value={monthDate.getFullYear()}
563
- onselect={chooseYear}
564
- />
565
- {/if}
566
- </div>
567
- </div>
568
- {/if}
569
- </div>
570
- </div>
217
+ {#snippet icon()}
218
+ <CalendarToday />
219
+ {/snippet}
220
+ <div class="np-docked-date-time-picker-container" style:--np-time-columns={hour12 ? 3 : 2}>
221
+ <div class="np-docked-date-time-picker-panels">
222
+ <div class="np-docked-date-time-picker-date">
223
+ <MonthPanel
224
+ bind:this={panel}
225
+ month={monthDate}
226
+ selected={pendingDate}
227
+ min={minDate}
228
+ max={maxDate}
229
+ {locale}
230
+ firstDayOfWeek={firstDayOfWeek ?? getFirstDayOfWeek(locale)}
231
+ {yearRange}
232
+ {isDateEnabled}
233
+ {adjacentMonthDays}
234
+ today={todayValue}
235
+ {previousMonthLabel}
236
+ {nextMonthLabel}
237
+ {previousYearLabel}
238
+ {nextYearLabel}
239
+ {selectMonthLabel}
240
+ {selectYearLabel}
241
+ {selectedDateLabel}
242
+ {shortcutLabels}
243
+ onmonth={setMonth}
244
+ onselect={selectDay}
245
+ />
246
+ </div>
571
247
 
572
- <div class="np-docked-date-time-picker-time">
573
- <div class="np-docked-date-time-picker-columns">
574
- <TimeColumn
575
- aria-label={hourLabel}
576
- options={hourOptions}
577
- value={pendingHour}
578
- onselect={(hour) =>
579
- setTime(hour * MINUTES_IN_HOUR + (pendingMinutes % MINUTES_IN_HOUR))}
580
- />
248
+ <div class="np-docked-date-time-picker-time">
249
+ <div class="np-docked-date-time-picker-columns">
250
+ <TimeColumn
251
+ aria-label={hourLabel}
252
+ options={hourOptions}
253
+ value={pendingHour}
254
+ onselect={(hour) =>
255
+ setTime(hour * MINUTES_IN_HOUR + (pendingMinutes % MINUTES_IN_HOUR))}
256
+ />
257
+ <TimeColumn
258
+ aria-label={minuteLabel}
259
+ options={minuteOptions}
260
+ value={pendingMinutes % MINUTES_IN_HOUR}
261
+ onselect={(minute) => setTime(pendingHour * MINUTES_IN_HOUR + minute)}
262
+ />
263
+ {#if hour12}
581
264
  <TimeColumn
582
- aria-label={minuteLabel}
583
- options={minuteOptions}
584
- value={pendingMinutes % MINUTES_IN_HOUR}
585
- onselect={(minute) => setTime(pendingHour * MINUTES_IN_HOUR + minute)}
265
+ aria-label={dayPeriodLabel}
266
+ options={periodOptions}
267
+ value={pendingPeriod}
268
+ onselect={(period) => setTime((pendingMinutes % HALF_DAY) + period * HALF_DAY)}
586
269
  />
587
- {#if hour12}
588
- <TimeColumn
589
- aria-label={dayPeriodLabel}
590
- options={periodOptions}
591
- value={pendingPeriod}
592
- onselect={(period) => setTime((pendingMinutes % HALF_DAY) + period * HALF_DAY)}
593
- />
594
- {/if}
595
- </div>
270
+ {/if}
596
271
  </div>
597
272
  </div>
273
+ </div>
598
274
 
599
- <Divider --np-divider-color="var(--np-color-outline-variant)" />
600
- <div class="np-docked-date-time-picker-actions">
601
- <Button type="button" variant="text" onclick={() => closePicker()}>{cancelLabel}</Button>
602
- <Button type="button" variant="text" onclick={confirm}>{confirmLabel}</Button>
603
- </div>
275
+ <Divider --np-divider-color="var(--np-color-outline-variant)" />
276
+ <div class="np-docked-date-time-picker-actions">
277
+ <Button type="button" variant="text" onclick={() => field?.closePicker()}
278
+ >{cancelLabel}</Button
279
+ >
280
+ <Button
281
+ type="button"
282
+ variant="text"
283
+ onclick={() => field?.confirm(pendingDate ? toISODateTime(pendingDate) : undefined)}
284
+ >{confirmLabel}</Button
285
+ >
604
286
  </div>
605
- {/if}
606
- </Menu>
287
+ </div>
288
+ </DockedPickerField>
607
289
 
608
290
  <style>
609
- :global(.np-docked-date-time-picker-menu .np-menu) {
610
- padding: 0;
611
- }
612
-
613
291
  .np-docked-date-time-picker-container {
614
292
  box-sizing: border-box;
615
293
  display: flex;
@@ -634,19 +312,6 @@
634
312
  padding-bottom: 0.75rem;
635
313
  }
636
314
 
637
- .np-docked-date-time-picker-date :global(.np-date-picker-header) {
638
- margin-inline: 0.75rem;
639
- }
640
-
641
- .np-docked-date-time-picker-views {
642
- position: relative;
643
- margin-top: 1.875rem;
644
- }
645
-
646
- .np-docked-date-time-picker-body {
647
- padding-inline: 0.75rem;
648
- }
649
-
650
315
  .np-docked-date-time-picker-time {
651
316
  position: relative;
652
317
  height: 12rem;
@@ -701,66 +366,4 @@
701
366
  :global(.np-docked-date-time-picker-actions .np-button) {
702
367
  height: 2.25rem;
703
368
  }
704
-
705
- .np-docked-date-time-picker-menu-view {
706
- position: absolute;
707
- inset: 0;
708
- overflow: hidden;
709
- visibility: hidden;
710
- }
711
-
712
- .np-docked-date-time-picker-menu-view.open {
713
- visibility: visible;
714
- }
715
-
716
- .np-docked-date-time-picker-menu-shade {
717
- display: flex;
718
- flex-direction: column;
719
- height: 100%;
720
- translate: 0 -100%;
721
- opacity: 0.6;
722
- background-color: var(
723
- --np-docked-date-time-picker-container-color,
724
- var(--np-color-surface-container-high)
725
- );
726
- }
727
-
728
- .np-docked-date-time-picker-menu-view.open .np-docked-date-time-picker-menu-shade {
729
- translate: 0 0;
730
- opacity: 1;
731
- }
732
-
733
- .np-docked-date-time-picker-menu-view :global(.np-date-picker-selection-list) {
734
- flex: 1;
735
- min-height: 0;
736
- }
737
-
738
- @media (prefers-reduced-motion: no-preference) {
739
- .np-docked-date-time-picker-menu-view {
740
- transition: visibility var(--np-motion-expressive-default-effects);
741
- }
742
-
743
- .np-docked-date-time-picker-menu-shade {
744
- transition:
745
- translate var(--np-motion-expressive-default-effects),
746
- opacity var(--np-motion-expressive-fast-effects);
747
- }
748
-
749
- .np-docked-date-time-picker-menu-view.open .np-docked-date-time-picker-menu-shade {
750
- transition:
751
- translate var(--np-motion-expressive-default-effects),
752
- opacity var(--np-motion-expressive-default-effects);
753
- }
754
-
755
- @starting-style {
756
- .np-docked-date-time-picker-menu-view.open .np-docked-date-time-picker-menu-shade {
757
- translate: 0 -100%;
758
- opacity: 0.6;
759
- }
760
- }
761
- }
762
-
763
- .np-docked-date-time-picker-container :global(.np-divider) {
764
- flex: none;
765
- }
766
369
  </style>