@sveltia/ui 0.35.0 → 0.35.2

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 (30) hide show
  1. package/dist/components/calendar/calendar.svelte +17 -25
  2. package/dist/components/select/combobox.svelte +10 -7
  3. package/dist/components/text-editor/constants.test.d.ts +1 -0
  4. package/dist/components/text-editor/constants.test.js +98 -0
  5. package/dist/components/text-editor/store.svelte.test.d.ts +1 -0
  6. package/dist/components/text-editor/store.svelte.test.js +196 -0
  7. package/dist/components/text-editor/transformers/hr.test.d.ts +1 -0
  8. package/dist/components/text-editor/transformers/hr.test.js +108 -0
  9. package/dist/components/text-editor/transformers/table.test.d.ts +1 -0
  10. package/dist/components/text-editor/transformers/table.test.js +28 -0
  11. package/dist/components/text-field/text-input.svelte +12 -6
  12. package/dist/components/toast/toast.svelte +7 -3
  13. package/dist/services/events.svelte.js +66 -8
  14. package/dist/services/events.test.d.ts +1 -0
  15. package/dist/services/events.test.js +221 -0
  16. package/dist/services/group.svelte.d.ts +1 -0
  17. package/dist/services/group.svelte.js +15 -10
  18. package/dist/services/group.test.d.ts +1 -0
  19. package/dist/services/group.test.js +763 -0
  20. package/dist/services/i18n.d.ts +6 -0
  21. package/dist/services/i18n.js +4 -2
  22. package/dist/services/i18n.test.d.ts +1 -0
  23. package/dist/services/i18n.test.js +106 -0
  24. package/dist/services/popup.svelte.d.ts +1 -0
  25. package/dist/services/popup.svelte.js +11 -2
  26. package/dist/services/popup.test.d.ts +1 -0
  27. package/dist/services/popup.test.js +536 -0
  28. package/dist/services/select.test.d.ts +1 -0
  29. package/dist/services/select.test.js +69 -0
  30. package/package.json +10 -9
@@ -15,6 +15,14 @@
15
15
  * @property {string} [value] Date.
16
16
  */
17
17
 
18
+ /**
19
+ * List of month names for month selector. We use a fixed date to avoid issues with daylight
20
+ * saving time.
21
+ */
22
+ const MONTH_NAMES = Array.from({ length: 12 }, (__, i) =>
23
+ new Date(2000, i, 10).toLocaleDateString('en', { month: 'short' }),
24
+ );
25
+
18
26
  /**
19
27
  * @type {Props & Record<string, any>}
20
28
  */
@@ -24,10 +32,6 @@
24
32
  /* eslint-enable prefer-const */
25
33
  } = $props();
26
34
 
27
- /** @type {{ day: Date }[]} */
28
- const dayList = [];
29
- /** @type {{ day: Date }[][]} */
30
- const weeks = [];
31
35
  const now = new Date();
32
36
 
33
37
  const date = $derived(value ? new Date(value) : now);
@@ -38,29 +42,21 @@
38
42
  firstDay = new Date(firstDayOfMonth);
39
43
  });
40
44
 
