@gjsify/adwaita-web 0.47.0 → 0.49.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 (42) hide show
  1. package/README.md +3 -3
  2. package/dist/adwaita-web.css +62 -0
  3. package/dist/adwaita-web.css.map +1 -1
  4. package/lib/types/elements/adw-combo-row.d.ts +56 -0
  5. package/lib/types/elements/adw-spin-row.d.ts +11 -1
  6. package/lib/types/elements/adw-split-button.d.ts +43 -3
  7. package/lib/types/elements/adw-tab-view.d.ts +23 -5
  8. package/lib/types/elements/adw-view-stack.d.ts +17 -2
  9. package/lib/types/elements/gtk-drop-down.d.ts +22 -7
  10. package/lib/types/elements/gtk-menu-button.d.ts +43 -26
  11. package/lib/types/elements/gtk-popover.d.ts +14 -1
  12. package/lib/types/elements/popover-menu.d.ts +99 -0
  13. package/lib/types/index.d.ts +1 -1
  14. package/lib/types/shared-trees.spec.d.ts +2 -0
  15. package/lib/types/styles.generated.d.ts +1 -1
  16. package/package.json +6 -6
  17. package/scss/_popover.scss +99 -0
  18. package/src/adw-avatar.spec.ts +17 -0
  19. package/src/adw-row-state.spec.ts +335 -17
  20. package/src/adw-tab-view.spec.ts +8 -5
  21. package/src/elements/adw-avatar.ts +11 -3
  22. package/src/elements/adw-combo-row.ts +161 -22
  23. package/src/elements/adw-spin-row.ts +49 -28
  24. package/src/elements/adw-split-button.ts +117 -39
  25. package/src/elements/adw-tab-view.ts +42 -22
  26. package/src/elements/adw-view-stack.ts +20 -4
  27. package/src/elements/adw-view-switcher-bar.ts +6 -3
  28. package/src/elements/gtk-drop-down.ts +109 -66
  29. package/src/elements/gtk-menu-button.ts +112 -88
  30. package/src/elements/gtk-popover.ts +17 -2
  31. package/src/elements/popover-menu.ts +366 -0
  32. package/src/gtk-drop-down.spec.ts +117 -25
  33. package/src/gtk-image.spec.ts +3 -3
  34. package/src/gtk-menu-button.spec.ts +165 -11
  35. package/src/index.ts +14 -1
  36. package/src/keyboard-operable.spec.ts +170 -1
  37. package/src/shared-trees.spec.ts +151 -0
  38. package/src/split-button.spec.ts +36 -22
  39. package/src/styles.generated.ts +1 -1
  40. package/src/tab-view.spec.ts +83 -15
  41. package/src/test.browser.mts +2 -0
  42. package/src/view-stack.spec.ts +5 -3
@@ -15,6 +15,7 @@ import {
15
15
  TAB_VIEW_VECTORS,
16
16
  TABS_REVEALED_VECTORS,
17
17
  applyTabViewOp,
18
+ replayTabPagesAsSplices,
18
19
  seedTabViewPages,
19
20
  tabViewClosing,
20
21
  tabViewOrder,
@@ -113,6 +114,19 @@ export const AdwTabViewConformanceTest = async () => {
113
114
  if (vector.pagesChanges) expect(mounted.pagesChanges).toStrictEqual([...vector.pagesChanges]);
114
115
  if (vector.closeAttempts) expect(mounted.closeAttempts).toStrictEqual([...vector.closeAttempts]);
115
116
 
117
+ // ADR 0046: the page signal, replayed as portable `items-changed` splices,
118
+ // must reproduce the order this widget actually ended on. That is the whole
119
+ // claim — `TabViewPagesChange` IS the positional list signal — measured
120
+ // against a real change stream rather than asserted in an ADR.
121
+ if (vector.pagesChanges) {
122
+ expect(
123
+ replayTabPagesAsSplices(
124
+ vector.pages.map((page) => page.id),
125
+ mounted.pagesChanges,
126
+ ),
127
+ ).toStrictEqual([...vector.order]);
128
+ }
129
+
116
130
  expect(tabViewOrder(view)).toStrictEqual([...vector.order]);
117
131
  expect(view.nPinnedPages).toBe(vector.nPinnedPages);
118
132
  expect(view.selectedId).toBe(vector.selectedId);
@@ -193,7 +207,7 @@ export const AdwTabViewConformanceTest = async () => {
193
207
  });
194
208
  });
195
209
 
