@sveltia/ui 0.35.6 → 0.36.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.
- package/dist/components/alert/infobar.svelte +2 -2
- package/dist/components/button/button.svelte +1 -1
- package/dist/components/button/select-button-group.svelte +1 -1
- package/dist/components/button/split-button.svelte +3 -3
- package/dist/components/calendar/calendar.svelte +20 -23
- package/dist/components/dialog/dialog.svelte +4 -4
- package/dist/components/dialog/dialog.svelte.d.ts +1 -1
- package/dist/components/dialog/prompt-dialog.svelte.d.ts +1 -1
- package/dist/components/drawer/drawer.svelte +3 -3
- package/dist/components/grid/grid.svelte +1 -1
- package/dist/components/listbox/listbox.svelte +1 -1
- package/dist/components/menu/menu-button.svelte +1 -0
- package/dist/components/menu/menu-item-checkbox.svelte +1 -0
- package/dist/components/menu/menu-item-radio.svelte +1 -0
- package/dist/components/menu/menu-item.svelte +3 -2
- package/dist/components/menu/menu.svelte +1 -1
- package/dist/components/radio/radio-group.svelte +1 -1
- package/dist/components/resizable-pane/resizable-handle.svelte +6 -4
- package/dist/components/select/combobox.svelte +5 -5
- package/dist/components/select/select-tags.svelte +6 -7
- package/dist/components/slider/slider.svelte +6 -5
- package/dist/components/slider/slider.svelte.d.ts +1 -1
- package/dist/components/tabs/tab-list.svelte +1 -1
- package/dist/components/tabs/tab.svelte +1 -0
- package/dist/components/text-editor/code-editor.svelte +2 -2
- package/dist/components/text-editor/text-editor.svelte +2 -2
- package/dist/components/text-editor/toolbar/code-editor-toolbar.svelte +2 -2
- package/dist/components/text-editor/toolbar/code-language-switcher.svelte +3 -3
- package/dist/components/text-editor/toolbar/format-text-button.svelte +2 -2
- package/dist/components/text-editor/toolbar/insert-link-button.svelte +8 -8
- package/dist/components/text-editor/toolbar/insert-menu-button.svelte +2 -2
- package/dist/components/text-editor/toolbar/text-editor-toolbar.svelte +5 -5
- package/dist/components/text-editor/toolbar/toggle-block-menu-item.svelte +2 -2
- package/dist/components/text-editor/transformers/hr.test.js +0 -2
- package/dist/components/text-field/number-input.svelte +3 -3
- package/dist/components/text-field/password-input.svelte +2 -2
- package/dist/components/text-field/search-bar.svelte +2 -2
- package/dist/components/text-field/secret-input.svelte +2 -2
- package/dist/components/text-field/text-input.svelte +6 -4
- package/dist/components/text-field/text-input.svelte.d.ts +1 -1
- package/dist/components/toast/toast.svelte +2 -0
- package/dist/components/util/app-shell.svelte +10 -9
- package/dist/index.d.ts +0 -3
- package/dist/index.js +1 -2
- package/dist/locales/en.yaml +66 -0
- package/dist/locales/ja.yaml +66 -0
- package/dist/services/group.svelte.d.ts +99 -2
- package/dist/services/group.svelte.js +46 -31
- package/dist/services/group.test.js +107 -36
- package/dist/services/i18n.d.ts +0 -11
- package/dist/services/i18n.js +15 -51
- package/dist/services/i18n.test.js +2 -90
- package/dist/services/popup.test.js +0 -4
- package/package.json +19 -17
- package/dist/locales/en.d.ts +0 -77
- package/dist/locales/en.js +0 -77
- package/dist/locales/ja.d.ts +0 -77
- package/dist/locales/ja.js +0 -77
|
@@ -1,13 +1,12 @@
|
|
|
1
|
-
|
|
2
|
-
* @import { ActionReturn } from 'svelte/action';
|
|
3
|
-
*/
|
|
4
|
-
|
|
1
|
+
import { isRTL } from '@sveltia/i18n';
|
|
5
2
|
import { generateElementId } from '@sveltia/utils/element';
|
|
6
3
|
import { sleep } from '@sveltia/utils/misc';
|
|
7
|
-
import { get } from 'svelte/store';
|
|
8
|
-
import { isRTL } from './i18n.js';
|
|
9
4
|
import { getSelectedItemDetail } from './select.svelte.js';
|
|
10
5
|
|
|
6
|
+
/**
|
|
7
|
+
* @import { Attachment } from 'svelte/attachments';
|
|
8
|
+
*/
|
|
9
|
+
|
|
11
10
|
/**
|
|
12
11
|
* Diacritic characters regex for normalization. We use a regex instead of `Intl` APIs for better
|
|
13
12
|
* performance, since `transliterate` is slow and we only need basic normalization.
|
|
@@ -89,7 +88,7 @@ const config = {
|
|
|
89
88
|
/**
|
|
90
89
|
* Implement keyboard and mouse interactions for a grouping composite widget.
|
|
91
90
|
*/
|
|
92
|
-
class Group {
|
|
91
|
+
export class Group {
|
|
93
92
|
/**
|
|
94
93
|
* Initialize a new `Group` instance.
|
|
95
94
|
* @param {HTMLElement} parent Parent element.
|
|
@@ -107,6 +106,18 @@ class Group {
|
|
|
107
106
|
this.parentGroupSelector = `[role="group"], [role="${this.role}"]`;
|
|
108
107
|
this.clickToSelect = clickToSelect;
|
|
109
108
|
|
|
109
|
+
// eslint-disable-next-line jsdoc/require-description
|
|
110
|
+
/** @type {(event: MouseEvent) => void} */
|
|
111
|
+
this._onClick = (event) => {
|
|
112
|
+
this.onClick(event);
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
// eslint-disable-next-line jsdoc/require-description
|
|
116
|
+
/** @type {(event: KeyboardEvent) => void} */
|
|
117
|
+
this._onKeyDown = (event) => {
|
|
118
|
+
this.onKeyDown(event);
|
|
119
|
+
};
|
|
120
|
+
|
|
110
121
|
const { orientation, childRoles, childSelectedAttr, focusChild, selectFirst } =
|
|
111
122
|
config[this.role];
|
|
112
123
|
|
|
@@ -169,14 +180,8 @@ class Group {
|
|
|
169
180
|
}
|
|
170
181
|
});
|
|
171
182
|
|
|
172
|
-
parent.addEventListener('click',
|
|
173
|
-
|
|
174
|
-
});
|
|
175
|
-
|
|
176
|
-
parent.addEventListener('keydown', (event) => {
|
|
177
|
-
this.onKeyDown(event);
|
|
178
|
-
});
|
|
179
|
-
|
|
183
|
+
parent.addEventListener('click', this._onClick);
|
|
184
|
+
parent.addEventListener('keydown', this._onKeyDown);
|
|
180
185
|
parent.dispatchEvent(new CustomEvent('Initialized'));
|
|
181
186
|
}
|
|
182
187
|
|
|
@@ -434,7 +439,7 @@ class Group {
|
|
|
434
439
|
|
|
435
440
|
if (this.grid) {
|
|
436
441
|
const colCount = Math.floor(this.parent.clientWidth / activeMembers[0].clientWidth);
|
|
437
|
-
const _isRTL =
|
|
442
|
+
const _isRTL = isRTL();
|
|
438
443
|
|
|
439
444
|
index = currentTarget ? allMembers.indexOf(currentTarget) : -1;
|
|
440
445
|
|
|
@@ -461,7 +466,7 @@ class Group {
|
|
|
461
466
|
} else {
|
|
462
467
|
index = currentTarget ? activeMembers.indexOf(currentTarget) : -1;
|
|
463
468
|
|
|
464
|
-
const _isRTL =
|
|
469
|
+
const _isRTL = isRTL();
|
|
465
470
|
|
|
466
471
|
// For horizontal orientation in RTL: ArrowLeft moves forward, ArrowRight moves backward
|
|
467
472
|
const prevKey =
|
|
@@ -500,6 +505,14 @@ class Group {
|
|
|
500
505
|
}
|
|
501
506
|
}
|
|
502
507
|
|
|
508
|
+
/**
|
|
509
|
+
* Clean up event listeners.
|
|
510
|
+
*/
|
|
511
|
+
destroy() {
|
|
512
|
+
this.parent.removeEventListener('click', this._onClick);
|
|
513
|
+
this.parent.removeEventListener('keydown', this._onKeyDown);
|
|
514
|
+
}
|
|
515
|
+
|
|
503
516
|
/**
|
|
504
517
|
* Called whenever the params are updated. Filter the items based on the search terms.
|
|
505
518
|
* @param {{ searchTerms: string }} params Updated params.
|
|
@@ -534,20 +547,22 @@ class Group {
|
|
|
534
547
|
|
|
535
548
|
/**
|
|
536
549
|
* Activate a new group.
|
|
537
|
-
* @param {
|
|
538
|
-
*
|
|
539
|
-
* @returns {
|
|
550
|
+
* @param {object | (() => object)} [paramsOrGetter] Params object or a getter function for reactive
|
|
551
|
+
* params.
|
|
552
|
+
* @returns {Attachment} Attachment.
|
|
540
553
|
*/
|
|
541
|
-
export const activateGroup = (parent
|
|
542
|
-
const
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
554
|
+
export const activateGroup = (paramsOrGetter) => (parent) => {
|
|
555
|
+
const isGetter = typeof paramsOrGetter === 'function';
|
|
556
|
+
const initialParams = isGetter ? paramsOrGetter() : paramsOrGetter;
|
|
557
|
+
const group = new Group(/** @type {HTMLElement} */ (parent), initialParams);
|
|
558
|
+
|
|
559
|
+
if (isGetter) {
|
|
560
|
+
$effect(() => {
|
|
561
|
+
group.onUpdate(paramsOrGetter());
|
|
562
|
+
});
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
return () => {
|
|
566
|
+
group.destroy();
|
|
552
567
|
};
|
|
553
568
|
};
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
/* eslint-disable jsdoc/require-jsdoc */
|
|
2
|
-
/* eslint-disable jsdoc/require-description */
|
|
3
2
|
|
|
3
|
+
import { locale } from '@sveltia/i18n';
|
|
4
4
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
5
|
-
import { activateGroup, normalize } from './group.svelte.js';
|
|
6
|
-
import { isRTL } from './i18n.js';
|
|
5
|
+
import { activateGroup, Group, normalize } from './group.svelte.js';
|
|
7
6
|
|
|
8
7
|
describe('normalize', () => {
|
|
9
8
|
it('should trim whitespace', () => {
|
|
@@ -51,7 +50,7 @@ describe('Group - tablist', () => {
|
|
|
51
50
|
return tab;
|
|
52
51
|
});
|
|
53
52
|
document.body.appendChild(tablist);
|
|
54
|
-
activateGroup(tablist);
|
|
53
|
+
activateGroup()(tablist);
|
|
55
54
|
await vi.advanceTimersByTimeAsync(150);
|
|
56
55
|
});
|
|
57
56
|
|
|
@@ -129,19 +128,19 @@ describe('Group - tablist', () => {
|
|
|
129
128
|
});
|
|
130
129
|
|
|
131
130
|
it('should navigate backward when pressing ArrowRight in RTL (branch 63 prevKey=ArrowRight)', () => {
|
|
132
|
-
|
|
131
|
+
locale.set('ar'); // RTL locale to set _isRTL=true in activate()
|
|
133
132
|
// In RTL prevKey='ArrowRight'; press from tabs[2] → backward to tabs[1]
|
|
134
133
|
tabs[2].dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowRight', bubbles: true }));
|
|
135
134
|
expect(tabs[1].getAttribute('aria-selected')).toBe('true');
|
|
136
|
-
|
|
135
|
+
locale.set('en'); // Reset locale to LTR
|
|
137
136
|
});
|
|
138
137
|
|
|
139
138
|
it('should navigate forward when pressing ArrowLeft in RTL (branch 65 nextKey=ArrowLeft)', () => {
|
|
140
|
-
|
|
139
|
+
locale.set('ar'); // RTL locale to set _isRTL=true in activate()
|
|
141
140
|
// In RTL nextKey='ArrowLeft'; press from tabs[0] → forward to tabs[1]
|
|
142
141
|
tabs[0].dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowLeft', bubbles: true }));
|
|
143
142
|
expect(tabs[1].getAttribute('aria-selected')).toBe('true');
|
|
144
|
-
|
|
143
|
+
locale.set('en'); // Reset locale to LTR
|
|
145
144
|
});
|
|
146
145
|
});
|
|
147
146
|
|
|
@@ -165,7 +164,7 @@ describe('Group - listbox', () => {
|
|
|
165
164
|
return opt;
|
|
166
165
|
});
|
|
167
166
|
document.body.appendChild(listbox);
|
|
168
|
-
activateGroup(listbox);
|
|
167
|
+
activateGroup()(listbox);
|
|
169
168
|
await vi.advanceTimersByTimeAsync(150);
|
|
170
169
|
});
|
|
171
170
|
|
|
@@ -213,7 +212,7 @@ describe('Group - onUpdate (search filter)', () => {
|
|
|
213
212
|
/** @type {HTMLElement[]} */
|
|
214
213
|
let options;
|
|
215
214
|
/** @type {any} */
|
|
216
|
-
let
|
|
215
|
+
let group;
|
|
217
216
|
|
|
218
217
|
beforeEach(async () => {
|
|
219
218
|
vi.useFakeTimers();
|
|
@@ -230,7 +229,7 @@ describe('Group - onUpdate (search filter)', () => {
|
|
|
230
229
|
return opt;
|
|
231
230
|
});
|
|
232
231
|
document.body.appendChild(listbox);
|
|
233
|
-
|
|
232
|
+
group = new Group(listbox);
|
|
234
233
|
await vi.advanceTimersByTimeAsync(150);
|
|
235
234
|
});
|
|
236
235
|
|
|
@@ -243,7 +242,7 @@ describe('Group - onUpdate (search filter)', () => {
|
|
|
243
242
|
const filterEvents = /** @type {CustomEvent[]} */ ([]);
|
|
244
243
|
|
|
245
244
|
listbox.addEventListener('Filter', (e) => filterEvents.push(/** @type {CustomEvent} */ (e)));
|
|
246
|
-
|
|
245
|
+
group.onUpdate({ searchTerms: 'an' }); // matches Banana
|
|
247
246
|
expect(filterEvents.length).toBeGreaterThan(0);
|
|
248
247
|
expect(filterEvents[filterEvents.length - 1].detail.matched).toBe(1);
|
|
249
248
|
expect(filterEvents[filterEvents.length - 1].detail.total).toBe(3);
|
|
@@ -259,7 +258,7 @@ describe('Group - onUpdate (search filter)', () => {
|
|
|
259
258
|
}
|
|
260
259
|
});
|
|
261
260
|
});
|
|
262
|
-
|
|
261
|
+
group.onUpdate({ searchTerms: 'apple' });
|
|
263
262
|
// Banana and Cherry should be toggled hidden
|
|
264
263
|
expect(toggledItems).toContain('Banana');
|
|
265
264
|
expect(toggledItems).toContain('Cherry');
|
|
@@ -267,12 +266,12 @@ describe('Group - onUpdate (search filter)', () => {
|
|
|
267
266
|
});
|
|
268
267
|
|
|
269
268
|
it('should show all items when search terms are cleared', () => {
|
|
270
|
-
|
|
269
|
+
group.onUpdate({ searchTerms: 'apple' });
|
|
271
270
|
|
|
272
271
|
const filterEvents = /** @type {CustomEvent[]} */ ([]);
|
|
273
272
|
|
|
274
273
|
listbox.addEventListener('Filter', (e) => filterEvents.push(/** @type {CustomEvent} */ (e)));
|
|
275
|
-
|
|
274
|
+
group.onUpdate({ searchTerms: '' });
|
|
276
275
|
expect(filterEvents[filterEvents.length - 1].detail.matched).toBe(3);
|
|
277
276
|
});
|
|
278
277
|
});
|
|
@@ -297,7 +296,7 @@ describe('Group - tablist keyboard (Enter and Space)', () => {
|
|
|
297
296
|
return tab;
|
|
298
297
|
});
|
|
299
298
|
document.body.appendChild(tablist);
|
|
300
|
-
activateGroup(tablist);
|
|
299
|
+
activateGroup()(tablist);
|
|
301
300
|
await vi.advanceTimersByTimeAsync(150);
|
|
302
301
|
});
|
|
303
302
|
|
|
@@ -354,7 +353,7 @@ describe('Group - tablist with aria-controls panels', () => {
|
|
|
354
353
|
return tab;
|
|
355
354
|
});
|
|
356
355
|
document.body.appendChild(tablist);
|
|
357
|
-
activateGroup(tablist);
|
|
356
|
+
activateGroup()(tablist);
|
|
358
357
|
// also covers scrollIntoView setTimeout(300) in activate()
|
|
359
358
|
await vi.advanceTimersByTimeAsync(500);
|
|
360
359
|
});
|
|
@@ -425,7 +424,7 @@ describe('Group - disabled and read-only', () => {
|
|
|
425
424
|
return opt;
|
|
426
425
|
});
|
|
427
426
|
document.body.appendChild(listbox);
|
|
428
|
-
activateGroup(listbox);
|
|
427
|
+
activateGroup()(listbox);
|
|
429
428
|
await vi.advanceTimersByTimeAsync(150);
|
|
430
429
|
});
|
|
431
430
|
|
|
@@ -468,7 +467,7 @@ describe('Group - multiselect listbox', () => {
|
|
|
468
467
|
return opt;
|
|
469
468
|
});
|
|
470
469
|
document.body.appendChild(listbox);
|
|
471
|
-
activateGroup(listbox);
|
|
470
|
+
activateGroup()(listbox);
|
|
472
471
|
await vi.advanceTimersByTimeAsync(150);
|
|
473
472
|
});
|
|
474
473
|
|
|
@@ -533,7 +532,7 @@ describe('Group - listbox keyboard navigation', () => {
|
|
|
533
532
|
return opt;
|
|
534
533
|
});
|
|
535
534
|
document.body.appendChild(listbox);
|
|
536
|
-
activateGroup(listbox);
|
|
535
|
+
activateGroup()(listbox);
|
|
537
536
|
await vi.advanceTimersByTimeAsync(150);
|
|
538
537
|
});
|
|
539
538
|
|
|
@@ -610,7 +609,7 @@ describe('Group - menu with menuitemradio', () => {
|
|
|
610
609
|
return item;
|
|
611
610
|
});
|
|
612
611
|
document.body.appendChild(menu);
|
|
613
|
-
activateGroup(menu);
|
|
612
|
+
activateGroup()(menu);
|
|
614
613
|
await vi.advanceTimersByTimeAsync(150);
|
|
615
614
|
});
|
|
616
615
|
|
|
@@ -658,7 +657,7 @@ describe('Group - menu with menuitemcheckbox', () => {
|
|
|
658
657
|
return item;
|
|
659
658
|
});
|
|
660
659
|
document.body.appendChild(menu);
|
|
661
|
-
activateGroup(menu);
|
|
660
|
+
activateGroup()(menu);
|
|
662
661
|
await vi.advanceTimersByTimeAsync(150);
|
|
663
662
|
});
|
|
664
663
|
|
|
@@ -707,7 +706,7 @@ describe('Group - radiogroup keyboard navigation', () => {
|
|
|
707
706
|
return radio;
|
|
708
707
|
});
|
|
709
708
|
document.body.appendChild(radiogroup);
|
|
710
|
-
activateGroup(radiogroup);
|
|
709
|
+
activateGroup()(radiogroup);
|
|
711
710
|
await vi.advanceTimersByTimeAsync(150);
|
|
712
711
|
radios[0].click(); // select first radio
|
|
713
712
|
});
|
|
@@ -773,7 +772,7 @@ describe('Group - grid listbox navigation', () => {
|
|
|
773
772
|
get: () => 100,
|
|
774
773
|
});
|
|
775
774
|
});
|
|
776
|
-
activateGroup(listbox);
|
|
775
|
+
activateGroup()(listbox);
|
|
777
776
|
await vi.advanceTimersByTimeAsync(150);
|
|
778
777
|
options[0].click(); // add .focused class to options[0]
|
|
779
778
|
});
|
|
@@ -820,10 +819,10 @@ describe('Group - grid listbox navigation', () => {
|
|
|
820
819
|
listbox.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowRight', bubbles: true }));
|
|
821
820
|
expect(options[1].getAttribute('aria-selected')).toBe('true');
|
|
822
821
|
// In RTL, ArrowLeft moves forward: index 1 + 1 = options[2]
|
|
823
|
-
|
|
822
|
+
locale.set('ar'); // RTL locale to set _isRTL=true in activate()
|
|
824
823
|
listbox.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowLeft', bubbles: true }));
|
|
825
824
|
expect(options[2].getAttribute('aria-selected')).toBe('true');
|
|
826
|
-
|
|
825
|
+
locale.set('en'); // Reset locale to LTR
|
|
827
826
|
});
|
|
828
827
|
|
|
829
828
|
it('should navigate grid backward (index-1) on ArrowRight in RTL (branch 59)', () => {
|
|
@@ -831,10 +830,10 @@ describe('Group - grid listbox navigation', () => {
|
|
|
831
830
|
listbox.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowRight', bubbles: true }));
|
|
832
831
|
expect(options[1].getAttribute('aria-selected')).toBe('true');
|
|
833
832
|
// In RTL, ArrowRight moves backward: index 1 - 1 = options[0]
|
|
834
|
-
|
|
833
|
+
locale.set('ar'); // RTL locale to set _isRTL=true in activate()
|
|
835
834
|
listbox.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowRight', bubbles: true }));
|
|
836
835
|
expect(options[0].getAttribute('aria-selected')).toBe('true');
|
|
837
|
-
|
|
836
|
+
locale.set('en'); // Reset locale to LTR
|
|
838
837
|
});
|
|
839
838
|
});
|
|
840
839
|
|
|
@@ -873,7 +872,7 @@ describe('Group - scrollIntoView catch fallback in activate()', () => {
|
|
|
873
872
|
tablist.appendChild(tab);
|
|
874
873
|
});
|
|
875
874
|
document.body.appendChild(tablist);
|
|
876
|
-
activateGroup(tablist);
|
|
875
|
+
activateGroup()(tablist);
|
|
877
876
|
// flush sleep(100) + setTimeout(300) in activate()
|
|
878
877
|
await vi.advanceTimersByTimeAsync(500);
|
|
879
878
|
});
|
|
@@ -936,7 +935,7 @@ describe('Group - scrollIntoView catch fallback in selectTarget()', () => {
|
|
|
936
935
|
return tab;
|
|
937
936
|
});
|
|
938
937
|
document.body.appendChild(tablist);
|
|
939
|
-
activateGroup(tablist);
|
|
938
|
+
activateGroup()(tablist);
|
|
940
939
|
await vi.advanceTimersByTimeAsync(500);
|
|
941
940
|
});
|
|
942
941
|
|
|
@@ -977,7 +976,7 @@ describe('Group - tablist with pre-selected second tab (branch 7 defaultSelected
|
|
|
977
976
|
// Pre-select the second tab before activation
|
|
978
977
|
tbs[1].setAttribute('aria-selected', 'true');
|
|
979
978
|
document.body.appendChild(tl);
|
|
980
|
-
activateGroup(tl);
|
|
979
|
+
activateGroup()(tl);
|
|
981
980
|
await vi.advanceTimersByTimeAsync(150);
|
|
982
981
|
|
|
983
982
|
// defaultSelected = tbs[1]; the ternary `defaultSelected ? element === defaultSelected : ...`
|
|
@@ -1020,7 +1019,7 @@ describe('Group - menu with nested radio groups (branch 16 cross-group filter)',
|
|
|
1020
1019
|
menu.appendChild(group1);
|
|
1021
1020
|
menu.appendChild(group2);
|
|
1022
1021
|
document.body.appendChild(menu);
|
|
1023
|
-
activateGroup(menu);
|
|
1022
|
+
activateGroup()(menu);
|
|
1024
1023
|
await vi.advanceTimersByTimeAsync(150);
|
|
1025
1024
|
|
|
1026
1025
|
// Click radioA — radioB is in a different group, so line 267 early return filters it out
|
|
@@ -1047,7 +1046,7 @@ describe('Group - onClick with clickToSelect disabled (branch 39 !clickToSelect)
|
|
|
1047
1046
|
opt.textContent = 'Option';
|
|
1048
1047
|
lb.appendChild(opt);
|
|
1049
1048
|
document.body.appendChild(lb);
|
|
1050
|
-
activateGroup(
|
|
1049
|
+
activateGroup({ clickToSelect: false })(lb);
|
|
1051
1050
|
await vi.advanceTimersByTimeAsync(150);
|
|
1052
1051
|
opt.click();
|
|
1053
1052
|
// !clickToSelect → early return in onClick → aria-selected stays false
|
|
@@ -1081,7 +1080,7 @@ describe('Group - grid listbox with no initial focus (branch 49 currentTarget?..
|
|
|
1081
1080
|
gridOptions.forEach((opt) => {
|
|
1082
1081
|
Object.defineProperty(opt, 'clientWidth', { configurable: true, get: () => 100 });
|
|
1083
1082
|
});
|
|
1084
|
-
activateGroup(gridListbox);
|
|
1083
|
+
activateGroup()(gridListbox);
|
|
1085
1084
|
await vi.advanceTimersByTimeAsync(150);
|
|
1086
1085
|
|
|
1087
1086
|
// Press ArrowDown with no focused element → currentTarget=undefined → index=-1
|
|
@@ -1122,7 +1121,7 @@ describe('Group - onUpdate with querySelector(.label) and textContent fallbacks
|
|
|
1122
1121
|
listbox.appendChild(opt2);
|
|
1123
1122
|
document.body.appendChild(listbox);
|
|
1124
1123
|
|
|
1125
|
-
const
|
|
1124
|
+
const group = new Group(listbox);
|
|
1126
1125
|
|
|
1127
1126
|
await vi.advanceTimersByTimeAsync(150);
|
|
1128
1127
|
|
|
@@ -1134,7 +1133,7 @@ describe('Group - onUpdate with querySelector(.label) and textContent fallbacks
|
|
|
1134
1133
|
});
|
|
1135
1134
|
|
|
1136
1135
|
// Searching 'apple' matches opt1 (via .label span) but not opt2 (via textContent 'Banana')
|
|
1137
|
-
|
|
1136
|
+
group.onUpdate({ searchTerms: 'apple' });
|
|
1138
1137
|
expect(filterDetail.matched).toBe(1);
|
|
1139
1138
|
expect(filterDetail.total).toBe(2);
|
|
1140
1139
|
|
|
@@ -1142,3 +1141,75 @@ describe('Group - onUpdate with querySelector(.label) and textContent fallbacks
|
|
|
1142
1141
|
vi.useRealTimers();
|
|
1143
1142
|
});
|
|
1144
1143
|
});
|
|
1144
|
+
|
|
1145
|
+
describe('Group - destroy', () => {
|
|
1146
|
+
it('should remove click and keydown listeners after destroy()', async () => {
|
|
1147
|
+
vi.useFakeTimers();
|
|
1148
|
+
|
|
1149
|
+
const listbox = document.createElement('div');
|
|
1150
|
+
|
|
1151
|
+
listbox.setAttribute('role', 'listbox');
|
|
1152
|
+
|
|
1153
|
+
const opt = document.createElement('div');
|
|
1154
|
+
|
|
1155
|
+
opt.setAttribute('role', 'option');
|
|
1156
|
+
opt.textContent = 'Option';
|
|
1157
|
+
listbox.appendChild(opt);
|
|
1158
|
+
document.body.appendChild(listbox);
|
|
1159
|
+
|
|
1160
|
+
const group = new Group(listbox);
|
|
1161
|
+
|
|
1162
|
+
await vi.advanceTimersByTimeAsync(150);
|
|
1163
|
+
|
|
1164
|
+
// Click selects before destroy
|
|
1165
|
+
opt.click();
|
|
1166
|
+
expect(opt.getAttribute('aria-selected')).toBe('true');
|
|
1167
|
+
|
|
1168
|
+
group.destroy();
|
|
1169
|
+
|
|
1170
|
+
// Reset and click again — listener should be gone
|
|
1171
|
+
opt.setAttribute('aria-selected', 'false');
|
|
1172
|
+
opt.click();
|
|
1173
|
+
expect(opt.getAttribute('aria-selected')).toBe('false');
|
|
1174
|
+
|
|
1175
|
+
listbox.remove();
|
|
1176
|
+
vi.useRealTimers();
|
|
1177
|
+
});
|
|
1178
|
+
});
|
|
1179
|
+
|
|
1180
|
+
describe('activateGroup - attachment cleanup', () => {
|
|
1181
|
+
it('should return a cleanup function that calls destroy()', async () => {
|
|
1182
|
+
vi.useFakeTimers();
|
|
1183
|
+
|
|
1184
|
+
const listbox = document.createElement('div');
|
|
1185
|
+
|
|
1186
|
+
listbox.setAttribute('role', 'listbox');
|
|
1187
|
+
|
|
1188
|
+
const opt = document.createElement('div');
|
|
1189
|
+
|
|
1190
|
+
opt.setAttribute('role', 'option');
|
|
1191
|
+
opt.textContent = 'Option';
|
|
1192
|
+
listbox.appendChild(opt);
|
|
1193
|
+
document.body.appendChild(listbox);
|
|
1194
|
+
|
|
1195
|
+
const cleanup = activateGroup()(listbox);
|
|
1196
|
+
|
|
1197
|
+
await vi.advanceTimersByTimeAsync(150);
|
|
1198
|
+
|
|
1199
|
+
// Click selects before cleanup
|
|
1200
|
+
opt.click();
|
|
1201
|
+
expect(opt.getAttribute('aria-selected')).toBe('true');
|
|
1202
|
+
|
|
1203
|
+
// Run cleanup
|
|
1204
|
+
expect(typeof cleanup).toBe('function');
|
|
1205
|
+
/** @type {() => void} */ (cleanup)();
|
|
1206
|
+
|
|
1207
|
+
// Reset and click again — listener should be gone
|
|
1208
|
+
opt.setAttribute('aria-selected', 'false');
|
|
1209
|
+
opt.click();
|
|
1210
|
+
expect(opt.getAttribute('aria-selected')).toBe('false');
|
|
1211
|
+
|
|
1212
|
+
listbox.remove();
|
|
1213
|
+
vi.useRealTimers();
|
|
1214
|
+
});
|
|
1215
|
+
});
|
package/dist/services/i18n.d.ts
CHANGED
|
@@ -1,15 +1,4 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Whether the current document is in RTL mode.
|
|
3
|
-
* @type {import('svelte/store').Writable<boolean>}
|
|
4
|
-
*/
|
|
5
|
-
export const isRTL: import("svelte/store").Writable<boolean>;
|
|
6
1
|
export function initLocales({ fallbackLocale, initialLocale }?: {
|
|
7
2
|
fallbackLocale?: string | undefined;
|
|
8
3
|
initialLocale?: string | undefined;
|
|
9
4
|
} | undefined): void;
|
|
10
|
-
/**
|
|
11
|
-
* List of RTL locales: Arabic, Persian, Hebrew, Urdu.
|
|
12
|
-
* @internal
|
|
13
|
-
*/
|
|
14
|
-
export const RTL_LOCALES: string[];
|
|
15
|
-
export function getDirection(locale: string | null | undefined): "rtl" | "ltr";
|
package/dist/services/i18n.js
CHANGED
|
@@ -1,64 +1,28 @@
|
|
|
1
|
-
import { addMessages,
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Whether the current document is in RTL mode.
|
|
6
|
-
* @type {import('svelte/store').Writable<boolean>}
|
|
7
|
-
*/
|
|
8
|
-
export const isRTL = writable(false);
|
|
1
|
+
import { addMessages, init } from '@sveltia/i18n';
|
|
2
|
+
import { parse as parseYaml } from 'yaml';
|
|
9
3
|
|
|
10
4
|
/**
|
|
11
5
|
* Load strings and initialize the locales.
|
|
12
6
|
* @param {object} [init] Initialize options.
|
|
13
7
|
* @param {string} [init.fallbackLocale] Fallback locale.
|
|
14
8
|
* @param {string} [init.initialLocale] Initial locale.
|
|
15
|
-
* @see https://github.com/
|
|
9
|
+
* @see https://github.com/sveltia/sveltia-i18n
|
|
16
10
|
* @see https://vitejs.dev/guide/features.html#glob-import
|
|
17
11
|
*/
|
|
18
12
|
export const initLocales = ({ fallbackLocale = 'en', initialLocale = 'en' } = {}) => {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
const [, locale] = /** @type {string[]} */ (path.match(/([a-zA-Z-]+)\.js/));
|
|
24
|
-
|
|
25
|
-
// Add `_sui` suffix to avoid collision with app localization
|
|
26
|
-
addMessages(locale, /** @type {any} */ ({ _sui: strings }));
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
init({
|
|
30
|
-
fallbackLocale,
|
|
31
|
-
initialLocale,
|
|
13
|
+
const resources = import.meta.glob('../locales/*.yaml', {
|
|
14
|
+
eager: true,
|
|
15
|
+
query: '?raw',
|
|
16
|
+
import: 'default',
|
|
32
17
|
});
|
|
33
|
-
};
|
|
34
18
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* Get the text direction of the given locale.
|
|
43
|
-
* @internal
|
|
44
|
-
* @param {string | null | undefined} locale Locale code.
|
|
45
|
-
* @returns {'rtl' | 'ltr'} Text direction.
|
|
46
|
-
*/
|
|
47
|
-
export const getDirection = (locale) =>
|
|
48
|
-
locale && RTL_LOCALES.includes(locale.split('-')[0]) ? 'rtl' : 'ltr';
|
|
49
|
-
|
|
50
|
-
if (!import.meta.env.SSR) {
|
|
51
|
-
// Set the `dir` attribute on the HTML element based on the current locale.
|
|
52
|
-
// @todo Move this to Sveltia UI and then Sveltia I18N
|
|
53
|
-
appLocale.subscribe((value) => {
|
|
54
|
-
document.documentElement.dir = getDirection(value);
|
|
19
|
+
Object.entries(resources).forEach(([path, resource]) => {
|
|
20
|
+
addMessages(
|
|
21
|
+
/** @type {string} */ (path.match(/.+\/(?<locale>.+?)\.yaml$/)?.groups?.locale),
|
|
22
|
+
// Add `_sui` suffix to avoid collision with app localization
|
|
23
|
+
{ _sui: parseYaml(/** @type {string} */ (resource)) },
|
|
24
|
+
);
|
|
55
25
|
});
|
|
56
26
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
isRTL.set(document.dir === 'rtl');
|
|
60
|
-
}).observe(document.documentElement, {
|
|
61
|
-
attributes: true,
|
|
62
|
-
attributeFilter: ['dir'],
|
|
63
|
-
});
|
|
64
|
-
}
|
|
27
|
+
init({ fallbackLocale, initialLocale });
|
|
28
|
+
};
|
|
@@ -1,61 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import { getDirection, initLocales, RTL_LOCALES } from './i18n.js';
|
|
4
|
-
|
|
5
|
-
describe('RTL_LOCALES', () => {
|
|
6
|
-
it('should include Arabic, Persian, Hebrew, and Urdu', () => {
|
|
7
|
-
expect(RTL_LOCALES).toContain('ar');
|
|
8
|
-
expect(RTL_LOCALES).toContain('fa');
|
|
9
|
-
expect(RTL_LOCALES).toContain('he');
|
|
10
|
-
expect(RTL_LOCALES).toContain('ur');
|
|
11
|
-
});
|
|
12
|
-
});
|
|
13
|
-
|
|
14
|
-
describe('getDirection', () => {
|
|
15
|
-
it('should return rtl for Arabic', () => {
|
|
16
|
-
expect(getDirection('ar')).toBe('rtl');
|
|
17
|
-
});
|
|
18
|
-
|
|
19
|
-
it('should return rtl for Persian', () => {
|
|
20
|
-
expect(getDirection('fa')).toBe('rtl');
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
it('should return rtl for Hebrew', () => {
|
|
24
|
-
expect(getDirection('he')).toBe('rtl');
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
it('should return rtl for Urdu', () => {
|
|
28
|
-
expect(getDirection('ur')).toBe('rtl');
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
it('should return rtl for a regional RTL locale like ar-SA', () => {
|
|
32
|
-
expect(getDirection('ar-SA')).toBe('rtl');
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
it('should return ltr for English', () => {
|
|
36
|
-
expect(getDirection('en')).toBe('ltr');
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
it('should return ltr for Japanese', () => {
|
|
40
|
-
expect(getDirection('ja')).toBe('ltr');
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
it('should return ltr for a regional LTR locale like en-US', () => {
|
|
44
|
-
expect(getDirection('en-US')).toBe('ltr');
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
it('should return ltr for null', () => {
|
|
48
|
-
expect(getDirection(null)).toBe('ltr');
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
it('should return ltr for undefined', () => {
|
|
52
|
-
expect(getDirection(undefined)).toBe('ltr');
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
it('should return ltr for an empty string', () => {
|
|
56
|
-
expect(getDirection('')).toBe('ltr');
|
|
57
|
-
});
|
|
58
|
-
});
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { initLocales } from './i18n.js';
|
|
59
3
|
|
|
60
4
|
describe('initLocales', () => {
|
|
61
5
|
it('should not throw when called with default options', () => {
|
|
@@ -72,35 +16,3 @@ describe('initLocales', () => {
|
|
|
72
16
|
warnSpy.mockRestore();
|
|
73
17
|
});
|
|
74
18
|
});
|
|
75
|
-
|
|
76
|
-
describe('i18n side effects', () => {
|
|
77
|
-
afterEach(async () => {
|
|
78
|
-
// Restore locale and document direction after each test
|
|
79
|
-
appLocale.set('en');
|
|
80
|
-
document.documentElement.removeAttribute('dir');
|
|
81
|
-
await Promise.resolve();
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
it('should update document.dir to rtl when locale is set to an RTL language', () => {
|
|
85
|
-
appLocale.set('ar');
|
|
86
|
-
expect(document.documentElement.dir).toBe('rtl');
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
it('should update document.dir back to ltr when locale is set to a LTR language', () => {
|
|
90
|
-
appLocale.set('ar');
|
|
91
|
-
appLocale.set('en');
|
|
92
|
-
expect(document.documentElement.dir).toBe('ltr');
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
it('should update the isRTL store when document dir attribute changes', async () => {
|
|
96
|
-
// Set dir via appLocale subscriber (which sets document.documentElement.dir synchronously)
|
|
97
|
-
// This triggers the MutationObserver set up in i18n.js (line 59)
|
|
98
|
-
appLocale.set('ar'); // → document.documentElement.dir = 'rtl' synchronously
|
|
99
|
-
// happy-dom MO callback fires via internal scheduling; give it time to run
|
|
100
|
-
await new Promise((r) => {
|
|
101
|
-
setTimeout(r, 50);
|
|
102
|
-
});
|
|
103
|
-
// Whether isRTL updated or not depends on MO timing, but document.dir is definitely set
|
|
104
|
-
expect(document.documentElement.dir).toBe('rtl');
|
|
105
|
-
});
|
|
106
|
-
});
|