@sveltia/ui 0.78.4 → 0.79.1

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 (183) hide show
  1. package/dist/components/alert/alert-content.svelte +51 -0
  2. package/dist/components/alert/alert-content.svelte.d.ts +39 -0
  3. package/dist/components/alert/alert.d.ts +3 -0
  4. package/dist/components/alert/alert.js +21 -0
  5. package/dist/components/alert/alert.svelte +6 -30
  6. package/dist/components/alert/infobar.svelte +7 -21
  7. package/dist/components/alert/infobar.svelte.d.ts +2 -2
  8. package/dist/components/button/button.svelte +11 -11
  9. package/dist/components/button/button.svelte.d.ts +2 -2
  10. package/dist/components/button/select-button.svelte +1 -1
  11. package/dist/components/button/select-button.svelte.d.ts +2 -2
  12. package/dist/components/button/split-button.svelte +1 -1
  13. package/dist/components/button/split-button.svelte.d.ts +3 -3
  14. package/dist/components/calendar/calendar.d.ts +2 -0
  15. package/dist/components/calendar/calendar.js +34 -0
  16. package/dist/components/calendar/calendar.svelte +49 -5
  17. package/dist/components/checkbox/checkbox.svelte +11 -4
  18. package/dist/components/checkbox/checkbox.svelte.d.ts +2 -2
  19. package/dist/components/dialog/alert-dialog.svelte +1 -1
  20. package/dist/components/dialog/alert-dialog.svelte.d.ts +2 -2
  21. package/dist/components/dialog/confirmation-dialog.svelte +1 -1
  22. package/dist/components/dialog/confirmation-dialog.svelte.d.ts +2 -2
  23. package/dist/components/dialog/dialog.svelte +8 -18
  24. package/dist/components/dialog/dialog.svelte.d.ts +2 -2
  25. package/dist/components/dialog/modal-close-button.svelte +50 -0
  26. package/dist/components/dialog/modal-close-button.svelte.d.ts +43 -0
  27. package/dist/components/dialog/prompt-dialog.svelte +1 -1
  28. package/dist/components/dialog/prompt-dialog.svelte.d.ts +4 -4
  29. package/dist/components/disclosure/disclosure.svelte +1 -1
  30. package/dist/components/drawer/drawer.svelte +12 -34
  31. package/dist/components/drawer/drawer.svelte.d.ts +1 -1
  32. package/dist/components/emoji/caret.d.ts +1 -1
  33. package/dist/components/emoji/caret.js +1 -1
  34. package/dist/components/emoji/emoji-suggestions.svelte +1 -1
  35. package/dist/components/emoji/emoji-suggestions.svelte.d.ts +3 -3
  36. package/dist/components/emoji/emoji.d.ts +2 -2
  37. package/dist/components/emoji/emoji.js +1 -1
  38. package/dist/components/emoji/position.d.ts +2 -2
  39. package/dist/components/emoji/position.js +1 -1
  40. package/dist/components/grid/grid-body.svelte +6 -2
  41. package/dist/components/grid/grid-cell.svelte +3 -9
  42. package/dist/components/grid/grid-col-header.svelte +3 -9
  43. package/dist/components/grid/grid-foot.svelte +4 -11
  44. package/dist/components/grid/grid-head.svelte +4 -11
  45. package/dist/components/grid/grid-row-header.svelte +3 -10
  46. package/dist/components/grid/grid-row.svelte +3 -16
  47. package/dist/components/listbox/listbox.svelte +1 -1
  48. package/dist/components/listbox/listbox.svelte.d.ts +1 -1
  49. package/dist/components/listbox/option.svelte +1 -1
  50. package/dist/components/listbox/option.svelte.d.ts +2 -2
  51. package/dist/components/menu/menu-button.svelte +2 -2
  52. package/dist/components/menu/menu-button.svelte.d.ts +4 -4
  53. package/dist/components/menu/menu-item-checkable.svelte +71 -0
  54. package/dist/components/menu/menu-item-checkable.svelte.d.ts +25 -0
  55. package/dist/components/menu/menu-item-checkbox.svelte +3 -36
  56. package/dist/components/menu/menu-item-checkbox.svelte.d.ts +3 -3
  57. package/dist/components/menu/menu-item-group.svelte +8 -5
  58. package/dist/components/menu/menu-item-group.svelte.d.ts +9 -1
  59. package/dist/components/menu/menu-item-radio.svelte +3 -36
  60. package/dist/components/menu/menu-item-radio.svelte.d.ts +3 -3
  61. package/dist/components/menu/menu-item.svelte +22 -6
  62. package/dist/components/menu/menu-item.svelte.d.ts +4 -4
  63. package/dist/components/radio/radio.svelte +12 -1
  64. package/dist/components/resizable-pane/resizable-handle.svelte +14 -18
  65. package/dist/components/resizable-pane/resizable-pane-group.svelte +9 -6
  66. package/dist/components/resizable-pane/resizable-pane.svelte +4 -4
  67. package/dist/components/resizable-pane/resizable-pane.svelte.d.ts +7 -6
  68. package/dist/components/resizable-pane/sizing.d.ts +5 -4
  69. package/dist/components/resizable-pane/sizing.js +84 -25
  70. package/dist/components/scroll/infinite-scroll.svelte +16 -21
  71. package/dist/components/select/combobox.svelte +43 -52
  72. package/dist/components/select/combobox.svelte.d.ts +2 -2
  73. package/dist/components/select/select-tags.svelte +7 -1
  74. package/dist/components/select/select.svelte +2 -22
  75. package/dist/components/select/select.svelte.d.ts +1 -1
  76. package/dist/components/slider/slider.d.ts +6 -0
  77. package/dist/components/slider/slider.js +55 -2
  78. package/dist/components/slider/slider.svelte +63 -32
  79. package/dist/components/table/table-cell.svelte +3 -9
  80. package/dist/components/table/table-col-header.svelte +3 -9
  81. package/dist/components/table/table-foot.svelte +4 -11
  82. package/dist/components/table/table-head.svelte +4 -11
  83. package/dist/components/table/table-part.svelte +69 -0
  84. package/dist/components/table/table-part.svelte.d.ts +48 -0
  85. package/dist/components/table/table-row-header.svelte +3 -10
  86. package/dist/components/table/table-row.svelte +3 -10
  87. package/dist/components/tabs/tab-list.svelte +12 -0
  88. package/dist/components/tabs/tab.svelte +1 -1
  89. package/dist/components/tabs/tab.svelte.d.ts +2 -2
  90. package/dist/components/text-editor/constants.d.ts +5 -5
  91. package/dist/components/text-editor/constants.js +1 -1
  92. package/dist/components/text-editor/core.d.ts +4 -5
  93. package/dist/components/text-editor/core.js +372 -285
  94. package/dist/components/text-editor/emoji-autocomplete.svelte +1 -1
  95. package/dist/components/text-editor/lexical-root.svelte +4 -2
  96. package/dist/components/text-editor/raw-markdown.d.ts +6 -33
  97. package/dist/components/text-editor/raw-markdown.js +63 -8
  98. package/dist/components/text-editor/shiki/facade.d.ts +1 -1
  99. package/dist/components/text-editor/shiki/facade.js +63 -48
  100. package/dist/components/text-editor/shiki/generated.d.ts +1 -1
  101. package/dist/components/text-editor/shiki/generated.js +1 -1
  102. package/dist/components/text-editor/shiki/highlighter.d.ts +6 -1
  103. package/dist/components/text-editor/shiki/highlighter.js +3 -4
  104. package/dist/components/text-editor/shiki/loader.d.ts +1 -1
  105. package/dist/components/text-editor/shiki/loader.js +1 -1
  106. package/dist/components/text-editor/shiki/version.d.ts +1 -1
  107. package/dist/components/text-editor/shiki/version.js +1 -1
  108. package/dist/components/text-editor/store.svelte.d.ts +1 -1
  109. package/dist/components/text-editor/store.svelte.js +29 -2
  110. package/dist/components/text-editor/text-editor.svelte +2 -2
  111. package/dist/components/text-editor/text-editor.svelte.d.ts +3 -3
  112. package/dist/components/text-editor/toolbar/code-language-switcher.svelte +24 -16
  113. package/dist/components/text-editor/toolbar/format-text-button.svelte +7 -31
  114. package/dist/components/text-editor/toolbar/format-text-button.svelte.d.ts +1 -1
  115. package/dist/components/text-editor/toolbar/insert-component.d.ts +3 -0
  116. package/dist/components/text-editor/toolbar/insert-component.js +34 -0
  117. package/dist/components/text-editor/toolbar/insert-item-button.svelte +4 -24
  118. package/dist/components/text-editor/toolbar/insert-item-button.svelte.d.ts +1 -1
  119. package/dist/components/text-editor/toolbar/insert-link-button.svelte +9 -25
  120. package/dist/components/text-editor/toolbar/insert-menu-button.svelte +4 -24
  121. package/dist/components/text-editor/toolbar/insert-menu-button.svelte.d.ts +1 -1
  122. package/dist/components/text-editor/toolbar/text-editor-toolbar.svelte +1 -1
  123. package/dist/components/text-editor/toolbar/toggle-block-menu-item.svelte +31 -35
  124. package/dist/components/text-editor/toolbar/toggle-block-menu-item.svelte.d.ts +1 -1
  125. package/dist/components/text-editor/toolbar/toolbar-wrapper.svelte +0 -10
  126. package/dist/components/text-editor/toolbar/toolbar-wrapper.svelte.d.ts +1 -11
  127. package/dist/components/text-editor/transformers/table.d.ts +0 -1
  128. package/dist/components/text-editor/transformers/table.js +1 -2
  129. package/dist/components/text-field/emoji-autocomplete.svelte +1 -1
  130. package/dist/components/text-field/masked-input.svelte +179 -0
  131. package/dist/components/text-field/masked-input.svelte.d.ts +42 -0
  132. package/dist/components/text-field/number-input.js +7 -1
  133. package/dist/components/text-field/number-input.svelte +13 -5
  134. package/dist/components/text-field/number-input.svelte.d.ts +3 -3
  135. package/dist/components/text-field/password-input.svelte +3 -116
  136. package/dist/components/text-field/password-input.svelte.d.ts +3 -3
  137. package/dist/components/text-field/search-bar.svelte +1 -1
  138. package/dist/components/text-field/search-bar.svelte.d.ts +3 -3
  139. package/dist/components/text-field/secret-input.svelte +3 -119
  140. package/dist/components/text-field/secret-input.svelte.d.ts +3 -3
  141. package/dist/components/text-field/text-area.svelte +1 -1
  142. package/dist/components/text-field/text-area.svelte.d.ts +3 -3
  143. package/dist/components/text-field/text-input.svelte +1 -1
  144. package/dist/components/text-field/text-input.svelte.d.ts +3 -3
  145. package/dist/components/toast/toast.svelte +11 -20
  146. package/dist/components/toast/toast.svelte.d.ts +1 -1
  147. package/dist/components/tree/tree-item.svelte +1 -1
  148. package/dist/components/tree/tree-item.svelte.d.ts +1 -1
  149. package/dist/components/tree/tree.svelte +1 -1
  150. package/dist/components/tree/tree.svelte.d.ts +1 -1
  151. package/dist/components/util/font-links.svelte +1 -0
  152. package/dist/components/util/modal.svelte +37 -1
  153. package/dist/components/util/modal.svelte.d.ts +1 -1
  154. package/dist/components/util/popup.svelte +2 -5
  155. package/dist/components/util/popup.svelte.d.ts +2 -2
  156. package/dist/components/util/visibility-observer.svelte +1 -0
  157. package/dist/locales/ar.yaml +1 -1
  158. package/dist/locales/sk.yaml +136 -0
  159. package/dist/services/group.svelte.d.ts +10 -1
  160. package/dist/services/group.svelte.js +361 -251
  161. package/dist/services/navigation.d.ts +19 -0
  162. package/dist/services/navigation.js +84 -0
  163. package/dist/services/popup-position.d.ts +90 -0
  164. package/dist/services/popup-position.js +275 -0
  165. package/dist/services/popup.svelte.d.ts +16 -17
  166. package/dist/services/popup.svelte.js +25 -169
  167. package/dist/services/scroll.d.ts +1 -0
  168. package/dist/services/scroll.js +17 -0
  169. package/dist/services/select.svelte.d.ts +2 -2
  170. package/dist/services/select.svelte.js +1 -1
  171. package/dist/services/text.d.ts +2 -0
  172. package/dist/services/text.js +28 -0
  173. package/dist/services/tree.svelte.d.ts +3 -11
  174. package/dist/services/tree.svelte.js +35 -77
  175. package/dist/services/type-ahead.d.ts +0 -4
  176. package/dist/services/type-ahead.js +3 -1
  177. package/dist/services/widget.d.ts +8 -0
  178. package/dist/services/widget.js +39 -0
  179. package/dist/shiki-engine.js +15 -15
  180. package/dist/styles/mixins.scss +126 -0
  181. package/dist/typedefs.d.ts +5 -0
  182. package/dist/typedefs.js +2 -0
  183. package/package.json +24 -20
