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