noph-ui 0.51.2 → 0.52.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 (130) hide show
  1. package/dist/animation.d.ts +26 -1
  2. package/dist/animation.js +50 -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 +55 -20
  11. package/dist/button/ButtonGroup.svelte +139 -19
  12. package/dist/button/ButtonGroup.svelte.d.ts +1 -1
  13. package/dist/button/IconButton.svelte +49 -18
  14. package/dist/button/SegmentedButton.svelte +44 -4
  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/checkbox/Checkbox.svelte +5 -0
  23. package/dist/chip/ChipSet.svelte +28 -2
  24. package/dist/chip/FilterChip.svelte +32 -4
  25. package/dist/chip/InputChip.svelte +127 -26
  26. package/dist/chip/SuggestionChip.svelte +6 -22
  27. package/dist/chip/SuggestionChip.svelte.d.ts +1 -2
  28. package/dist/chip/types.d.ts +8 -1
  29. package/dist/date-picker/Calendar.svelte +4 -2
  30. package/dist/date-picker/DatePickerDialog.svelte +1 -0
  31. package/dist/date-picker/DatePickerDialog.svelte.d.ts +1 -1
  32. package/dist/date-picker/DateRangePicker.svelte +1 -0
  33. package/dist/date-picker/DateRangePicker.svelte.d.ts +1 -1
  34. package/dist/date-picker/DockedDatePicker.svelte +12 -5
  35. package/dist/date-picker/DockedDatePicker.svelte.d.ts +1 -1
  36. package/dist/date-picker/DockedDateTimePicker.svelte +12 -5
  37. package/dist/date-picker/DockedDateTimePicker.svelte.d.ts +1 -1
  38. package/dist/date-picker/YearGrid.svelte +4 -2
  39. package/dist/dialog/Dialog.svelte +171 -33
  40. package/dist/dialog/Dialog.svelte.d.ts +2 -1
  41. package/dist/dialog/types.d.ts +6 -0
  42. package/dist/divider/Divider.svelte +36 -10
  43. package/dist/divider/types.d.ts +1 -0
  44. package/dist/fab/ExtendedFab.svelte +26 -60
  45. package/dist/fab/Fab.svelte +13 -48
  46. package/dist/fab/FabMenu.svelte +102 -44
  47. package/dist/fab/FabMenu.svelte.d.ts +2 -1
  48. package/dist/fab/FabMenuItem.svelte +8 -0
  49. package/dist/fab/FabMenuItem.svelte.d.ts +4 -0
  50. package/dist/fab/index.d.ts +1 -0
  51. package/dist/fab/index.js +1 -0
  52. package/dist/fab/types.d.ts +6 -4
  53. package/dist/icons/MenuIcon.svelte +13 -0
  54. package/dist/icons/MenuIcon.svelte.d.ts +7 -0
  55. package/dist/icons/MenuOpenIcon.svelte +15 -0
  56. package/dist/icons/MenuOpenIcon.svelte.d.ts +7 -0
  57. package/dist/icons/index.d.ts +2 -0
  58. package/dist/icons/index.js +2 -0
  59. package/dist/internal/exit.css +47 -0
  60. package/dist/internal/fieldTypes.d.ts +6 -0
  61. package/dist/internal/focus-ring.css +4 -0
  62. package/dist/internal/interest.css +3 -1
  63. package/dist/keyboard-nav.d.ts +12 -1
  64. package/dist/keyboard-nav.js +104 -14
  65. package/dist/list/Item.svelte +52 -6
  66. package/dist/list/List.svelte +35 -2
  67. package/dist/list/ListItem.svelte +16 -2
  68. package/dist/list/context.d.ts +6 -0
  69. package/dist/list/context.js +4 -0
  70. package/dist/list/types.d.ts +7 -0
  71. package/dist/media.d.ts +1 -0
  72. package/dist/media.js +1 -0
  73. package/dist/menu/Menu.svelte +70 -18
  74. package/dist/menu/Menu.svelte.d.ts +2 -1
  75. package/dist/menu/MenuItem.svelte +9 -3
  76. package/dist/menu/types.d.ts +7 -1
  77. package/dist/navigation-bar/NavigationBar.svelte +9 -3
  78. package/dist/navigation-bar/NavigationBarItem.svelte +29 -13
  79. package/dist/navigation-drawer/NavigationDrawer.svelte +96 -20
  80. package/dist/navigation-drawer/NavigationDrawer.svelte.d.ts +2 -1
  81. package/dist/navigation-drawer/NavigationDrawerItem.svelte +8 -3
  82. package/dist/navigation-drawer/NavigationDrawerSection.svelte +49 -0
  83. package/dist/navigation-drawer/NavigationDrawerSection.svelte.d.ts +4 -0
  84. package/dist/navigation-drawer/index.d.ts +1 -0
  85. package/dist/navigation-drawer/index.js +1 -0
  86. package/dist/navigation-drawer/types.d.ts +12 -0
  87. package/dist/navigation-rail/NavigationRail.svelte +367 -7
  88. package/dist/navigation-rail/NavigationRail.svelte.d.ts +1 -1
  89. package/dist/navigation-rail/NavigationRailItem.svelte +190 -53
  90. package/dist/navigation-rail/NavigationRailSection.svelte +57 -0
  91. package/dist/navigation-rail/NavigationRailSection.svelte.d.ts +4 -0
  92. package/dist/navigation-rail/context.d.ts +6 -0
  93. package/dist/navigation-rail/context.js +5 -0
  94. package/dist/navigation-rail/index.d.ts +1 -0
  95. package/dist/navigation-rail/index.js +1 -0
  96. package/dist/navigation-rail/morph.d.ts +14 -0
  97. package/dist/navigation-rail/morph.js +78 -0
  98. package/dist/navigation-rail/types.d.ts +21 -0
  99. package/dist/progress/CircularProgress.svelte +2 -1
  100. package/dist/progress/LinearProgress.svelte +48 -20
  101. package/dist/progress/types.d.ts +1 -0
  102. package/dist/radio/Radio.svelte +7 -1
  103. package/dist/search/Search.svelte +245 -58
  104. package/dist/search/types.d.ts +4 -0
  105. package/dist/select/NativeSelect.svelte +20 -12
  106. package/dist/select/Option.svelte +1 -2
  107. package/dist/select/Select.svelte +59 -140
  108. package/dist/select/VirtualList.svelte +16 -1
  109. package/dist/select/VirtualList.svelte.d.ts +6 -2
  110. package/dist/sheet/Sheet.svelte +223 -5
  111. package/dist/sheet/Sheet.svelte.d.ts +3 -1
  112. package/dist/sheet/types.d.ts +10 -0
  113. package/dist/slider/Slider.svelte +1 -27
  114. package/dist/snackbar/Snackbar.svelte +3 -0
  115. package/dist/snackbar/Snackbar.svelte.d.ts +2 -1
  116. package/dist/switch/Switch.svelte +51 -11
  117. package/dist/tabs/Tab.svelte +28 -14
  118. package/dist/tabs/Tabs.svelte +28 -4
  119. package/dist/tabs/types.d.ts +3 -0
  120. package/dist/text-field/TextField.svelte +49 -13
  121. package/dist/time-picker/DockedTimePicker.svelte +6 -1
  122. package/dist/time-picker/DockedTimePicker.svelte.d.ts +1 -1
  123. package/dist/time-picker/TimePickerDialog.svelte +1 -0
  124. package/dist/time-picker/TimePickerDialog.svelte.d.ts +1 -1
  125. package/dist/toolbar/Toolbar.svelte +21 -11
  126. package/dist/toolbar/types.d.ts +1 -0
  127. package/dist/tooltip/RichTooltip.svelte.d.ts +1 -1
  128. package/dist/tooltip/Tooltip.svelte +3 -1
  129. package/dist/tooltip/Tooltip.svelte.d.ts +1 -1
  130. package/package.json +1 -1