@@ -4,6 +4,12 @@
4
4
  * size here is a percentage of the pane group unless stated otherwise.
5
5
  */
6
6
 
7
+ import { getArrowKeys } from '../../services/navigation.js';
8
+
9
+ /**
10
+ * @import { ResizablePaneSize } from '../../typedefs.js';
11
+ */
12
+
7
13
  /**
8
14
  * @typedef {object} SizeEnvironment
9
15
  * @property {number} containerSize Size of the pane group in pixels along its direction, or `0`
@@ -20,7 +26,7 @@
20
26
 
21
27
  /**
22
28
  * Resolve a size given as a percentage number or a CSS length string to a percentage.
23
- * @param {number | string | undefined} value Size as a percentage number, or a CSS size string
29
+ * @param {ResizablePaneSize | undefined} value Size as a percentage number, or a CSS size string
24
30
  * such as `240px`, `20%` or `20dvw`.
25
31
  * @param {number} fallback Percentage to return when the value can’t be resolved.
26
32
  * @param {SizeEnvironment} env Sizes the lengths are relative to. A length other than a percentage
@@ -72,7 +78,7 @@ export const resolveToPercent = (value, fallback, env) => {
72
78
  /**
73
79
  * Resolve a pane’s size constraints to percentages, clamped to the `0`–`100` range, with the
74
80
  * maximum never below the minimum.
75
- * @param {{ minSize: number | string, maxSize: number | string } | undefined} paneDef Pane
81
+ * @param {{ minSize: ResizablePaneSize, maxSize: ResizablePaneSize } | undefined} paneDef Pane
76
82
  * definition, or `undefined` for a pane that hasn’t registered, which is unconstrained.
77
83
  * @param {SizeEnvironment} env Sizes the lengths are relative to.
78
84
  * @returns {PaneConstraints} Constraints.
@@ -91,21 +97,83 @@ export const resolvePaneConstraints = (paneDef, env) => {
91
97
  };
92
98
  };
93
99
 
100
+ /**
101
+ * Spread an amount over some of the panes, as far as their constraints allow. A positive amount
102
+ * grows the panes and a negative one shrinks them.
103
+ * @param {number[]} sizes Size of each pane, updated in place.
104
+ * @param {PaneConstraints[]} constraints Constraints of each pane.
105
+ * @param {number[]} indexes Indexes of the panes to spread the amount over.
106
+ * @param {number} amount Amount to spread.
107
+ * @returns {number} What is left of the amount once the panes are full or empty.
108
+ */
109
+ const spreadSize = (sizes, constraints, indexes, amount) => {
110
+ let remaining = amount;
111
+ let adjustable = indexes;
112
+
113
+ // Each round either uses up the amount or fills at least one pane, so the loop is bounded
114
+ while (adjustable.length && Math.abs(remaining) > 1e-9) {
115
+ const share = remaining / adjustable.length;
116
+
117
+ const moved = adjustable.map((i) => {
118
+ const { minSize, maxSize } = constraints[i];
119
+ const size = Math.min(maxSize, Math.max(minSize, sizes[i] + share));
120
+ const diff = size - sizes[i];
121
+
122
+ sizes[i] = size;
123
+
124
+ return diff;
125
+ });
126
+
127
+ remaining -= moved.reduce((sum, diff) => sum + diff, 0);
128
+ adjustable = adjustable.filter((i) =>
129
+ share > 0 ? sizes[i] < constraints[i].maxSize : sizes[i] > constraints[i].minSize,
130
+ );
131
+ }
132
+
133
+ return remaining;
134
+ };
135
+
94
136
  /**
95
137
  * Work out the initial size of each pane. Panes with a default size get it; the others share
96
- * whatever is left of the group equally.
138
+ * whatever is left of the group equally. When constraints are given, each size is then brought
139
+ * within its pane’s constraints, and whatever that frees up or takes away is made up by the other
140
+ * panes, those without a default size first, so the total stays the same where possible.
97
141
  * @param {number[]} resolvedDefaults Each pane’s default size as a percentage, or `NaN` for a pane
98
142
  * that has none, or whose default couldn’t be resolved.
143
+ * @param {PaneConstraints[]} [constraints] Constraints of each pane.
99
144
  * @returns {number[]} Size of each pane.
100
145
  */
