@classic-homes/theme-svelte 1.7.2 → 1.8.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.
|
@@ -23,11 +23,20 @@ const footerLinks = [
|
|
|
23
23
|
],
|
|
24
24
|
},
|
|
25
25
|
];
|
|
26
|
-
function renderLayout() {
|
|
26
|
+
function renderLayout(extra = {}) {
|
|
27
27
|
return render(PublicLayout, {
|
|
28
|
-
props: { navigation, footerLinks, children: textSnippet('Page content') },
|
|
28
|
+
props: { navigation, footerLinks, children: textSnippet('Page content'), ...extra },
|
|
29
29
|
});
|
|
30
30
|
}
|
|
31
|
+
/** A drawer CTA that calls the `close` argument PublicLayout hands to mobileNavEnd. */
|
|
32
|
+
function ctaSnippet(label) {
|
|
33
|
+
return createRawSnippet((args) => ({
|
|
34
|
+
render: () => `<button type="button">${label}</button>`,
|
|
35
|
+
setup: (el) => {
|
|
36
|
+
el.addEventListener('click', () => args().close());
|
|
37
|
+
},
|
|
38
|
+
}));
|
|
39
|
+
}
|
|
31
40
|
/**
|
|
32
41
|
* jsdom has no layout engine, so getBoundingClientRect() returns zeros and target
|
|
33
42
|
* size cannot be measured here. These assert the classes that produce a 44x44px
|
|
@@ -44,10 +53,12 @@ describe('PublicLayout accessibility', () => {
|
|
|
44
53
|
const links = [...nav.querySelectorAll(':scope > a')];
|
|
45
54
|
expect(links).toHaveLength(navigation.length);
|
|
46
55
|
for (const link of links) {
|
|
47
|
-
// 20px text line box + 2 x 12px
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
expect(link.className).toContain('
|
|
56
|
+
// 20px text line box + 2 x 12px padding = 44px border box; -m-3 cancels
|
|
57
|
+
// it in layout. A real box, not an ::after overlay: iPadOS trackpad
|
|
58
|
+
// hit-testing missed the pseudo-element but honours padding.
|
|
59
|
+
expect(link.className).toContain('p-3');
|
|
60
|
+
expect(link.className).toContain('-m-3');
|
|
61
|
+
expect(link.className).not.toContain('after:');
|
|
51
62
|
}
|
|
52
63
|
});
|
|
53
64
|
it('should give the logo link a 44px-tall touch target', () => {
|
|
@@ -74,6 +85,33 @@ describe('PublicLayout accessibility', () => {
|
|
|
74
85
|
expect(link.className).toContain('py-3');
|
|
75
86
|
}
|
|
76
87
|
});
|
|
88
|
+
it('should render headerEnd unwrapped when there is no mobileNavEnd', () => {
|
|
89
|
+
renderLayout({ headerEnd: textSnippet('Service Request') });
|
|
90
|
+
const cta = screen.getByText('Service Request');
|
|
91
|
+
expect(cta.parentElement?.className).not.toContain('hidden');
|
|
92
|
+
});
|
|
93
|
+
it('should hide headerEnd below the mobile breakpoint when mobileNavEnd is given', () => {
|
|
94
|
+
renderLayout({
|
|
95
|
+
headerEnd: textSnippet('Service Request'),
|
|
96
|
+
mobileNavEnd: ctaSnippet('Drawer CTA'),
|
|
97
|
+
});
|
|
98
|
+
const cta = screen.getByText('Service Request');
|
|
99
|
+
expect(cta.parentElement?.className).toContain('hidden');
|
|
100
|
+
expect(cta.parentElement?.className).toContain('md:contents');
|
|
101
|
+
});
|
|
102
|
+
it('should render mobileNavEnd in the drawer and close it via the close argument', async () => {
|
|
103
|
+
renderLayout({
|
|
104
|
+
headerEnd: textSnippet('Service Request'),
|
|
105
|
+
mobileNavEnd: ctaSnippet('Drawer CTA'),
|
|
106
|
+
});
|
|
107
|
+
expect(screen.queryByRole('button', { name: 'Drawer CTA' })).toBeNull();
|
|
108
|
+
await userEvent.click(screen.getByRole('button', { name: 'Open menu' }));
|
|
109
|
+
const drawerNav = screen.getByRole('navigation', { name: 'Mobile navigation' });
|
|
110
|
+
const cta = screen.getByRole('button', { name: 'Drawer CTA' });
|
|
111
|
+
expect(drawerNav.contains(cta)).toBe(true);
|
|
112
|
+
await userEvent.click(cta);
|
|
113
|
+
expect(screen.queryByRole('navigation', { name: 'Mobile navigation' })).toBeNull();
|
|
114
|
+
});
|
|
77
115
|
it('should cue hover for a trackpad pointer that reports hover: none', () => {
|
|
78
116
|
renderLayout();
|
|
79
117
|
const nav = screen.getByRole('navigation', { name: 'Main navigation' });
|
|
@@ -34,6 +34,13 @@
|
|
|
34
34
|
logoEnvironment?: 'local' | 'dev' | 'demo';
|
|
35
35
|
/** Custom header end content */
|
|
36
36
|
headerEnd?: Snippet;
|
|
37
|
+
/**
|
|
38
|
+
* Content rendered at the bottom of the mobile drawer (e.g. a CTA button).
|
|
39
|
+
* When provided, `headerEnd` is hidden below the mobile breakpoint so a
|
|
40
|
+
* wide button no longer competes with the logo on phone widths. Receives
|
|
41
|
+
* `close` so the drawer can be dismissed after navigation.
|
|
42
|
+
*/
|
|
43
|
+
mobileNavEnd?: Snippet<[{ close: () => void }]>;
|
|
37
44
|
/** Use strong accent border on footer (10px top border) */
|
|
38
45
|
strongFooterBorder?: boolean;
|
|
39
46
|
/** Use dark footer variant */
|
|
@@ -55,6 +62,7 @@
|
|
|
55
62
|
logoSubtitle,
|
|
56
63
|
logoEnvironment,
|
|
57
64
|
headerEnd,
|
|
65
|
+
mobileNavEnd,
|
|
58
66
|
strongFooterBorder = false,
|
|
59
67
|
darkFooter = false,
|
|
60
68
|
headerSearch,
|
|
@@ -102,11 +110,12 @@
|
|
|
102
110
|
<a
|
|
103
111
|
href={item.href}
|
|
104
112
|
class={cn(
|
|
105
|
-
|
|
106
|
-
//
|
|
107
|
-
// -
|
|
108
|
-
//
|
|
109
|
-
|
|
113
|
+
// Touch target: p-3 around the 20px line box makes the anchor's own
|
|
114
|
+
// border box 44px tall, and -m-3 cancels it in layout so the nav's
|
|
115
|
+
// gap-6 is unchanged (adjacent boxes meet without overlapping). A real
|
|
116
|
+
// box rather than an ::after overlay: iPadOS trackpad hit-testing
|
|
117
|
+
// landed below the text with the pseudo-element, but honours padding.
|
|
118
|
+
'-m-3 flex items-center p-3 text-sm font-bold uppercase hover:text-primary',
|
|
110
119
|
// iPadOS reports hover:none even with a trackpad attached, so the preset's
|
|
111
120
|
// hoverOnlyWhenSupported gating means hover: never fires there. any-pointer:fine
|
|
112
121
|
// matches when any attached pointer is precise, giving trackpad users a cue.
|
|
@@ -123,13 +132,18 @@
|
|
|
123
132
|
{/snippet}
|
|
124
133
|
|
|
125
134
|
{#snippet end()}
|
|
126
|
-
{#if headerEnd}
|
|
135
|
+
{#if headerEnd && mobileNavEnd}
|
|
136
|
+
<!-- Desktop only: the mobile drawer carries mobileNavEnd instead -->
|
|
137
|
+
<div class="hidden md:contents">
|
|
138
|
+
{@render headerEnd()}
|
|
139
|
+
</div>
|
|
140
|
+
{:else if headerEnd}
|
|
127
141
|
{@render headerEnd()}
|
|
128
142
|
{/if}
|
|
129
143
|
{/snippet}
|
|
130
144
|
|
|
131
145
|
{#snippet mobileNav()}
|
|
132
|
-
<ul class=
|
|
146
|
+
<ul class={cn('space-y-2 px-4 py-4', !mobileNavEnd && 'border-b border-black')}>
|
|
133
147
|
{#each navigation as item}
|
|
134
148
|
<li>
|
|
135
149
|
<a
|
|
@@ -150,6 +164,11 @@
|
|
|
150
164
|
</li>
|
|
151
165
|
{/each}
|
|
152
166
|
</ul>
|
|
167
|
+
{#if mobileNavEnd}
|
|
168
|
+
<div class="border-b border-black px-4 pb-4">
|
|
169
|
+
{@render mobileNavEnd({ close: () => (mobileMenuOpen = false) })}
|
|
170
|
+
</div>
|
|
171
|
+
{/if}
|
|
153
172
|
{/snippet}
|
|
154
173
|
</Header>
|
|
155
174
|
|
|
@@ -27,6 +27,15 @@ interface Props {
|
|
|
27
27
|
logoEnvironment?: 'local' | 'dev' | 'demo';
|
|
28
28
|
/** Custom header end content */
|
|
29
29
|
headerEnd?: Snippet;
|
|
30
|
+
/**
|
|
31
|
+
* Content rendered at the bottom of the mobile drawer (e.g. a CTA button).
|
|
32
|
+
* When provided, `headerEnd` is hidden below the mobile breakpoint so a
|
|
33
|
+
* wide button no longer competes with the logo on phone widths. Receives
|
|
34
|
+
* `close` so the drawer can be dismissed after navigation.
|
|
35
|
+
*/
|
|
36
|
+
mobileNavEnd?: Snippet<[{
|
|
37
|
+
close: () => void;
|
|
38
|
+
}]>;
|
|
30
39
|
/** Use strong accent border on footer (10px top border) */
|
|
31
40
|
strongFooterBorder?: boolean;
|
|
32
41
|
/** Use dark footer variant */
|