@@ -87,6 +87,7 @@
87
87
  cursor: pointer;
88
88
  -webkit-tap-highlight-color: transparent;
89
89
  margin: var(--np-checkbox-margin, max(0px, (48px - 18px)/2));
90
+ --np-ripple-pressed-color: var(--np-color-primary);
90
91
  }
91
92
  :global(label) {
92
93
  -webkit-tap-highlight-color: transparent;
@@ -188,6 +189,10 @@
188
189
  .np-error .np-icon {
189
190
  fill: var(--np-color-on-error);
190
191
  }
192
+ .np-checkbox-container:not(.np-error):has(input:checked, input:indeterminate) {
193
+ --np-ripple-hover-color: var(--np-color-primary);
194
+ --np-ripple-pressed-color: var(--np-color-on-surface);
195
+ }
191
196
  .np-error {
192
197
  --np-ripple-hover-color: var(--np-color-error);
193
198
  --np-ripple-pressed-color: var(--np-color-error);
@@ -1,5 +1,5 @@
1
1
  <script lang="ts">
2
- import { arrowKeyNav, rovingTabindex } from '../keyboard-nav.js'
2
+ import { arrowKeyNav, focusableItems, focusedItem, rovingTabindex } from '../keyboard-nav.js'
3
3
  import type { ChipSetProps } from './types.js'
4
4
 
5
5
  let { children, element = $bindable(), ...attributes }: ChipSetProps = $props()
@@ -9,7 +9,33 @@
9
9
  currentAttr: 'aria-current',
10
10
  currentValue: 'true',
11
11
  })