101
- export const getInitialSizes = (resolvedDefaults) => {
146
+ export const getInitialSizes = (resolvedDefaults, constraints) => {
102
147
  const specified = resolvedDefaults.filter((v) => !Number.isNaN(v));
103
148
  const totalSpecified = specified.reduce((sum, v) => sum + v, 0);
104
149
  const unspecifiedCount = resolvedDefaults.length - specified.length;
105
150
  const remaining = Math.max(0, 100 - totalSpecified);
106
151
  const defaultSize = unspecifiedCount > 0 ? remaining / unspecifiedCount : 0;
152
+ const sizes = resolvedDefaults.map((v) => (Number.isNaN(v) ? defaultSize : v));
153
+
154
+ if (!constraints) {
155
+ return sizes;
156
+ }
157
+
158
+ const total = sizes.reduce((sum, v) => sum + v, 0);
159
+ const allConstraints = sizes.map((_v, i) => constraints[i] ?? { minSize: 0, maxSize: 100 });
160
+
161
+ const fitted = sizes.map((v, i) =>
162
+ Math.min(allConstraints[i].maxSize, Math.max(allConstraints[i].minSize, v)),
163
+ );
164
+
165
+ const indexes = sizes.map((_v, i) => i);
166
+ const unspecified = indexes.filter((i) => Number.isNaN(resolvedDefaults[i]));
167
+ const excess = total - fitted.reduce((sum, v) => sum + v, 0);
107
168
 
108
- return resolvedDefaults.map((v) => (Number.isNaN(v) ? defaultSize : v));
169
+ spreadSize(
170
+ fitted,
171
+ allConstraints,
172
+ indexes,
173
+ spreadSize(fitted, allConstraints, unspecified, excess),
174
+ );
175
+
176
+ return fitted;
109
177
  };
110
178
 
111
179
  /**
@@ -124,14 +192,16 @@ export const clampResizeDelta = (
124
192
  deltaPercent,
125
193
  { sizeBefore, sizeAfter, constraintsBefore, constraintsAfter },
126
194
  ) => {
127
- const canGrow = Math.min(
128
- constraintsBefore.maxSize - sizeBefore,
129
- sizeAfter - constraintsAfter.minSize,
195
+ // Never below zero, so a pane that is already out of its constraints can’t make the handle move
196
+ // the wrong way
197
+ const canGrow = Math.max(
198
+ 0,
199
+ Math.min(constraintsBefore.maxSize - sizeBefore, sizeAfter - constraintsAfter.minSize),
130
200
  );
131
201
 
132
- const canShrink = Math.min(
133
- sizeBefore - constraintsBefore.minSize,
134
- constraintsAfter.maxSize - sizeAfter,
202
+ const canShrink = Math.max(
203
+ 0,
204
+ Math.min(sizeBefore - constraintsBefore.minSize, constraintsAfter.maxSize - sizeAfter),
135
205
  );
136
206
 
137
207
  return deltaPercent > 0 ? Math.min(deltaPercent, canGrow) : -Math.min(-deltaPercent, canShrink);
@@ -165,24 +235,13 @@ export const getHandleKeyAction = ({ key, shiftKey, isHorizontal, rtl }) => {
165
235
  }
166
236
 
167
237
  const step = shiftKey ? 10 : 1;
238
+ const { prevKey, nextKey } = getArrowKeys(isHorizontal ? 'horizontal' : 'vertical', rtl);
168
239
 
169
- if (isHorizontal) {
170
- if (key === 'ArrowLeft') {
171
- return { type: 'resize', delta: rtl ? step : -step };
172
- }
173
-
174
- if (key === 'ArrowRight') {
175
- return { type: 'resize', delta: rtl ? -step : step };
176
- }
177
-
178
- return undefined;
179
- }
180
-
181
- if (key === 'ArrowUp') {
240
+ if (key === prevKey) {
182
241
  return { type: 'resize', delta: -step };
183
242
  }
184
243
 
185
- if (key === 'ArrowDown') {
244
+ if (key === nextKey) {
186
245
  return { type: 'resize', delta: step };
187
246
  }
188
247
 
@@ -35,35 +35,29 @@
35
35
 
36
36
  const loading = $derived(items.length > loadedItemSize);
37
37
 
38
- const observer = new IntersectionObserver(([{ isIntersecting }]) => {
39
- if (isIntersecting) {
40
- // The spinner is unmounted as soon as everything is loaded, so it can’t intersect afterwards
41
- /* v8 ignore else */
42
- if (loading) {
43
- loadedItemSize += itemChunkSize;
44
- } else {
45
- observer.disconnect();
46
- }
47
- }
48
- });
49
-
50
38
  // Observe the spinner anew once each chunk is rendered: the observer only reports changes, so a
