@sveltia/ui 0.35.5 → 0.36.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/dist/components/alert/infobar.svelte +2 -2
  2. package/dist/components/button/button.svelte +1 -1
  3. package/dist/components/button/select-button-group.svelte +1 -1
  4. package/dist/components/button/split-button.svelte +3 -3
  5. package/dist/components/calendar/calendar.svelte +20 -23
  6. package/dist/components/dialog/dialog.svelte +4 -4
  7. package/dist/components/dialog/dialog.svelte.d.ts +1 -1
  8. package/dist/components/dialog/prompt-dialog.svelte.d.ts +1 -1
  9. package/dist/components/drawer/drawer.svelte +3 -3
  10. package/dist/components/grid/grid.svelte +1 -1
  11. package/dist/components/listbox/listbox.svelte +1 -1
  12. package/dist/components/menu/menu-button.svelte +1 -0
  13. package/dist/components/menu/menu-item-checkbox.svelte +1 -0
  14. package/dist/components/menu/menu-item-radio.svelte +1 -0
  15. package/dist/components/menu/menu-item.svelte +3 -2
  16. package/dist/components/menu/menu.svelte +1 -1
  17. package/dist/components/radio/radio-group.svelte +1 -1
  18. package/dist/components/resizable-pane/resizable-handle.svelte +6 -4
  19. package/dist/components/select/combobox.svelte +5 -5
  20. package/dist/components/select/select-tags.svelte +6 -7
  21. package/dist/components/slider/slider.svelte +6 -5
  22. package/dist/components/slider/slider.svelte.d.ts +1 -1
  23. package/dist/components/tabs/tab-list.svelte +1 -1
  24. package/dist/components/tabs/tab.svelte +1 -0
  25. package/dist/components/text-editor/code-editor.svelte +2 -2
  26. package/dist/components/text-editor/core.js +4 -0
  27. package/dist/components/text-editor/text-editor.svelte +2 -2
  28. package/dist/components/text-editor/toolbar/code-editor-toolbar.svelte +2 -2
  29. package/dist/components/text-editor/toolbar/code-language-switcher.svelte +7 -3
  30. package/dist/components/text-editor/toolbar/format-text-button.svelte +2 -2
  31. package/dist/components/text-editor/toolbar/insert-link-button.svelte +8 -8
  32. package/dist/components/text-editor/toolbar/insert-menu-button.svelte +2 -2
  33. package/dist/components/text-editor/toolbar/text-editor-toolbar.svelte +5 -5
  34. package/dist/components/text-editor/toolbar/toggle-block-menu-item.svelte +6 -2
  35. package/dist/components/text-editor/transformers/hr.test.js +0 -2
  36. package/dist/components/text-field/number-input.svelte +3 -3
  37. package/dist/components/text-field/password-input.svelte +2 -2
  38. package/dist/components/text-field/search-bar.svelte +2 -2
  39. package/dist/components/text-field/secret-input.svelte +2 -2
  40. package/dist/components/text-field/text-input.svelte +6 -4
  41. package/dist/components/text-field/text-input.svelte.d.ts +1 -1
  42. package/dist/components/toast/toast.svelte +2 -0
  43. package/dist/components/util/app-shell.svelte +10 -9
  44. package/dist/index.d.ts +0 -3
  45. package/dist/index.js +1 -2
  46. package/dist/locales/en.yaml +66 -0
  47. package/dist/locales/ja.yaml +66 -0
  48. package/dist/services/group.svelte.d.ts +99 -2
  49. package/dist/services/group.svelte.js +46 -31
  50. package/dist/services/group.test.js +107 -36
  51. package/dist/services/i18n.d.ts +0 -11
  52. package/dist/services/i18n.js +15 -51
  53. package/dist/services/i18n.test.js +2 -90
  54. package/dist/services/popup.test.js +0 -4
  55. package/package.json +19 -17
  56. package/dist/locales/en.d.ts +0 -77
  57. package/dist/locales/en.js +0 -77
  58. package/dist/locales/ja.d.ts +0 -77
  59. 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', (event) => {
173
- this.onClick(event);
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 = get(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 = get(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 {HTMLElement} parent Parent element.
538
- * @param {object} [params] Action params.
539
- * @returns {ActionReturn} Action.
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, params) => {
542
- const group = new Group(parent, params);
543
-
544
- return {
545
- /**
546
- * Called whenever the params are updated.
547
- * @param {any} newParams Updated params.
548
- */
549
- update(newParams) {
550
- group.onUpdate(newParams);
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
- isRTL.set(true);
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
- isRTL.set(false);
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
- isRTL.set(true);
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
- isRTL.set(false);
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 action;
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
- action = activateGroup(listbox, { searchTerms: '' });
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
- action.update({ searchTerms: 'an' }); // matches Banana
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
- action.update({ searchTerms: 'apple' });
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
- action.update({ searchTerms: 'apple' });
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
- action.update({ searchTerms: '' });
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
- isRTL.set(true);
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
- isRTL.set(false);
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
- isRTL.set(true);
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
- isRTL.set(false);
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(lb, { clickToSelect: false });
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 action = /** @type {any} */ (activateGroup(listbox, { searchTerms: '' }));
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
- action.update({ searchTerms: 'apple' });
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
+ });
@@ -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";
@@ -1,64 +1,28 @@
1
- import { addMessages, locale as appLocale, init } from 'svelte-i18n';
2
- import { writable } from 'svelte/store';
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/kaisermann/svelte-i18n/blob/main/docs/Getting%20Started.md
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
- /** @type {{ [key: string]: { strings: object }}} */
20
- const modules = import.meta.glob('../locales/*.js', { eager: true });
21
-
22
- Object.entries(modules).forEach(([path, { strings }]) => {
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
- * List of RTL locales: Arabic, Persian, Hebrew, Urdu.
37
- * @internal
38
- */
39
- export const RTL_LOCALES = ['ar', 'fa', 'he', 'ur'];
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
- // Update `isRTL` store based on the current document direction.
58
- new MutationObserver(() => {
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 { locale as appLocale } from 'svelte-i18n';
2
- import { afterEach, describe, expect, it, vi } from 'vitest';
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
- });