12
- const onkeydown = arrowKeyNav(CHIP_SELECTOR, 'horizontal')
12
+ const arrows = arrowKeyNav(CHIP_SELECTOR, 'horizontal')
13
+
14
+ // In a set that wraps, ↑ and ↓ move to the chip closest above or below the focused one.
15
+ const toRow = (event: KeyboardEvent & { currentTarget: EventTarget & HTMLElement }) => {
16
+ const chips = focusableItems(event.currentTarget, CHIP_SELECTOR)
17
+ const focused = focusedItem(event.currentTarget, CHIP_SELECTOR, chips)
18
+ if (!focused) return
19
+ const from = focused.getBoundingClientRect()
20
+ const fromX = from.left + from.width / 2
21
+ const below = event.key === 'ArrowDown'
22
+ const candidates = chips
23
+ .map((chip) => ({ chip, box: chip.getBoundingClientRect() }))
24
+ .filter(({ box }) => (below ? box.top >= from.bottom : box.bottom <= from.top))
25
+ if (!candidates.length) return
26
+ const rowTop = below
27
+ ? Math.min(...candidates.map(({ box }) => box.top))
28
+ : Math.max(...candidates.map(({ box }) => box.top))
29
+ const row = candidates.filter(({ box }) => Math.abs(box.top - rowTop) < 1)
30
+ const distance = (box: DOMRect) => Math.abs(box.left + box.width / 2 - fromX)
31
+ row.sort((a, b) => distance(a.box) - distance(b.box))[0].chip.focus()
32
+ event.preventDefault()
33
+ }
34
+
35
+ const onkeydown = (event: KeyboardEvent & { currentTarget: EventTarget & HTMLElement }) => {
36
+ if (event.key === 'ArrowUp' || event.key === 'ArrowDown') toRow(event)
37
+ else arrows(event)
38
+ }
13
39
  </script>
14
40
 
15
41
  <div {...attributes} bind:this={element} class={['np-chip-set-wrapper', attributes.class]}>
@@ -13,8 +13,9 @@
13
13
  disabled = false,
14
14
  label = '',
15
15
  icon,
16
+ trailingIcon,
16
17
  element = $bindable(),
17
- removeAriaLabel = 'Remove',
18
+ removeAriaLabel,
18
19
  onremove,
19
20
  name,
20
21
  value,
@@ -69,10 +70,22 @@
69
70
  <CheckIcon width={18} height={18} />
70
71
  </div>
71
72
  <div class="np-chip-label">{label}</div>