51
39
  // spinner that stays in view because the chunk didn’t fill the viewport would stall the loading.
52
- // Observing an element always reports its current state first. Stop observing on unmount too.
40
+ // Observing an element always reports its current state first. The observer is created here
41
+ // rather than when the component is set up, since `IntersectionObserver` doesn’t exist during
42
+ // server-side rendering, and it’s disconnected before the next chunk and on unmount.
53
43
  $effect(() => {
54
44
  void loadedItemSize;
55
45
 
56
- if (spinner) {
57
- const target = spinner;
46
+ if (!spinner) {
47
+ return undefined;
48
+ }
58
49
 
59
- observer.observe(target);
50
+ const observer = new IntersectionObserver(([{ isIntersecting }]) => {
51
+ if (isIntersecting && loading) {
52
+ loadedItemSize += itemChunkSize;
53
+ }
54
+ });
60
55
 
61
- return () => {
62
- observer.unobserve(target);
63
- };
64
- }
56
+ observer.observe(spinner);
65
57
 
66
- return undefined;
58
+ return () => {
59
+ observer.disconnect();
60
+ };
67
61
  });
68
62
  </script>
69
63
 
@@ -79,4 +73,5 @@
79
73
  .spinner {
80
74
  height: 1px;
81
75
  }
76
+
82
77
  </style>
