@akinon/pz-theme 2.0.60-rc.0 → 2.0.60
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/CHANGELOG.md +7 -31
- package/package.json +4 -4
- package/src/blocks/__tests__/panel-block.test.tsx +46 -0
- package/src/blocks/__tests__/panel-legacy-golden.json +1 -1
- package/src/blocks/panel-block.tsx +34 -20
- package/src/hooks/__tests__/use-overlay.test.tsx +26 -0
- package/src/hooks/use-overlay.ts +30 -0
- package/src/utils/__tests__/scrolled-state-styles.test.ts +51 -0
- package/src/utils/part-styles.ts +11 -5
package/CHANGELOG.md
CHANGED
|
@@ -1,36 +1,12 @@
|
|
|
1
1
|
# @akinon/pz-theme
|
|
2
2
|
|
|
3
|
-
## 2.0.60
|
|
4
|
-
|
|
5
|
-
### Patch Changes
|
|
6
|
-
|
|
7
|
-
-
|
|
8
|
-
-
|
|
9
|
-
-
|
|
10
|
-
- Updated dependencies
|
|
11
|
-
- Updated dependencies [2fea4143]
|
|
12
|
-
- Updated dependencies [b55acb768]
|
|
13
|
-
- Updated dependencies [760258c1c]
|
|
14
|
-
- Updated dependencies [143be2b9d]
|
|
15
|
-
- Updated dependencies [7889b08fe]
|
|
16
|
-
- Updated dependencies [9f8cd3bc5]
|
|
17
|
-
- Updated dependencies [bfafa3f49]
|
|
18
|
-
- Updated dependencies [57d7eb305]
|
|
19
|
-
- Updated dependencies [d99a6a7d5]
|
|
20
|
-
- Updated dependencies [9db81a714]
|
|
21
|
-
- Updated dependencies [591e345e1]
|
|
22
|
-
- Updated dependencies [4de5303c5]
|
|
23
|
-
- Updated dependencies [95b139dc1]
|
|
24
|
-
- Updated dependencies [1d00f2d06]
|
|
25
|
-
- Updated dependencies [4ac7b2a1e]
|
|
26
|
-
- Updated dependencies [e9598c71]
|
|
27
|
-
- Updated dependencies [4998a9631]
|
|
28
|
-
- Updated dependencies [804d2bd6]
|
|
29
|
-
- Updated dependencies [3909d3224]
|
|
30
|
-
- Updated dependencies [6a3d8a63]
|
|
31
|
-
- Updated dependencies [e18836b20]
|
|
32
|
-
- @akinon/next@2.0.60-rc.0
|
|
33
|
-
- @akinon/theme-contract@2.0.60-rc.0
|
|
3
|
+
## 2.0.60
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 29b039a: ZERO-4809: Publish an overlay-open page state and close the panel in the canvas
|
|
8
|
+
- @akinon/next@2.0.60
|
|
9
|
+
- @akinon/theme-contract@2.0.60
|
|
34
10
|
|
|
35
11
|
## 2.0.59
|
|
36
12
|
|
package/package.json
CHANGED
|
@@ -1,26 +1,26 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@akinon/pz-theme",
|
|
3
3
|
"description": "Theme package for Project Zero Next — ThemePlaceholder system and theme editor infrastructure",
|
|
4
|
-
"version": "2.0.60
|
|
4
|
+
"version": "2.0.60",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"main": "src/index.ts",
|
|
7
7
|
"scripts": {
|
|
8
8
|
"test": "jest"
|
|
9
9
|
},
|
|
10
10
|
"peerDependencies": {
|
|
11
|
-
"@akinon/next": "2.0.60
|
|
11
|
+
"@akinon/next": "2.0.60",
|
|
12
12
|
"react": "^18.0.0 || ^19.0.0",
|
|
13
13
|
"react-dom": "^18.0.0 || ^19.0.0"
|
|
14
14
|
},
|
|
15
15
|
"dependencies": {
|
|
16
|
-
"@akinon/theme-contract": "2.0.60
|
|
16
|
+
"@akinon/theme-contract": "2.0.60",
|
|
17
17
|
"clsx": "2.1.1",
|
|
18
18
|
"lottie-web": "^5.13.0",
|
|
19
19
|
"tailwind-merge": "^2.5.4",
|
|
20
20
|
"zod": "4.4.1"
|
|
21
21
|
},
|
|
22
22
|
"devDependencies": {
|
|
23
|
-
"@akinon/next": "2.0.60
|
|
23
|
+
"@akinon/next": "2.0.60",
|
|
24
24
|
"@testing-library/jest-dom": "5.16.4",
|
|
25
25
|
"@testing-library/react": "16.3.2",
|
|
26
26
|
"@testing-library/user-event": "14.1.1",
|
|
@@ -90,6 +90,12 @@ describe('the legacy size-guide markup, byte for byte', () => {
|
|
|
90
90
|
// generalised. Every one of these is markup a live storefront is already
|
|
91
91
|
// styling, so nothing here may move: the centred modal keeps its combined
|
|
92
92
|
// root/backdrop node, its attribute order and its missing `tabIndex`.
|
|
93
|
+
//
|
|
94
|
+
// `legacy-designer` is the exception, and only because no storefront ever
|
|
95
|
+
// renders it — `isDesigner` is false everywhere outside the editor canvas.
|
|
96
|
+
// It was re-captured when the canvas stopped rendering a closed panel's
|
|
97
|
+
// children inline: that copy pushed the composition around the panel out of
|
|
98
|
+
// place, and "Hold open in canvas" is the control that shows it open.
|
|
93
99
|
const CASES: Array<[string, Record<string, unknown>, boolean, boolean]> = [
|
|
94
100
|
['legacy-closed', {}, false, false],
|
|
95
101
|
['legacy-open', {}, false, true],
|
|
@@ -341,3 +347,43 @@ describe('an empty panel', () => {
|
|
|
341
347
|
expect(placeholder?.textContent).not.toMatch(/size/i);
|
|
342
348
|
});
|
|
343
349
|
});
|
|
350
|
+
|
|
351
|
+
describe('what the canvas shows while the panel is closed', () => {
|
|
352
|
+
it('renders the trigger only, so the composition around it keeps its layout', () => {
|
|
353
|
+
// The defect: a panel dropped into a header rendered its whole menu inline,
|
|
354
|
+
// under the trigger, and shoved the logo and the icons out of place — while
|
|
355
|
+
// the author was editing exactly that header. It also showed something no
|
|
356
|
+
// shopper ever sees.
|
|
357
|
+
const { container } = renderPanel({}, { isDesigner: true });
|
|
358
|
+
|
|
359
|
+
expect(container.querySelector('[data-size-guide-trigger]')).toBeTruthy();
|
|
360
|
+
expect(container.querySelector('[data-id="child-1"]')).toBeNull();
|
|
361
|
+
expect(
|
|
362
|
+
container.querySelector('[data-size-guide]')?.getAttribute(
|
|
363
|
+
'data-size-guide-state'
|
|
364
|
+
)
|
|
365
|
+
).toBe('closed');
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
it('shows the open panel once it is held open, children and all', () => {
|
|
369
|
+
// "Hold open in canvas" is the control that answers "let me see it open",
|
|
370
|
+
// and it draws the real overlay rather than an inline copy of it.
|
|
371
|
+
const { container } = renderPanel(
|
|
372
|
+
{},
|
|
373
|
+
{ isDesigner: true, styles: { forceOpen: { desktop: true } } }
|
|
374
|
+
);
|
|
375
|
+
|
|
376
|
+
expect(container.querySelector('[data-id="child-1"]')).toBeTruthy();
|
|
377
|
+
expect(container.querySelector('[data-part="modal"]')).toBeTruthy();
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
it('keeps the empty-state hint, which has no composition to disturb', () => {
|
|
381
|
+
// Without it a panel just added from the block list looks like nothing
|
|
382
|
+
// happened; with no children there is nothing to push out of place.
|
|
383
|
+
const { container } = renderPanel({}, { isDesigner: true, blocks: [] });
|
|
384
|
+
|
|
385
|
+
expect(container.textContent).toContain(
|
|
386
|
+
'Panel (drop the blocks it opens here)'
|
|
387
|
+
);
|
|
388
|
+
});
|
|
389
|
+
});
|
|
@@ -3,5 +3,5 @@
|
|
|
3
3
|
"legacy-open": "<div data-size-guide=\"\" data-size-guide-state=\"open\"><button type=\"button\" data-size-guide-trigger=\"\" data-part=\"trigger\" aria-haspopup=\"dialog\" aria-expanded=\"true\" style=\"display: inline-flex; align-items: center; gap: 6px; padding: 0px; margin: 0px; background: transparent; text-decoration: underline; cursor: pointer;\" data-state=\"open\">Size guide</button><div data-size-guide-overlay=\"\" data-part=\"overlay\" data-state=\"open\" style=\"position: fixed; inset: 0; z-index: 1000; display: flex; align-items: center; justify-content: center; padding: 16px; background: rgba(0, 0, 0, 0.5);\"><div data-size-guide-content=\"\" data-size-guide-mode=\"modal\" data-part=\"modal\" data-state=\"open\" role=\"dialog\" aria-modal=\"true\" aria-label=\"Size guide\" style=\"position: relative; width: 100%; max-width: 640px; max-height: 80vh; overflow: auto; padding: 24px; border-radius: 4px; background: rgb(255, 255, 255); color: rgb(17, 24, 39); text-align: left;\"><button type=\"button\" data-size-guide-close=\"\" data-part=\"close\" aria-label=\"Close\" style=\"position: absolute; top: 12px; right: 12px; background: transparent; cursor: pointer; padding: 0px;\"><svg width=\"18\" height=\"18\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.6\" stroke-linecap=\"round\"><path d=\"M6 6l12 12M18 6L6 18\"></path></svg></button><div data-testid=\"tb\" data-id=\"child-1\"></div></div></div></div>",
|
|
4
4
|
"legacy-open-titled": "<div data-size-guide=\"\" data-size-guide-state=\"open\"><button type=\"button\" data-size-guide-trigger=\"\" data-part=\"trigger\" aria-haspopup=\"dialog\" aria-expanded=\"true\" style=\"display: inline-flex; align-items: center; gap: 6px; padding: 0px; margin: 0px; background: transparent; text-decoration: underline; cursor: pointer;\" data-state=\"open\">Size guide</button><div data-size-guide-overlay=\"\" data-part=\"overlay\" data-state=\"open\" style=\"position: fixed; inset: 0; z-index: 1000; display: flex; align-items: center; justify-content: center; padding: 16px; background: rgba(0, 0, 0, 0.5);\"><div data-size-guide-content=\"\" data-size-guide-mode=\"modal\" data-part=\"modal\" data-state=\"open\" role=\"dialog\" aria-modal=\"true\" aria-label=\"Size chart\" style=\"position: relative; width: 100%; max-width: 900px; max-height: 80vh; overflow: auto; padding: 24px; border-radius: 4px; background: rgb(255, 255, 255); color: rgb(17, 24, 39); text-align: left;\"><button type=\"button\" data-size-guide-close=\"\" data-part=\"close\" aria-label=\"Close\" style=\"position: absolute; top: 12px; right: 12px; background: transparent; cursor: pointer; padding: 0px;\"><svg width=\"18\" height=\"18\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.6\" stroke-linecap=\"round\"><path d=\"M6 6l12 12M18 6L6 18\"></path></svg></button><div data-size-guide-title=\"\" data-part=\"title\" style=\"margin-bottom: 12px; font-weight: 600;\">Size chart</div><div data-testid=\"tb\" data-id=\"child-1\"></div></div></div></div>",
|
|
5
5
|
"legacy-open-noclose": "<div data-size-guide=\"\" data-size-guide-state=\"open\"><button type=\"button\" data-size-guide-trigger=\"\" data-part=\"trigger\" aria-haspopup=\"dialog\" aria-expanded=\"true\" style=\"display: inline-flex; align-items: center; gap: 6px; padding: 0px; margin: 0px; background: transparent; text-decoration: underline; cursor: pointer;\" data-state=\"open\">Size guide</button><div data-size-guide-overlay=\"\" data-part=\"overlay\" data-state=\"open\" style=\"position: fixed; inset: 0; z-index: 1000; display: flex; align-items: center; justify-content: center; padding: 16px; background: rgba(0, 0, 0, 0.5);\"><div data-size-guide-content=\"\" data-size-guide-mode=\"modal\" data-part=\"modal\" data-state=\"open\" role=\"dialog\" aria-modal=\"true\" aria-label=\"Size guide\" style=\"position: relative; width: 100%; max-width: 640px; max-height: 80vh; overflow: auto; padding: 24px; border-radius: 4px; background: rgb(255, 255, 255); color: rgb(17, 24, 39); text-align: left;\"><div data-testid=\"tb\" data-id=\"child-1\"></div></div></div></div>",
|
|
6
|
-
"legacy-designer": "<div data-size-guide=\"\" data-size-guide-state=\"
|
|
6
|
+
"legacy-designer": "<div data-size-guide=\"\" data-size-guide-state=\"closed\"><button type=\"button\" data-size-guide-trigger=\"\" data-part=\"trigger\" aria-haspopup=\"dialog\" aria-expanded=\"false\" style=\"display: inline-flex; align-items: center; gap: 6px; padding: 0px; margin: 0px; background: transparent; text-decoration: underline; cursor: pointer;\">Size guide</button></div>"
|
|
7
7
|
}
|
|
@@ -272,29 +272,43 @@ const PanelBlock = ({
|
|
|
272
272
|
) : null;
|
|
273
273
|
|
|
274
274
|
if (isDesigner && !forcedOpen) {
|
|
275
|
+
/**
|
|
276
|
+
* Closed in the canvas, exactly as it is on the storefront.
|
|
277
|
+
*
|
|
278
|
+
* The children used to render INLINE here, in flow under the trigger, so
|
|
279
|
+
* they stayed selectable without a click the author cannot perform inside
|
|
280
|
+
* the preview iframe. That predates the force-open toggle, and once the
|
|
281
|
+
* toggle existed the inline copy became the worse of the two: it is why a
|
|
282
|
+
* panel sitting in a header shoves the logo and the icons out of place
|
|
283
|
+
* while that header is being composed, and what the author sees there is
|
|
284
|
+
* not what a shopper gets.
|
|
285
|
+
*
|
|
286
|
+
* "Hold open in canvas" is the answer to seeing it open, and it draws the
|
|
287
|
+
* REAL overlay — right geometry, dressable backdrop. With the panel closed
|
|
288
|
+
* the layer tree still reaches every child.
|
|
289
|
+
*
|
|
290
|
+
* The empty state is the one thing that stays: a panel just added from the
|
|
291
|
+
* block list would otherwise look like nothing happened, and an empty
|
|
292
|
+
* panel has no content to push anything out of place.
|
|
293
|
+
*/
|
|
275
294
|
return (
|
|
276
|
-
<div data-size-guide="" data-size-guide-state="
|
|
295
|
+
<div data-size-guide="" data-size-guide-state="closed">
|
|
277
296
|
{trigger}
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
>
|
|
291
|
-
{titleNode}
|
|
292
|
-
{childBlocks.length > 0 ? (
|
|
293
|
-
renderChildren()
|
|
294
|
-
) : (
|
|
297
|
+
{childBlocks.length === 0 ? (
|
|
298
|
+
<div
|
|
299
|
+
data-size-guide-content=""
|
|
300
|
+
data-size-guide-mode="empty"
|
|
301
|
+
{...partAttrs('modal')}
|
|
302
|
+
style={{
|
|
303
|
+
marginTop: '8px',
|
|
304
|
+
padding: '12px',
|
|
305
|
+
border: '1px dashed #cbd5e1',
|
|
306
|
+
borderRadius: '4px'
|
|
307
|
+
}}
|
|
308
|
+
>
|
|
295
309
|
<DesignerPlaceholder label="Panel (drop the blocks it opens here)" />
|
|
296
|
-
|
|
297
|
-
|
|
310
|
+
</div>
|
|
311
|
+
) : null}
|
|
298
312
|
</div>
|
|
299
313
|
);
|
|
300
314
|
}
|
|
@@ -157,6 +157,7 @@ function Page({ a, b }: PageProps) {
|
|
|
157
157
|
describe('useOverlay', () => {
|
|
158
158
|
afterEach(() => {
|
|
159
159
|
document.body.style.overflow = '';
|
|
160
|
+
document.body.removeAttribute('data-overlay-state');
|
|
160
161
|
});
|
|
161
162
|
|
|
162
163
|
it('reproduces the loop the per-block traps have: two document-level traps pull focus at each other forever', () => {
|
|
@@ -487,6 +488,31 @@ describe('useOverlay', () => {
|
|
|
487
488
|
expect(closeA).not.toHaveBeenCalled();
|
|
488
489
|
});
|
|
489
490
|
|
|
491
|
+
it('publishes an overlay-open state on the body, and withdraws it only when the last overlay closes', () => {
|
|
492
|
+
// The styling hook a composition needs and cannot derive: a header that
|
|
493
|
+
// has to repaint while its own menu panel is open is not inside that
|
|
494
|
+
// panel, so no part state on the panel can reach it. `data-overlay-state`
|
|
495
|
+
// is the page saying so, in the same shape as the chrome slot's
|
|
496
|
+
// `data-scroll-state`.
|
|
497
|
+
const { rerender } = render(<Page a={{ open: false }} b={{ open: false }} />);
|
|
498
|
+
expect(document.body.hasAttribute('data-overlay-state')).toBe(false);
|
|
499
|
+
|
|
500
|
+
rerender(<Page a={{ open: true }} b={{ open: false }} />);
|
|
501
|
+
expect(document.body.getAttribute('data-overlay-state')).toBe('open');
|
|
502
|
+
|
|
503
|
+
// Two open, then one closes: the page is still held, so the attribute has
|
|
504
|
+
// to stay — the scroll lock learned this the hard way and the attribute is
|
|
505
|
+
// driven off the same stack for the same reason.
|
|
506
|
+
rerender(<Page a={{ open: true }} b={{ open: true }} />);
|
|
507
|
+
expect(document.body.getAttribute('data-overlay-state')).toBe('open');
|
|
508
|
+
|
|
509
|
+
rerender(<Page a={{ open: true }} b={{ open: false }} />);
|
|
510
|
+
expect(document.body.getAttribute('data-overlay-state')).toBe('open');
|
|
511
|
+
|
|
512
|
+
rerender(<Page a={{ open: false }} b={{ open: false }} />);
|
|
513
|
+
expect(document.body.hasAttribute('data-overlay-state')).toBe(false);
|
|
514
|
+
});
|
|
515
|
+
|
|
490
516
|
it('does not touch the document while rendering on the server', () => {
|
|
491
517
|
const addEventListener = jest.spyOn(document, 'addEventListener');
|
|
492
518
|
document.body.style.overflow = 'auto';
|
package/src/hooks/use-overlay.ts
CHANGED
|
@@ -51,7 +51,37 @@ import { useEffect, useId, useMemo, useRef, useSyncExternalStore } from 'react';
|
|
|
51
51
|
const overlayStack: string[] = [];
|
|
52
52
|
const stackListeners = new Set<() => void>();
|
|
53
53
|
|
|
54
|
+
/**
|
|
55
|
+
* The attribute a composition styles against while any overlay holds the page.
|
|
56
|
+
*
|
|
57
|
+
* Same shape and same reason as `data-scroll-state` on the chrome slot: a fact
|
|
58
|
+
* about the PAGE that an element cannot learn about itself. The header that
|
|
59
|
+
* turns opaque while its own menu panel is open is the case that forced it —
|
|
60
|
+
* the header is not inside the panel, so no part state on the panel can reach
|
|
61
|
+
* it, and the alternative was every block growing its own listener.
|
|
62
|
+
*
|
|
63
|
+
* It goes on `body` rather than on the chrome slot because an overlay can be
|
|
64
|
+
* opened from anywhere — a page body, a footer, another overlay — and
|
|
65
|
+
* everything that might want to react is a descendant of `body`.
|
|
66
|
+
*/
|
|
67
|
+
export const OVERLAY_STATE_ATTRIBUTE = 'data-overlay-state';
|
|
68
|
+
export const OVERLAY_STYLE_STATE = 'open';
|
|
69
|
+
|
|
70
|
+
const syncOverlayStateAttribute = () => {
|
|
71
|
+
if (typeof document === 'undefined') return;
|
|
72
|
+
|
|
73
|
+
// Driven off the stack rather than off each open/close call: two overlays
|
|
74
|
+
// closing in either order must leave exactly one attribute state behind, and
|
|
75
|
+
// the stack is already the thing that knows whether anything is left.
|
|
76
|
+
if (overlayStack.length > 0) {
|
|
77
|
+
document.body.setAttribute(OVERLAY_STATE_ATTRIBUTE, OVERLAY_STYLE_STATE);
|
|
78
|
+
} else {
|
|
79
|
+
document.body.removeAttribute(OVERLAY_STATE_ATTRIBUTE);
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
|
|
54
83
|
const notifyStack = () => {
|
|
84
|
+
syncOverlayStateAttribute();
|
|
55
85
|
stackListeners.forEach((listener) => listener());
|
|
56
86
|
};
|
|
57
87
|
|
|
@@ -204,3 +204,54 @@ describe('checkout safety', () => {
|
|
|
204
204
|
).toEqual({ ':scrolled/background-color': '#ffffff' });
|
|
205
205
|
});
|
|
206
206
|
});
|
|
207
|
+
|
|
208
|
+
describe('the overlay-open state', () => {
|
|
209
|
+
const OPEN = '[data-overlay-state~="open"]';
|
|
210
|
+
|
|
211
|
+
it('conditions a part on the page, exactly as scrolled does', () => {
|
|
212
|
+
const [selector] = buildPartSelectors('[data-block-id="blk-1"]', {
|
|
213
|
+
part: 'trigger',
|
|
214
|
+
state: 'overlay-open',
|
|
215
|
+
styles: {}
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
expect(selector).toBe(
|
|
219
|
+
`${OPEN} [data-block-id="blk-1"] [data-part="trigger"]` +
|
|
220
|
+
':not(:where([data-block-id="blk-1"] [data-block-id] *))'
|
|
221
|
+
);
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
it('repaints a whole header band while a panel is open', () => {
|
|
225
|
+
// The case it exists for: the header is not inside the panel, so the panel
|
|
226
|
+
// cannot style it, and the alternative was a second copy of the logo and
|
|
227
|
+
// the icons authored inside the panel.
|
|
228
|
+
const css = generateThemeCSS([
|
|
229
|
+
section('sec-1', {}, [
|
|
230
|
+
block('bar', {
|
|
231
|
+
'background-color': 'transparent',
|
|
232
|
+
':overlay-open/background-color': '#ffffff'
|
|
233
|
+
})
|
|
234
|
+
])
|
|
235
|
+
]);
|
|
236
|
+
|
|
237
|
+
expect(css).toContain(
|
|
238
|
+
`${OPEN} [data-block-id="bar"] {
|
|
239
|
+
background-color: rgba(255, 255, 255, 1);
|
|
240
|
+
}`
|
|
241
|
+
);
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
it('matches nothing while every overlay is closed', () => {
|
|
245
|
+
const [selector] = buildPartSelectors('[data-block-id="blk-1"]', {
|
|
246
|
+
part: 'trigger',
|
|
247
|
+
state: 'overlay-open',
|
|
248
|
+
styles: {}
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
document.body.removeAttribute('data-overlay-state');
|
|
252
|
+
document.body.innerHTML =
|
|
253
|
+
'<div data-block-id="blk-1"><button data-part="trigger"></button></div>';
|
|
254
|
+
|
|
255
|
+
expect(document.querySelectorAll(selector)).toHaveLength(0);
|
|
256
|
+
});
|
|
257
|
+
});
|
package/src/utils/part-styles.ts
CHANGED
|
@@ -69,13 +69,19 @@ const STATE_SELECTORS: Record<string, string[]> = {
|
|
|
69
69
|
available: ['[data-state~="available"]'],
|
|
70
70
|
unavailable: ['[data-state~="unavailable"]'],
|
|
71
71
|
unknown: ['[data-state~="unknown"]'],
|
|
72
|
-
// The odd
|
|
73
|
-
// on the element itself.
|
|
74
|
-
// carries `data-scroll-state` (see chrome/scroll-state.tsx)
|
|
75
|
-
//
|
|
72
|
+
// The odd ones out: every state above is a fact about the ELEMENT and matches
|
|
73
|
+
// on the element itself. These two are facts about the PAGE — the chrome slot
|
|
74
|
+
// carries `data-scroll-state` (see chrome/scroll-state.tsx) and `body` carries
|
|
75
|
+
// `data-overlay-state` (see hooks/use-overlay.ts) — so they are matched
|
|
76
|
+
// through an ancestor and no renderer has to stamp anything for them to
|
|
76
77
|
// apply. That is what lets a header become opaque on scroll, which nothing in
|
|
77
78
|
// a composition could previously say.
|
|
78
|
-
scrolled: ['[data-scroll-state~="scrolled"] &']
|
|
79
|
+
scrolled: ['[data-scroll-state~="scrolled"] &'],
|
|
80
|
+
// The same trick for the other half of that header: transparent over the
|
|
81
|
+
// hero, opaque while its menu panel is open. The panel cannot express it —
|
|
82
|
+
// the header is not inside the panel — and the header cannot know it without
|
|
83
|
+
// the page saying so.
|
|
84
|
+
'overlay-open': ['[data-overlay-state~="open"] &']
|
|
79
85
|
};
|
|
80
86
|
|
|
81
87
|
export const PART_STYLE_STATES = Object.keys(STATE_SELECTORS);
|