196
- await describe('adw-tab-view selected attribute', async () => {
210
+ await describe('adw-tab-view selected-page attribute', async () => {
197
211
  function mountThree(attrs = ''): { view: AdwTabView; host: HTMLElement } {
198
212
  const host = document.createElement('div');
199
213
  host.innerHTML = `<adw-tab-view ${attrs}>
@@ -205,38 +219,92 @@ export const AdwTabViewConformanceTest = async () => {
205
219
  return { view: host.querySelector('adw-tab-view') as AdwTabView, host };
206
220
  }
207
221
 
208
- await it('selects the declared index instead of the auto-pick', () => {
209
- const { view, host } = mountThree('selected="2"');
222
+ await it('selects the declared PAGE instead of the auto-pick', () => {
223
+ const { view, host } = mountThree('selected-page="c"');
210
224
  expect(view.selectedIndex).toBe(2);
225
+ expect(view.selectedId).toBe('c');
211
226
  expect(shownPanels(view)).toStrictEqual([false, false, true]);
212
227
  host.remove();
213
228
  });
214
229
 
215
- await it('IGNORES an out-of-range or unparseable index instead of clamping it', () => {
216
- // A clamping `Number.isNaN(raw) ? 0 : Math.min(Math.max(raw, 0), max)` selects
217
- // the last page for `99` and the first for `-1`/`oops`; libadwaita refuses all
218
- // three.
230
+ await it('IGNORES an id no page holds instead of falling back to the first', () => {
231
+ // The ordinal spellings are just unknown ids now (ADR 0048), and a fallback to
232
+ // page 0 would be the same wrong answer clamping used to give: libadwaita
233
+ // refuses a foreign `AdwTabPage` outright.
219
234
  const { view, host } = mountThree();
220
235
  for (const value of ['99', '-1', 'oops']) {
221
- view.setAttribute('selected', value);
236
+ view.setAttribute('selected-page', value);
222
237
  expect(view.selectedIndex).toBe(0);
223
238
  }
224
239
  //...and the attribute is put back in sync rather than left lying.
225
- view.setAttribute('selected', '1');
240
+ view.setAttribute('selected-page', 'b');
226
241
  expect(view.selectedIndex).toBe(1);
227
- expect(view.getAttribute('selected')).toBe('1');
242
+ expect(view.getAttribute('selected-page')).toBe(view.pages[1]!.id);
243
+ host.remove();
244
+ });
245
+
246
+ await it('REFUSES a page belonging to ANOTHER view, and RECORDS it', () => {
247
+ // `page_belongs_to_this_view`. An id is only unique WITHIN a view — declared ones
248
+ // are the author's to repeat and `_nextId` counts per element, so two views both
249
+ // hold a `tab-1`. Both views here hold 'a'/'b'/'c', which is the collision.
250
+ const first = mountThree();
251
+ const second = mountThree();
252
+ second.view.selectedPage = second.view.pages[2]!;
253
+ expect(second.view.selectedIndex).toBe(2);
254
+
255
+ first.view.selectedPage = second.view.pages[2]!;
256
+ expect(first.view.selectedIndex).toBe(0);
257
+ expect(second.view.selectedIndex).toBe(2);
258
+ // The refusal carries C's own assertion. It did NOT while the check was a
259
+ // renderer-side guard returning in silence, which is why the check moved into
260
+ // `TabViewState` — the one place that owns the page list.
261
+ expect(first.view.diagnostics).toStrictEqual([
262
+ "adw_tab_view_set_selected_page: assertion 'page_belongs_to_this_view (self, selected_page)' failed",
263
+ ]);
264
+ expect(second.view.diagnostics).toStrictEqual([]);
265
+
266
+ // ...and this view's OWN page of that same id is accepted, so what was refused
267
+ // is the IDENTITY and not the id.
268
+ first.view.selectedPage = first.view.pages[2]!;
269
+ expect(first.view.selectedIndex).toBe(2);
270
+ first.host.remove();
271
+ second.host.remove();
272
+ });
273
+
274
+ await it('RECORDS the refusal, where the ordinal door recorded nothing', () => {
275
+ // The behaviour change ADR 0048 does not name: `selected="99"` went through
276
+ // `selectNthPage`, which refuses an out-of-range index SILENTLY, while an
277
+ // unknown id goes through `setSelectedPage`, which is where C's
278
+ // `page_belongs_to_this_view` assertion lives. Three refusals, three records —
279
+ // if this list stays empty the element is swallowing the typo the reader made.
280
+ const { view, host } = mountThree();
281
+ expect(view.diagnostics).toStrictEqual([]);
282
+ for (const value of ['99', 'oops']) view.setAttribute('selected-page', value);
283
+ expect(view.diagnostics).toStrictEqual([
284
+ "adw_tab_view_set_selected_page: assertion 'page_belongs_to_this_view (self, selected_page)' failed",
285
+ "adw_tab_view_set_selected_page: assertion 'page_belongs_to_this_view (self, selected_page)' failed",
286
+ ]);
287
+
288
+ // ...and `null` while pages exist is the OTHER half of the same C assertion,
289
+ // which `view.selected = -1` also used to reach in silence.
290
+ view.selectedPage = null;
291
+ expect(view.selectedIndex).toBe(0);
292
+ expect(view.diagnostics).toHaveLength(3);
293
+ expect(view.diagnostics[2]).toBe(
294
+ "adw_tab_view_set_selected_page: assertion 'ADW_IS_TAB_PAGE (selected_page)' failed",
295
+ );
228
296
  host.remove();
229
297
  });
230
298
 
231
299
  await it('notifies on a PROGRAMMATIC selection, not only on a click', () => {
232
- // C notifies on EVERY path: a programmatic `view.selected = 2` must notify as
233
- // a tab click does.
300
+ // C notifies on EVERY path: a programmatic `view.selectedPage = …` must notify
301
+ // as a tab click does.
234
302
  const { view, host } = mountThree();
235
303
  const seen: number[] = [];
236
304
  view.addEventListener('notify::selected-page', (event) =>
237
305
  seen.push((event as CustomEvent).detail.selected),
238
306
  );
239
- view.selected = 2;
307
+ view.selectedPage = view.pages[2]!;
240
308
  expect(seen).toStrictEqual([2]);
241
309
  host.remove();
242
310
  });
@@ -245,11 +313,11 @@ export const AdwTabViewConformanceTest = async () => {
245
313
  const { view, host } = mountThree();
246
314
  const chips = tabChips(view);
247
315
  chips[1]!.click();
248
- expect(view.getAttribute('selected')).toBe('1');
316
+ expect(view.getAttribute('selected-page')).toBe(view.pages[1]!.id);
249
317
 
250
318
  for (const id of ['a', 'b', 'c']) view.closePage(id);
251
319
  expect(view.nPages).toBe(0);
252
- expect(view.hasAttribute('selected')).toBe(false);
320
+ expect(view.hasAttribute('selected-page')).toBe(false);
253
321
  expect(view.selectedIndex).toBe(-1);
254
322
  host.remove();
255
323
  });
@@ -53,8 +53,10 @@ import { AdwEmptySectionsTest } from './empty-sections.spec.js';
53
53
  import { AdwSlottedChildrenTest } from './slotted-children.spec.js';
54
54
  import { AdwKeyboardOperableTest } from './keyboard-operable.spec.js';
55
55
  import { AdwFontsTest } from './adw-fonts.spec.js';
56
+ import { AdwSharedTreesTest } from './shared-trees.spec.js';
56
57
 
57
58
  run({
59
+ AdwSharedTreesTest,
58
60
  AdwKeyboardOperableTest,
59
61
  AdwConnectLifecycleTest,
60
62
  AdwEmptySectionsTest,
@@ -66,7 +66,9 @@ function mountStack(pages: readonly ViewStackVectorPage[], visibleChildName?: st
66
66
  function applyOp(stack: AdwViewStack, op: ViewStackVectorOp): void {
67
67
  switch (op.kind) {
68
68
  case 'selectIndex':
69
- stack.visibleChildIndex = op.index;
69
+ // ADR 0048: the element has no ordinal SETTER any more; the ordinal is this
70
+ // call, and it forwards to the same core guard the vector pins.
71
+ stack.selectNthPage(op.index);
70
72
  break;
71
73
  case 'selectName':
72
74
  stack.visibleChildName = op.name;
@@ -224,7 +226,7 @@ export const AdwViewStackTest = async () => {
224
226
  await it('reflects the selection back, including an empty name', () => {
225
227
  const { stack, host } = mountStack([{ name: 'a' }, { name: '' }]);
226
228
  expect(stack.getAttribute('visible-child-name')).toBe('a');
227
- stack.visibleChildIndex = 1;
229
+ stack.selectNthPage(1);
228
230
  // Reflection guarded by `if (current && …)` never fires for nameless pages.
229
231
  expect(stack.getAttribute('visible-child-name')).toBe('');
230
232
  host.remove();
@@ -252,7 +254,7 @@ export const AdwViewStackTest = async () => {
252
254
  // with duplicate names a naive re-lookup resolves to the FIRST match and
253
255
  // undoes the selection.
254
256
  const { stack, host } = mountStack([{ name: 'dup' }, { name: 'dup' }]);
255
- stack.visibleChildIndex = 1;
257
+ stack.selectNthPage(1);
256
258
  expect(stack.visibleChildIndex).toBe(1);
257
259
  expect(stack.getAttribute('visible-child-name')).toBe('dup');
258
260
  host.remove();