@@ -8,7 +8,7 @@
8
8
  <script>
9
9
  import { _ } from '@sveltia/i18n';
10
10
  import { onMount } from 'svelte';
11
- import { normalize } from '../../services/group.svelte.js';
11
+ import { normalize } from '../../services/text.js';
12
12
  import { createOptionRegistry, getSelectedItemDetail } from '../../services/select.svelte.js';
13
13
  import { findTypeAheadMatch, TypeAhead } from '../../services/type-ahead.js';
14
14
  import Button from '../button/button.svelte';
@@ -20,7 +20,7 @@
20
20
  import Popup from '../util/popup.svelte';
21
21
 
22
22
  /**
23
- * @import { ComboboxProps, TextInputProps } from '../../typedefs';
23
+ * @import { ComboboxProps, OptionEntry, TextInputProps } from '../../typedefs.js';
24
24
  */
25
25
 
26
26
  /**
@@ -102,15 +102,29 @@
102
102
  const registry = createOptionRegistry();
103
103
 
104
104
  /**
105
- * Update the {@link label} and selected option when the {@link value} is changed.
105
+ * Update the {@link label} and selected option when the {@link value} is changed. A value that
106
+ * matches no option, including `undefined`, clears both, so the previous option doesn’t stay
107
+ * selected.
106
108
  */
