@weasel-js/labkit 1.4.3 → 1.5.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/README.md +13 -2
- package/dist/_dts/{CanvasStackContext-Dde8_1my.d.ts → CanvasStackContext-CqkxkFJW.d.ts} +1 -1
- package/dist/_dts/ToggleBar.d-DbHoWKIA.d.ts +116 -0
- package/dist/_dts/{frac-B5mB8LsJ.d.ts → frac-8R6UmlvN.d.ts} +256 -33
- package/dist/_dts/index-B8uu2ba7.d.ts +237 -0
- package/dist/_dts/{index-DelFfeV0.d.ts → index-retnAut7.d.ts} +45 -12
- package/dist/_dts/{types-B9_zrHmb.d.ts → types-Ca2LCnPq.d.ts} +65 -2
- package/dist/_dts/usePanZoom-nS798rOF.d.ts +160 -0
- package/dist/_dts/useTrialState-jcx_TdU6.d.ts +212 -0
- package/dist/canvas/index.d.ts +17 -128
- package/dist/canvas/index.js +4 -2
- package/dist/chrome/index.d.ts +4 -92
- package/dist/chrome/index.js +9 -5
- package/dist/chunk-67SJLMC7.js +704 -0
- package/dist/chunk-67SJLMC7.js.map +1 -0
- package/dist/chunk-6ZDGOZQV.js +310 -0
- package/dist/chunk-6ZDGOZQV.js.map +1 -0
- package/dist/{chunk-QAGAHJKZ.js → chunk-EKIICY6X.js} +111 -25
- package/dist/chunk-EKIICY6X.js.map +1 -0
- package/dist/{chunk-CTRKTLYZ.js → chunk-H6ZAOWNE.js} +11 -11
- package/dist/chunk-H6ZAOWNE.js.map +1 -0
- package/dist/{chunk-2RQPGOQF.js → chunk-KJALCDWE.js} +9 -7
- package/dist/chunk-KJALCDWE.js.map +1 -0
- package/dist/{chunk-ISCOFAUO.js → chunk-MUKOW3TC.js} +3 -3
- package/dist/{chunk-ISCOFAUO.js.map → chunk-MUKOW3TC.js.map} +1 -1
- package/dist/chunk-NDRYLVOW.js +15 -0
- package/dist/chunk-NDRYLVOW.js.map +1 -0
- package/dist/{chunk-C42T6DJT.js → chunk-NS64DXMN.js} +226 -159
- package/dist/chunk-NS64DXMN.js.map +1 -0
- package/dist/{chunk-I6JCVE24.js → chunk-PGETSEDK.js} +135 -13
- package/dist/chunk-PGETSEDK.js.map +1 -0
- package/dist/{chunk-FQMJUVHJ.js → chunk-QLSV2N3G.js} +32 -37
- package/dist/chunk-QLSV2N3G.js.map +1 -0
- package/dist/chunk-RWNDDKOH.js +682 -0
- package/dist/chunk-RWNDDKOH.js.map +1 -0
- package/dist/chunk-S2SHZRD7.js +7681 -0
- package/dist/chunk-S2SHZRD7.js.map +1 -0
- package/dist/{chunk-ASWKLRKJ.js → chunk-TQBBYTAF.js} +19 -82
- package/dist/chunk-TQBBYTAF.js.map +1 -0
- package/dist/chunk-UU3NJO6N.js +72 -0
- package/dist/chunk-UU3NJO6N.js.map +1 -0
- package/dist/chunk-WIG6XHJ7.js +35 -0
- package/dist/chunk-WIG6XHJ7.js.map +1 -0
- package/dist/{chunk-TJ7QY3OC.js → chunk-ZPH5WTWF.js} +21 -15
- package/dist/chunk-ZPH5WTWF.js.map +1 -0
- package/dist/config/index.d.ts +232 -0
- package/dist/config/index.js +6 -0
- package/dist/config/index.js.map +1 -0
- package/dist/controls/index.d.ts +2 -3
- package/dist/controls/index.js +3 -2
- package/dist/dragdrop/index.d.ts +2 -4
- package/dist/index.d.ts +123 -348
- package/dist/index.js +794 -837
- package/dist/index.js.map +1 -1
- package/dist/layers/index.d.ts +3 -5
- package/dist/layers/index.js +2 -2
- package/dist/loupe/index.d.ts +6 -8
- package/dist/loupe/index.js +2 -2
- package/dist/passthrough/weasel-ui.d.ts +169 -117
- package/dist/passthrough/weasel-ui.js +1 -1
- package/dist/primitives/index.d.ts +53 -6
- package/dist/primitives/index.js +6 -4
- package/dist/state/index.d.ts +12 -7
- package/dist/state/index.js +16 -13
- package/dist/state/index.js.map +1 -1
- package/dist/styles.css +163 -62
- package/dist/surface/index.d.ts +44 -7
- package/dist/surface/index.js +2 -1
- package/dist/ui/layers/index.js +1 -1
- package/dist/undo/index.d.ts +2 -4
- package/package.json +15 -8
- package/src/annotations/AnnotationOverlay.tsx +53 -5
- package/src/annotations/Annotations.overlay.test.tsx +85 -6
- package/src/annotations/drawOne.test.ts +1 -1
- package/src/annotations/drawOne.ts +2 -2
- package/src/annotations/frac.test.ts +4 -4
- package/src/annotations/frac.ts +3 -2
- package/src/annotations/index.ts +1 -1
- package/src/annotations/paint.test.ts +2 -2
- package/src/annotations/paint.ts +1 -1
- package/src/annotations/preload.ts +6 -0
- package/src/annotations/store.test.ts +18 -0
- package/src/annotations/store.ts +7 -6
- package/src/annotations/svgNodes.test.ts +2 -2
- package/src/annotations/svgNodes.ts +1 -2
- package/src/annotations/toolMap.ts +3 -3
- package/src/annotations/types.ts +6 -3
- package/src/canvas/CameraWheelContext.ts +12 -0
- package/src/canvas/CanvasStack.test.tsx +25 -0
- package/src/canvas/CanvasStack.tsx +31 -4
- package/src/canvas/Stage.less +18 -0
- package/src/canvas/Stage.test.tsx +186 -0
- package/src/canvas/Stage.tsx +134 -0
- package/src/canvas/camera.test.ts +17 -2
- package/src/canvas/camera.ts +9 -9
- package/src/canvas/index.ts +4 -0
- package/src/canvas/usePanZoom.ts +10 -5
- package/src/chrome/ChromeRegions.stories.tsx +0 -3
- package/src/chrome/LabChrome.tsx +114 -0
- package/src/chrome/builtins.tsx +20 -31
- package/src/chrome/index.ts +15 -0
- package/src/chrome/labRegions.test.tsx +208 -0
- package/src/chrome/labTypes.ts +50 -0
- package/src/chrome/merge.ts +7 -9
- package/src/chrome/regions/PaletteRegion.test.tsx +6 -4
- package/src/chrome/regions/PaletteRegion.tsx +21 -9
- package/src/chrome/regions/SidebarRegion.tsx +25 -13
- package/src/chrome/regions/StatusRegion.tsx +15 -8
- package/src/chrome/regions/ToolbarRegion.tsx +30 -16
- package/src/chrome/types.ts +47 -20
- package/src/config/builder.test.ts +4 -0
- package/src/config/builder.ts +16 -10
- package/src/config/declarationEmit.fixture.ts +11 -0
- package/src/config/declarationEmit.test.ts +51 -0
- package/src/config/entry.test.ts +46 -0
- package/src/config/index.ts +13 -1
- package/src/config/nodeClasses.test.ts +18 -0
- package/src/config/types.ts +3 -1
- package/src/controls/ControlPanel.test.tsx +16 -0
- package/src/controls/ControlPanel.tsx +5 -2
- package/src/fake-indexeddb-auto.d.ts +5 -0
- package/src/index.test.ts +7 -0
- package/src/index.ts +39 -6
- package/src/instrument/serializers.test.ts +22 -0
- package/src/instrument/serializers.ts +11 -0
- package/src/instrument/types.ts +25 -1
- package/src/lab/Lab.less +4 -13
- package/src/lab/Lab.persist.test.tsx +147 -0
- package/src/lab/Lab.stories.tsx +0 -2
- package/src/lab/Lab.surface.test.tsx +66 -3
- package/src/lab/Lab.test.tsx +91 -17
- package/src/lab/Lab.tsx +337 -124
- package/src/lab/LabFit.stories.less +18 -0
- package/src/lab/LabFit.stories.tsx +219 -0
- package/src/lab/LabFullChrome.stories.tsx +0 -3
- package/src/lab/LabHeader.test.tsx +40 -9
- package/src/lab/LabHeader.tsx +7 -13
- package/src/lab/LabPalette.tsx +13 -26
- package/src/lab/LabShell.less +49 -12
- package/src/lab/LabShell.tsx +14 -2
- package/src/lab/LabSwitcher.less +92 -0
- package/src/lab/LabSwitcher.test.tsx +96 -0
- package/src/lab/LabSwitcher.tsx +112 -0
- package/src/lab/Workspace.surface.test.tsx +2 -1
- package/src/lab/fitCheck.test.ts +57 -0
- package/src/lab/fitCheck.ts +125 -0
- package/src/lab/index.ts +2 -0
- package/src/loupe/AGENTS.md +1 -1
- package/src/loupe/Loupe.less +1 -1
- package/src/loupe/canvasLens.ts +1 -1
- package/src/loupe/types.ts +1 -1
- package/src/loupe/useLoupe.test.tsx +1 -1
- package/src/loupe/useLoupe.ts +1 -1
- package/src/passthrough/weasel-ui.ts +9 -0
- package/src/primitives/FloatingPanel.stories.tsx +13 -1
- package/src/primitives/FloatingPanel.test.tsx +47 -16
- package/src/primitives/FloatingPanel.tsx +11 -25
- package/src/primitives/Split.less +14 -0
- package/src/primitives/Split.test.tsx +108 -0
- package/src/primitives/Split.tsx +159 -0
- package/src/primitives/ZoomControl.tsx +5 -2
- package/src/primitives/index.ts +2 -0
- package/src/state/Persistence.stories.tsx +75 -0
- package/src/state/Persistence.tsx +33 -0
- package/src/state/SingletonExperiment.test.tsx +47 -53
- package/src/state/SingletonExperiment.tsx +24 -13
- package/src/state/adapterContract.ts +116 -0
- package/src/state/adapters.test.ts +113 -61
- package/src/state/adapters.ts +328 -69
- package/src/state/document.test.ts +71 -96
- package/src/state/document.ts +66 -52
- package/src/state/helpers.test.ts +20 -0
- package/src/state/helpers.ts +16 -0
- package/src/state/index.ts +8 -0
- package/src/state/labRecords.test.ts +105 -0
- package/src/state/labRecords.ts +160 -0
- package/src/state/openLabStore.test.ts +658 -0
- package/src/state/openLabStore.ts +403 -0
- package/src/state/records.test.ts +234 -0
- package/src/state/records.ts +219 -0
- package/src/state/store.test.ts +104 -635
- package/src/state/store.ts +117 -158
- package/src/state/toolSlot.test.ts +1 -1
- package/src/state/types.ts +39 -15
- package/src/state/useOpenOnce.ts +68 -0
- package/src/state/usePersistedState.test.tsx +125 -0
- package/src/state/usePersistedState.ts +66 -0
- package/src/state/useTrialState.test.tsx +2 -3
- package/src/state/view.test.ts +49 -15
- package/src/state/view.ts +18 -1
- package/src/styles.less +3 -0
- package/src/surface/AGENTS.md +39 -7
- package/src/surface/SurfaceContext.ts +26 -3
- package/src/surface/index.ts +2 -0
- package/src/surface/useSurfaceTile.test.tsx +2 -1
- package/src/surface/useSurfaceTile.ts +6 -5
- package/src/surface/useTiledSurface.test.tsx +175 -0
- package/src/surface/useTiledSurface.ts +54 -5
- package/src/theme/Interstellar.stories.tsx +7 -7
- package/src/theme/base.less +4 -4
- package/src/theme/interstellar.test.ts +1 -1
- package/src/theme/interstellar.tokens.json +2 -2
- package/src/tools/labTool.ts +11 -0
- package/src/trial/Trial.annotations.persist.test.tsx +100 -58
- package/src/trial/Trial.annotations.test.tsx +47 -4
- package/src/trial/Trial.job.test.tsx +3 -3
- package/src/trial/Trial.less +1 -26
- package/src/trial/Trial.stories.tsx +1 -2
- package/src/trial/Trial.test.tsx +28 -4
- package/src/trial/Trial.trialId.test.tsx +23 -1
- package/src/trial/Trial.tsx +118 -38
- package/src/trial/TrialBody.tsx +17 -141
- package/src/trial/TrialChrome.tsx +7 -4
- package/src/trial/trialOps.ts +7 -4
- package/dist/_dts/types-BP2OCcpg.d.ts +0 -155
- package/dist/_dts/types-x92Kfeme.d.ts +0 -62
- package/dist/_dts/useTrialState-D6Vb3T-g.d.ts +0 -99
- package/dist/chunk-2RQPGOQF.js.map +0 -1
- package/dist/chunk-5TROQVQ2.js +0 -547
- package/dist/chunk-5TROQVQ2.js.map +0 -1
- package/dist/chunk-ASWKLRKJ.js.map +0 -1
- package/dist/chunk-C42T6DJT.js.map +0 -1
- package/dist/chunk-CTRKTLYZ.js.map +0 -1
- package/dist/chunk-FQMJUVHJ.js.map +0 -1
- package/dist/chunk-I6JCVE24.js.map +0 -1
- package/dist/chunk-MHAGC6VD.js +0 -7320
- package/dist/chunk-MHAGC6VD.js.map +0 -1
- package/dist/chunk-QAGAHJKZ.js.map +0 -1
- package/dist/chunk-TJ7QY3OC.js.map +0 -1
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { fireEvent, render, screen } from '@testing-library/react';
|
|
2
|
+
import { describe, expect, it } from 'vitest';
|
|
3
|
+
import { currentPage, LabSwitcher } from './LabSwitcher';
|
|
4
|
+
import { LabShell } from './LabShell';
|
|
5
|
+
|
|
6
|
+
const PAGES = [
|
|
7
|
+
{ href: '/corpus', label: 'Wall' },
|
|
8
|
+
{ href: '/stats', label: 'Dashboard' },
|
|
9
|
+
{ href: '/ingest', label: 'Ingestion' },
|
|
10
|
+
];
|
|
11
|
+
|
|
12
|
+
const open = () => fireEvent.click(screen.getByRole('button', { name: /corpus stats/ }));
|
|
13
|
+
|
|
14
|
+
describe('currentPage', () => {
|
|
15
|
+
it('matches a bare path', () => {
|
|
16
|
+
expect(currentPage('/stats', PAGES)).toBe(1);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it('survives a query string, a trailing slash and a leftover extension', () => {
|
|
20
|
+
// A dev server maps `/stats` to `stats.html` and serves both spellings, so
|
|
21
|
+
// an old bookmark must still resolve to the same page.
|
|
22
|
+
for (const path of ['/stats?kind=base', '/stats/', '/stats.html',
|
|
23
|
+
'/lab/stats.html#top']) {
|
|
24
|
+
expect(currentPage(path, PAGES)).toBe(1);
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('reports -1 for a page that is not one of them', () => {
|
|
29
|
+
expect(currentPage('/bench', PAGES)).toBe(-1);
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
describe('<LabSwitcher>', () => {
|
|
34
|
+
it('shows the title and nothing else until it is opened', () => {
|
|
35
|
+
render(<LabSwitcher title="corpus stats" pages={PAGES} path="/stats" />);
|
|
36
|
+
expect(screen.getByRole('button', { name: /corpus stats/ })).toHaveAttribute(
|
|
37
|
+
'aria-expanded', 'false');
|
|
38
|
+
expect(screen.queryByRole('menu')).toBeNull();
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('opens a menu of every lab, the open one included', () => {
|
|
42
|
+
// Filtering the current page out shifts every other entry as you move
|
|
43
|
+
// between pages, so it stays and is marked instead.
|
|
44
|
+
render(<LabSwitcher title="corpus stats" pages={PAGES} path="/stats" />);
|
|
45
|
+
open();
|
|
46
|
+
const items = screen.getAllByRole('menuitem');
|
|
47
|
+
expect(items.map((i) => i.textContent)).toEqual(['Wall', 'Dashboard', 'Ingestion']);
|
|
48
|
+
expect(items[1]).toHaveAttribute('aria-current', 'page');
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('offers real links, so cmd-click and the back button keep working', () => {
|
|
52
|
+
// These are separate documents. A <select> would fire a change event and
|
|
53
|
+
// get none of the three for free.
|
|
54
|
+
render(<LabSwitcher title="corpus stats" pages={PAGES} path="/stats" />);
|
|
55
|
+
open();
|
|
56
|
+
expect(screen.getByRole('menuitem', { name: 'Wall' }).tagName).toBe('A');
|
|
57
|
+
expect(screen.getByRole('menuitem', { name: 'Wall' })).toHaveAttribute(
|
|
58
|
+
'href', '/corpus');
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('closes on Escape and on a press outside it', () => {
|
|
62
|
+
render(
|
|
63
|
+
<div>
|
|
64
|
+
<LabSwitcher title="corpus stats" pages={PAGES} path="/stats" />
|
|
65
|
+
<button type="button">elsewhere</button>
|
|
66
|
+
</div>);
|
|
67
|
+
open();
|
|
68
|
+
fireEvent.keyDown(document, { key: 'Escape' });
|
|
69
|
+
expect(screen.queryByRole('menu')).toBeNull();
|
|
70
|
+
|
|
71
|
+
open();
|
|
72
|
+
fireEvent.pointerDown(screen.getByRole('button', { name: 'elsewhere' }));
|
|
73
|
+
expect(screen.queryByRole('menu')).toBeNull();
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('stays a plain heading when there is nowhere to switch to', () => {
|
|
77
|
+
// A disclosure arrow promising a menu of the page you are already on is
|
|
78
|
+
// worse than no control.
|
|
79
|
+
render(<LabSwitcher title="corpus stats" pages={[PAGES[1]!]} path="/stats" />);
|
|
80
|
+
expect(screen.getByRole('heading', { name: 'corpus stats' })).toBeInTheDocument();
|
|
81
|
+
expect(screen.queryByRole('button')).toBeNull();
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
describe('<LabShell> with pages', () => {
|
|
86
|
+
it('turns its title into the switcher', () => {
|
|
87
|
+
render(<LabShell title="corpus stats" pages={PAGES} path="/stats">body</LabShell>);
|
|
88
|
+
expect(screen.getByRole('button', { name: /corpus stats/ })).toBeInTheDocument();
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('leaves the title alone when no pages are given', () => {
|
|
92
|
+
render(<LabShell title="corpus stats">body</LabShell>);
|
|
93
|
+
expect(screen.getByRole('heading', { name: 'corpus stats' })).toBeInTheDocument();
|
|
94
|
+
expect(screen.queryByRole('button', { name: /corpus stats/ })).toBeNull();
|
|
95
|
+
});
|
|
96
|
+
});
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { useCallback, useEffect, useId, useRef, useState } from 'react';
|
|
2
|
+
|
|
3
|
+
/** One page in a project's set of labs. */
|
|
4
|
+
export interface LabPage {
|
|
5
|
+
/** The page's own URL, and how the switcher knows which page is open. */
|
|
6
|
+
href: string;
|
|
7
|
+
label: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/** Props for `<LabSwitcher>`. */
|
|
11
|
+
export interface LabSwitcherProps {
|
|
12
|
+
/** The open page's title, shown as the control's own label. */
|
|
13
|
+
title: string;
|
|
14
|
+
pages: readonly LabPage[];
|
|
15
|
+
/** Defaults to the current location. Pass it in tests, or to render a
|
|
16
|
+
* switcher for a page other than the one being viewed. */
|
|
17
|
+
path?: string;
|
|
18
|
+
className?: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Which of `pages` a path is on, or -1.
|
|
22
|
+
*
|
|
23
|
+
* Matched on the end of the path so a query string, a trailing slash or a
|
|
24
|
+
* leftover `.html` cannot lose it: a project whose dev server maps `/stats`
|
|
25
|
+
* to `stats.html` serves both spellings, and a bookmark from before the URLs
|
|
26
|
+
* lost their extension still resolves to the same page. */
|
|
27
|
+
export function currentPage(path: string, pages: readonly LabPage[]): number {
|
|
28
|
+
const here = path.split('?')[0]!.split('#')[0]!
|
|
29
|
+
.replace(/\/$/, '').replace(/\.html$/, '');
|
|
30
|
+
return pages.findIndex((p) => here.endsWith(p.href.replace(/\.html$/, '')));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** A project's lab title, doubling as the way to reach its other labs.
|
|
34
|
+
*
|
|
35
|
+
* The menu holds real anchors, not a `<select>` and not buttons. These are
|
|
36
|
+
* separate documents, so an `href` is what gets middle-click, cmd-click and
|
|
37
|
+
* the back button for free; a select fires a change event and can do none of
|
|
38
|
+
* the three. The trigger is a button because it opens something rather than
|
|
39
|
+
* going anywhere.
|
|
40
|
+
*
|
|
41
|
+
* Pages accumulate -- a project grows a wall, a dashboard, an ingest page, a
|
|
42
|
+
* bench -- and a tab strip is the layout that stops working first. */
|
|
43
|
+
export function LabSwitcher({ title, pages, path, className }: LabSwitcherProps) {
|
|
44
|
+
const [open, setOpen] = useState(false);
|
|
45
|
+
const root = useRef<HTMLDivElement>(null);
|
|
46
|
+
const menuId = useId();
|
|
47
|
+
const here = currentPage(path ?? (typeof window === 'undefined'
|
|
48
|
+
? '' : window.location.pathname), pages);
|
|
49
|
+
|
|
50
|
+
const close = useCallback(() => setOpen(false), []);
|
|
51
|
+
|
|
52
|
+
useEffect(() => {
|
|
53
|
+
if (!open) return;
|
|
54
|
+
const onKey = (e: KeyboardEvent) => {
|
|
55
|
+
if (e.key === 'Escape') {
|
|
56
|
+
close();
|
|
57
|
+
root.current?.querySelector('button')?.focus();
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
// Pointer, not click: a press that starts outside should dismiss before
|
|
61
|
+
// whatever it lands on reacts, the way every other menu on the page does.
|
|
62
|
+
const onDown = (e: PointerEvent) => {
|
|
63
|
+
if (!root.current?.contains(e.target as Node)) close();
|
|
64
|
+
};
|
|
65
|
+
document.addEventListener('keydown', onKey);
|
|
66
|
+
document.addEventListener('pointerdown', onDown);
|
|
67
|
+
return () => {
|
|
68
|
+
document.removeEventListener('keydown', onKey);
|
|
69
|
+
document.removeEventListener('pointerdown', onDown);
|
|
70
|
+
};
|
|
71
|
+
}, [open, close]);
|
|
72
|
+
|
|
73
|
+
// One lab is not a set to choose from, and a disclosure arrow promising a
|
|
74
|
+
// menu of itself is worse than no control.
|
|
75
|
+
if (pages.length < 2) {
|
|
76
|
+
return <h1 className={`lk-switcher-title${className ? ` ${className}` : ''}`}>{title}</h1>;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return (
|
|
80
|
+
<div className={`lk-switcher${className ? ` ${className}` : ''}`} ref={root}>
|
|
81
|
+
<h1 className="lk-switcher-title">
|
|
82
|
+
<button
|
|
83
|
+
type="button"
|
|
84
|
+
className="lk-switcher-trigger"
|
|
85
|
+
aria-expanded={open}
|
|
86
|
+
aria-haspopup="menu"
|
|
87
|
+
aria-controls={open ? menuId : undefined}
|
|
88
|
+
onClick={() => setOpen((was) => !was)}
|
|
89
|
+
>
|
|
90
|
+
{title}
|
|
91
|
+
<span className="lk-switcher-caret" aria-hidden="true" />
|
|
92
|
+
</button>
|
|
93
|
+
</h1>
|
|
94
|
+
{open && (
|
|
95
|
+
<div className="lk-switcher-menu" id={menuId} role="menu">
|
|
96
|
+
{pages.map((page, i) => (
|
|
97
|
+
<a
|
|
98
|
+
key={page.href}
|
|
99
|
+
href={page.href}
|
|
100
|
+
role="menuitem"
|
|
101
|
+
className="lk-switcher-item"
|
|
102
|
+
aria-current={i === here ? 'page' : undefined}
|
|
103
|
+
onClick={close}
|
|
104
|
+
>
|
|
105
|
+
{page.label}
|
|
106
|
+
</a>
|
|
107
|
+
))}
|
|
108
|
+
</div>
|
|
109
|
+
)}
|
|
110
|
+
</div>
|
|
111
|
+
);
|
|
112
|
+
}
|
|
@@ -10,7 +10,8 @@ function fakeHandle(): SurfaceHandle {
|
|
|
10
10
|
invalidateAll: vi.fn(),
|
|
11
11
|
invalidateRects: vi.fn(),
|
|
12
12
|
registerTile: vi.fn(),
|
|
13
|
-
|
|
13
|
+
registerClear: vi.fn(() => () => {}),
|
|
14
|
+
registerPainter: vi.fn(() => () => {}),
|
|
14
15
|
containerRef: vi.fn(),
|
|
15
16
|
getContainer: vi.fn(() => null),
|
|
16
17
|
};
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { describeElement, findOvershoot } from './fitCheck';
|
|
3
|
+
|
|
4
|
+
const bound = { top: 0, left: 0, bottom: 600, right: 800 };
|
|
5
|
+
const box = (bottom: number, right: number) => ({ top: 0, left: 0, bottom, right });
|
|
6
|
+
|
|
7
|
+
describe('findOvershoot', () => {
|
|
8
|
+
it('returns null when everything is inside the bound', () => {
|
|
9
|
+
expect(findOvershoot(bound, [{ item: 'a', edges: box(600, 800) }])).toBeNull();
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it('ignores sub-pixel overshoot within the tolerance', () => {
|
|
13
|
+
expect(findOvershoot(bound, [{ item: 'a', edges: box(600.8, 800) }])).toBeNull();
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it('names the item reaching furthest past the bound', () => {
|
|
17
|
+
const hit = findOvershoot(bound, [
|
|
18
|
+
{ item: 'header', edges: box(640, 800) },
|
|
19
|
+
{ item: 'tile', edges: box(900, 800) },
|
|
20
|
+
{ item: 'footer', edges: box(700, 800) },
|
|
21
|
+
]);
|
|
22
|
+
expect(hit).toEqual({ item: 'tile', by: 300, axis: 'y' });
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('compares both axes and reports the worse one', () => {
|
|
26
|
+
const hit = findOvershoot(bound, [
|
|
27
|
+
{ item: 'tall', edges: box(650, 800) },
|
|
28
|
+
{ item: 'wide', edges: box(600, 1000) },
|
|
29
|
+
]);
|
|
30
|
+
expect(hit).toEqual({ item: 'wide', by: 200, axis: 'x' });
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('keeps the first item on a tie, which is the outermost in document order', () => {
|
|
34
|
+
const hit = findOvershoot(bound, [
|
|
35
|
+
{ item: 'shell', edges: box(616, 800) },
|
|
36
|
+
{ item: 'footer', edges: box(616, 800) },
|
|
37
|
+
]);
|
|
38
|
+
expect(hit?.item).toBe('shell');
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
describe('describeElement', () => {
|
|
43
|
+
it('writes the tag and every class', () => {
|
|
44
|
+
const el = document.createElement('DIV');
|
|
45
|
+
el.className = 'lk-shell-body extra';
|
|
46
|
+
expect(describeElement(el)).toBe('div.lk-shell-body.extra');
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('places a classless element by its nearest classed ancestor', () => {
|
|
50
|
+
const outer = document.createElement('div');
|
|
51
|
+
outer.className = 'lk-trial';
|
|
52
|
+
const mid = document.createElement('div');
|
|
53
|
+
const leaf = document.createElement('canvas');
|
|
54
|
+
outer.appendChild(mid).appendChild(leaf);
|
|
55
|
+
expect(describeElement(leaf)).toBe('canvas in div.lk-trial');
|
|
56
|
+
});
|
|
57
|
+
});
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { useEffect } from 'react';
|
|
2
|
+
|
|
3
|
+
/** An element's edges, in the same coordinate space as the bound it is tested against. */
|
|
4
|
+
export interface Edges {
|
|
5
|
+
top: number;
|
|
6
|
+
left: number;
|
|
7
|
+
bottom: number;
|
|
8
|
+
right: number;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface Overshoot<T> {
|
|
12
|
+
item: T;
|
|
13
|
+
/** How far past the bound the item reaches, in px, on whichever axis is worse. */
|
|
14
|
+
by: number;
|
|
15
|
+
axis: 'x' | 'y';
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** The item reaching furthest past the bottom or right of `bound`. On a tie the
|
|
19
|
+
* first item wins, so given document order it names the outermost box. */
|
|
20
|
+
export function findOvershoot<T>(
|
|
21
|
+
bound: Edges,
|
|
22
|
+
items: Iterable<{ item: T; edges: Edges }>,
|
|
23
|
+
tolerance = 1,
|
|
24
|
+
): Overshoot<T> | null {
|
|
25
|
+
let worst: Overshoot<T> | null = null;
|
|
26
|
+
for (const { item, edges } of items) {
|
|
27
|
+
const y = edges.bottom - bound.bottom;
|
|
28
|
+
const x = edges.right - bound.right;
|
|
29
|
+
const by = Math.max(x, y);
|
|
30
|
+
if (by <= tolerance) continue;
|
|
31
|
+
if (!worst || by > worst.by) worst = { item, by, axis: y >= x ? 'y' : 'x' };
|
|
32
|
+
}
|
|
33
|
+
return worst;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function tagAndClasses(el: Element): string {
|
|
37
|
+
return el.tagName.toLowerCase() + [...el.classList].map((c) => `.${c}`).join('');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** `div.lk-shell-body` — the tag and classes, which is what a reader greps for.
|
|
41
|
+
* A classless element is placed by its nearest classed ancestor. */
|
|
42
|
+
export function describeElement(el: Element): string {
|
|
43
|
+
if (el.classList.length > 0) return tagAndClasses(el);
|
|
44
|
+
let up = el.parentElement;
|
|
45
|
+
while (up && up.classList.length === 0) up = up.parentElement;
|
|
46
|
+
return up ? `${tagAndClasses(el)} in ${tagAndClasses(up)}` : tagAndClasses(el);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function edgesOf(el: Element): Edges {
|
|
50
|
+
const r = el.getBoundingClientRect();
|
|
51
|
+
return { top: r.top, left: r.left, bottom: r.bottom, right: r.right };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function* candidates(root: Element): Iterable<{ item: Element; edges: Edges }> {
|
|
55
|
+
yield { item: root, edges: edgesOf(root) };
|
|
56
|
+
for (const el of root.querySelectorAll('*')) yield { item: el, edges: edgesOf(el) };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function overflows(el: Element): boolean {
|
|
60
|
+
return el.scrollHeight > el.clientHeight + 1 || el.scrollWidth > el.clientWidth + 1;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Why a mounted lab scrolls, or null when it fits. The document only counts
|
|
64
|
+
* when the lab is what reaches past the viewport, so a lab embedded in a long
|
|
65
|
+
* page does not report the page's own scroll. */
|
|
66
|
+
export function explainLabScroll(lab: Element, shellBody: Element): string | null {
|
|
67
|
+
if (overflows(shellBody)) {
|
|
68
|
+
const r = shellBody.getBoundingClientRect();
|
|
69
|
+
const bound = {
|
|
70
|
+
top: r.top,
|
|
71
|
+
left: r.left,
|
|
72
|
+
bottom: r.top + shellBody.clientHeight,
|
|
73
|
+
right: r.left + shellBody.clientWidth,
|
|
74
|
+
};
|
|
75
|
+
const hit = findOvershoot(bound, candidates(shellBody));
|
|
76
|
+
return hit
|
|
77
|
+
? `.lk-shell-body scrolls: ${describeElement(hit.item)} reaches ${Math.round(hit.by)}px past it (${hit.axis})`
|
|
78
|
+
: '.lk-shell-body scrolls';
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const doc = document.scrollingElement ?? document.documentElement;
|
|
82
|
+
if (!overflows(doc)) return null;
|
|
83
|
+
const r = lab.getBoundingClientRect();
|
|
84
|
+
const bound = {
|
|
85
|
+
top: r.top,
|
|
86
|
+
left: r.left,
|
|
87
|
+
bottom: r.top + Math.min(r.height, doc.clientHeight),
|
|
88
|
+
right: r.left + Math.min(r.width, doc.clientWidth),
|
|
89
|
+
};
|
|
90
|
+
const hit = findOvershoot(bound, candidates(lab));
|
|
91
|
+
return hit
|
|
92
|
+
? `the page scrolls: ${describeElement(hit.item)} reaches ${Math.round(hit.by)}px past the lab's box (${hit.axis})`
|
|
93
|
+
: null;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const SETTLE_MS = 300;
|
|
97
|
+
|
|
98
|
+
/** Dev only: warns once, naming the culprit, when a mounted lab scrolls. */
|
|
99
|
+
export function useLabFitWarning(labBody: Element | null): void {
|
|
100
|
+
useEffect(() => {
|
|
101
|
+
if (process.env.NODE_ENV === 'production') return;
|
|
102
|
+
if (!labBody || typeof ResizeObserver === 'undefined') return;
|
|
103
|
+
const lab = labBody.closest('.lk-lab');
|
|
104
|
+
const shellBody = labBody.closest('.lk-shell-body');
|
|
105
|
+
if (!lab || !shellBody) return;
|
|
106
|
+
|
|
107
|
+
// A shrinking window overflows for a frame or two before the workspace
|
|
108
|
+
// re-lays out, so only overflow that outlasts the resize is reported.
|
|
109
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
110
|
+
const observer = new ResizeObserver(() => {
|
|
111
|
+
clearTimeout(timer);
|
|
112
|
+
timer = setTimeout(() => {
|
|
113
|
+
const why = explainLabScroll(lab, shellBody);
|
|
114
|
+
if (!why) return;
|
|
115
|
+
observer.disconnect();
|
|
116
|
+
console.warn(`[labkit] <Lab> should fit its container without scrolling, but ${why}.`);
|
|
117
|
+
}, SETTLE_MS);
|
|
118
|
+
});
|
|
119
|
+
for (const el of [document.documentElement, lab, shellBody, labBody]) observer.observe(el);
|
|
120
|
+
return () => {
|
|
121
|
+
clearTimeout(timer);
|
|
122
|
+
observer.disconnect();
|
|
123
|
+
};
|
|
124
|
+
}, [labBody]);
|
|
125
|
+
}
|
package/src/lab/index.ts
CHANGED
|
@@ -6,5 +6,7 @@ export type { LabPaletteProps } from './LabPalette';
|
|
|
6
6
|
export { LabPalette } from './LabPalette';
|
|
7
7
|
export type { LabShellProps } from './LabShell';
|
|
8
8
|
export { LabShell } from './LabShell';
|
|
9
|
+
export type { LabPage, LabSwitcherProps } from './LabSwitcher';
|
|
10
|
+
export { currentPage, LabSwitcher } from './LabSwitcher';
|
|
9
11
|
export type { PanelDescriptor, TrialLayout, WorkspaceProps } from './Workspace';
|
|
10
12
|
export { Workspace } from './Workspace';
|
package/src/loupe/AGENTS.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
on, painted by whichever painter suits the instrument's content.
|
|
5
5
|
|
|
6
6
|
The magnifier itself is not here. `@weasel-js/loupe` holds the model — aim,
|
|
7
|
-
factor, mode,
|
|
7
|
+
factor, mode, color, picking — over a `LoupeSurface` it asks five questions.
|
|
8
8
|
This directory binds that model to a labkit trial and draws it.
|
|
9
9
|
|
|
10
10
|
## Files
|
package/src/loupe/Loupe.less
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
// Nothing under the lens stops working while it is up: the pan, the wheel and
|
|
8
8
|
// any overlay of the instrument's own keep receiving the pointer.
|
|
9
9
|
pointer-events: none;
|
|
10
|
-
border: var(--wzl-border-w) solid var(--wzl-border-
|
|
10
|
+
border: var(--wzl-border-w) solid var(--wzl-border-strong);
|
|
11
11
|
box-shadow: 0 2px 8px var(--wzl-shadow);
|
|
12
12
|
// A painter's ground shows through wherever it draws nothing, and a vector
|
|
13
13
|
// lens leaves the gaps between marks transparent. This is the trial's own
|
package/src/loupe/canvasLens.ts
CHANGED
|
@@ -61,7 +61,7 @@ export function lensSourceRect(
|
|
|
61
61
|
}
|
|
62
62
|
|
|
63
63
|
/**
|
|
64
|
-
* The
|
|
64
|
+
* The color a stack presents at a point: the topmost visible layer with
|
|
65
65
|
* anything opaque there, or `null` when every layer is transparent — which is
|
|
66
66
|
* the honest answer for a point showing nothing but the workspace behind.
|
|
67
67
|
*/
|
package/src/loupe/types.ts
CHANGED
|
@@ -43,7 +43,7 @@ export interface LoupeCapability<TS = unknown, TC = unknown> {
|
|
|
43
43
|
/** Held for a momentary peek while the loupe is off. Default `'Alt'`; `null`
|
|
44
44
|
* turns hold-to-peek off. Matched against `KeyboardEvent.key`. */
|
|
45
45
|
peekKey?: string | null;
|
|
46
|
-
/** Called with the
|
|
46
|
+
/** Called with the color under the aim, wherever the surface can say. The
|
|
47
47
|
* canvas painter reads it back; a DOM loupe has no pixels to sample. */
|
|
48
48
|
onColorChange?: (hex: string) => void;
|
|
49
49
|
}
|
|
@@ -148,7 +148,7 @@ describe('useLoupe', () => {
|
|
|
148
148
|
expect(wheel.defaultPrevented).toBe(true);
|
|
149
149
|
});
|
|
150
150
|
|
|
151
|
-
it('reports the
|
|
151
|
+
it('reports the color under the aim', () => {
|
|
152
152
|
const onColorChange = vi.fn();
|
|
153
153
|
render(
|
|
154
154
|
<Harness sample={(p) => (p.x > 10 ? '#ff0000' : '#00ff00')} onColorChange={onColorChange} />,
|
package/src/loupe/useLoupe.ts
CHANGED
|
@@ -14,7 +14,7 @@ export interface UseLoupeOptions {
|
|
|
14
14
|
hostRef: RefObject<HTMLElement | null>;
|
|
15
15
|
/** Whether the loupe is turned on. Hold-to-peek shows it regardless. */
|
|
16
16
|
enabled: boolean;
|
|
17
|
-
/** Hex
|
|
17
|
+
/** Hex color at a host point. Omitted, the loupe reports no color — which
|
|
18
18
|
* is the honest answer for a surface with no pixels to read. */
|
|
19
19
|
sample?: (p: LoupePoint) => string | null;
|
|
20
20
|
}
|
|
@@ -37,6 +37,8 @@ export {
|
|
|
37
37
|
type ChromaCurve,
|
|
38
38
|
type ChromaCurvePoint,
|
|
39
39
|
ComboBox,
|
|
40
|
+
type ComboBoxCommit,
|
|
41
|
+
type ComboBoxFilter,
|
|
40
42
|
ComboBoxItem,
|
|
41
43
|
type ComboBoxItemProps,
|
|
42
44
|
type ComboBoxOption,
|
|
@@ -100,6 +102,9 @@ export {
|
|
|
100
102
|
type LayerStackProps,
|
|
101
103
|
type LogicalMod,
|
|
102
104
|
type LogicalModSpec,
|
|
105
|
+
MenuButton,
|
|
106
|
+
type MenuButtonItem,
|
|
107
|
+
type MenuButtonProps,
|
|
103
108
|
MINUS_SIGN,
|
|
104
109
|
NumberField,
|
|
105
110
|
type NumberFieldProps,
|
|
@@ -188,6 +193,10 @@ export {
|
|
|
188
193
|
type TrackCtx,
|
|
189
194
|
type UseReorderDragListOptions,
|
|
190
195
|
type UseRovingTabIndexOptions,
|
|
196
|
+
useAsyncOptions,
|
|
197
|
+
type AsyncOption,
|
|
198
|
+
type AsyncOptionsResult,
|
|
199
|
+
type UseAsyncOptionsProps,
|
|
191
200
|
useReorderDragList,
|
|
192
201
|
useRovingTabIndex,
|
|
193
202
|
visiblePrefSubtree,
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import type { Meta, StoryObj } from '@storybook/react-vite';
|
|
2
|
+
import { localStorageAdapter } from '../state/adapters';
|
|
3
|
+
import { Persistence } from '../state/Persistence';
|
|
2
4
|
import { FloatingPanel } from './FloatingPanel';
|
|
3
5
|
import { Legend } from './Legend';
|
|
4
6
|
|
|
@@ -43,4 +45,14 @@ export const HoldingALegend: Story = {
|
|
|
43
45
|
},
|
|
44
46
|
};
|
|
45
47
|
|
|
46
|
-
|
|
48
|
+
// Remembered only under a provider; this one keeps it across story reloads.
|
|
49
|
+
export const Remembered: Story = {
|
|
50
|
+
args: { persist: 'story-panel' },
|
|
51
|
+
decorators: [
|
|
52
|
+
(Story) => (
|
|
53
|
+
<Persistence storageKey="labkit-story" storage={localStorageAdapter}>
|
|
54
|
+
<Story />
|
|
55
|
+
</Persistence>
|
|
56
|
+
),
|
|
57
|
+
],
|
|
58
|
+
};
|
|
@@ -1,5 +1,8 @@
|
|
|
1
|
-
import { fireEvent, render, screen } from '@testing-library/react';
|
|
1
|
+
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
|
|
2
2
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
3
|
+
import { createMemoryAdapter } from '../state/adapters';
|
|
4
|
+
import { labPrefix } from '../state/labRecords';
|
|
5
|
+
import { Persistence } from '../state/Persistence';
|
|
3
6
|
import { FloatingPanel } from './FloatingPanel';
|
|
4
7
|
|
|
5
8
|
// The shared stub reports one size for every element, which makes the panel and
|
|
@@ -219,34 +222,62 @@ describe('FloatingPanel dragging', () => {
|
|
|
219
222
|
});
|
|
220
223
|
|
|
221
224
|
describe('FloatingPanel persistence', () => {
|
|
222
|
-
|
|
223
|
-
|
|
225
|
+
const PLACE = `${labPrefix('fp')}value:lab:legend`;
|
|
226
|
+
|
|
227
|
+
function persisted(backing: Map<string, unknown>, ui: React.ReactElement) {
|
|
228
|
+
return render(
|
|
229
|
+
<Persistence storageKey="fp" storage={createMemoryAdapter(backing)}>
|
|
230
|
+
<div>{ui}</div>
|
|
231
|
+
</Persistence>,
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
it('writes its placement under its persist name once dragged', async () => {
|
|
236
|
+
const backing = new Map<string, unknown>();
|
|
237
|
+
const { container } = persisted(backing, <FloatingPanel persist="legend">x</FloatingPanel>);
|
|
238
|
+
await waitFor(() => expect(container.querySelector('.lk-floating-panel')).not.toBeNull());
|
|
239
|
+
const panel = container.querySelector('.lk-floating-panel') as HTMLElement;
|
|
224
240
|
fireEvent.pointerDown(panel, { clientX: 100, clientY: 100 });
|
|
225
241
|
fireEvent.pointerMove(panel, { clientX: 140, clientY: 160 });
|
|
226
242
|
fireEvent.pointerUp(panel);
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
243
|
+
await waitFor(() =>
|
|
244
|
+
expect(backing.get(PLACE)).toMatchObject({ x: expect.any(Number), y: expect.any(Number) }),
|
|
245
|
+
);
|
|
246
|
+
expect(backing.get(PLACE)).toHaveProperty('anchor');
|
|
230
247
|
});
|
|
231
248
|
|
|
232
|
-
it('restores what it stored on a later mount', () => {
|
|
233
|
-
|
|
234
|
-
const {
|
|
235
|
-
|
|
249
|
+
it('restores what it stored on a later mount', async () => {
|
|
250
|
+
const backing = new Map<string, unknown>([[PLACE, { x: 140, y: 90, anchor: null }]]);
|
|
251
|
+
const { container } = persisted(backing, <FloatingPanel persist="legend">x</FloatingPanel>);
|
|
252
|
+
// Placed only once both boxes are measured, which can land a render later.
|
|
253
|
+
await waitFor(() =>
|
|
254
|
+
expect(
|
|
255
|
+
(container.querySelector('.lk-floating-panel') as HTMLElement | null)?.style.left,
|
|
256
|
+
).toBe('140px'),
|
|
257
|
+
);
|
|
258
|
+
const panel = container.querySelector('.lk-floating-panel') as HTMLElement;
|
|
236
259
|
expect(panel.style.top).toBe('90px');
|
|
237
260
|
});
|
|
238
261
|
|
|
239
|
-
it('writes nothing
|
|
240
|
-
const
|
|
262
|
+
it('writes nothing without a persist name', async () => {
|
|
263
|
+
const backing = new Map<string, unknown>();
|
|
264
|
+
const { container, unmount } = persisted(backing, <FloatingPanel>x</FloatingPanel>);
|
|
265
|
+
await waitFor(() => expect(container.querySelector('.lk-floating-panel')).not.toBeNull());
|
|
266
|
+
const panel = container.querySelector('.lk-floating-panel') as HTMLElement;
|
|
241
267
|
fireEvent.pointerDown(panel, { clientX: 100, clientY: 100 });
|
|
242
268
|
fireEvent.pointerMove(panel, { clientX: 140, clientY: 160 });
|
|
243
269
|
fireEvent.pointerUp(panel);
|
|
244
|
-
|
|
270
|
+
unmount();
|
|
271
|
+
await new Promise((r) => setTimeout(r, 20));
|
|
272
|
+
expect(backing.size).toBe(0);
|
|
245
273
|
});
|
|
246
274
|
|
|
247
|
-
it('
|
|
248
|
-
|
|
249
|
-
|
|
275
|
+
it('forgets its placement with no provider above it', () => {
|
|
276
|
+
const { panel } = renderPanel(<FloatingPanel persist="legend">x</FloatingPanel>);
|
|
277
|
+
fireEvent.pointerDown(panel, { clientX: 100, clientY: 100 });
|
|
278
|
+
fireEvent.pointerMove(panel, { clientX: 140, clientY: 160 });
|
|
279
|
+
fireEvent.pointerUp(panel);
|
|
280
|
+
expect(localStorage.length).toBe(0);
|
|
250
281
|
});
|
|
251
282
|
});
|
|
252
283
|
|