@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.
- package/dist/adwaita-web.css +72 -50
- package/dist/adwaita-web.css.map +1 -1
- package/lib/types/elements/adw-combo-row.d.ts +10 -0
- package/lib/types/elements/adw-inline-view-switcher.d.ts +37 -1
- package/lib/types/elements/adw-navigation-split-view.d.ts +42 -0
- package/lib/types/elements/adw-overlay-split-view.d.ts +56 -0
- package/lib/types/elements/adw-spinner.d.ts +23 -0
- package/lib/types/elements/adw-view-stack.d.ts +12 -0
- package/lib/types/elements/adw-view-switcher-bar.d.ts +7 -5
- package/lib/types/elements/adw-view-switcher.d.ts +55 -3
- package/lib/types/elements/adw-wrap-box.d.ts +58 -3
- package/lib/types/styles.generated.d.ts +1 -1
- package/package.json +6 -6
- package/scss/_combo_row.scss +19 -0
- package/scss/_navigation_split_view.scss +47 -8
- package/scss/_overlay_split_view.scss +117 -32
- package/scss/_spinner.scss +35 -21
- package/scss/_tab_view.scss +12 -13
- package/scss/_wrap_box.scss +9 -0
- package/src/adw-avatar.spec.ts +37 -0
- package/src/adw-button-content.spec.ts +24 -0
- package/src/adw-row-state.spec.ts +49 -0
- package/src/adw-wrap-box.spec.ts +148 -1
- package/src/chrome.spec.ts +111 -12
- package/src/elements/adw-action-row.ts +11 -1
- package/src/elements/adw-combo-row.ts +17 -0
- package/src/elements/adw-inline-view-switcher.ts +91 -8
- package/src/elements/adw-navigation-split-view.ts +179 -14
- package/src/elements/adw-overlay-split-view.ts +378 -20
- package/src/elements/adw-spinner.ts +165 -21
- package/src/elements/adw-tab-view.ts +20 -2
- package/src/elements/adw-view-stack.ts +21 -0
- package/src/elements/adw-view-switcher-bar.ts +13 -5
- package/src/elements/adw-view-switcher.ts +103 -11
- package/src/elements/adw-wrap-box.ts +290 -135
- package/src/split-views.spec.ts +586 -2
- package/src/styles.generated.ts +1 -1
- 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
|
};
|