107
109
  const _onChange = () => {
108
110
  const entry = registry.find(value);
109
111
 
110
- if (entry) {
111
- label = entry.label;
112
- registry.selectOnly(value);
113
- }
112
+ label = entry?.label ?? '';
113
+ // With no matching entry, this deselects every option
114
+ registry.selectOnly(value);
115
+ };
116
+
117
+ /**
118
+ * Raise a `Change` event for an option known only through its registry entry. There is no option
119
+ * element to report while the dropdown is collapsed.
120
+ * @param {OptionEntry} entry Selected option’s entry.
121
+ */
122
+ const dispatchEntryChange = ({ type, name, label: entryLabel, value: entryValue }) => {
123
+ onChange?.(
124
+ new CustomEvent('Change', {
125
+ detail: { target: undefined, type, name, label: entryLabel, value: entryValue },
126
+ }),
127
+ );
114
128
  };
115
129
 
116
130
  /**
@@ -126,6 +140,24 @@
126
140
  onChange?.(new CustomEvent('Change', { detail }));
127
141
  };
128
142
 
143
+ /**
144
+ * Update the {@link value} as the user types in the editable combobox. The text is a free-form
145
+ * value, so the `Change` event carries the text itself rather than an option.
146
+ * @param {Event} event The `input` event.
147
+ */
148
+ const onTextInput = (event) => {
149
+ const { target } = /** @type {Event & { target: HTMLInputElement }} */ (event);
150
+
151
+ value = target.value;
152
+ _onChange();
153
+ restProps.oninput?.(event);
154
+ onChange?.(
155
+ new CustomEvent('Change', {
156
+ detail: { target, type: 'string', name: undefined, label: target.value, value },
157
+ }),
158
+ );
159
+ };
160
+
129
161
  /**
130
162
  * Keystroke buffer for type-ahead on the collapsed select.
131
163
  */
@@ -178,17 +210,7 @@
178
210
 
179
211
  value = entry.value;
180
212
  _onChange();
181
- onChange?.(
182
- new CustomEvent('Change', {
183
- detail: {
184
- target: undefined,
185
- type: entry.type,
186
- name: entry.name,
187
- label: entry.label,
188
- value: entry.value,
189
- },
190
- }),
191
- );
213
+ dispatchEntryChange(entry);
192
214
  };
193
215
 
