noph-ui 0.44.0 → 0.45.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 (135) hide show
  1. package/README.md +10 -1
  2. package/dist/animation.d.ts +17 -0
  3. package/dist/animation.js +48 -0
  4. package/dist/app-bar/AppBar.svelte +280 -0
  5. package/dist/app-bar/AppBar.svelte.d.ts +4 -0
  6. package/dist/app-bar/index.d.ts +1 -0
  7. package/dist/app-bar/index.js +1 -0
  8. package/dist/app-bar/types.d.ts +35 -0
  9. package/dist/app-bar/types.js +1 -0
  10. package/dist/autocomplete/AutoComplete.svelte +10 -11
  11. package/dist/autocomplete/AutoComplete.svelte.d.ts +1 -1
  12. package/dist/autocomplete/types.d.ts +1 -1
  13. package/dist/badge/Badge.svelte +6 -1
  14. package/dist/badge/types.d.ts +0 -1
  15. package/dist/button/Button.svelte +5 -15
  16. package/dist/button/IconButton.svelte +5 -15
  17. package/dist/button/SegmentedButton.svelte +15 -1
  18. package/dist/button/SplitButton.svelte +92 -0
  19. package/dist/button/SplitButton.svelte.d.ts +4 -0
  20. package/dist/button/index.d.ts +1 -0
  21. package/dist/button/index.js +1 -0
  22. package/dist/button/types.d.ts +23 -4
  23. package/dist/card/types.d.ts +1 -1
  24. package/dist/checkbox/Checkbox.svelte +49 -58
  25. package/dist/chip/AssistChip.svelte +5 -5
  26. package/dist/chip/ChipSet.svelte +3 -11
  27. package/dist/chip/FilterChip.svelte +35 -27
  28. package/dist/chip/InputChip.svelte +13 -2
  29. package/dist/chip/SuggestionChip.svelte +29 -0
  30. package/dist/chip/SuggestionChip.svelte.d.ts +4 -0
  31. package/dist/chip/index.d.ts +1 -0
  32. package/dist/chip/index.js +1 -0
  33. package/dist/chip/types.d.ts +24 -9
  34. package/dist/date-picker/Calendar.svelte +1 -1
  35. package/dist/date-picker/DatePickerDialog.svelte +30 -8
  36. package/dist/date-picker/DatePickerDialog.svelte.d.ts +4 -1
  37. package/dist/date-picker/DateRangePicker.svelte +45 -10
  38. package/dist/date-picker/DateRangePicker.svelte.d.ts +4 -1
  39. package/dist/date-picker/DockedDatePicker.svelte +18 -4
  40. package/dist/date-picker/DockedDatePicker.svelte.d.ts +4 -1
  41. package/dist/date-picker/DockedDateTimePicker.svelte +18 -4
  42. package/dist/date-picker/DockedDateTimePicker.svelte.d.ts +4 -1
  43. package/dist/date-picker/SelectionList.svelte +6 -32
  44. package/dist/date-picker/SelectionList.svelte.d.ts +2 -12
  45. package/dist/date-picker/TimeColumn.svelte +4 -29
  46. package/dist/date-picker/exitVisibility.svelte.d.ts +10 -0
  47. package/dist/date-picker/exitVisibility.svelte.js +30 -0
  48. package/dist/date-picker/optionList.d.ts +16 -0
  49. package/dist/date-picker/optionList.js +43 -0
  50. package/dist/date-picker/types.d.ts +11 -4
  51. package/dist/dialog/Dialog.svelte +77 -68
  52. package/dist/dialog/Dialog.svelte.d.ts +3 -3
  53. package/dist/dialog/types.d.ts +5 -3
  54. package/dist/fab/ExtendedFab.svelte +281 -0
  55. package/dist/fab/ExtendedFab.svelte.d.ts +5 -0
  56. package/dist/fab/Fab.svelte +239 -0
  57. package/dist/fab/Fab.svelte.d.ts +5 -0
  58. package/dist/fab/FabMenu.svelte +255 -0
  59. package/dist/fab/FabMenu.svelte.d.ts +7 -0
  60. package/dist/fab/index.d.ts +3 -0
  61. package/dist/fab/index.js +3 -0
  62. package/dist/fab/types.d.ts +58 -0
  63. package/dist/fab/types.js +1 -0
  64. package/dist/icons/ArrowBackIcon.svelte +13 -0
  65. package/dist/icons/ArrowBackIcon.svelte.d.ts +7 -0
  66. package/dist/icons/ChevronDownIcon.svelte +3 -0
  67. package/dist/icons/ChevronDownIcon.svelte.d.ts +26 -0
  68. package/dist/icons/SearchIcon.svelte +15 -0
  69. package/dist/icons/SearchIcon.svelte.d.ts +7 -0
  70. package/dist/icons/index.d.ts +3 -0
  71. package/dist/icons/index.js +3 -0
  72. package/dist/index.d.ts +6 -0
  73. package/dist/index.js +6 -0
  74. package/dist/internal/focus-ring.css +12 -0
  75. package/dist/menu/Menu.svelte +12 -7
  76. package/dist/menu/Menu.svelte.d.ts +2 -2
  77. package/dist/menu/types.d.ts +0 -2
  78. package/dist/navigation-bar/NavigationBar.svelte +66 -0
  79. package/dist/navigation-bar/NavigationBar.svelte.d.ts +4 -0
  80. package/dist/navigation-bar/NavigationBarItem.svelte +261 -0
  81. package/dist/navigation-bar/NavigationBarItem.svelte.d.ts +5 -0
  82. package/dist/navigation-bar/index.d.ts +2 -0
  83. package/dist/navigation-bar/index.js +2 -0
  84. package/dist/navigation-bar/types.d.ts +29 -0
  85. package/dist/navigation-bar/types.js +1 -0
  86. package/dist/navigation-drawer/NavigationDrawer.svelte +128 -76
  87. package/dist/navigation-drawer/NavigationDrawer.svelte.d.ts +4 -1
  88. package/dist/navigation-drawer/NavigationDrawerItem.svelte +10 -3
  89. package/dist/navigation-drawer/types.d.ts +6 -1
  90. package/dist/navigation-rail/NavigationRailItem.svelte +16 -0
  91. package/dist/navigation-rail/types.d.ts +3 -0
  92. package/dist/popover.svelte.d.ts +14 -0
  93. package/dist/popover.svelte.js +31 -0
  94. package/dist/press.svelte.d.ts +19 -0
  95. package/dist/press.svelte.js +45 -0
  96. package/dist/radio/Radio.svelte +26 -7
  97. package/dist/radio/types.d.ts +3 -0
  98. package/dist/search/Search.svelte +472 -0
  99. package/dist/search/Search.svelte.d.ts +5 -0
  100. package/dist/search/index.d.ts +1 -0
  101. package/dist/search/index.js +1 -0
  102. package/dist/search/types.d.ts +39 -0
  103. package/dist/search/types.js +1 -0
  104. package/dist/select/Select.svelte +48 -87
  105. package/dist/sheet/Sheet.svelte +260 -0
  106. package/dist/sheet/Sheet.svelte.d.ts +7 -0
  107. package/dist/sheet/index.d.ts +1 -0
  108. package/dist/sheet/index.js +1 -0
  109. package/dist/sheet/types.d.ts +19 -0
  110. package/dist/sheet/types.js +1 -0
  111. package/dist/slider/Slider.svelte +154 -30
  112. package/dist/slider/types.d.ts +3 -0
  113. package/dist/snackbar/Snackbar.svelte +48 -37
  114. package/dist/snackbar/Snackbar.svelte.d.ts +2 -2
  115. package/dist/snackbar/types.d.ts +3 -2
  116. package/dist/switch/Switch.svelte +51 -20
  117. package/dist/switch/types.d.ts +3 -0
  118. package/dist/tabs/Tab.svelte +8 -12
  119. package/dist/tabs/Tabs.svelte +3 -3
  120. package/dist/text-field/TextField.svelte +2 -2
  121. package/dist/toolbar/Toolbar.svelte +133 -0
  122. package/dist/toolbar/Toolbar.svelte.d.ts +4 -0
  123. package/dist/toolbar/index.d.ts +1 -0
  124. package/dist/toolbar/index.js +1 -0
  125. package/dist/toolbar/types.d.ts +15 -0
  126. package/dist/toolbar/types.js +1 -0
  127. package/dist/tooltip/RichTooltip.svelte +25 -12
  128. package/dist/tooltip/RichTooltip.svelte.d.ts +2 -2
  129. package/dist/tooltip/Tooltip.svelte +22 -11
  130. package/dist/tooltip/Tooltip.svelte.d.ts +4 -1
  131. package/dist/types.d.ts +6 -0
  132. package/dist/types.js +6 -0
  133. package/package.json +13 -13
  134. package/dist/date-picker/enterAnimation.d.ts +0 -1
  135. package/dist/date-picker/enterAnimation.js +0 -10
