@gjsify/adwaita-web 0.32.0 → 0.33.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 (38) hide show
  1. package/dist/adwaita-web.css +72 -50
  2. package/dist/adwaita-web.css.map +1 -1
  3. package/lib/types/elements/adw-combo-row.d.ts +10 -0
  4. package/lib/types/elements/adw-inline-view-switcher.d.ts +37 -1
  5. package/lib/types/elements/adw-navigation-split-view.d.ts +42 -0
  6. package/lib/types/elements/adw-overlay-split-view.d.ts +56 -0
  7. package/lib/types/elements/adw-spinner.d.ts +23 -0
  8. package/lib/types/elements/adw-view-stack.d.ts +12 -0
  9. package/lib/types/elements/adw-view-switcher-bar.d.ts +7 -5
  10. package/lib/types/elements/adw-view-switcher.d.ts +55 -3
  11. package/lib/types/elements/adw-wrap-box.d.ts +58 -3
  12. package/lib/types/styles.generated.d.ts +1 -1
  13. package/package.json +6 -6
  14. package/scss/_combo_row.scss +19 -0
  15. package/scss/_navigation_split_view.scss +47 -8
  16. package/scss/_overlay_split_view.scss +117 -32
  17. package/scss/_spinner.scss +35 -21
  18. package/scss/_tab_view.scss +12 -13
  19. package/scss/_wrap_box.scss +9 -0
  20. package/src/adw-avatar.spec.ts +37 -0
  21. package/src/adw-button-content.spec.ts +24 -0
  22. package/src/adw-row-state.spec.ts +49 -0
  23. package/src/adw-wrap-box.spec.ts +148 -1
  24. package/src/chrome.spec.ts +111 -12
  25. package/src/elements/adw-action-row.ts +11 -1
  26. package/src/elements/adw-combo-row.ts +17 -0
  27. package/src/elements/adw-inline-view-switcher.ts +91 -8
  28. package/src/elements/adw-navigation-split-view.ts +179 -14
  29. package/src/elements/adw-overlay-split-view.ts +378 -20
  30. package/src/elements/adw-spinner.ts +165 -21
  31. package/src/elements/adw-tab-view.ts +20 -2
  32. package/src/elements/adw-view-stack.ts +21 -0
  33. package/src/elements/adw-view-switcher-bar.ts +13 -5
  34. package/src/elements/adw-view-switcher.ts +103 -11
  35. package/src/elements/adw-wrap-box.ts +290 -135
  36. package/src/split-views.spec.ts +586 -2
  37. package/src/styles.generated.ts +1 -1
  38. package/src/view-switcher.spec.ts +219 -0
@@ -17,9 +17,16 @@ import { describe, expect, it } from '@gjsify/unit';
17
17
  import {
18
18
  INLINE_TOGGLE_VECTORS,
19
19
  INLINE_TOOLTIP_VECTORS,
20
+ VIEW_SWITCHER_BADGE_VECTORS,
20
21
  VIEW_SWITCHER_BAR_REVEAL_VECTORS,
21
22
  VIEW_SWITCHER_BUTTON_VECTORS,
23
+ VIEW_SWITCHER_BUTTON_VISIBILITY_VECTORS,
24
+ VIEW_SWITCHER_DRAG_VECTORS,
25
+ VIEW_SWITCHER_ICON_VECTORS,
26
+ VIEW_SWITCHER_MNEMONIC_VECTORS,
27
+ VIEW_SWITCHER_REBUILD_VECTORS,
22
28
  VIEW_SWITCHER_SELECTION_VECTORS,
29
+ createViewSwitcherClock,
23
30
  } from '@gjsify/adwaita-core/conformance';
24
31
  import type {
25
32
  ViewSwitcherVectorChange,
@@ -443,4 +450,216 @@ export const AdwViewSwitcherTest = async () => {
443
450
  host.remove();
444
451
  });
445
452
  });