194
216
  /**
@@ -280,17 +302,7 @@
280
302
  label = entry.label;
281
303
 
282
304
  // There is no element to report while the dropdown has never been expanded
283
- onChange?.(
284
- new CustomEvent('Change', {
285
- detail: {
286
- target: undefined,
287
- type: entry.type,
288
- name: entry.name,
289
- label: entry.label,
290
- value: entry.value,
291
- },
292
- }),
293
- );
305
+ dispatchEntryChange(entry);
294
306
  });
295
307
 
296
308
  $effect(() => {
@@ -304,7 +316,7 @@
304
316
  only in the DOM tree while it’s expanded, and a reference to a missing element is worse than no
305
317
  reference at all. The popup service adds it on open and removes it on close.
306
318
  -->
307
- <div {...restProps} role="none" class="sui combobox {className}" class:editable {hidden}>
319
+ <div role="none" class="sui combobox {className}" class:editable {hidden}>
308
320
  {#if !editable}
309
321
  <div
310
322
  bind:this={comboboxElement}
@@ -345,6 +357,7 @@
345
357
  aria-expanded={isPopupOpen}
346
358
  aria-haspopup="listbox"
347
359
  {ariaLabel}
360
+ oninput={onTextInput}
348
361
  />
349
362
  {/if}
350
363
  <Button
@@ -493,12 +506,6 @@
493
506
  font-size: var(--sui-font-size-xx-large);
494
507
  opacity: 0.5;
495
508
  }
496
- .combobox > :global(.icon) {
497
- position: absolute;
498
- inset-block-start: 8px;
499
- inset-inline-start: 8px;
500
- z-index: 1;
501
- }
502
509
  .combobox > :global(button) {
503
510
  position: absolute;
504
511
  inset-block-start: 0;
@@ -521,22 +528,6 @@
521
528
  padding-inline: 8px 32px;
522
529
  width: 0;
523
530
  }
524
- .combobox + :global([role=listbox]) {
525
- position: fixed;
526
- z-index: 100;
527
- border-radius: var(--sui-control-medium-border-radius);
528
- box-shadow: 0 8px 16px var(--sui-popup-shadow-color);
529
- overflow: auto;
530
- background-color: var(--sui-secondary-background-color);
531
- -webkit-backdrop-filter: blur(32px);
532
- backdrop-filter: blur(32px);
533
- /* Add .1s delay before the position can be determined */
534
- transition: opacity 100ms 100ms;
535
- }
536
- .combobox + :global([role=listbox]:not(.open)) {
537
- opacity: 0;
538
- pointer-events: none;
539
- }
540
531
 