@@ -1,4 +1,5 @@
1
1
  export { default as FilterChip } from './FilterChip.svelte';
2
2
  export { default as AssistChip } from './AssistChip.svelte';
3
3
  export { default as InputChip } from './InputChip.svelte';
4
+ export { default as SuggestionChip } from './SuggestionChip.svelte';
4
5
  export { default as ChipSet } from './ChipSet.svelte';
@@ -1,44 +1,59 @@
1
- import type { ButtonElement, ButtonProps } from '../button/types.js';
1
+ import type { ButtonElement } from '../button/types.js';
2
2
  import type { Snippet } from 'svelte';
3
- import type { HTMLAttributes, MouseEventHandler } from 'svelte/elements';
3
+ import type { HTMLAnchorAttributes, HTMLAttributes, HTMLButtonAttributes, MouseEventHandler } from 'svelte/elements';
4
4
  export interface ChipSetProps extends HTMLAttributes<HTMLDivElement> {
5
- chipsCount?: number;
6
5
  element?: HTMLDivElement;
7
6
  }
8
- export interface AssistChipProps extends Omit<ButtonProps, 'variant' | 'start' | 'end' | 'element'> {
9
- elevated?: boolean;
10
- disabled?: boolean;
7
+ /**
8
+ * Declared from the element attributes rather than `ButtonProps`, which leaked `size`, `shape`,
9
+ * `toggle`, `loading` and `selected` onto a chip. A chip has one size and one shape.
10
+ */
11
+ export interface AssistChipProps extends HTMLAttributes<ButtonElement>, Omit<HTMLButtonAttributes, keyof HTMLAttributes<HTMLButtonElement> | 'type'>, Omit<HTMLAnchorAttributes, keyof HTMLAttributes<HTMLAnchorElement> | 'type'> {
12
+ variant?: 'outlined' | 'elevated';
13
+ disabled?: boolean | null;
11
14
  label?: string;
12
15
  icon?: Snippet;
13
16
  element?: HTMLElement;
17
+ type?: 'submit' | 'reset' | 'button' | (string & {}) | null;
14
18
  }
19
+ /**
20
+ * A suggestion chip carries a generated suggestion rather than an action, so it is text-first and
21
+ * takes no leading icon. Everything else matches an assist chip.
22
+ */
23
+ export type SuggestionChipProps = Omit<AssistChipProps, 'icon'>;
15
24
  export interface FilterChipProps extends HTMLAttributes<HTMLDivElement> {
16
25
  selected?: boolean;
17
26
  removable?: boolean;
18
27
  disabled?: boolean;
19
- elevated?: boolean;
28
+ variant?: 'outlined' | 'elevated';
20
29
  label?: string;
21
30
  icon?: Snippet;
22
- ariaLabelRemove?: string;
31
+ removeAriaLabel?: string;
23
32
  element?: HTMLDivElement;
24
33
  name?: string;
25
34
  value?: string;
26
35
  group?: (string | number)[] | null;
27
36
  defaultSelected?: boolean | null;
28
37
  onremove?: MouseEventHandler<ButtonElement>;
38
+ issues?: {
39
+ message: string;
40
+ }[];
29
41
  }
30
42
  export interface InputChipProps extends Omit<HTMLAttributes<HTMLDivElement>, 'onclick'> {
31
43
  selected?: boolean;
32
44
  disabled?: boolean;
33
45
  label?: string;
34
46
  icon?: Snippet;
35
- ariaLabelRemove?: string;
47
+ removeAriaLabel?: string;
36
48
  element?: HTMLDivElement;
37
49
  actionElement?: HTMLButtonElement;
38
50
  name?: string;
39
51
  value?: string | number;
40
52
  onclick?: MouseEventHandler<HTMLButtonElement>;
41
53
  onremove?: MouseEventHandler<ButtonElement>;
54
+ issues?: {
55
+ message: string;
56
+ }[];
42
57
  }
43
58
  export interface ChipSetContext {
44
59
  chips: {
@@ -20,7 +20,7 @@
20
20
  toISODate,
21
21
  today as getToday,
22
22
  } from './dateUtils.js'
23
- import { afterTwoFrames } from './enterAnimation.js'
23
+ import { afterTwoFrames } from '../animation.js'
24
24
  import type { CalendarProps } from './types.js'
25
25
 
26
26
  let {
@@ -1,8 +1,11 @@
1
1
  <script lang="ts">
2
+ import { afterTwoFrames } from '../animation.js'
2
3
  import Button from '../button/Button.svelte'
3
4
  import IconButton from '../button/IconButton.svelte'
4
5
  import Dialog from '../dialog/Dialog.svelte'
6
+ import { syncOpenEffect } from '../popover.svelte.js'
5
7
  import Divider from '../divider/Divider.svelte'
8
+ import { exitVisibility } from './exitVisibility.svelte.js'
6
9
  import ArrowDropDownIcon from '../icons/ArrowDropDownIcon.svelte'
7
10
  import CalendarToday from '../icons/CalendarToday.svelte'
8
11
  import ChevronLeftIcon from '../icons/ChevronLeftIcon.svelte'
@@ -29,7 +32,6 @@
29
32
  toISODate,
30
33
  today as getToday,
31
34
  } from './dateUtils.js'
32
- import { afterTwoFrames } from './enterAnimation.js'
33
35
  import type { DatePickerDialogProps, ISODate } from './types.js'
34
36
 
35
37
  let {
@@ -48,6 +50,7 @@
48
50
  headline,
49
51
  label = 'Date',
50
52
  supportingText,
53
+ issues,
51
54
  name,
52
55
  form,
53
56
  modeToggle = false,
@@ -77,6 +80,8 @@
77
80
  let confirming = false
78
81
  let monthBeforeOpen: ISODate | undefined
79
82
 
83
+ const exit = exitVisibility()
84
+
80
85
  let todayValue = $derived(getToday())
81
86
  let week = $derived(firstDayOfWeek ?? getFirstDayOfWeek(locale))
82
87
  let minDate = $derived(parseISODate(min))
@@ -125,12 +130,25 @@
125
130
  cancelYearEnter = afterTwoFrames(() => (yearEnter = false))
126
131
  }
127
132
 
128
- const isOpen = () => !!element?.matches(':popover-open')
133
+ /*
134
+ * The same pair every overlay in the library exports. A trigger with `command="show-modal"` and
135
+ * `commandfor` needs neither, so these are for the times there is no trigger to point at the
136
+ * dialog. `open` follows either way, written back from the dialog's own toggle event.
137
+ */
138
+ export const show = () => {
139
+ if (element && !element.open) element.showModal()
140
+ }
129
141
 
130
- $effect(() => {
131
- if (open && !isOpen()) element?.showPopover()
132
- if (!open && isOpen()) element?.hidePopover()
133
- })
142
+ export const close = () => {
143
+ if (element?.open) element.close()
144
+ }
145
+
146
+ syncOpenEffect(
147
+ () => element,
148
+ () => open,
149
+ show,
150
+ close,
151
+ )
134
152
 
135
153
  const setMonth = (date: Date) => {
136
154
  displayMonth = toISODate(startOfMonth(date))
@@ -197,6 +215,7 @@
197
215
  const nowOpen = event.newState === 'open'
198
216
  open = nowOpen
199
217
  if (nowOpen) {
218
+ exit.show()
200
219
  monthBeforeOpen = displayMonth
201
220
  confirming = false
202
221
  pending = value ?? undefined
@@ -206,14 +225,16 @@
206
225
  syncText()
207
226
  focusCalendarWhenReady()
208
227
  } else {
209
- displayMonth = monthBeforeOpen
210
228
  if (!confirming) oncancel?.()
211
229
  confirming = false
230
+ // Handing the month back before the dialog has gone would step the calendar to another
231
+ // month in front of the user, so that waits for the fade as well.
232
+ exit.scheduleExit(element, () => (displayMonth = monthBeforeOpen))
212
233
  }
213
234
  }}
214
235
  >
215
236
  <input class="np-date-picker-dialog-value" type="hidden" value={value ?? ''} {name} {form} />
216
- {#if open}
237
+ {#if exit.visible}
217
238
  <div class="np-date-picker-dialog-content">
218
239
  <div class="np-date-picker-dialog-header">
219
240
  <div class="np-date-picker-dialog-header-text">
@@ -247,6 +268,7 @@
247
268
  <div class={['np-date-picker-dialog-input', modeEnter && modeEnter]}>
248
269
  <TextField
249
270
  {label}
271
+ {issues}
250
272
  supportingText={supportingText ?? pattern}
251
273
  value={text}
252
274
  aria-invalid={inputInvalid ? 'true' : undefined}
@@ -1,4 +1,7 @@
1
1
  import type { DatePickerDialogProps } from './types.ts';
2
- declare const DatePickerDialog: import("svelte").Component<DatePickerDialogProps, {}, "element" | "value" | "displayMonth" | "open">;
2
+ declare const DatePickerDialog: import("svelte").Component<DatePickerDialogProps, {
3
+ show: () => void;
4
+ close: () => void;
5
+ }, "element" | "value" | "open" | "displayMonth">;
3
6
  type DatePickerDialog = ReturnType<typeof DatePickerDialog>;
4
7
  export default DatePickerDialog;
@@ -4,8 +4,10 @@
4
4
  import Dialog from '../dialog/Dialog.svelte'
5
5
  import Divider from '../divider/Divider.svelte'
6
6
  import CloseIcon from '../icons/CloseIcon.svelte'
7
+ import { syncOpenEffect } from '../popover.svelte.js'
7
8
  import { tick } from 'svelte'
8
9
  import Calendar from './Calendar.svelte'
10
+ import { exitVisibility } from './exitVisibility.svelte.js'
9
11
  import {
10
12
  clampDate,
11
13
  compareDays,
@@ -34,6 +36,10 @@
34
36
  isDateEnabled,
35
37
  adjacentMonthDays = false,
36
38
  title = 'Select range',
39
+ headline,
40
+ name,
41
+ endName,
42
+ form,
37
43
  startLabel = 'Start date',
38
44
  endLabel = 'End date',
39
45
  cancelLabel = 'Cancel',
@@ -52,6 +58,8 @@
52
58
  let focusedDay = $state<Date | undefined>(undefined)
53
59
  let confirming = false
54
60
 
61
+ const exit = exitVisibility()
62
+
55
63
  let todayValue = $derived(getToday())
56
64
  let week = $derived(firstDayOfWeek ?? getFirstDayOfWeek(locale))
57
65
  let minDate = $derived(parseISODate(min))
@@ -108,12 +116,25 @@
108
116
  if (target && scroller) scroller.scrollTop = target.offsetTop
109
117
  }
110
118
 
111
- const isOpen = () => !!element?.matches(':popover-open')
119
+ /*
120
+ * The same pair every overlay in the library exports. A trigger with `command="show-modal"` and
121
+ * `commandfor` needs neither, so these are for the times there is no trigger to point at the
122
+ * dialog. `open` follows either way, written back from the dialog's own toggle event.
123
+ */
124
+ export const show = () => {
125
+ if (element && !element.open) element.showModal()
126
+ }
112
127
 
113
- $effect(() => {
114
- if (open && !isOpen()) element?.showPopover()
115
- if (!open && isOpen()) element?.hidePopover()
116
- })
128
+ export const close = () => {
129
+ if (element?.open) element.close()
130
+ }
131
+
132
+ syncOpenEffect(
133
+ () => element,
134
+ () => open,
135
+ show,
136
+ close,
137
+ )
117
138
 
118
139
  let prepending = false
119
140
 
@@ -176,6 +197,7 @@
176
197
  const nowOpen = event.newState === 'open'
177
198
  open = nowOpen
178
199
  if (nowOpen) {
200
+ exit.show()
179
201
  confirming = false
180
202
  pending = { ...value }
181
203
  focusedDay = undefined
@@ -184,10 +206,19 @@
184
206
  } else {
185
207
  if (!confirming) oncancel?.()
186
208
  confirming = false
209
+ exit.scheduleExit(element)
187
210
  }
188
211
  }}
189
212
  >
190
- {#if open}
213
+ <input class="np-date-range-picker-value" type="hidden" value={value.start ?? ''} {name} {form} />
214
+ <input
215
+ class="np-date-range-picker-value"
216
+ type="hidden"
217
+ value={value.end ?? ''}
218
+ name={endName}
219
+ {form}
220
+ />
221
+ {#if exit.visible}
191
222
  <div class="np-date-range-picker-content">
192
223
  <div class="np-date-range-picker-header">
193
224
  <div class="np-date-range-picker-top-bar">
@@ -201,9 +232,13 @@
201
232
  <div class="np-date-range-picker-header-text">
202
233
  <span class="np-date-range-picker-title">{title}</span>
203
234
  <h2 class="np-date-range-picker-headline">
204
- <span class={[!startDate && 'placeholder']}>{headlineStart}</span>
205
- <span aria-hidden="true">–</span>
206
- <span class={[!endDate && 'placeholder']}>{headlineEnd}</span>
235
+ {#if headline}
236
+ <span>{headline}</span>
237
+ {:else}
238
+ <span class={[!startDate && 'placeholder']}>{headlineStart}</span>
239
+ <span aria-hidden="true">–</span>
240
+ <span class={[!endDate && 'placeholder']}>{headlineEnd}</span>
241
+ {/if}
207
242
  </h2>
208
243
  </div>
209
244
  </div>
@@ -248,7 +283,7 @@
248
283
  {/if}
249
284
 
250
285
  {#snippet actions()}
251
- {#if open}
286
+ {#if exit.visible}
252
287
  <Button type="button" variant="text" onclick={cancel}>{cancelLabel}</Button>
253
288
  <Button type="button" variant="text" disabled={!canConfirm} onclick={confirm}>
254
289
  {confirmLabel}
@@ -1,4 +1,7 @@
1
1
  import type { DateRangePickerProps } from './types.ts';
2
- declare const DateRangePicker: import("svelte").Component<DateRangePickerProps, {}, "element" | "value" | "open">;
2
+ declare const DateRangePicker: import("svelte").Component<DateRangePickerProps, {
3
+ show: () => void;
4
+ close: () => void;
5
+ }, "element" | "value" | "open">;
3
6
  type DateRangePicker = ReturnType<typeof DateRangePicker>;
4
7
  export default DateRangePicker;
@@ -5,6 +5,7 @@
5
5
  import CalendarToday from '../icons/CalendarToday.svelte'
6
6
  import Menu from '../menu/Menu.svelte'
7
7
  import { onFormReset } from '../form-reset.js'
8
+ import { syncOpenEffect } from '../popover.svelte.js'
8
9
  import TextField from '../text-field/TextField.svelte'
9
10
  import { onMount, tick } from 'svelte'
10
11
  import Calendar from './Calendar.svelte'
@@ -214,10 +215,23 @@
214
215
  focusCalendar()
215
216
  }
216
217
 
217
- $effect(() => {
218
- if (open) openPicker()
219
- else if (isShowing()) menuElement?.hidePopover()
220
- })
218
+ /*
219
+ * The same pair every overlay in the library exports, for the times there is no trigger to point
220
+ * at the picker. `close()` leaves focus where it is; only a close the user asked for, through the
221
+ * field or the cancel button, hands focus back to the input.
222
+ */
223
+ export const show = () => openPicker()
224
+
225
+ export const close = () => {
226
+ if (isShowing()) menuElement?.hidePopover()
227
+ }
228
+
229
+ syncOpenEffect(
230
+ () => menuElement,
231
+ () => open,
232
+ show,
233
+ close,
234
+ )
221
235
 
222
236
  const focusCalendar = () => {
223
237
  menuElement?.querySelector<HTMLElement>('.np-calendar-day[tabindex="0"]')?.focus()
@@ -1,4 +1,7 @@
1
1
  import type { DockedDatePickerProps } from './types.ts';
2
- declare const DockedDatePicker: import("svelte").Component<DockedDatePickerProps, {}, "element" | "value" | "displayMonth" | "open">;
2
+ declare const DockedDatePicker: import("svelte").Component<DockedDatePickerProps, {
3
+ show: () => void;
4
+ close: () => void;
5
+ }, "element" | "value" | "open" | "displayMonth">;
3
6
  type DockedDatePicker = ReturnType<typeof DockedDatePicker>;
4
7
  export default DockedDatePicker;
@@ -5,6 +5,7 @@
5
5
  import CalendarToday from '../icons/CalendarToday.svelte'
6
6
  import Menu from '../menu/Menu.svelte'
7
7
  import { onFormReset } from '../form-reset.js'
8
+ import { syncOpenEffect } from '../popover.svelte.js'
8
9
  import TextField from '../text-field/TextField.svelte'
9
10
  import { onMount, tick } from 'svelte'
10
11
  import Calendar from './Calendar.svelte'
@@ -297,10 +298,23 @@
297
298
  focusCalendar()
298
299
  }
299
300
 
300
- $effect(() => {
301
- if (open) openPicker()
302
- else if (isShowing()) menuElement?.hidePopover()
303
- })
301
+ /*
302
+ * The same pair every overlay in the library exports, for the times there is no trigger to point
303
+ * at the picker. `close()` leaves focus where it is; only a close the user asked for, through the
304
+ * field or the cancel button, hands focus back to the input.
305
+ */
306
+ export const show = () => openPicker()
307
+
308
+ export const close = () => {
309
+ if (isShowing()) menuElement?.hidePopover()
310
+ }
311
+
312
+ syncOpenEffect(
313
+ () => menuElement,
314
+ () => open,
315
+ show,
316
+ close,
317
+ )
304
318
 
305
319
  const focusCalendar = () => {
306
320
  menuElement?.querySelector<HTMLElement>('.np-calendar-day[tabindex="0"]')?.focus()
@@ -1,4 +1,7 @@
1
1
  import type { DockedDateTimePickerProps } from './types.ts';
2
- declare const DockedDateTimePicker: import("svelte").Component<DockedDateTimePickerProps, {}, "element" | "value" | "displayMonth" | "open">;
2
+ declare const DockedDateTimePicker: import("svelte").Component<DockedDateTimePickerProps, {
3
+ show: () => void;
4
+ close: () => void;
5
+ }, "element" | "value" | "open" | "displayMonth">;
3
6
  type DockedDateTimePicker = ReturnType<typeof DockedDateTimePicker>;
4
7
  export default DockedDateTimePicker;
@@ -2,41 +2,15 @@
2
2
  import CheckIcon from '../icons/CheckIcon.svelte'
3
3
  import { arrowKeyNav } from '../keyboard-nav.js'
4
4
  import Ripple from '../ripple/Ripple.svelte'
5
- import type { Attachment } from 'svelte/attachments'
6
- import type { HTMLAttributes } from 'svelte/elements'
5
+ import { revealSelected, tabStop } from './optionList.js'
6
+ import type { TimeColumnProps } from './types.js'
7
7
 
8
- interface SelectionOption {
9
- value: number
10
- label: string
11
- disabled?: boolean
12
- }
13
-
14
- interface SelectionListProps extends Omit<HTMLAttributes<HTMLDivElement>, 'onselect'> {
15
- options: SelectionOption[]
16
- value?: number
17
- onselect?: (value: number) => void
18
- }
19
-
20
- let { options, value, onselect, ...attributes }: SelectionListProps = $props()
8
+ let { options, value, onselect, ...attributes }: TimeColumnProps = $props()
21
9
 
22
10
  const handleKeydown = arrowKeyNav('[role="option"]')
11
+ const reveal = revealSelected(true)
23
12
 
24
- let tabStopValue = $derived.by(() => {
25
- if (options.some((option) => option.value === value)) return value
26
- const offered = options.filter((option) => !option.disabled)
27
- const pool = offered.length ? offered : options
28
- if (!pool.length) return undefined
29
- if (value === undefined) return pool[0].value
30
- return pool.reduce((nearest, option) =>
31
- Math.abs(option.value - value) < Math.abs(nearest.value - value) ? option : nearest,
32
- ).value
33
- })
34
-
35
- const centerInList: Attachment<HTMLElement> = (element) => {
36
- const list = element.closest<HTMLElement>('.np-date-picker-selection-list')
37
- if (!list) return
38
- list.scrollTop = element.offsetTop - list.clientHeight / 2 + element.offsetHeight / 2
39
- }
13
+ let tabStopValue = $derived(tabStop(options, value))
40
14
  </script>
41
15
 
42
16
  <div
@@ -49,7 +23,7 @@
49
23
  {#each options as option (option.value)}
50
24
  {@const selected = option.value === value}
51
25
  <button
52
- {@attach selected && centerInList}
26
+ {@attach selected && reveal}
53
27
  type="button"
54
28
  role="option"
55
29
  aria-selected={selected}
@@ -1,14 +1,4 @@
1
- import type { HTMLAttributes } from 'svelte/elements';
2
- interface SelectionOption {
3
- value: number;
4
- label: string;
5
- disabled?: boolean;
6
- }
7
- interface SelectionListProps extends Omit<HTMLAttributes<HTMLDivElement>, 'onselect'> {
8
- options: SelectionOption[];
9
- value?: number;
10
- onselect?: (value: number) => void;
11
- }
12
- declare const SelectionList: import("svelte").Component<SelectionListProps, {}, "">;
1
+ import type { TimeColumnProps } from './types.ts';
2
+ declare const SelectionList: import("svelte").Component<TimeColumnProps, {}, "">;
13
3
  type SelectionList = ReturnType<typeof SelectionList>;
14
4
  export default SelectionList;
@@ -1,40 +1,15 @@
1
1
  <script lang="ts">
2
2
  import { arrowKeyNav } from '../keyboard-nav.js'
3
3
  import Ripple from '../ripple/Ripple.svelte'
4
- import type { Attachment } from 'svelte/attachments'
4
+ import { revealSelected, tabStop } from './optionList.js'
5
5
  import type { TimeColumnProps } from './types.js'
6
6
 
7
7
  let { options, value, onselect, ...attributes }: TimeColumnProps = $props()
8
8
 
9
9
  const handleKeydown = arrowKeyNav('[role="option"]')
10
+ const reveal = revealSelected()
10
11
 
11
- let tabStopValue = $derived.by(() => {
12
- if (options.some((option) => option.value === value)) return value
13
- const offered = options.filter((option) => !option.disabled)
14
- const pool = offered.length ? offered : options
15
- if (!pool.length) return undefined
16
- if (value === undefined) return pool[0].value
17
- return pool.reduce((nearest, option) =>
18
- Math.abs(option.value - value) < Math.abs(nearest.value - value) ? option : nearest,
19
- ).value
20
- })
21
-
22
- let centred = false
23
-
24
- const revealSelected: Attachment<HTMLElement> = (element) => {
25
- const column = element.parentElement
26
- if (!column) return
27
- const top = element.offsetTop
28
- const bottom = top + element.offsetHeight
29
- if (!centred) {
30
- centred = true
31
- column.scrollTop = top - column.clientHeight / 2 + element.offsetHeight / 2
32
- } else if (top < column.scrollTop) {
33
- column.scrollTop = top
34
- } else if (bottom > column.scrollTop + column.clientHeight) {
35
- column.scrollTop = bottom - column.clientHeight
36
- }
37
- }
12
+ let tabStopValue = $derived(tabStop(options, value))
38
13
  </script>
39
14
 
40
15
  <div
@@ -47,7 +22,7 @@
47
22
  {#each options as option (option.value)}
48
23
  {@const selected = option.value === value}
49
24
  <button
50
- {@attach selected && revealSelected}
25
+ {@attach selected && reveal}
51
26
  type="button"
52
27
  role="option"
53
28
  aria-selected={selected}
@@ -0,0 +1,10 @@
1
+ /**
2
+ * `visible` for a dialog that fades itself out, so it stays on screen after `open` has flipped to
3
+ * `false`. Content tied to `open` would leave the fading dialog empty a frame before it starts to
4
+ * leave, so this holds `visible` true until the exit animation has actually finished.
5
+ */
6
+ export declare const exitVisibility: () => {
7
+ readonly visible: boolean;
8
+ show: () => void;
9
+ scheduleExit: (element: Element | undefined, onDone?: () => void) => void;
10
+ };
@@ -0,0 +1,30 @@
1
+ import { afterExit } from '../animation.js';
2
+ /**
3
+ * `visible` for a dialog that fades itself out, so it stays on screen after `open` has flipped to
4
+ * `false`. Content tied to `open` would leave the fading dialog empty a frame before it starts to
5
+ * leave, so this holds `visible` true until the exit animation has actually finished.
6
+ */
7
+ export const exitVisibility = () => {
8
+ let visible = $state(false);
9
+ let cancelExit;
10
+ // A dialog torn down mid fade has nothing left to reveal, and running a stale `onDone` against a
11
+ // parent that has already dropped its state is worse than not running it at all.
12
+ $effect(() => () => cancelExit?.());
13
+ return {
14
+ get visible() {
15
+ return visible;
16
+ },
17
+ show: () => {
18
+ cancelExit?.();
19
+ cancelExit = undefined;
20
+ visible = true;
21
+ },
22
+ scheduleExit: (element, onDone) => {
23
+ cancelExit = afterExit(element, () => {
24
+ visible = false;
25
+ onDone?.();
26
+ cancelExit = undefined;
27
+ });
28
+ },
29
+ };
30
+ };
@@ -0,0 +1,16 @@
1
+ import type { Attachment } from 'svelte/attachments';
2
+ import type { TimeOption } from './types.ts';
3
+ /**
4
+ * The option that carries the list's single tab stop: the selected one, or else the offered option
5
+ * nearest to the value, so arriving with the keyboard lands next to where the value already is.
6
+ */
7
+ export declare const tabStop: (options: TimeOption[], value: number | undefined) => number | undefined;
8
+ /**
9
+ * Scrolls the selected option into view inside its own list, rather than through `scrollIntoView`,
10
+ * which takes every scrollable ancestor with it and would move the page behind the picker.
11
+ *
12
+ * `centre` puts the option in the middle of the list every time it changes. Otherwise the list is
13
+ * centred once, on the option it opens with, and afterwards the selection is only pulled to the
14
+ * nearest edge when it has moved out of view, so stepping through options does not jump the list.
15
+ */
16
+ export declare const revealSelected: (centre?: boolean) => Attachment<HTMLElement>;
@@ -0,0 +1,43 @@
1
+ /**
2
+ * The option that carries the list's single tab stop: the selected one, or else the offered option
3
+ * nearest to the value, so arriving with the keyboard lands next to where the value already is.
4
+ */
5
+ export const tabStop = (options, value) => {
6
+ if (options.some((option) => option.value === value))
7
+ return value;
8
+ const offered = options.filter((option) => !option.disabled);
9
+ const pool = offered.length ? offered : options;
10
+ if (!pool.length)
11
+ return undefined;
12
+ if (value === undefined)
13
+ return pool[0].value;
14
+ return pool.reduce((nearest, option) => Math.abs(option.value - value) < Math.abs(nearest.value - value) ? option : nearest).value;
15
+ };
16
+ /**
17
+ * Scrolls the selected option into view inside its own list, rather than through `scrollIntoView`,
18
+ * which takes every scrollable ancestor with it and would move the page behind the picker.
19
+ *
20
+ * `centre` puts the option in the middle of the list every time it changes. Otherwise the list is
21
+ * centred once, on the option it opens with, and afterwards the selection is only pulled to the
22
+ * nearest edge when it has moved out of view, so stepping through options does not jump the list.
23
+ */
24
+ export const revealSelected = (centre = false) => {
25
+ let centred = false;
26
+ return (element) => {
27
+ const list = element.parentElement;
28
+ if (!list)
29
+ return;
30
+ const top = element.offsetTop;
31
+ const bottom = top + element.offsetHeight;
32
+ if (centre || !centred) {
33
+ centred = true;
34
+ list.scrollTop = top - list.clientHeight / 2 + element.offsetHeight / 2;
35
+ }
36
+ else if (top < list.scrollTop) {
37
+ list.scrollTop = top;
38
+ }
39
+ else if (bottom > list.scrollTop + list.clientHeight) {
40
+ list.scrollTop = bottom - list.clientHeight;
41
+ }
42
+ };
43
+ };