@sveltia/ui 0.34.0 → 0.35.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 (42) hide show
  1. package/dist/components/button/button.svelte +2 -1
  2. package/dist/components/calendar/calendar.svelte +17 -25
  3. package/dist/components/divider/spacer.svelte +2 -1
  4. package/dist/components/select/combobox.svelte +10 -7
  5. package/dist/components/text-editor/code-editor.svelte +3 -0
  6. package/dist/components/text-editor/constants.test.d.ts +1 -0
  7. package/dist/components/text-editor/constants.test.js +98 -0
  8. package/dist/components/text-editor/core.js +13 -8
  9. package/dist/components/text-editor/store.svelte.test.d.ts +1 -0
  10. package/dist/components/text-editor/store.svelte.test.js +196 -0
  11. package/dist/components/text-editor/text-editor.svelte +3 -0
  12. package/dist/components/text-editor/transformers/hr.test.d.ts +1 -0
  13. package/dist/components/text-editor/transformers/hr.test.js +108 -0
  14. package/dist/components/text-editor/transformers/table.test.d.ts +1 -0
  15. package/dist/components/text-editor/transformers/table.test.js +28 -0
  16. package/dist/components/text-field/number-input.svelte +2 -1
  17. package/dist/components/text-field/password-input.svelte +2 -1
  18. package/dist/components/text-field/search-bar.svelte +2 -1
  19. package/dist/components/text-field/secret-input.svelte +2 -1
  20. package/dist/components/text-field/text-area.svelte +2 -1
  21. package/dist/components/text-field/text-input.svelte +41 -2
  22. package/dist/components/toast/toast.svelte +7 -3
  23. package/dist/services/events.svelte.js +66 -8
  24. package/dist/services/events.test.d.ts +1 -0
  25. package/dist/services/events.test.js +221 -0
  26. package/dist/services/group.svelte.d.ts +1 -0
  27. package/dist/services/group.svelte.js +15 -10
  28. package/dist/services/group.test.d.ts +1 -0
  29. package/dist/services/group.test.js +763 -0
  30. package/dist/services/i18n.d.ts +6 -0
  31. package/dist/services/i18n.js +4 -2
  32. package/dist/services/i18n.test.d.ts +1 -0
  33. package/dist/services/i18n.test.js +106 -0
  34. package/dist/services/popup.svelte.d.ts +1 -0
  35. package/dist/services/popup.svelte.js +11 -2
  36. package/dist/services/popup.test.d.ts +1 -0
  37. package/dist/services/popup.test.js +536 -0
  38. package/dist/services/select.test.d.ts +1 -0
  39. package/dist/services/select.test.js +69 -0
  40. package/dist/typedefs.d.ts +7 -0
  41. package/dist/typedefs.js +4 -0
  42. package/package.json +12 -11
@@ -107,7 +107,8 @@
107
107
  margin: var(--sui-focus-ring-width);
108
108
  min-width: var(--sui-textbox-singleline-min-width);
109
109
  }