541
532
  .combobox-inner {
542
533
  flex: auto;
@@ -10,5 +10,5 @@ type Combobox = {
10
10
  * @todo Add DOM API compatibility.
11
11
  */
12
12
  declare const Combobox: import("svelte").Component<ComboboxProps & TextInputProps & Record<string, any>, {}, "value">;
13
- import type { ComboboxProps } from '../../typedefs';
14
- import type { TextInputProps } from '../../typedefs';
13
+ import type { ComboboxProps } from '../../typedefs.js';
14
+ import type { TextInputProps } from '../../typedefs.js';
@@ -67,6 +67,12 @@
67
67
  * @type {Set<any>}
68
68
  */
69
69
  const selectedValues = $derived(new Set(values));
70
+ /**
71
+ * Whether any option is left to pick. This can’t be told by comparing the number of selected
72
+ * values with the number of options, as the values may include entries missing from the options.
73
+ * @type {boolean}
74
+ */
75
+ const hasUnselectedOptions = $derived(options.some(({ value }) => !selectedValues.has(value)));
70
76
 
71
77
  /**
72
78
  * Reference to the wrapper element.
@@ -241,7 +247,7 @@
241
247
  </span>
242
248
  {/each}
243
249
  </span>
244
- {#if (typeof max !== 'number' || values.length < max) && values.length < options.length}
250
+ {#if (typeof max !== 'number' || values.length < max) && hasUnselectedOptions}
245
251
  <Select
246
252
  {...restProps}
247
253
  bind:value={selectedValue}
@@ -9,7 +9,7 @@
9
9
  import Combobox from './combobox.svelte';
10
10
 
11
11
  /**
12
- * @import { ComboboxProps } from '../../typedefs';
12
+ * @import { ComboboxProps } from '../../typedefs.js';
13
13
  */
14
14
 
15
15
  /**
@@ -19,29 +19,9 @@
19
19
  /* eslint-disable prefer-const */
20
20
  value = $bindable(),
21
21
  class: className,
22
- hidden = false,
23
- disabled = false,
24
- readonly = false,
25
- required = false,
26
- invalid = false,
27
- children,
28
- onChange,
29
22
  ...restProps
30
23
  /* eslint-enable prefer-const */
31
24
  } = $props();
32
25
  </script>
33
26
 
34
- <Combobox
35
- {...restProps}
36
- bind:value
37
- class="sui select {className}"
38
- {hidden}
39
- {disabled}
40
- {readonly}
41
- {required}
42
- {invalid}
43
- {onChange}
44
- editable={false}
45
- >
46
- {@render children?.()}
47
- </Combobox>
27
+ <Combobox {...restProps} bind:value class="sui select {className}" editable={false} />
@@ -10,4 +10,4 @@ type Select = {
10
10
  * @see https://www.w3.org/WAI/ARIA/apg/patterns/combobox/examples/combobox-select-only/
11
11
  */
12
12
  declare const Select: import("svelte").Component<ComboboxProps & Record<string, any>, {}, "value">;
13
- import type { ComboboxProps } from '../../typedefs';
13
+ import type { ComboboxProps } from '../../typedefs.js';
@@ -21,3 +21,9 @@ export function wouldCrossThumbs({ valueIndex, targetPosition, sliderPositions }
21
21
  targetPosition: number;
22
22
  sliderPositions: number[];
23
23
  }): boolean;
24
+ export function getSliderCurrentIndex({ valueList, value, decreasing }: {
25
+ valueList: number[];
26
+ value: number;
27
+ decreasing: boolean;
28
+ }): number;
29
+ export function getNearestThumbIndex(sliderPositions: number[], logicalX: number): number;
@@ -1,3 +1,5 @@
1
+ import { getArrowKeys } from '../../services/navigation.js';
2
+
1
3
  /**
2
4
  * Geometry and keyboard logic behind `<Slider>`, kept apart from the component so the value
3
5
  * calculations can be tested without a rendered track.
@@ -89,11 +91,14 @@ export const findNearestStepIndex = (positionList, logicalX) => {
89
91
  * @returns {1 | -1 | 0} `1` to increase, `-1` to decrease, `0` if the key doesn’t move the thumb.
90
92
  */
91
93
  export const getSliderKeyDirection = (key, rtl) => {
92
- if (key === 'ArrowUp' || key === (rtl ? 'ArrowLeft' : 'ArrowRight')) {
94
+ // Only the horizontal keys are mirrored; `ArrowUp` increases the value, unlike in a list
95
+ const { prevKey, nextKey } = getArrowKeys('horizontal', rtl);
96
+
97
+ if (key === 'ArrowUp' || key === nextKey) {
93
98
  return 1;
94
99
  }
95
100
 
96
- if (key === 'ArrowDown' || key === (rtl ? 'ArrowRight' : 'ArrowLeft')) {
101
+ if (key === 'ArrowDown' || key === prevKey) {
97
102
  return -1;
98
103
  }
99
104
 
@@ -152,3 +157,51 @@ export const getSliderKeyTargetIndex = ({ key, rtl, currentIndex, length }) => {
152
157
  export const wouldCrossThumbs = ({ valueIndex, targetPosition, sliderPositions }) =>
153
158
  (valueIndex === 0 && sliderPositions[1] <= targetPosition) ||
154
159
  (valueIndex === 1 && sliderPositions[0] >= targetPosition);
160
+
161
+ /**
162
+ * Get the step index a key press starts from. A value that sits between two steps, such as `55`
163
+ * with a step of `10`, has no index of its own, so it starts from the step below when the key
164
+ * increases the value and from the step above when it decreases it. That way the first press
165
+ * moves the thumb to the adjacent step in the direction of the key.
166
+ * @param {object} options Options.
167
+ * @param {number[]} options.valueList Values of each step, in ascending order.
168
+ * @param {number} options.value Current value.
169
+ * @param {boolean} options.decreasing Whether the key decreases the value.
170
+ * @returns {number} Index of the current step. It can be `-1` or `valueList.length` for a value
171
+ * below the minimum or above the maximum, so any move brings it back into the range.
172
+ */
173
+ export const getSliderCurrentIndex = ({ valueList, value, decreasing }) => {
174
+ const index = valueList.indexOf(value);
175
+
176
+ if (index !== -1) {
177
+ return index;
178
+ }
179
+
180
+ if (decreasing) {
181
+ const upperIndex = valueList.findIndex((v) => v > value);
182
+
183
+ return upperIndex === -1 ? valueList.length : upperIndex;
184
+ }
185
+
186
+ return valueList.findLastIndex((v) => v < value);
187
+ };
188
+
189
+ /**
190
+ * Find the thumb of a multi-thumb slider closest to the given position, so a click on the track
191
+ * moves that one. When both thumbs sit at the same spot, the second one is picked for a position
192
+ * after it and the first one otherwise, so the click never has to cross the other thumb.
193
+ * @param {number[]} sliderPositions Current position of each thumb, in pixels.
194
+ * @param {number} logicalX Logical position in pixels.
195
+ * @returns {number} Index of the nearest thumb, `0` or `1`.
196
+ */
197
+ export const getNearestThumbIndex = (sliderPositions, logicalX) => {
198
+ const [position0, position1] = sliderPositions;
199
+ const diff0 = Math.abs(position0 - logicalX);
200
+ const diff1 = Math.abs(position1 - logicalX);
201
+
202
+ if (diff0 === diff1) {
203
+ return logicalX > position1 ? 1 : 0;
204
+ }
205
+
206
+ return diff1 < diff0 ? 1 : 0;
207
+ };