453
+
454
+ await describe('<adw-view-switcher> the seven tables this suite did not drive (#1067)', async () => {
455
+ /** Mount a switcher whose dwell timer a test controls. */
456
+ function mountWithClock(pages: readonly ViewSwitcherVectorPage[], initial: number) {
457
+ const host = document.createElement('div');
458
+ document.body.appendChild(host);
459
+ const switcher = document.createElement('adw-view-switcher') as AdwViewSwitcher & {
460
+ scheduler: { schedule(cb: () => void, ms: number): unknown; cancel(handle: unknown): void };
461
+ };
462
+ const clock = createViewSwitcherClock();
463
+ // The seam that did not exist: the elements built their state inline
464
+ // with the DOM scheduler, so VIEW_SWITCHER_DRAG_VECTORS was driven by
465
+ // the core suite alone while a header claimed all three drove it.
466
+ switcher.scheduler = clock;
467
+ for (const page of pages) switcher.appendChild(declarePage('adw-view-switcher-page', page));
468
+ switcher.setAttribute('active', String(initial));
469
+ host.appendChild(switcher);
470
+ return { switcher, host, clock };
471
+ }
472
+
473
+ for (const vector of VIEW_SWITCHER_DRAG_VECTORS) {
474
+ await it(`drag: ${vector.rule}`, () => {
475
+ const { switcher, host, clock } = mountWithClock(vector.pages, vector.initial);
476
+ const buttons = [...switcher.querySelectorAll('.adw-view-switcher-button')] as HTMLElement[];
477
+ for (const step of vector.steps) {
478
+ if (step.kind === 'advance') {
479
+ clock.advance(step.ms);
480
+ continue;
481
+ }
482
+ const button = buttons[step.index];
483
+ // `relatedTarget` OUTSIDE the button, which is what a real
484
+ // cross-button move looks like — the internal case is the
485
+ // separate test below.
486
+ button?.dispatchEvent(
487
+ new DragEvent(step.kind === 'enter' ? 'dragenter' : 'dragleave', {
488
+ bubbles: true,
489
+ relatedTarget: host,
490
+ }),
491
+ );
492
+ }
493
+ expect(switcher.active).toBe(vector.selected);
494
+ host.remove();
495
+ });
496
+ }
497
+
498
+ await it('a drag sliding from the icon to the LABEL does not restart the dwell', () => {
499
+ // The bug the vectors could not see: the element bound enter/leave on
500
+ // a button that CONTAINS an icon and a label, so crossing between them
501
+ // fired leave + enter, `ViewSwitcherDragSwitch.leave` cleared the timer
502
+ // and `enter` re-armed a fresh 500 ms. A drag that merely slid across
503
+ // the button never switched at all.
504
+ const pages: ViewSwitcherVectorPage[] = [
505
+ { name: 'one', title: 'One' },
506
+ { name: 'two', title: 'Two' },
507
+ ];
508
+ const { switcher, host, clock } = mountWithClock(pages, 0);
509
+ const button = switcher.querySelectorAll('.adw-view-switcher-button')[1] as HTMLElement;
510
+ const icon = button.querySelector('.adw-view-switcher-icon') as HTMLElement;
511
+ const label = button.querySelector('.adw-view-switcher-label') as HTMLElement;
512
+
513
+ button.dispatchEvent(new DragEvent('dragenter', { bubbles: true, relatedTarget: host }));
514
+ clock.advance(300);
515
+ // Icon → label: both are INSIDE the button, so neither event counts.
516
+ button.dispatchEvent(new DragEvent('dragleave', { bubbles: true, relatedTarget: label }));
517
+ button.dispatchEvent(new DragEvent('dragenter', { bubbles: true, relatedTarget: icon }));
518
+ clock.advance(300);
519
+
520
+ // 600 ms of continuous hover: past TIMEOUT_EXPAND, so it switched.
521
+ expect(switcher.active).toBe(1);
522
+ host.remove();
523
+ });
524
+
525
+ await it('a page attribute changed AFTER connect reaches the button', async () => {
526
+ // `observedAttributes` without an `attributeChangedCallback` is a dead
527
+ // declaration; C rebinds on every page notify (adw-view-switcher.c:184-193).
528
+ const host = document.createElement('div');
529
+ document.body.appendChild(host);
530
+ const switcher = document.createElement('adw-view-switcher') as AdwViewSwitcher;
531
+ const page = declarePage('adw-view-switcher-page', { name: 'one', title: 'One' });
532
+ switcher.appendChild(page);
533
+ switcher.appendChild(declarePage('adw-view-switcher-page', { name: 'two', title: 'Two' }));
534
+ host.appendChild(switcher);
535
+
536
+ const labelOf = () =>
537
+ (switcher.querySelector('.adw-view-switcher-label') as HTMLElement | null)?.textContent;
538
+ expect(labelOf()).toBe('One');
539
+ page.setAttribute('title', 'Renamed');
540
+ // A MutationObserver delivers on a MICROTASK, where GTK's `notify`
541
+ // is synchronous. One microtask is invisible to a user and is what
542
+ // the DOM offers for watching a detached node; nothing here depends
543
+ // on the repaint happening inside the setter.
544
+ await Promise.resolve();
545
+ expect(labelOf()).toBe('Renamed');
546
+ host.remove();
547
+ });
548
+
549
+ for (const vector of VIEW_SWITCHER_BADGE_VECTORS) {
550
+ await it(`badge ${vector.badgeNumber}/${vector.needsAttention} → "${vector.badgeLabel}" — ${vector.rule}`, () => {
551
+ const host = document.createElement('div');
552
+ document.body.appendChild(host);
553
+ const switcher = document.createElement('adw-view-switcher') as AdwViewSwitcher;
554
+ const page = declarePage('adw-view-switcher-page', { name: 'one', title: 'One' });
555
+ if (vector.badgeNumber) page.setAttribute('badge-number', String(vector.badgeNumber));
556
+ if (vector.needsAttention) page.setAttribute('needs-attention', '');
557
+ switcher.appendChild(page);
558
+ host.appendChild(switcher);
559
+
560
+ const indicator = switcher.querySelector('.adw-view-switcher-indicator') as HTMLElement;
561
+ expect(indicator.textContent).toBe(vector.badgeLabel);
562
+ // The DESCRIPTION is what a screen reader announces; a badge with
563
+ // no accessible text is a dot nobody hears.
564
+ const button = switcher.querySelector('.adw-view-switcher-button') as HTMLElement;
565
+ expect(button.getAttribute('aria-description') ?? '').toBe(vector.description);
566
+ host.remove();
567
+ });
568
+ }
569
+
570
+ await it('names the three tables that stay the core suite’s', () => {
571
+ // MNEMONIC and ICON are pure string derivations with no DOM surface
572
+ // beyond the label this suite already asserts through
573
+ // VIEW_SWITCHER_BUTTON_VECTORS; BUTTON_VISIBILITY and REBUILD are
574
+ // asserted through the rendered button set by the same table. Named
575
+ // so the omission is a decision rather than a gap.
576
+ expect(VIEW_SWITCHER_MNEMONIC_VECTORS.length).toBeGreaterThan(0);
577
+ expect(VIEW_SWITCHER_ICON_VECTORS.length).toBeGreaterThan(0);
578
+ expect(VIEW_SWITCHER_BUTTON_VISIBILITY_VECTORS.length).toBeGreaterThan(0);
579
+ expect(VIEW_SWITCHER_REBUILD_VECTORS.length).toBeGreaterThan(0);
580
+ });
581
+ });
582
+
583
+ await describe('<adw-view-switcher-bar> reveal follows the pages model, not just the selection', async () => {
584
+ await it('retracts when a page is REMOVED without moving the selection', () => {
585
+ // `update_bar_revealed` re-runs on the pages model's `items-changed`
586
+ // (adw-view-switcher-bar.c:340). Both ports listened only on
587
+ // `notify::visible-child`, so the bar went stale and a manual
588
+ // `refresh()` was the documented workaround.
589
+ const host = document.createElement('div');
590
+ document.body.appendChild(host);
591
+ const stack = document.createElement('adw-view-stack') as AdwViewStack;
592
+ host.appendChild(stack);
593
+ const bar = document.createElement('adw-view-switcher-bar') as AdwViewSwitcherBar;
594
+ bar.setAttribute('reveal', '');
595
+ host.appendChild(bar);
596
+ bar.stack = stack;
597
+
598
+ const first = document.createElement('div');
599
+ const second = document.createElement('div');
600
+ stack.add(first, 'one', 'One');
601
+ stack.add(second, 'two', 'Two');
602
+ expect(bar.revealed).toBe(true);
603
+
604
+ // Removing the NON-selected page leaves the selection alone, so only
605
+ // items-changed can carry the news.
606
+ stack.removePage('two');
607
+ expect(bar.revealed).toBe(false);
608
+ host.remove();
609
+ });
610
+
611
+ await it('and when a non-selected page is merely HIDDEN', () => {
612
+ const host = document.createElement('div');
613
+ document.body.appendChild(host);
614
+ const stack = document.createElement('adw-view-stack') as AdwViewStack;
615
+ host.appendChild(stack);
616
+ const bar = document.createElement('adw-view-switcher-bar') as AdwViewSwitcherBar;
617
+ bar.setAttribute('reveal', '');
618
+ host.appendChild(bar);
619
+ bar.stack = stack;
620
+
621
+ stack.add(document.createElement('div'), 'one', 'One');
622
+ stack.add(document.createElement('div'), 'two', 'Two');
623
+ expect(bar.revealed).toBe(true);
624
+ stack.setPageVisible('two', false);
625
+ expect(bar.revealed).toBe(false);
626
+ host.remove();
627
+ });
628
+ });
629
+
630
+ // MOVING A SWITCHER MUST NOT MAKE IT DEAF.
631
+ //
632
+ // `disconnectedCallback` drops the page observer, and `connectedCallback`
633
+ // returned early once the element was built — so a switcher that had merely
634
+ // been moved between parents (a slideshow slide, a client-side route change)
635
+ // stopped tracking its pages permanently, and silently. Same shape as the
636
+ // overlay split view's ResizeObserver, and found with it.
637
+ await describe('adw-view-switcher — a moved switcher keeps watching its pages', async () => {
638
+ await it('still notices a page attribute change after being re-parented', async () => {
639
+ const from = document.createElement('div');
640
+ const to = document.createElement('div');
641
+ document.body.append(from, to);
642
+
643
+ const element = document.createElement('adw-view-switcher') as AdwViewSwitcher;
644
+ const page = declarePage('adw-view-switcher-page', {
645
+ name: 'one',
646
+ title: 'One',
647
+ iconName: 'go-next-symbolic',
648
+ visible: true,
649
+ } as ViewSwitcherVectorPage);
650
+ element.appendChild(page);
651
+ from.appendChild(element);
652
+
653
+ // Appending elsewhere is a disconnect followed by a connect.
654
+ to.appendChild(element);
655
+ page.setAttribute('title', 'Renamed');
656
+ // The observer delivers on a microtask; GTK's notify is synchronous.
657
+ await Promise.resolve();
658
+
659
+ const label = element.querySelector('.adw-view-switcher-label') as HTMLElement;
660
+ expect(label.textContent).toBe('Renamed');
661
+ from.remove();
662
+ to.remove();
663
+ });
664
+ });
446
665
  };