73
+ {#if trailingIcon && !removable}
74
+ <div class="np-chip-trailing-icon" aria-hidden="true">
75
+ {@render trailingIcon()}
76
+ </div>
77
+ {/if}
78
+ <span class="np-touch"></span>
72
79
  <input
73
80
  type="checkbox"
74
81
  checked={isChecked}
75
82
  {onchange}
83
+ onkeydown={(event) => {
84
+ // Enter selects like Space, instead of submitting the form.
85
+ if (event.key !== 'Enter') return
86
+ event.preventDefault()
87
+ event.currentTarget.click()
88
+ }}
76
89
  {value}
77
90
  {name}
78
91
  {disabled}
@@ -89,7 +102,7 @@
89
102
  type="button"
90
103
  size="xs"
91
104
  --np-icon-button-icon-size="1.125rem"
92
- aria-label={removeAriaLabel}
105
+ aria-label={removeAriaLabel ?? `Remove ${label}`}
93
106
  onclick={onremove}
94
107
  >
95
108
  <CloseIcon />
@@ -99,6 +112,7 @@
99
112
 
100
113
  <style>
101
114
  .np-filter-chip {
115
+ box-sizing: border-box;
102
116
  position: relative;
103
117
  display: inline-flex;
104
118
  align-items: center;
@@ -107,7 +121,6 @@
107
121
  --np-icon-button-icon-color: var(--np-color-on-surface-variant);
108
122
  --np-icon-size: 1.125rem;
109
123
  --np-ripple-pressed-opacity: 0.1;
110
- min-width: 0;
111
124
  transition: box-shadow var(--np-motion-expressive-slow-effects);
112
125
  }
113
126
  .np-filter-chip-label input {
@@ -117,6 +130,7 @@
117
130
  }
118
131
  .np-filter-chip-label {
119
132
  cursor: pointer;
133
+ position: relative;
120
134
  display: inline-flex;
121
135
  align-items: center;
122
136
  height: 2rem;
@@ -126,7 +140,19 @@
126
140
  gap: 0.5rem;
127
141
  z-index: 1;
128
142
  padding-inline: 1rem;
129
- overflow: hidden;
143
+ }
144
+ .np-chip-trailing-icon {
145
+ display: flex;
146
+ margin-inline-end: -0.5rem;
147
+ }
148
+ /* The chip keeps a 48px tall target. */
149
+ .np-touch {
150
+ position: absolute;
151
+ inset-inline: 0;
152
+ top: 50%;
153
+ translate: 0 -50%;
154
+ height: 3rem;
155
+ z-index: -1;
130
156
  }
131
157
  .np-chip-icon-checked {
132
158
  display: flex;
@@ -154,8 +180,10 @@
154
180
  .np-filter-chip-icon .np-filter-chip-label {
155
181
  padding-inline-start: 0.5rem;
156
182
  }
183
+ /* Removable, selecting and removing each keep a 48px target side by side. */
157
184
  .np-filter-chip-removable {
158
185
  padding-inline-end: 1px;
186
+ min-width: 5.5rem;
159
187
  }
160
188
  .np-filter-chip-removable .np-filter-chip-label {
161
189
  padding-inline-end: 1px;
@@ -10,9 +10,11 @@
10
10
  disabled = false,
11
11
  label = '',
12
12
  icon,
13
+ avatar,
14
+ variant = 'outlined',
13
15
  element = $bindable(),
14
16
  actionElement = $bindable(),
15
- removeAriaLabel = 'Remove',
17
+ removeAriaLabel,
16
18
  onclick,
17
19
  onremove,
18
20
  name,
@@ -22,46 +24,88 @@
22
24
  }: InputChipProps = $props()
23
25
 
24
26
  let hasError = $derived(!!issues?.length)
27
+ let content = $derived(label || `${value ?? ''}`)
28
+ // With nothing to do but remove, the chip is one focusable element: its remove button.
29
+ let hasAction = $derived(!!onclick)
30
+ let removeElement: HTMLElement | undefined = $state()
31
+
32
+ // Backspace and Delete remove the focused chip. Focus moves on to a neighbour first, so it is
33
+ // not lost when the chip leaves the page.
34
+ const removeWithKey = (event: KeyboardEvent) => {
35
+ if (disabled || (event.key !== 'Backspace' && event.key !== 'Delete')) return
36
+ event.preventDefault()
37
+ const set = element?.closest('.np-chip-set')
38
+ if (set && element) {
39
+ const chips = [
40
+ ...set.querySelectorAll<HTMLElement>('.np-input-chip:not(.np-input-chip-disabled)'),
41
+ ]
42
+ const index = chips.indexOf(element)
43
+ const neighbour = chips[index + 1] ?? chips[index - 1]
44
+ neighbour?.querySelector<HTMLElement>('button')?.focus()
45
+ }
46
+ removeElement?.click()
47
+ }
25
48
  </script>
26
49
 
27
50
  <div
28
51
  {...attributes}
29
52
  bind:this={element}
30
53
  aria-disabled={disabled}
54
+ onkeydown={(event) => {
55
+ attributes.onkeydown?.(event)
56
+ if (!event.defaultPrevented) removeWithKey(event)
57
+ }}
31
58
  class={[
32
59
  'np-input-chip',
33
- icon ? 'np-input-chip-icon' : '',
60
+ variant === 'elevated' && 'np-input-chip-elevated',
61
+ hasAction && 'np-input-chip-action',
62
+ avatar ? 'np-input-chip-avatar' : icon ? 'np-input-chip-icon' : '',
34
63
  disabled ? 'np-input-chip-disabled' : '',
35
64
  selected ? 'np-input-chip-selected' : '',
36
65
  hasError && !disabled && 'np-chip-error',
37
66
  attributes.class,
38
67
  ]}
39
68
  >
40
- <button
41
- bind:this={actionElement}
42
- type="button"
43
- class="np-input-chip-label"
44
- aria-pressed={selected === undefined ? undefined : selected}
45
- {disabled}
46
- {onclick}
47
- >
48
- {#if icon}
49
- <div class="np-chip-icon">
69
+ {#snippet chipContent()}
70
+ {#if avatar}
71
+ <div class="np-chip-avatar" aria-hidden="true">
72
+ {@render avatar()}
73
+ </div>
74
+ {:else if icon}
75
+ <div class="np-chip-icon" aria-hidden="true">
50
76
  {@render icon()}
51
77
  </div>
52
78
  {/if}
53
- <div class="np-chip-label">{label || value}</div>
54
- </button>
55
- <input type="hidden" {value} {name} {disabled} />
56
- {#if !disabled}
57
- <Ripple forElement={actionElement} />
79
+ <div class="np-chip-label">{content}</div>
80
+ {/snippet}
81
+ {#if hasAction}
82
+ <button
83
+ bind:this={actionElement}
84
+ type="button"
85
+ class="np-input-chip-label"
86
+ aria-pressed={selected === undefined ? undefined : selected}
87
+ {disabled}
88
+ {onclick}
89
+ >
90
+ {@render chipContent()}
91
+ <span class="np-touch"></span>
92
+ </button>
93
+ {#if !disabled}
94
+ <Ripple forElement={actionElement} />
95
+ {/if}
96
+ {:else}
97
+ <div class="np-input-chip-label">
98
+ {@render chipContent()}
99
+ </div>
58
100
  {/if}
101
+ <input type="hidden" {value} {name} {disabled} />
59
102
  <IconButton
60
103
  {disabled}
61
104
  type="button"
62
105
  size="xs"
63
106
  --np-icon-button-icon-size="1.125rem"
64
- aria-label={removeAriaLabel}
107
+ aria-label={removeAriaLabel ?? `Remove ${content}`}
108
+ bind:element={removeElement}
65
109
  onclick={onremove}
66
110
  >
67
111
  <CloseIcon />
@@ -70,6 +114,7 @@
70
114
 
71
115
  <style>
72
116
  .np-input-chip {
117
+ box-sizing: border-box;
73
118
  position: relative;
74
119
  display: inline-flex;
75
120
  align-items: center;
@@ -78,8 +123,12 @@
78
123
  --np-icon-button-icon-color: var(--np-color-on-surface-variant);
79
124
  --np-icon-size: 1.125rem;
80
125
  --np-ripple-pressed-opacity: 0.1;
126
+ --_state-color: var(--np-color-on-surface-variant);
81
127
  padding-inline-end: 1px;
82
- min-width: 0;
128
+ }
129
+ /* With an action besides remove, both keep a 48px target side by side. */
130
+ .np-input-chip-action {
131
+ min-width: 5.5rem;
83
132
  }
84
133
  .np-chip-error {
85
134
  --np-input-chip-outline-color: var(--np-color-error);
@@ -97,6 +146,7 @@
97
146
  padding-block: 0;
98
147
  padding-inline-end: 0;
99
148
  cursor: pointer;
149
+ position: relative;
100
150
  display: inline-flex;
101
151
  align-items: center;
102
152
  height: 2rem;
@@ -119,9 +169,36 @@
119
169
  .np-input-chip-selected .np-chip-icon {
120
170
  color: var(--np-color-primary);
121
171
  }
172
+ div.np-input-chip-label {
173
+ cursor: default;
174
+ }
122
175
  .np-input-chip-icon .np-input-chip-label {
123
176
  padding-inline-start: 0.5rem;
124
177
  }
178
+ .np-input-chip-avatar .np-input-chip-label {
179
+ padding-inline-start: 0.25rem;
180
+ }
181
+ /* A leading image is larger than an icon, with rounded corners. */
182
+ .np-chip-avatar {
183
+ display: flex;
184
+ flex: none;
185
+ width: 1.5rem;
186
+ height: 1.5rem;
187
+ border-radius: 0.75rem;
188
+ overflow: hidden;
189
+ }
190
+ .np-chip-avatar > :global(*) {
191
+ width: 100%;
192
+ height: 100%;
193
+ object-fit: cover;
194
+ }
195
+ .np-touch {
196
+ position: absolute;
197
+ inset-inline: 0;
198
+ top: 50%;
199
+ translate: 0 -50%;
200
+ height: 3rem;
201
+ }
125
202
  .np-chip-label {
126
203
  line-height: 1.25rem;
127
204
  font-size: 0.875rem;
@@ -150,17 +227,47 @@
150
227
  outline-color: transparent;
151
228
  background-color: var(--np-color-secondary-container);
152
229
  }
230
+ .np-input-chip-elevated {
231
+ box-shadow: var(--np-elevation-1);
232
+ }
233
+ .np-input-chip-elevated:not(.np-input-chip-selected)::before {
234
+ outline-color: transparent;
235
+ background-color: var(--np-color-surface-container-low);
236
+ }
237
+ .np-input-chip-elevated.np-input-chip-disabled {
238
+ box-shadow: none;
239
+ }
153
240
  .np-input-chip-selected {
154
241
  --np-icon-button-icon-color: var(--np-color-on-secondary-container);
242
+ --_state-color: var(--np-color-on-secondary-container);
155
243
  }
156
244
  .np-input-chip-selected .np-input-chip-label {
157
245
  color: var(--np-color-on-secondary-container);
158
246
  }
159
- .np-input-chip:has(:focus-visible) {
247
+ /* The whole chip shows focus, with its ring and a 10% state layer: when its body is focused, or its
248
+ remove button when that is all the chip has. A remove button next to a body shows its own. */
249
+ .np-input-chip:is(
250
+ :has(> .np-input-chip-label:focus-visible),
251
+ :not(.np-input-chip-action):has(:global(:focus-visible))
252
+ ) {
160
253
  outline-style: solid;
161
254
  outline-color: var(--np-color-secondary);
162
255
  outline-width: 3px;
163
256
  outline-offset: 2px;
257
+ &::before {
258
+ --_state-layer: color-mix(in srgb, var(--_state-color) 10%, transparent);
259
+ background-image: linear-gradient(var(--_state-layer), var(--_state-layer));
260
+ }
261
+ @media (prefers-reduced-motion: no-preference) {
262
+ animation: focusAnimation var(--np-motion-expressive-slow-effects) forwards;
263
+ }
264
+ }
265
+ .np-input-chip:not(.np-input-chip-action) {
266
+ --np-ripple-focus-opacity: 0;
267
+ }
268
+ .np-input-chip:not(.np-input-chip-action) :global(.np-icon-button:focus-visible) {
269
+ outline: none;
270
+ animation: none;
164
271
  }
165
272
 
166
273
  .np-input-chip-disabled .np-input-chip-label {
@@ -176,10 +283,4 @@
176
283
  outline-color: transparent;
177
284
  background-color: color-mix(in srgb, var(--np-color-on-surface) 12%, transparent);
178
285
  }
179
-
180
- @media (prefers-reduced-motion: no-preference) {
181
- .np-input-chip:has(:focus-visible) {
182
- animation: focusAnimation var(--np-motion-expressive-slow-effects) forwards;
183
- }
184
- }
185
286
  </style>
@@ -1,29 +1,13 @@
1
1
  <script lang="ts">
2
- import Button from '../button/Button.svelte'
2
+ import AssistChip from './AssistChip.svelte'
3
3
  import type { SuggestionChipProps } from './types.js'
4
4
 
5
- let {
6
- variant = 'outlined',
7
- disabled = false,
8
- label = '',
9
- element = $bindable(),
10
- ...attributes
11
- }: SuggestionChipProps = $props()
5
+ let { element = $bindable(), ...attributes }: SuggestionChipProps = $props()
12
6
  </script>
13
7
 
14
- <Button
15
- --np-elevated-button-label-text-color="var(--np-suggestion-chip-label-text-color, var(--np-color-on-surface-variant))"
16
- --np-outlined-button-label-text-color="var(--np-suggestion-chip-label-text-color, var(--np-color-on-surface-variant))"
17
- --np-outlined-button-outline-color="var(--np-suggestion-chip-outline-color, var(--np-color-outline-variant))"
18
- --np-button-padding="1rem"
19
- --np-button-shape="0.5rem"
20
- style="letter-spacing: 0.006rem"
8
+ <AssistChip
9
+ --np-assist-chip-label-text-color="var(--np-suggestion-chip-label-text-color, var(--np-color-on-surface-variant))"
10
+ --np-assist-chip-outline-color="var(--np-suggestion-chip-outline-color, var(--np-color-outline-variant))"
21
11
  bind:element
22
12
  {...attributes}
23
- size="xs"
24
- shape="square"
25
- {variant}
26
- {disabled}
27
- >
28
- {label}
29
- </Button>
13
+ />
@@ -1,4 +1,3 @@
1
- import type { SuggestionChipProps } from './types.js';
2
- declare const SuggestionChip: import("svelte").Component<SuggestionChipProps, {}, "element">;
1
+ declare const SuggestionChip: import("svelte").Component<import("./types.js").AssistChipProps, {}, "element">;
3
2
  type SuggestionChip = ReturnType<typeof SuggestionChip>;
4
3
  export default SuggestionChip;
@@ -13,7 +13,7 @@ export interface AssistChipProps extends HTMLAttributes<ButtonElement>, Omit<HTM
13
13
  element?: HTMLElement;
14
14
  type?: 'submit' | 'reset' | 'button' | (string & {}) | null;
15
15
  }
16
- export type SuggestionChipProps = Omit<AssistChipProps, 'icon'>;
16
+ export type SuggestionChipProps = AssistChipProps;
17
17
  export interface FilterChipProps extends HTMLAttributes<HTMLDivElement> {
18
18
  selected?: boolean;
19
19
  removable?: boolean;
@@ -21,6 +21,9 @@ export interface FilterChipProps extends HTMLAttributes<HTMLDivElement> {
21
21
  variant?: 'outlined' | 'elevated';
22
22
  label?: string;
23
23
  icon?: Snippet;
24
+ /** Trailing icon, such as an arrow for a chip that opens a menu. Not shown when `removable`. */
25
+ trailingIcon?: Snippet;
26
+ /** Defaults to "Remove" and the chip's label. */
24
27
  removeAriaLabel?: string;
25
28
  element?: HTMLDivElement;
26
29
  name?: string;
@@ -33,8 +36,12 @@ export interface FilterChipProps extends HTMLAttributes<HTMLDivElement> {
33
36
  export interface InputChipProps extends Omit<HTMLAttributes<HTMLDivElement>, 'onclick'> {
34
37
  selected?: boolean;
35
38
  disabled?: boolean | null;
39
+ variant?: 'outlined' | 'elevated';
36
40
  label?: string;
37
41
  icon?: Snippet;
42
+ /** A 24px leading image, such as a person's photo. Takes the place of `icon`. */
43
+ avatar?: Snippet;
44
+ /** Defaults to "Remove" and the chip's label. */
38
45
  removeAriaLabel?: string;
39
46
  element?: HTMLDivElement;
40
47
  actionElement?: HTMLButtonElement;
@@ -110,12 +110,14 @@
110
110
  const handleKeydown = (event: KeyboardEvent & { currentTarget: EventTarget & HTMLElement }) => {
111
111
  const grid = event.currentTarget
112
112
  const from = focusedDate ?? tabStop
113
+ // Right to left, the next day sits to the left.
114
+ const forward = getComputedStyle(grid).direction === 'rtl' ? -1 : 1
113
115
  switch (event.key) {
114
116
  case 'ArrowLeft':
115
- moveFocus(grid, addDays(from, -1))
117
+ moveFocus(grid, addDays(from, -forward))
116
118
  break
117
119
  case 'ArrowRight':
118
- moveFocus(grid, addDays(from, 1))
120
+ moveFocus(grid, addDays(from, forward))
119
121
  break
120
122
  case 'ArrowUp':
121
123
  moveFocus(grid, addDays(from, -DAYS_IN_WEEK))
@@ -197,6 +197,7 @@
197
197
  </script>
198
198
 
199
199
  <Dialog
200
+ role="dialog"
200
201
  {...attributes}
201
202
  bind:element
202
203
  aria-label={title}
@@ -2,6 +2,6 @@ import type { DatePickerDialogProps } from './types.js';
2
2
  declare const DatePickerDialog: import("svelte").Component<DatePickerDialogProps, {
3
3
  show: () => void;
4
4
  close: () => void;
5
- }, "element" | "value" | "open" | "displayMonth">;
5
+ }, "open" | "element" | "value" | "displayMonth">;
6
6
  type DatePickerDialog = ReturnType<typeof DatePickerDialog>;
7
7
  export default DatePickerDialog;
@@ -184,6 +184,7 @@
184
184
  </script>
185
185
 
186
186
  <Dialog
187
+ role="dialog"
187
188
  {...attributes}
188
189
  bind:element
189
190
  aria-label={title}
@@ -2,6 +2,6 @@ import type { DateRangePickerProps } from './types.js';
2
2
  declare const DateRangePicker: import("svelte").Component<DateRangePickerProps, {
3
3
  show: () => void;
4
4
  close: () => void;
5
- }, "element" | "value" | "open">;
5
+ }, "open" | "element" | "value">;
6
6
  type DateRangePicker = ReturnType<typeof DateRangePicker>;
7
7
  export default DateRangePicker;
@@ -1,4 +1,5 @@
1
1
  <script lang="ts">
2
+ import { exitVisibility } from './exitVisibility.svelte.js'
2
3
  import Button from '../button/Button.svelte'
3
4
  import IconButton from '../button/IconButton.svelte'
4
5
  import Divider from '../divider/Divider.svelte'
@@ -82,6 +83,8 @@
82
83
  const uid = $props.id()
83
84
 
84
85
  let menuElement = $state<HTMLDivElement>()
86
+ // Keeps the content while the menu animates out, so it fades as a whole, not as an empty box.
87
+ const exit = exitVisibility()
85
88
  let mode = $state<'days' | 'months' | 'years'>('days')
86
89
  let listMode = $state<'months' | 'years' | undefined>(undefined)
87
90
  let pending = $state<ISODate | undefined>(undefined)
@@ -368,19 +371,23 @@
368
371
  bind:open
369
372
  ontoggle={({ newState }) => {
370
373
  if (newState === 'open') {
374
+ exit.show()
371
375
  monthBeforeOpen = displayMonth
372
376
  pending = current
373
377
  focusCalendarWhenReady()
374
378
  }
375
379
  if (newState === 'closed') {
376
- setMode('days')
377
- listMode = undefined
378
- focusedDay = undefined
379
- displayMonth = monthBeforeOpen
380
+ // Reset once the menu is gone, not while it still shows.
381
+ exit.scheduleExit(menuElement, () => {
382
+ setMode('days')
383
+ listMode = undefined
384
+ focusedDay = undefined
385
+ displayMonth = monthBeforeOpen
386
+ })
380
387
  }
381
388
  }}
382
389
  >
383
- {#if open}
390
+ {#if open || exit.visible}
384
391
  <div class="np-docked-date-picker-container" style:--np-calendar-rows={rowCount}>
385
392
  <CalendarHeader
386
393
  {mode}
@@ -2,6 +2,6 @@ import type { DockedDatePickerProps } from './types.js';
2
2
  declare const DockedDatePicker: import("svelte").Component<DockedDatePickerProps, {
3
3
  show: () => void;
4
4
  close: () => void;
5
- }, "element" | "value" | "open" | "displayMonth">;
5
+ }, "open" | "element" | "value" | "displayMonth">;
6
6
  type DockedDatePicker = ReturnType<typeof DockedDatePicker>;
7
7
  export default DockedDatePicker;
@@ -1,4 +1,5 @@
1
1
  <script lang="ts">
2
+ import { exitVisibility } from './exitVisibility.svelte.js'
2
3
  import Button from '../button/Button.svelte'
3
4
  import IconButton from '../button/IconButton.svelte'
4
5
  import Divider from '../divider/Divider.svelte'
@@ -103,6 +104,8 @@
103
104
  const uid = $props.id()
104
105
 
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()
106
109
  let mode = $state<'days' | 'months' | 'years'>('days')
107
110
  let listMode = $state<'months' | 'years' | undefined>(undefined)
108
111
  let pendingDay = $state<ISODate | undefined>(undefined)
@@ -461,20 +464,24 @@
461
464
  bind:open
462
465
  ontoggle={({ newState }) => {
463
466
  if (newState === 'open') {
467
+ exit.show()
464
468
  monthBeforeOpen = displayMonth
465
469
  pendingDay = committed ? toISODate(committed) : undefined
466
470
  pendingMinutes = committed ? minutesOfDay(committed) : defaultMinutes
467
471
  focusCalendarWhenReady()
468
472
  }
469
473
  if (newState === 'closed') {
470
- setMode('days')
471
- listMode = undefined
472
- focusedDay = undefined
473
- displayMonth = monthBeforeOpen
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
+ })
474
481
  }
475
482
  }}
476
483
  >
477
- {#if open}
484
+ {#if open || exit.visible}
478
485
  <div
479
486
  class="np-docked-date-time-picker-container"
480
487
  style:--np-calendar-rows={rowCount}
@@ -2,6 +2,6 @@ import type { DockedDateTimePickerProps } from './types.js';
2
2
  declare const DockedDateTimePicker: import("svelte").Component<DockedDateTimePickerProps, {
3
3
  show: () => void;
4
4
  close: () => void;
5
- }, "element" | "value" | "open" | "displayMonth">;
5
+ }, "open" | "element" | "value" | "displayMonth">;
6
6
  type DockedDateTimePicker = ReturnType<typeof DockedDateTimePicker>;
7
7
  export default DockedDateTimePicker;
@@ -45,12 +45,14 @@
45
45
  )
46
46
  const from = Number(focused?.dataset.year)
47
47
  if (!focused || Number.isNaN(from)) return
48
+ // Right to left, the next year sits to the left.
49
+ const forward = getComputedStyle(grid).direction === 'rtl' ? -1 : 1
48
50
  switch (event.key) {
49
51
  case 'ArrowLeft':
50
- focusYear(grid, from - 1)
52
+ focusYear(grid, from - forward)
51
53
  break
52
54
  case 'ArrowRight':
53
- focusYear(grid, from + 1)
55
+ focusYear(grid, from + forward)
54
56
  break
55
57
  case 'ArrowUp':
56
58
  focusYear(grid, from - YEARS_IN_ROW)