41
- /**
42
- * Populate {@link weeks} as per the current {@link firstDay}.
43
- */
44
- const getWeeks = () => {
45
+ const dayList = $derived.by(() => {
46
+ if (!firstDay) return [];
47
+
45
48
  const cursor = new Date(firstDay);
46
49
 
47
50
  // Start from Sunday
48
51
  cursor.setDate(1 - cursor.getUTCDay());
49
52
 
50
- for (let i = 0; i < 42; i += 1) {
51
- const week = Math.floor(i / 7);
53
+ return Array.from({ length: 42 }, () => {
54
+ const day = new Date(cursor);
52
55
 
53
- dayList[i] = { day: new Date(cursor) };
54
- weeks[week] ||= [];
55
- weeks[week][cursor.getUTCDay() % 7] = { day: new Date(cursor) };
56
56
  cursor.setUTCDate(cursor.getUTCDate() + 1);
57
- }
58
- };
59
57
 
60
- $effect(() => {
61
- if (firstDay) {
62
- getWeeks();
63
- }
58
+ return { day };
59
+ });
64
60
  });
65
61
  </script>
66
62
 
@@ -113,13 +109,9 @@
113
109
  <Divider orientation="vertical" />
114
110
  <div role="group" aria-label={$_('_sui.calendar.month')}>
115
111
  <div role="none" class="grid">
116
- {#each [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11] as month}
112
+ {#each MONTH_NAMES as monthName}
117
113
  <div role="none">
118
- <Button>
119
- {new Date(date.getUTCFullYear(), month, 10).toLocaleDateString('en', {
120
- month: 'short',
121
- })}
122
- </Button>
114
+ <Button>{monthName}</Button>
123
115
  </div>
124
116
  {/each}
125
117
  </div>
@@ -20,6 +20,12 @@
20
20
  * @import { ComboboxProps, TextInputProps } from '../../typedefs';
21
21
  */
22
22
 
23
+ /**
24
+ * Selector for the currently selected option in the popup. Used to update the selected option
25
+ * when the value is changed externally.
26
+ */
27
+ const SELECTED_SELECTOR = '[role="option"][aria-selected="true"]';
28
+
23
29
  /**
24
30
  * @type {ComboboxProps & TextInputProps & Record<string, any>}
25
31
  */
@@ -43,7 +49,6 @@
43
49
  } = $props();
44
50
 
45
51
  const id = $props.id();
46
- const selectedSelector = '[role="option"][aria-selected="true"]';
47
52
  let isPopupOpen = $state(false);
48
53
 
49
54
  /** @type {HTMLElement | undefined} */
@@ -67,17 +72,15 @@
67
72
  * Update the {@link label} and selected option when the {@link value} is changed.
68
73
  */
69
74
  const _onChange = () => {
70
- const selected = popupContent?.querySelector(selectedSelector);
71
-
72
- const target = /** @type {HTMLButtonElement} */ (
75
+ const target = /** @type {HTMLButtonElement | null} */ (
73
76
  popupContent?.querySelector(`[role="option"][data-value="${value}"]`)
74
77
  );
75
78
 
76
79
  if (target) {
77
80
  label = target.dataset.label || target.dataset.value || target.textContent || '';
78
81
 
79
- if (selected !== target) {
80
- selected?.setAttribute('aria-selected', 'false');
82
+ if (target.getAttribute('aria-selected') !== 'true') {
83
+ popupContent?.querySelector(SELECTED_SELECTOR)?.setAttribute('aria-selected', 'false');
81
84
  target.setAttribute('aria-selected', 'true');
82
85
  }
83
86
  }
@@ -99,7 +102,7 @@
99
102
  $effect(() => {
100
103
  if (popupContent) {
101
104
  globalThis.requestAnimationFrame(() => {
102
- const selected = popupContent?.querySelector(selectedSelector);
105
+ const selected = popupContent?.querySelector(SELECTED_SELECTOR);
103
106
 
104
107
  if (selected) {
105
108
  _onSelect(/** @type {HTMLButtonElement} */ (selected));
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,98 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import {
3
+ AVAILABLE_BUTTONS,
4
+ BLOCK_BUTTON_TYPES,
5
+ IMAGE_COMPONENT_IDS,
6
+ INLINE_BUTTON_TYPES,
7
+ TEXT_FORMAT_BUTTON_TYPES,
8
+ } from './constants.js';
9
+
10
+ describe('AVAILABLE_BUTTONS', () => {
11
+ it('should contain all expected button keys', () => {
12
+ const expectedKeys = [
13
+ 'bold',
14
+ 'italic',
15
+ 'strikethrough',
16
+ 'code',
17
+ 'link',
18
+ 'paragraph',
19
+ 'heading-1',
20
+ 'heading-2',
21
+ 'heading-3',
22
+ 'heading-4',
23
+ 'heading-5',
24
+ 'heading-6',
25
+ 'bulleted-list',
26
+ 'numbered-list',
27
+ 'blockquote',
28
+ 'code-block',
29
+ ];
30
+
31
+ expectedKeys.forEach((key) => {
32
+ expect(AVAILABLE_BUTTONS).toHaveProperty(key);
33
+ });
34
+ });
35
+
36
+ it('should mark inline buttons correctly', () => {
37
+ expect(AVAILABLE_BUTTONS.bold.inline).toBe(true);
38
+ expect(AVAILABLE_BUTTONS.italic.inline).toBe(true);
39
+ expect(AVAILABLE_BUTTONS.link.inline).toBe(true);
40
+ expect(AVAILABLE_BUTTONS.paragraph.inline).toBe(false);
41
+ expect(AVAILABLE_BUTTONS['heading-1'].inline).toBe(false);
42
+ });
43
+
44
+ it('should have a labelKey and icon for each button', () => {
45
+ Object.values(AVAILABLE_BUTTONS).forEach(({ labelKey, icon }) => {
46
+ expect(typeof labelKey).toBe('string');
47
+ expect(labelKey.length).toBeGreaterThan(0);
48
+ expect(typeof icon).toBe('string');
49
+ expect(icon.length).toBeGreaterThan(0);
50
+ });
51
+ });
52
+ });
53
+
54
+ describe('TEXT_FORMAT_BUTTON_TYPES', () => {
55
+ it('should include bold, italic, strikethrough, code', () => {
56
+ expect(TEXT_FORMAT_BUTTON_TYPES).toEqual(['bold', 'italic', 'strikethrough', 'code']);
57
+ });
58
+ });
59
+
60
+ describe('INLINE_BUTTON_TYPES', () => {
61
+ it('should include all text format types plus link', () => {
62
+ expect(INLINE_BUTTON_TYPES).toContain('bold');
63
+ expect(INLINE_BUTTON_TYPES).toContain('italic');
64
+ expect(INLINE_BUTTON_TYPES).toContain('strikethrough');
65
+ expect(INLINE_BUTTON_TYPES).toContain('code');
66
+ expect(INLINE_BUTTON_TYPES).toContain('link');
67
+ });
68
+
69
+ it('should be a superset of TEXT_FORMAT_BUTTON_TYPES', () => {
70
+ TEXT_FORMAT_BUTTON_TYPES.forEach((type) => {
71
+ expect(INLINE_BUTTON_TYPES).toContain(type);
72
+ });
73
+ });
74
+ });
75
+
76
+ describe('BLOCK_BUTTON_TYPES', () => {
77
+ it('should include paragraph and all heading levels', () => {
78
+ expect(BLOCK_BUTTON_TYPES).toContain('paragraph');
79
+
80
+ for (let i = 1; i <= 6; i += 1) {
81
+ expect(BLOCK_BUTTON_TYPES).toContain(`heading-${i}`);
82
+ }
83
+ });
84
+
85
+ it('should include list, blockquote, and code-block types', () => {
86
+ expect(BLOCK_BUTTON_TYPES).toContain('bulleted-list');
87
+ expect(BLOCK_BUTTON_TYPES).toContain('numbered-list');
88
+ expect(BLOCK_BUTTON_TYPES).toContain('blockquote');
89
+ expect(BLOCK_BUTTON_TYPES).toContain('code-block');
90
+ });
91
+ });
92
+
93
+ describe('IMAGE_COMPONENT_IDS', () => {
94
+ it('should include image and linked-image', () => {
95
+ expect(IMAGE_COMPONENT_IDS).toContain('image');
96
+ expect(IMAGE_COMPONENT_IDS).toContain('linked-image');
97
+ });
98
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,196 @@
1
+ /* eslint-disable jsdoc/require-jsdoc */
2
+
3
+ import { describe, expect, it, vi } from 'vitest';
4
+ import { createEditorStore } from './store.svelte.js';
5
+
6
+ describe('createEditorStore', () => {
7
+ it('should have correct initial values', () => {
8
+ const store = createEditorStore();
9
+
10
+ expect(store.initialized).toBe(false);
11
+ expect(store.editor).toBeUndefined();
12
+ expect(store.inputValue).toBe('');
13
+ expect(store.useRichText).toBe(true);
14
+ expect(store.hasConverterError).toBe(false);
15
+ expect(store.showConverterError).toBe(false);
16
+ expect(store.selection).toEqual({
17
+ blockNodeKey: null,
18
+ blockType: 'paragraph',
19
+ inlineTypes: [],
20
+ });
21
+ });
22
+
23
+ it('should return a non-empty editorId string', () => {
24
+ const store = createEditorStore();
25
+
26
+ expect(typeof store.editorId).toBe('string');
27
+ expect(store.editorId.length).toBeGreaterThan(0);
28
+ });
29
+
30
+ it('should generate unique editorIds for each store instance', () => {
31
+ const a = createEditorStore();
32
+ const b = createEditorStore();
33
+
34
+ expect(a.editorId).not.toBe(b.editorId);
35
+ });
36
+
37
+ it('should set initialized', () => {
38
+ const store = createEditorStore();
39
+
40
+ store.initialized = true;
41
+ expect(store.initialized).toBe(true);
42
+ });
43
+
44
+ it('should set useRichText to true when first mode is rich-text', () => {
45
+ const store = createEditorStore();
46
+
47
+ store.config = {
48
+ modes: ['rich-text'],
49
+ enabledButtons: [],
50
+ components: [],
51
+ isCodeEditor: false,
52
+ };
53
+ expect(store.useRichText).toBe(true);
54
+ });
55
+
56
+ it('should set useRichText to false when first mode is plain-text', () => {
57
+ const store = createEditorStore();
58
+
59
+ store.config = {
60
+ modes: ['plain-text'],
61
+ enabledButtons: [],
62
+ components: [],
63
+ isCodeEditor: false,
64
+ };
65
+ expect(store.useRichText).toBe(false);
66
+ });
67
+
68
+ it('should set useRichText to true when isCodeEditor is true regardless of modes', () => {
69
+ const store = createEditorStore();
70
+
71
+ store.config = { modes: [], enabledButtons: [], components: [], isCodeEditor: true };
72
+ expect(store.useRichText).toBe(true);
73
+ });
74
+
75
+ it('should set useRichText to false when modes is empty and isCodeEditor is false', () => {
76
+ const store = createEditorStore();
77
+
78
+ store.config = { modes: [], enabledButtons: [], components: [], isCodeEditor: false };
79
+ expect(store.useRichText).toBe(false);
80
+ });
81
+
82
+ it('should set hasConverterError and cascade useRichText and showConverterError', () => {
83
+ const store = createEditorStore();
84
+
85
+ store.hasConverterError = true;
86
+ expect(store.hasConverterError).toBe(true);
87
+ expect(store.useRichText).toBe(false);
88
+ expect(store.showConverterError).toBe(true);
89
+ });
90
+
91
+ it('should allow clearing hasConverterError without re-enabling rich text', () => {
92
+ const store = createEditorStore();
93
+
94
+ store.hasConverterError = true;
95
+ store.hasConverterError = false;
96
+ // Setting to false does NOT automatically re-enable rich text
97
+ expect(store.hasConverterError).toBe(false);
98
+ expect(store.useRichText).toBe(false);
99
+ });
100
+
101
+ it('should allow setting showConverterError directly', () => {
102
+ const store = createEditorStore();
103
+
104
+ store.showConverterError = true;
105
+ expect(store.showConverterError).toBe(true);
106
+ store.showConverterError = false;
107
+
108
+ expect(store.showConverterError).toBe(false);
109
+ });
110
+
111
+ it('should allow setting and reading selection', () => {
112
+ const store = createEditorStore();
113
+
114
+ const sel = {
115
+ blockNodeKey: 'abc',
116
+ blockType: /** @type {const} */ ('heading-2'),
117
+ inlineTypes: /** @type {import('../../typedefs').TextEditorInlineType[]} */ ([
118
+ 'bold',
119
+ 'italic',
120
+ ]),
121
+ };
122
+
123
+ store.selection = sel;
124
+ expect(store.selection).toEqual(sel);
125
+ });
126
+
127
+ it('should allow setting inputValue when useRichText is false', () => {
128
+ const store = createEditorStore();
129
+
130
+ store.useRichText = false;
131
+ store.inputValue = 'hello world';
132
+ expect(store.inputValue).toBe('hello world');
133
+ });
134
+
135
+ it('should not change inputValue on duplicate assignment', () => {
136
+ const store = createEditorStore();
137
+
138
+ store.useRichText = false;
139
+ store.inputValue = 'same';
140
+ store.inputValue = 'same'; // no-op since unchanged
141
+ expect(store.inputValue).toBe('same');
142
+ });
143
+
144
+ it('should expose convertMarkdown as a function', () => {
145
+ const store = createEditorStore();
146
+
147
+ expect(typeof store.convertMarkdown).toBe('function');
148
+ });
149
+
150
+ it('should allow getting and setting the editor (covers line 63)', () => {
151
+ const store = createEditorStore();
152
+ const fakeEditor = /** @type {any} */ ({ __testId: 'test-editor' });
153
+
154
+ store.editor = fakeEditor; // line 63: editor = newValue
155
+ // Svelte 5 $state wraps objects in a Proxy, so use toEqual for value comparison
156
+ // @ts-ignore
157
+ expect(store.editor?.__testId).toBe('test-editor');
158
+ });
159
+
160
+ it('should allow reading the config via getter (covers line 72)', () => {
161
+ const store = createEditorStore();
162
+ const cfg = store.config; // line 72: return config
163
+
164
+ expect(cfg.modes).toHaveLength(0);
165
+ expect(cfg.isCodeEditor).toBe(false);
166
+ });
167
+
168
+ it('should call convertMarkdown (returns early) when inputValue changes with useRichText=true', async () => {
169
+ const store = createEditorStore();
170
+
171
+ // useRichText is true by default; no editor set → convertMarkdown returns early (line 40)
172
+ store.inputValue = 'hello'; // triggers line 89: convertMarkdown()
173
+ await new Promise((r) => {
174
+ setTimeout(r, 0);
175
+ });
176
+ expect(store.inputValue).toBe('hello');
177
+ expect(store.hasConverterError).toBe(false);
178
+ });
179
+
180
+ it('should set hasConverterError when convertMarkdownToLexical throws', async () => {
181
+ const store = createEditorStore();
182
+ const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
183
+ // Mock editor that has no .update() method → convertMarkdownToLexical will throw inside the
184
+ // new Promise executor, rejecting it and triggering the catch block (lines 48-52)
185
+ const mockEditor = /** @type {any} */ ({ getEditorState: () => ({ isEmpty: () => false }) });
186
+
187
+ store.editor = mockEditor;
188
+ store.initialized = true;
189
+ store.inputValue = 'some text'; // triggers convertMarkdown (line 89), which awaits the rejection
190
+ await new Promise((r) => {
191
+ setTimeout(r, 0);
192
+ });
193
+ expect(store.hasConverterError).toBe(true);
194
+ consoleSpy.mockRestore();
195
+ });
196
+ });
@@ -0,0 +1,108 @@
1
+ // cSpell:ignore-words horizontalrule
2
+
3
+ import { describe, expect, it, vi } from 'vitest';
4
+ import { HR } from './hr.js';
5
+
6
+ // Mock @lexical/extension so we can call HR.replace without a real Lexical editor context
7
+ vi.mock('@lexical/extension', () => {
8
+ const mockHRNode = {
9
+ __type: 'horizontalrule',
10
+ // eslint-disable-next-line jsdoc/require-jsdoc
11
+ selectNext: vi.fn(),
12
+ };
13
+
14
+ return {
15
+ // eslint-disable-next-line jsdoc/require-jsdoc
16
+ $createHorizontalRuleNode: vi.fn(() => mockHRNode),
17
+ // eslint-disable-next-line jsdoc/require-jsdoc
18
+ $isHorizontalRuleNode: (/** @type {{ __type: string; }} */ node) =>
19
+ node?.__type === 'horizontalrule',
20
+ /**
21
+ *
22
+ */
23
+ HorizontalRuleNode: class {},
24
+ };
25
+ });
26
+
27
+ describe('HR transformer', () => {
28
+ it('should have the correct type', () => {
29
+ expect(HR.type).toBe('element');
30
+ });
31
+
32
+ it('should include HorizontalRuleNode in dependencies', () => {
33
+ expect(HR.dependencies).toHaveLength(1);
34
+ });
35
+
36
+ it('regExp should match --- divider', () => {
37
+ expect(HR.regExp.test('---')).toBe(true);
38
+ });
39
+
40
+ it('regExp should match *** divider', () => {
41
+ expect(HR.regExp.test('***')).toBe(true);
42
+ });
43
+
44
+ it('regExp should match ___ divider', () => {
45
+ expect(HR.regExp.test('___')).toBe(true);
46
+ });
47
+
48
+ it('regExp should match dividers with trailing space', () => {
49
+ expect(HR.regExp.test('--- ')).toBe(true);
50
+ expect(HR.regExp.test('*** ')).toBe(true);
51
+ });
52
+
53
+ it('regExp should not match partial dividers', () => {
54
+ expect(HR.regExp.test('--')).toBe(false);
55
+ expect(HR.regExp.test('**')).toBe(false);
56
+ expect(HR.regExp.test('__')).toBe(false);
57
+ });
58
+
59
+ it('export should return null for a non-HR node', () => {
60
+ const fakeNode = { __type: 'paragraph' };
61
+
62
+ expect(HR.export(/** @type {any} */ (fakeNode), () => '')).toBeNull();
63
+ });
64
+
65
+ it('export should return "***" for an HR node', () => {
66
+ const hrNode = { __type: 'horizontalrule' };
67
+
68
+ expect(HR.export(/** @type {any} */ (hrNode), () => '')).toBe('***');
69
+ });
70
+
71
+ it('replace should call parentNode.replace when isImport is true', () => {
72
+ const parentNode = {
73
+ replace: vi.fn(),
74
+ insertBefore: vi.fn(),
75
+ getNextSibling: vi.fn().mockReturnValue(null),
76
+ };
77
+
78
+ HR.replace(/** @type {any} */ (parentNode), [], /** @type {any} */ (['---']), true);
79
+ expect(parentNode.replace).toHaveBeenCalledOnce();
80
+ expect(parentNode.insertBefore).not.toHaveBeenCalled();
81
+ });
82
+
83
+ it('replace should call parentNode.replace when there is a next sibling', () => {
84
+ const siblingNode = { __type: 'paragraph' };
85
+
86
+ const parentNode = {
87
+ replace: vi.fn(),
88
+ insertBefore: vi.fn(),
89
+ getNextSibling: vi.fn().mockReturnValue(siblingNode),
90
+ };
91
+
92
+ HR.replace(/** @type {any} */ (parentNode), [], /** @type {any} */ (['---']), false);
93
+ expect(parentNode.replace).toHaveBeenCalledOnce();
94
+ expect(parentNode.insertBefore).not.toHaveBeenCalled();
95
+ });
96
+
97
+ it('replace should call parentNode.insertBefore when not import and no next sibling', () => {
98
+ const parentNode = {
99
+ replace: vi.fn(),
100
+ insertBefore: vi.fn(),
101
+ getNextSibling: vi.fn().mockReturnValue(null),
102
+ };
103
+
104
+ HR.replace(/** @type {any} */ (parentNode), [], /** @type {any} */ (['---']), false);
105
+ expect(parentNode.insertBefore).toHaveBeenCalledOnce();
106
+ expect(parentNode.replace).not.toHaveBeenCalled();
107
+ });
108
+ });
@@ -0,0 +1,28 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { TABLE } from './table.js';
3
+
4
+ describe('TABLE transformer', () => {
5
+ it('should have the correct type', () => {
6
+ expect(TABLE.type).toBe('element');
7
+ });
8
+
9
+ it('should have TableNode, TableRowNode, and TableCellNode in dependencies', () => {
10
+ expect(TABLE.dependencies).toHaveLength(3);
11
+ });
12
+
13
+ it('regExp should match a table row line', () => {
14
+ expect(TABLE.regExp.test('| foo | bar |')).toBe(true);
15
+ expect(TABLE.regExp.test('| single |')).toBe(true);
16
+ });
17
+
18
+ it('regExp should not match lines without pipe borders', () => {
19
+ expect(TABLE.regExp.test('foo bar')).toBe(false);
20
+ expect(TABLE.regExp.test('- item')).toBe(false);
21
+ });
22
+
23
+ it('export should return null for a non-table node', () => {
24
+ const fakeNode = { __type: 'paragraph' };
25
+
26
+ expect(TABLE.export(/** @type {any} */ (fakeNode), () => '')).toBeNull();
27
+ });
28
+ });
@@ -59,20 +59,26 @@
59
59
  * @param {InputEvent} event The `input` event object.
60
60
  */
61
61
  const fireInput = (event) => {
62
- value = /** @type {HTMLInputElement} */ (event.target).value;
62
+ value = element?.value;
63
63
  oninput?.(event);
64
64
  };
65
65
 
66
66
  /**
67
- * Handle the `input` event. If `debounce` is `true`, the event will be debounced by 300ms.
67
+ * Handle the `input` event. If `debounce` is `true`, the event will be debounced by 300ms. We use
68
+ * `oninputcapture` to ensure that the event is fired before any `oninput` handlers on parent
69
+ * elements, including the Lexical editor.
68
70
  * @param {InputEvent} event The `input` event object.
69
71
  */
70
72
  const handleInput = (event) => {
71
73
  if (debounce) {
72
74
  clearTimeout(debounceTimer);
73
- debounceTimer = setTimeout(() => {
74
- fireInput(event);
75
- }, timeout);
75
+ debounceTimer = /** @type {number} */ (
76
+ /** @type {unknown} */ (
77
+ setTimeout(() => {
78
+ fireInput(event);
79
+ }, timeout)
80
+ )
81
+ );
76
82
  } else {
77
83
  fireInput(event);
78
84
  }
@@ -106,7 +112,7 @@
106
112
  aria-readonly={readonly}
107
113
  aria-required={required}
108
114
  aria-invalid={invalid}
109
- oninput={handleInput}
115
+ oninputcapture={handleInput}
110
116
  use:activateKeyShortcuts={keyShortcuts}
111
117
  />
112
118
  {#if ariaLabel && showInlineLabel}
@@ -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>