110
- .password-input.flex {
110
+ .password-input.flex:not([hidden]) {
111
+ display: inline-flex;
111
112
  width: -moz-available;
112
113
  width: -webkit-fill-available;
113
114
  width: stretch;
@@ -121,7 +121,8 @@
121
121
  margin: var(--sui-focus-ring-width);
122
122
  min-width: var(--sui-textbox-singleline-min-width);
123
123
  }
124
- .search-bar.flex {
124
+ .search-bar.flex:not([hidden]) {
125
+ display: inline-flex;
125
126
  width: -moz-available;
126
127
  width: -webkit-fill-available;
127
128
  width: stretch;
@@ -101,7 +101,8 @@
101
101
  margin: var(--sui-focus-ring-width);
102
102
  min-width: var(--sui-textbox-singleline-min-width);
103
103
  }
104
- .secret-input.flex {
104
+ .secret-input.flex:not([hidden]) {
105
+ display: inline-flex;
105
106
  width: -moz-available;
106
107
  width: -webkit-fill-available;
107
108
  width: stretch;
@@ -87,7 +87,8 @@
87
87
  .text-area[hidden] {
88
88
  display: none;
89
89
  }
90
- .text-area.flex {
90
+ .text-area.flex:not([hidden]) {
91
+ display: inline-grid;
91
92
  width: -moz-available;
92
93
  width: -webkit-fill-available;
93
94
  width: stretch;
@@ -31,6 +31,7 @@
31
31
  inputmode = 'text',
32
32
  flex = false,
33
33
  monospace = false,
34
+ debounce = false,
34
35
  class: className,
35
36
  hidden = false,
36
37
  disabled = false,
@@ -39,11 +40,47 @@
39
40
  invalid = false,
40
41
  'aria-label': ariaLabel,
41
42
  children,
43
+ oninput,
42
44
  ...restProps
43
45
  /* eslint-enable prefer-const */
44
46
  } = $props();
45
47
 
46
48
  const id = $props.id();
49
+ const timeout = $derived(typeof debounce === 'number' ? debounce : 300);
50
+
51
+ let debounceTimer = 0;
52
+
53
+ $effect(() => () => {
54
+ clearTimeout(debounceTimer);
55
+ });
56
+
57
+ /**
58
+ * Update the `value` and call the `oninput` callback.
59
+ * @param {InputEvent} event The `input` event object.
60
+ */
61
+ const fireInput = (event) => {
62
+ value = /** @type {HTMLInputElement} */ (event.target).value;
63
+ oninput?.(event);
64
+ };
65
+
66
+ /**
67
+ * Handle the `input` event. If `debounce` is `true`, the event will be debounced by 300ms.
68
+ * @param {InputEvent} event The `input` event object.
69
+ */
70
+ const handleInput = (event) => {
71
+ if (debounce) {
72
+ clearTimeout(debounceTimer);
73
+ debounceTimer = /** @type {number} */ (
74
+ /** @type {unknown} */ (
75
+ setTimeout(() => {
76
+ fireInput(event);
77
+ }, timeout)
78
+ )
79
+ );
80
+ } else {
81
+ fireInput(event);
82
+ }
83
+ };
47
84
  </script>
48
85
 
49
86
  <div
@@ -58,7 +95,7 @@
58
95
  <input
59
96
  bind:this={element}
60
97
  {...restProps}
61
- bind:value
98
+ {value}
62
99
  type="text"
63
100
  {role}
64
101
  dir="auto"
@@ -73,6 +110,7 @@
73
110
  aria-readonly={readonly}
74
111
  aria-required={required}
75
112
  aria-invalid={invalid}
113
+ oninput={handleInput}
76
114
  use:activateKeyShortcuts={keyShortcuts}
77
115
  />
78
116
  {#if ariaLabel && showInlineLabel}
@@ -91,7 +129,8 @@
91
129
  margin: var(--sui-focus-ring-width);
92
130
  min-width: var(--sui-textbox-singleline-min-width);
93
131
  }
94
- .text-input.flex {
132
+ .text-input.flex:not([hidden]) {
133
+ display: inline-flex;
95
134
  width: -moz-available;
96
135
  width: -webkit-fill-available;
97
136
  width: stretch;
@@ -112,9 +112,13 @@
112
112
  });
113
113
 
114
114
  if (show && duration) {
115
- timerId = globalThis.setTimeout(() => {
116
- show = false;
117
- }, duration);
115
+ timerId = /** @type {number} */ (
116
+ /** @type {unknown} */ (
117
+ globalThis.setTimeout(() => {
118
+ show = false;
119
+ }, duration)
120
+ )
121
+ );
118
122
  }
119
123
  });
120
124
  </script>
@@ -2,13 +2,23 @@
2
2
  * @import { ActionReturn } from 'svelte/action';
3
3
  */
4
4
 
5
+ /** @type {boolean | undefined} */
6
+ let _isMac;
7
+
5
8
  /**
6
9
  * Check if the user agent is macOS.
7
10
  * @returns {boolean} Result.
8
11
  */
9
- export const isMac = () =>
10
- /** @type {any} */ (navigator).userAgentData?.platform === 'macOS' ||
11
- navigator.platform.startsWith('Mac');
12
+ export const isMac = () => {
13
+ _isMac ??=
14
+ /** @type {any} */ (navigator).userAgentData?.platform === 'macOS' ||
15
+ navigator.platform.startsWith('Mac');
16
+
17
+ return _isMac;
18
+ };
19
+
20
+ const MODIFIER_KEYS = ['Ctrl', 'Meta', 'Alt', 'Shift'];
21
+ const CODE_RE = /^(?:Digit|Key)(.)$/;
12
22
 
13
23
  /**
14
24
  * Whether the event matches the given keyboard shortcuts.
@@ -19,7 +29,13 @@ export const isMac = () =>
19
29
  */
20
30
  export const matchShortcuts = (event, shortcuts) => {
21
31
  const { ctrlKey, metaKey, altKey, shiftKey, code } = event;
22
- const key = code.replace(/^(?:Digit|Key)(.)$/, '$1');
32
+
33
+ // The `code` property can be `undefined` in some cases
34
+ if (!code) {
35
+ return false;
36
+ }
37
+
38
+ const key = code.replace(CODE_RE, '$1');
23
39
 
24
40
  return shortcuts.split(/\s+/).some((shortcut) => {
25
41
  const keys = shortcut.split('+');
@@ -45,7 +61,7 @@ export const matchShortcuts = (event, shortcuts) => {
45
61
  }
46
62
 
47
63
  return keys
48
- .filter((_key) => !['Ctrl', 'Meta', 'Alt', 'Shift'].includes(_key))
64
+ .filter((_key) => !MODIFIER_KEYS.includes(_key))
49
65
  .every((_key) => _key.toUpperCase() === key.toUpperCase());
50
66
  });
51
67
  };
@@ -61,6 +77,12 @@ export const matchShortcuts = (event, shortcuts) => {
61
77
  export const activateKeyShortcuts = (element, shortcuts = '') => {
62
78
  /** @type {string | undefined} */
63
79
  let platformKeyShortcuts;
80
+ /**
81
+ * Pre-parsed shortcuts for fast per-event matching without string allocations.
82
+ * @type {{ ctrl: boolean, meta: boolean, alt: boolean, shift: boolean, nonModifierKeys: string[]
83
+ * }[] | undefined}
84
+ */
85
+ let parsedShortcuts;
64
86
 
65
87
  /**
66
88
  * Handle the event.
@@ -68,13 +90,33 @@ export const activateKeyShortcuts = (element, shortcuts = '') => {
68
90
  */
69
91
  const handler = (event) => {
70
92
  const { disabled, offsetParent } = element;
71
- const { top, left } = element.getBoundingClientRect();
72
93
 
73
- // Check if the element is visible
74
- if (!matchShortcuts(event, /** @type {string} */ (platformKeyShortcuts)) || !offsetParent) {
94
+ // Check shortcut match and visibility first — no layout reflow until needed
95
+ if (
96
+ !offsetParent ||
97
+ !parsedShortcuts?.some(({ ctrl, meta, alt, shift, nonModifierKeys }) => {
98
+ const { ctrlKey, metaKey, altKey, shiftKey, code } = event;
99
+
100
+ if (!code) {
101
+ return false;
102
+ }
103
+
104
+ const key = code.replace(CODE_RE, '$1').toUpperCase();
105
+
106
+ return (
107
+ ctrl === ctrlKey &&
108
+ meta === metaKey &&
109
+ alt === altKey &&
110
+ shift === shiftKey &&
111
+ nonModifierKeys.every((k) => k === key)
112
+ );
113
+ })
114
+ ) {
75
115
  return;
76
116
  }
77
117
 
118
+ const { top, left } = element.getBoundingClientRect();
119
+
78
120
  if (disabled) {
79
121
  // Make sure `elementsFromPoint()` works as expected
80
122
  element.style.setProperty('pointer-events', 'auto');
@@ -113,8 +155,24 @@ export const activateKeyShortcuts = (element, shortcuts = '') => {
113
155
  : undefined;
114
156
 
115
157
  if (platformKeyShortcuts) {
158
+ parsedShortcuts = platformKeyShortcuts.split(/\s+/).map((shortcut) => {
159
+ const parts = shortcut.split('+');
160
+
161
+ return {
162
+ ctrl: parts.includes('Ctrl'),
163
+ meta: parts.includes('Meta'),
164
+ alt: parts.includes('Alt'),
165
+ shift: parts.includes('Shift'),
166
+ nonModifierKeys: parts
167
+ .filter((k) => !MODIFIER_KEYS.includes(k))
168
+ .map((k) => k.toUpperCase()),
169
+ };
170
+ });
171
+
116
172
  globalThis.addEventListener('keydown', handler, { capture: true });
117
173
  element.setAttribute('aria-keyshortcuts', platformKeyShortcuts);
174
+ } else {
175
+ parsedShortcuts = undefined;
118
176
  }
119
177
  };
120
178
 
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,221 @@
1
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
2
+ import { activateKeyShortcuts, isMac, matchShortcuts } from './events.svelte.js';
3
+
4
+ /**
5
+ * Helper to create a minimal KeyboardEvent-like object.
6
+ * @param {Partial<KeyboardEvent>} overrides Event property overrides.
7
+ * @returns {KeyboardEvent} A fake keyboard event.
8
+ */
9
+ const makeEvent = (overrides = {}) =>
10
+ /** @type {KeyboardEvent} */ ({
11
+ ctrlKey: false,
12
+ metaKey: false,
13
+ altKey: false,
14
+ shiftKey: false,
15
+ code: 'KeyA',
16
+ ...overrides,
17
+ });
18
+
19
+ describe('matchShortcuts', () => {
20
+ it('should match a plain key shortcut', () => {
21
+ expect(matchShortcuts(makeEvent({ code: 'KeyS' }), 'S')).toBe(true);
22
+ });
23
+
24
+ it('should not match when the key differs', () => {
25
+ expect(matchShortcuts(makeEvent({ code: 'KeyA' }), 'S')).toBe(false);
26
+ });
27
+
28
+ it('should match Ctrl+S', () => {
29
+ expect(matchShortcuts(makeEvent({ code: 'KeyS', ctrlKey: true }), 'Ctrl+S')).toBe(true);
30
+ });
31
+
32
+ it('should not match Ctrl+S when Ctrl is not pressed', () => {
33
+ expect(matchShortcuts(makeEvent({ code: 'KeyS' }), 'Ctrl+S')).toBe(false);
34
+ });
35
+
36
+ it('should not match Ctrl+S when an extra modifier is pressed', () => {
37
+ expect(
38
+ matchShortcuts(makeEvent({ code: 'KeyS', ctrlKey: true, shiftKey: true }), 'Ctrl+S'),
39
+ ).toBe(false);
40
+ });
41
+
42
+ it('should match Shift+A', () => {
43
+ expect(matchShortcuts(makeEvent({ code: 'KeyA', shiftKey: true }), 'Shift+A')).toBe(true);
44
+ });
45
+
46
+ it('should match Alt+F4', () => {
47
+ expect(matchShortcuts(makeEvent({ code: 'KeyF', altKey: true }), 'Alt+F')).toBe(true);
48
+ });
49
+
50
+ it('should match any of multiple space-separated shortcuts', () => {
51
+ expect(matchShortcuts(makeEvent({ code: 'KeyZ', ctrlKey: true }), 'Ctrl+Z Ctrl+Y')).toBe(true);
52
+ expect(matchShortcuts(makeEvent({ code: 'KeyY', ctrlKey: true }), 'Ctrl+Z Ctrl+Y')).toBe(true);
53
+ });
54
+
55
+ it('should return false when code is empty', () => {
56
+ expect(matchShortcuts(makeEvent({ code: '' }), 'S')).toBe(false);
57
+ });
58
+
59
+ it('should match digit keys using Digit prefix', () => {
60
+ expect(matchShortcuts(makeEvent({ code: 'Digit1' }), '1')).toBe(true);
61
+ });
62
+
63
+ it('should be case-insensitive for the key', () => {
64
+ expect(matchShortcuts(makeEvent({ code: 'KeyS' }), 's')).toBe(true);
65
+ });
66
+ });
67
+
68
+ describe('isMac', () => {
69
+ it('should return a boolean', () => {
70
+ expect(typeof isMac()).toBe('boolean');
71
+ });
72
+ });
73
+
74
+ describe('activateKeyShortcuts', () => {
75
+ /** @type {HTMLButtonElement} */
76
+ let button;
77
+
78
+ beforeEach(() => {
79
+ button = /** @type {HTMLButtonElement} */ (document.createElement('button'));
80
+ document.body.appendChild(button);
81
+
82
+ // happy-dom doesn't expose document.elementsFromPoint as a configurable property;
83
+ // define a stub so vi.spyOn can wrap it in handler tests.
84
+ if (!document.elementsFromPoint) {
85
+ Object.defineProperty(document, 'elementsFromPoint', {
86
+ configurable: true,
87
+ writable: true,
88
+ // eslint-disable-next-line jsdoc/require-jsdoc
89
+ value: () => /** @type {Element[]} */ ([]),
90
+ });
91
+ }
92
+ });
93
+
94
+ afterEach(() => {
95
+ button.remove();
96
+ vi.restoreAllMocks();
97
+ });
98
+
99
+ it('should set aria-keyshortcuts when shortcuts are provided', () => {
100
+ const action = activateKeyShortcuts(button, 'Ctrl+S');
101
+
102
+ expect(button.getAttribute('aria-keyshortcuts')).toBe('Ctrl+S');
103
+ action.destroy?.();
104
+ });
105
+
106
+ it('should not set aria-keyshortcuts when no shortcuts are provided', () => {
107
+ const action = activateKeyShortcuts(button);
108
+
109
+ expect(button.getAttribute('aria-keyshortcuts')).toBeNull();
110
+ action.destroy?.();
111
+ });
112
+
113
+ it('should remove aria-keyshortcuts after destroy', () => {
114
+ const action = activateKeyShortcuts(button, 'Ctrl+S');
115
+
116
+ action.destroy?.();
117
+ expect(button.getAttribute('aria-keyshortcuts')).toBeNull();
118
+ });
119
+
120
+ it('should replace Accel with Meta or Ctrl depending on platform', () => {
121
+ const action = activateKeyShortcuts(button, 'Accel+S');
122
+ const attr = button.getAttribute('aria-keyshortcuts');
123
+
124
+ expect(attr === 'Meta+S' || attr === 'Ctrl+S').toBe(true);
125
+ action.destroy?.();
126
+ });
127
+
128
+ it('should re-register the original shortcuts when update() is called', () => {
129
+ // update() re-applies the same original shortcuts (param is intentionally ignored)
130
+ const action = activateKeyShortcuts(button, 'Ctrl+S');
131
+
132
+ /** @type {any} */ (action).update('Ctrl+Z');
133
+ expect(button.getAttribute('aria-keyshortcuts')).toBe('Ctrl+S');
134
+ action.destroy?.();
135
+ });
136
+
137
+ it('should keep no aria-keyshortcuts when update() is called on a no-shortcut action', () => {
138
+ const action = activateKeyShortcuts(button);
139
+
140
+ /** @type {any} */ (action).update('Ctrl+Z'); // original shortcuts was '' so still none
141
+ expect(button.getAttribute('aria-keyshortcuts')).toBeNull();
142
+ action.destroy?.();
143
+ });
144
+
145
+ it('should trigger click on element when matching shortcut key is pressed', () => {
146
+ // eslint-disable-next-line jsdoc/require-jsdoc
147
+ Object.defineProperty(button, 'offsetParent', { configurable: true, get: () => document.body });
148
+ vi.spyOn(document, 'elementsFromPoint').mockReturnValue(/** @type {any} */ ([button]));
149
+
150
+ const clickSpy = vi.fn();
151
+
152
+ button.addEventListener('click', clickSpy);
153
+ activateKeyShortcuts(button, 'Ctrl+S');
154
+ globalThis.dispatchEvent(
155
+ new KeyboardEvent('keydown', { code: 'KeyS', ctrlKey: true, bubbles: true }),
156
+ );
157
+ expect(clickSpy).toHaveBeenCalledOnce();
158
+ });
159
+
160
+ it('should not trigger click when a non-matching key is pressed', () => {
161
+ // Handler returns early before reaching elementsFromPoint when shortcut doesn't match
162
+ const clickSpy = vi.fn();
163
+
164
+ button.addEventListener('click', clickSpy);
165
+ activateKeyShortcuts(button, 'Ctrl+S');
166
+ globalThis.dispatchEvent(
167
+ new KeyboardEvent('keydown', { code: 'KeyZ', ctrlKey: true, bubbles: true }),
168
+ );
169
+ expect(clickSpy).not.toHaveBeenCalled();
170
+ });
171
+
172
+ it('should not trigger click when element is not in elementsFromPoint result', () => {
173
+ // eslint-disable-next-line jsdoc/require-jsdoc
174
+ Object.defineProperty(button, 'offsetParent', { configurable: true, get: () => document.body });
175
+ vi.spyOn(document, 'elementsFromPoint').mockReturnValue(/** @type {any} */ ([]));
176
+
177
+ const clickSpy = vi.fn();
178
+
179
+ button.addEventListener('click', clickSpy);
180
+ activateKeyShortcuts(button, 'Ctrl+S');
181
+ globalThis.dispatchEvent(
182
+ new KeyboardEvent('keydown', { code: 'KeyS', ctrlKey: true, bubbles: true }),
183
+ );
184
+ expect(clickSpy).not.toHaveBeenCalled();
185
+ });
186
+
187
+ it('should not trigger click when the event code is empty (covers inner return false branch)', () => {
188
+ // eslint-disable-next-line jsdoc/require-jsdoc
189
+ Object.defineProperty(button, 'offsetParent', { configurable: true, get: () => document.body });
190
+ activateKeyShortcuts(button, 'Ctrl+S');
191
+
192
+ const clickSpy = vi.fn();
193
+
194
+ button.addEventListener('click', clickSpy);
195
+ // Dispatch with empty code — the parsedShortcuts.some() callback returns false (line 101)
196
+ globalThis.dispatchEvent(
197
+ new KeyboardEvent('keydown', { code: '', ctrlKey: true, bubbles: true }),
198
+ );
199
+ expect(clickSpy).not.toHaveBeenCalled();
200
+ });
201
+
202
+ it('should manipulate pointer-events for disabled button but not trigger click', () => {
203
+ // eslint-disable-next-line jsdoc/require-jsdoc
204
+ Object.defineProperty(button, 'offsetParent', { configurable: true, get: () => document.body });
205
+ vi.spyOn(document, 'elementsFromPoint').mockReturnValue(/** @type {any} */ ([button]));
206
+ button.disabled = true;
207
+
208
+ const clickSpy = vi.fn();
209
+ const setPropertySpy = vi.spyOn(button.style, 'setProperty');
210
+ const removePropertySpy = vi.spyOn(button.style, 'removeProperty');
211
+
212
+ button.addEventListener('click', clickSpy);
213
+ activateKeyShortcuts(button, 'Ctrl+S');
214
+ globalThis.dispatchEvent(
215
+ new KeyboardEvent('keydown', { code: 'KeyS', ctrlKey: true, bubbles: true }),
216
+ );
217
+ expect(clickSpy).not.toHaveBeenCalled();
218
+ expect(setPropertySpy).toHaveBeenCalledWith('pointer-events', 'auto');
219
+ expect(removePropertySpy).toHaveBeenCalledWith('pointer-events');
220
+ });
221
+ });
@@ -1,2 +1,3 @@
1
+ export function normalize(value: string): string;
1
2
  export function activateGroup(parent: HTMLElement, params?: object | undefined): ActionReturn;
2
3
  import type { ActionReturn } from 'svelte/action';
@@ -8,24 +8,28 @@ import { get } from 'svelte/store';
8
8
  import { isRTL } from './i18n.js';
9
9
  import { getSelectedItemDetail } from './select.svelte.js';
10
10
 
11
+ /**
12
+ * Diacritic characters regex for normalization. We use a regex instead of `Intl` APIs for better
13
+ * performance, since `transliterate` is slow and we only need basic normalization.
14
+ */
15
+ const DIACRITIC_RE = /\p{Diacritic}/gu;
16
+
11
17
  /**
12
18
  * Normalize the given string for search value comparison. Since `transliterate` is slow, we only
13
19
  * apply basic normalization.
20
+ * @internal
14
21
  * @param {string} value Original value.
15
22
  * @returns {string} Normalized value.
16
23
  * @todo Move this to `@sveltia/utils`.
17
24
  */
18
- const normalize = (value) => {
25
+ export const normalize = (value) => {
19
26
  value = value.trim();
20
27
 
21
28
  if (!value) {
22
29
  return '';
23
30
  }
24
31
 
25
- return value
26
- .normalize('NFD')
27
- .replace(/\p{Diacritic}/gu, '')
28
- .toLocaleLowerCase();
32
+ return value.normalize('NFD').replace(DIACRITIC_RE, '').toLocaleLowerCase();
29
33
  };
30
34
 
31
35
  /**
@@ -472,8 +476,8 @@ class Group {
472
476
  newTarget = activeMembers[index - 1];
473
477
  }
474
478
 
475
- if (index === 0) {
476
- // Last member
479
+ if (index <= 0) {
480
+ // Last member (also handles the case when nothing is focused, index === -1)
477
481
  newTarget = activeMembers[activeMembers.length - 1];
478
482
  }
479
483
  }
@@ -503,8 +507,9 @@ class Group {
503
507
  onUpdate({ searchTerms }) {
504
508
  const terms = normalize(searchTerms);
505
509
  const _terms = terms ? terms.split(/\s+/) : [];
510
+ const { allMembers, parent } = this;
506
511
 
507
- const matched = this.allMembers
512
+ const matched = allMembers
508
513
  .map((member) => {
509
514
  const searchValue = normalize(
510
515
  member.dataset.searchValue ??
@@ -522,8 +527,8 @@ class Group {
522
527
  })
523
528
  .filter((hidden) => !hidden).length;
524
529
 
525
- this.parent.dispatchEvent(
526
- new CustomEvent('Filter', { detail: { matched, total: this.allMembers.length } }),
530
+ parent.dispatchEvent(
531
+ new CustomEvent('Filter', { detail: { matched, total: allMembers.length } }),
527
532
  );
528
533
  }
529
534
  }
@@ -0,0 +1 @@
1
+ export {};