@weasel-js/labkit 1.7.0 → 1.7.2
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 +3 -3
- package/dist/_dts/{CanvasStackContext-D2jnIwfk.d.ts → CanvasStackContext-Cjy6I-Iv.d.ts} +4 -1
- package/dist/_dts/{Stage-CCWfQQWM.d.ts → Stage-D6t7o0pF.d.ts} +3 -2
- package/dist/_dts/{index-CsYqUHD3.d.ts → index-BTmaRy6a.d.ts} +10 -6
- package/dist/_dts/{types-CSm9jgol.d.ts → index-DCMA4Fy1.d.ts} +59 -113
- package/dist/_dts/{types-CRPSGFAp.d.ts → types-BUBSIZuV.d.ts} +8 -7
- package/dist/_dts/{useTrialState-CTZPgeZ7.d.ts → useTrialState-Bh8bzFgE.d.ts} +1 -1
- package/dist/canvas/index.d.ts +8 -6
- package/dist/canvas/index.js +6 -6
- package/dist/chrome/index.d.ts +7 -5
- package/dist/chrome/index.js +10 -9
- package/dist/chunk-2ES4W47N.js +8 -0
- package/dist/chunk-2ES4W47N.js.map +1 -0
- package/dist/{chunk-WPLAANH4.js → chunk-2N7UBSS2.js} +12 -12
- package/dist/chunk-2N7UBSS2.js.map +1 -0
- package/dist/{chunk-275JRRVD.js → chunk-E23Q6A3Y.js} +232 -37
- package/dist/chunk-E23Q6A3Y.js.map +1 -0
- package/dist/{chunk-GOZLNM4L.js → chunk-E6NARGU7.js} +5 -5
- package/dist/{chunk-GOZLNM4L.js.map → chunk-E6NARGU7.js.map} +1 -1
- package/dist/{chunk-IJJUK4FU.js → chunk-EESJUSYR.js} +5 -6
- package/dist/chunk-EESJUSYR.js.map +1 -0
- package/dist/{chunk-HXXTULDN.js → chunk-EFXRM6XQ.js} +8 -11
- package/dist/chunk-EFXRM6XQ.js.map +1 -0
- package/dist/chunk-F4ACIK7Z.js +210 -0
- package/dist/chunk-F4ACIK7Z.js.map +1 -0
- package/dist/{chunk-AWQG7FCC.js → chunk-FG3EXZDV.js} +3 -3
- package/dist/{chunk-AWQG7FCC.js.map → chunk-FG3EXZDV.js.map} +1 -1
- package/dist/{chunk-YT75VYRD.js → chunk-IBVRMHW5.js} +5 -5
- package/dist/{chunk-YT75VYRD.js.map → chunk-IBVRMHW5.js.map} +1 -1
- package/dist/{chunk-RWNDDKOH.js → chunk-PMLZP7RT.js} +18 -15
- package/dist/chunk-PMLZP7RT.js.map +1 -0
- package/dist/{chunk-Y23G2VZL.js → chunk-READTDQX.js} +2 -2
- package/dist/chunk-READTDQX.js.map +1 -0
- package/dist/{chunk-I5DUQ5HM.js → chunk-RODIY7HI.js} +75 -72
- package/dist/chunk-RODIY7HI.js.map +1 -0
- package/dist/{chunk-JVBYV6TL.js → chunk-ROGJWDQA.js} +3 -3
- package/dist/{chunk-JVBYV6TL.js.map → chunk-ROGJWDQA.js.map} +1 -1
- package/dist/{chunk-ZIFPHIWZ.js → chunk-T6DLGEJB.js} +12 -14
- package/dist/chunk-T6DLGEJB.js.map +1 -0
- package/dist/chunk-U6UQVGFP.js +15583 -0
- package/dist/chunk-U6UQVGFP.js.map +1 -0
- package/dist/{chunk-24W5VEJW.js → chunk-ULYYNWGP.js} +2 -2
- package/dist/{chunk-24W5VEJW.js.map → chunk-ULYYNWGP.js.map} +1 -1
- package/dist/{chunk-UNAZSLWE.js → chunk-VYVGVBGY.js} +17 -20
- package/dist/chunk-VYVGVBGY.js.map +1 -0
- package/dist/chunk-WFALF2X2.js +302 -0
- package/dist/chunk-WFALF2X2.js.map +1 -0
- package/dist/{chunk-KMJ7RWD3.js → chunk-WLMDTI2Q.js} +3 -3
- package/dist/{chunk-KMJ7RWD3.js.map → chunk-WLMDTI2Q.js.map} +1 -1
- package/dist/{chunk-T5IBSPAM.js → chunk-WWJDPNZZ.js} +43 -33
- package/dist/chunk-WWJDPNZZ.js.map +1 -0
- package/dist/{chunk-J7OWWTCW.js → chunk-XE5HJOPL.js} +5 -6
- package/dist/chunk-XE5HJOPL.js.map +1 -0
- package/dist/{chunk-7BO7HNGF.js → chunk-XRX5S5NT.js} +8 -61
- package/dist/chunk-XRX5S5NT.js.map +1 -0
- package/dist/chunk-XVUKDSYV.js +38 -0
- package/dist/chunk-XVUKDSYV.js.map +1 -0
- package/dist/{chunk-FMK7UQ6V.js → chunk-XZDCEMEL.js} +64 -47
- package/dist/chunk-XZDCEMEL.js.map +1 -0
- package/dist/{chunk-FZ7YFQN7.js → chunk-YCFKW5RX.js} +3 -3
- package/dist/chunk-YCFKW5RX.js.map +1 -0
- package/dist/{chunk-CQLQPQ4P.js → chunk-ZQIHJXP4.js} +2 -2
- package/dist/chunk-ZQIHJXP4.js.map +1 -0
- package/dist/config/index.d.ts +18 -10
- package/dist/config/index.js +4 -4
- package/dist/controls/index.d.ts +58 -9
- package/dist/controls/index.js +7 -5
- package/dist/dragdrop/index.d.ts +23 -3
- package/dist/dragdrop/index.js +4 -3
- package/dist/fonts/oswald-tabular-digits.woff2 +0 -0
- package/dist/index.d.ts +61 -715
- package/dist/index.js +456 -432
- package/dist/index.js.map +1 -1
- package/dist/job/index.d.ts +8 -0
- package/dist/job/index.js +1 -1
- package/dist/layers/index.d.ts +6 -5
- package/dist/layers/index.js +3 -2
- package/dist/loupe/index.d.ts +100 -30
- package/dist/loupe/index.js +585 -6
- package/dist/loupe/index.js.map +1 -1
- package/dist/overview/index.d.ts +4 -2
- package/dist/overview/index.js +15 -14
- package/dist/overview/index.js.map +1 -1
- package/dist/passthrough/weasel-canvas.d.ts +2 -0
- package/dist/passthrough/weasel-ui.d.ts +2 -2871
- package/dist/passthrough/weasel-ui.js +3 -2
- package/dist/primitives/index.d.ts +213 -3
- package/dist/primitives/index.js +8 -7
- package/dist/state/index.d.ts +7 -5
- package/dist/state/index.js +6 -6
- package/dist/styles.css +284 -51
- package/dist/surface/index.d.ts +10 -2
- package/dist/surface/index.js +1 -1
- package/dist/undo/index.d.ts +5 -3
- package/package.json +11 -11
- package/src/annotations/AnnotationOverlay.abandon.test.tsx +143 -0
- package/src/annotations/AnnotationOverlay.tsx +50 -31
- package/src/annotations/Annotations.overlay.test.tsx +31 -0
- package/src/annotations/capture.browser.test.ts +47 -0
- package/src/annotations/capture.ts +14 -7
- package/src/annotations/drawOne.ts +2 -2
- package/src/annotations/frac.test.ts +44 -1
- package/src/annotations/frac.ts +46 -18
- package/src/annotations/index.ts +1 -0
- package/src/annotations/paint.test.ts +74 -33
- package/src/annotations/paint.ts +70 -32
- package/src/annotations/store.test.ts +212 -2
- package/src/annotations/store.ts +130 -27
- package/src/annotations/svgNodes.test.ts +22 -10
- package/src/annotations/svgNodes.ts +3 -3
- package/src/annotations/types.ts +29 -10
- package/src/canvas/CameraInput.test.tsx +30 -0
- package/src/canvas/CameraInput.tsx +16 -15
- package/src/canvas/CanvasStack.tsx +5 -5
- package/src/canvas/CanvasStackContext.ts +4 -1
- package/src/canvas/Stage.test.tsx +34 -25
- package/src/canvas/Stage.tsx +3 -3
- package/src/canvas/cameraRegistry.ts +1 -0
- package/src/canvas/worldSpec.ts +3 -1
- package/src/chrome/builtins.test.ts +7 -6
- package/src/chrome/builtins.tsx +5 -1
- package/src/chrome/index.ts +1 -1
- package/src/chrome/labRegions.test.tsx +37 -26
- package/src/chrome/merge.test.ts +5 -0
- package/src/chrome/merge.ts +4 -2
- package/src/chrome/regions/PaletteRegion.test.tsx +4 -5
- package/src/chrome/regions/PaletteRegion.tsx +3 -4
- package/src/chrome/regions/ViewportRegion.browser.test.less +6 -0
- package/src/chrome/regions/ViewportRegion.browser.test.tsx +38 -0
- package/src/chrome/regions/ViewportRegion.less +6 -0
- package/src/chrome/regions/ViewportRegion.tsx +10 -4
- package/src/chrome/types.ts +4 -2
- package/src/config/auto.ts +1 -0
- package/src/config/builder.ts +6 -0
- package/src/config/types.ts +2 -1
- package/src/controls/ControlMatrix.less +245 -0
- package/src/controls/ControlMatrix.stories.less +13 -0
- package/src/controls/ControlMatrix.stories.tsx +135 -0
- package/src/controls/ControlMatrix.test.tsx +206 -0
- package/src/controls/ControlMatrix.tsx +446 -0
- package/src/controls/ControlPanel.test.tsx +37 -3
- package/src/controls/ControlPanel.tsx +16 -5
- package/src/controls/index.ts +6 -0
- package/src/dragdrop/DragDropRuntime.tsx +22 -13
- package/src/dragdrop/DragGhost.tsx +3 -0
- package/src/dragdrop/Palette.tsx +2 -0
- package/src/dragdrop/dragDrop.test.tsx +34 -33
- package/src/index.test.ts +5 -0
- package/src/index.ts +7 -1
- package/src/instrument/types.ts +0 -6
- package/src/job/useJob.ts +14 -9
- package/src/lab/Lab.chrome.test.tsx +15 -10
- package/src/lab/Lab.nebula.test.tsx +5 -5
- package/src/lab/Lab.surface.test.tsx +30 -21
- package/src/lab/Lab.test.tsx +75 -74
- package/src/lab/Lab.tsx +5 -1
- package/src/lab/Lab.undock.test.tsx +15 -10
- package/src/lab/LabHeader.test.tsx +15 -14
- package/src/lab/LabPalette.test.tsx +12 -7
- package/src/lab/LabShell.less +1 -1
- package/src/lab/LabSwitcher.browser.test.tsx +27 -0
- package/src/lab/LabSwitcher.less +2 -0
- package/src/lab/LabZoom.test.tsx +55 -44
- package/src/lab/LabZoom.tsx +4 -4
- package/src/lab/Workspace.surface.test.tsx +16 -13
- package/src/lab/Workspace.test.tsx +32 -27
- package/src/lab/Workspace.tsx +48 -52
- package/src/loupe/AGENTS.md +28 -15
- package/src/loupe/CanvasLoupe.tsx +2 -3
- package/src/loupe/DomLoupe.tsx +1 -7
- package/src/loupe/Loupe.less +16 -0
- package/src/loupe/TrialLoupe.tsx +120 -57
- package/src/loupe/index.ts +1 -6
- package/src/loupe/types.test.ts +3 -3
- package/src/loupe/types.ts +24 -27
- package/src/loupe/useLoupe.test.tsx +29 -4
- package/src/loupe/useLoupe.ts +20 -17
- package/src/overview/OverviewMarks.test.tsx +9 -8
- package/src/overview/OverviewMarks.tsx +12 -4
- package/src/overview/TrialOverview.test.tsx +18 -17
- package/src/overview/TrialOverview.tsx +3 -3
- package/src/passthrough/weasel-ui.test.ts +1 -1
- package/src/passthrough/weasel-ui.ts +1 -1
- package/src/primitives/FloatingPanel.tsx +7 -3
- package/src/primitives/FpsMeter.less +1 -2
- package/src/primitives/JobProgress.less +1 -1
- package/src/primitives/Readout.test.tsx +2 -2
- package/src/primitives/ScaleIndicator.less +1 -1
- package/src/primitives/Split.tsx +69 -41
- package/src/primitives/StatusBar.less +1 -1
- package/src/primitives/Toolbar.tsx +3 -3
- package/src/primitives/ZoomControl.less +1 -2
- package/src/specimen/Specimen.less +1 -0
- package/src/specimen/Specimen.tsx +0 -1
- package/src/state/undock.ts +3 -0
- package/src/state/useOpenOnce.ts +20 -16
- package/src/state/view.ts +1 -0
- package/src/styles.less +3 -0
- package/src/surface/index.ts +1 -1
- package/src/surface/useTiledSurface.test.tsx +14 -0
- package/src/surface/useTiledSurface.ts +11 -3
- package/src/trial/Trial.annotations.persist.test.tsx +20 -9
- package/src/trial/Trial.annotations.test.tsx +105 -27
- package/src/trial/Trial.canvas.test.tsx +21 -17
- package/src/trial/Trial.config.test.tsx +34 -27
- package/src/trial/Trial.less +0 -16
- package/src/trial/Trial.loupe.test.tsx +51 -25
- package/src/trial/Trial.targets.test.tsx +9 -8
- package/src/trial/Trial.test.tsx +51 -46
- package/src/trial/Trial.trialId.test.tsx +7 -6
- package/src/trial/Trial.tsx +74 -62
- package/src/trial/TrialChrome.tsx +6 -3
- package/src/trial/index.ts +2 -0
- package/src/trial/loupeSwitch.ts +15 -0
- package/dist/_dts/LayerList.d-DiuEArKq.d.ts +0 -83
- package/dist/_dts/PrefsRow.d-TOZ7BgD9.d.ts +0 -79
- package/dist/_dts/PropertyPanel.d-D-1paCbE.d.ts +0 -141
- package/dist/_dts/Select.d-Bensr_MT.d.ts +0 -579
- package/dist/_dts/index-B2Yj5aA0.d.ts +0 -274
- package/dist/chunk-275JRRVD.js.map +0 -1
- package/dist/chunk-4LJSC7MB.js +0 -143
- package/dist/chunk-4LJSC7MB.js.map +0 -1
- package/dist/chunk-7BO7HNGF.js.map +0 -1
- package/dist/chunk-CQLQPQ4P.js.map +0 -1
- package/dist/chunk-FMK7UQ6V.js.map +0 -1
- package/dist/chunk-FZ7YFQN7.js.map +0 -1
- package/dist/chunk-HXXTULDN.js.map +0 -1
- package/dist/chunk-I5DUQ5HM.js.map +0 -1
- package/dist/chunk-IJJUK4FU.js.map +0 -1
- package/dist/chunk-J7OWWTCW.js.map +0 -1
- package/dist/chunk-M7HP3FTK.js +0 -549
- package/dist/chunk-M7HP3FTK.js.map +0 -1
- package/dist/chunk-MIM5R4ZT.js +0 -14972
- package/dist/chunk-MIM5R4ZT.js.map +0 -1
- package/dist/chunk-RWNDDKOH.js.map +0 -1
- package/dist/chunk-T5IBSPAM.js.map +0 -1
- package/dist/chunk-UNAZSLWE.js.map +0 -1
- package/dist/chunk-WPLAANH4.js.map +0 -1
- package/dist/chunk-Y23G2VZL.js.map +0 -1
- package/dist/chunk-ZIFPHIWZ.js.map +0 -1
- package/src/primitives/useRovingTabIndex.test.ts +0 -48
- package/src/primitives/useRovingTabIndex.ts +0 -88
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { act, fireEvent,
|
|
1
|
+
import { act, fireEvent, screen, within } from '@testing-library/react';
|
|
2
|
+
import { renderSettled } from '@weasel-js/react/testing/renderSettled';
|
|
2
3
|
import { useState } from 'react';
|
|
3
4
|
import { beforeAll, describe, expect, it, vi } from 'vitest';
|
|
4
5
|
import { defineInstrument } from '../instrument/defineInstrument';
|
|
@@ -47,8 +48,8 @@ describe('fitStage', () => {
|
|
|
47
48
|
});
|
|
48
49
|
|
|
49
50
|
describe('<Stage>', () => {
|
|
50
|
-
it('draws its content through the camera', () => {
|
|
51
|
-
const { container } =
|
|
51
|
+
it('draws its content through the camera', async () => {
|
|
52
|
+
const { container } = await renderSettled(
|
|
52
53
|
<Stage
|
|
53
54
|
size={{ width: 300, height: 100 }}
|
|
54
55
|
view={{ zoom: 2, pan: { x: 10, y: 20 } }}
|
|
@@ -66,7 +67,7 @@ describe('<Stage>', () => {
|
|
|
66
67
|
expect(el.style.getPropertyValue('--lk-stage-h')).toBe('100px');
|
|
67
68
|
});
|
|
68
69
|
|
|
69
|
-
it('zooms on the wheel', () => {
|
|
70
|
+
it('zooms on the wheel', async () => {
|
|
70
71
|
function Host() {
|
|
71
72
|
const [view, setView] = useState<ViewTransform>({ zoom: 1, pan: { x: 0, y: 0 } });
|
|
72
73
|
return (
|
|
@@ -75,14 +76,14 @@ describe('<Stage>', () => {
|
|
|
75
76
|
</Stage>
|
|
76
77
|
);
|
|
77
78
|
}
|
|
78
|
-
const { container } =
|
|
79
|
+
const { container } = await renderSettled(<Host />);
|
|
79
80
|
const host = container.querySelector('.lk-stage');
|
|
80
81
|
if (!host) throw new Error('no stage');
|
|
81
82
|
fireEvent.wheel(host, { deltaY: -200, clientX: 0, clientY: 0 });
|
|
82
83
|
expect(zoomOf(content(container))).toBeGreaterThan(1);
|
|
83
84
|
});
|
|
84
85
|
|
|
85
|
-
it('zooms on a wheel handed to its camera from outside the stage', () => {
|
|
86
|
+
it('zooms on a wheel handed to its camera from outside the stage', async () => {
|
|
86
87
|
// What an annotation target's input box does: it is portalled out of the
|
|
87
88
|
// stage, so its wheel never bubbles here.
|
|
88
89
|
const slot: CameraWheelSlot = { current: null };
|
|
@@ -96,19 +97,19 @@ describe('<Stage>', () => {
|
|
|
96
97
|
</CameraWheelContext.Provider>
|
|
97
98
|
);
|
|
98
99
|
}
|
|
99
|
-
const { container } =
|
|
100
|
-
act(() => {
|
|
100
|
+
const { container } = await renderSettled(<Host />);
|
|
101
|
+
await act(async () => {
|
|
101
102
|
slot.current?.(new WheelEvent('wheel', { deltaY: -200, cancelable: true }));
|
|
102
103
|
});
|
|
103
104
|
expect(zoomOf(content(container))).toBeGreaterThan(1);
|
|
104
105
|
});
|
|
105
106
|
|
|
106
|
-
it('listens for the wheel actively, so it can keep the page from scrolling', () => {
|
|
107
|
+
it('listens for the wheel actively, so it can keep the page from scrolling', async () => {
|
|
107
108
|
// A proxy: jsdom does not enforce passive listeners, so a preventDefault in
|
|
108
109
|
// a passive one succeeds here and fails in a browser. React registers its
|
|
109
110
|
// wheel listeners passive; asserting the registration is what can fail.
|
|
110
111
|
const add = vi.spyOn(HTMLElement.prototype, 'addEventListener');
|
|
111
|
-
const { container } =
|
|
112
|
+
const { container } = await renderSettled(
|
|
112
113
|
<Stage
|
|
113
114
|
size={{ width: 100, height: 100 }}
|
|
114
115
|
view={{ zoom: 1, pan: { x: 0, y: 0 } }}
|
|
@@ -127,7 +128,7 @@ describe('<Stage>', () => {
|
|
|
127
128
|
add.mockRestore();
|
|
128
129
|
});
|
|
129
130
|
|
|
130
|
-
it('tells the surface its tiles moved when the camera does', () => {
|
|
131
|
+
it('tells the surface its tiles moved when the camera does', async () => {
|
|
131
132
|
// A transform moves a tile without resizing anything, which is the one
|
|
132
133
|
// kind of move a ResizeObserver never reports.
|
|
133
134
|
const invalidateRects = vi.fn();
|
|
@@ -139,7 +140,7 @@ describe('<Stage>', () => {
|
|
|
139
140
|
</Stage>
|
|
140
141
|
</SurfaceContext.Provider>
|
|
141
142
|
);
|
|
142
|
-
const { rerender } =
|
|
143
|
+
const { rerender } = await renderSettled(at({ zoom: 1, pan: { x: 0, y: 0 } }));
|
|
143
144
|
invalidateRects.mockClear();
|
|
144
145
|
rerender(at({ zoom: 1.5, pan: { x: 0, y: 0 } }));
|
|
145
146
|
expect(invalidateRects).toHaveBeenCalled();
|
|
@@ -162,25 +163,29 @@ const plain = defineInstrument<Record<string, never>, Record<string, never>>({
|
|
|
162
163
|
});
|
|
163
164
|
|
|
164
165
|
describe('an instrument that declares a stage', () => {
|
|
165
|
-
it('renders its DOM on the stage', () => {
|
|
166
|
-
const { container } =
|
|
166
|
+
it('renders its DOM on the stage', async () => {
|
|
167
|
+
const { container } = await renderSettled(
|
|
168
|
+
<Lab instruments={[staged]} defaultInstrument="Staged" />,
|
|
169
|
+
);
|
|
167
170
|
expect(content(container)).toContainElement(screen.getByTestId('art'));
|
|
168
171
|
});
|
|
169
172
|
|
|
170
|
-
it('gets the zoom controls, and they move the stage', () => {
|
|
171
|
-
const { container } =
|
|
173
|
+
it('gets the zoom controls, and they move the stage', async () => {
|
|
174
|
+
const { container } = await renderSettled(
|
|
175
|
+
<Lab instruments={[staged]} defaultInstrument="Staged" />,
|
|
176
|
+
);
|
|
172
177
|
const trialView = within(screen.getByRole('toolbar', { name: 'View' }));
|
|
173
|
-
act(() => {
|
|
178
|
+
await act(async () => {
|
|
174
179
|
fireEvent.click(trialView.getByRole('button', { name: 'Zoom in' }));
|
|
175
180
|
});
|
|
176
181
|
expect(zoomOf(content(container))).toBeCloseTo(1.25);
|
|
177
|
-
act(() => {
|
|
182
|
+
await act(async () => {
|
|
178
183
|
fireEvent.click(trialView.getByRole('button', { name: 'Actual size' }));
|
|
179
184
|
});
|
|
180
185
|
expect(zoomOf(content(container))).toBe(1);
|
|
181
186
|
});
|
|
182
187
|
|
|
183
|
-
it('zooms about the middle of the view, not the stage origin', () => {
|
|
188
|
+
it('zooms about the middle of the view, not the stage origin', async () => {
|
|
184
189
|
const rect = vi.spyOn(HTMLElement.prototype, 'getBoundingClientRect').mockReturnValue({
|
|
185
190
|
x: 0,
|
|
186
191
|
y: 0,
|
|
@@ -193,9 +198,11 @@ describe('an instrument that declares a stage', () => {
|
|
|
193
198
|
toJSON: () => ({}),
|
|
194
199
|
});
|
|
195
200
|
try {
|
|
196
|
-
const { container } =
|
|
201
|
+
const { container } = await renderSettled(
|
|
202
|
+
<Lab instruments={[staged]} defaultInstrument="Staged" />,
|
|
203
|
+
);
|
|
197
204
|
const trialView = within(screen.getByRole('toolbar', { name: 'View' }));
|
|
198
|
-
act(() => {
|
|
205
|
+
await act(async () => {
|
|
199
206
|
fireEvent.click(trialView.getByRole('button', { name: 'Zoom in' }));
|
|
200
207
|
});
|
|
201
208
|
const el = content(container);
|
|
@@ -208,12 +215,12 @@ describe('an instrument that declares a stage', () => {
|
|
|
208
215
|
}
|
|
209
216
|
});
|
|
210
217
|
|
|
211
|
-
it('is what earns them: DOM content without one gets none', () => {
|
|
212
|
-
|
|
218
|
+
it('is what earns them: DOM content without one gets none', async () => {
|
|
219
|
+
await renderSettled(<Lab instruments={[plain]} defaultInstrument="Plain" />);
|
|
213
220
|
expect(screen.queryByRole('toolbar', { name: 'View' })).toBeNull();
|
|
214
221
|
});
|
|
215
222
|
|
|
216
|
-
it('draws its overlay on the stage, outside the camera', () => {
|
|
223
|
+
it('draws its overlay on the stage, outside the camera', async () => {
|
|
217
224
|
const overlaid = defineInstrument<{ n: number }, Record<string, never>>({
|
|
218
225
|
name: 'Overlaid',
|
|
219
226
|
defaultConfig: () => ({}),
|
|
@@ -224,7 +231,9 @@ describe('an instrument that declares a stage', () => {
|
|
|
224
231
|
overlay: ({ state }) => <div data-testid="legend">{state.n}</div>,
|
|
225
232
|
},
|
|
226
233
|
});
|
|
227
|
-
const { container } =
|
|
234
|
+
const { container } = await renderSettled(
|
|
235
|
+
<Lab instruments={[overlaid]} defaultInstrument="Overlaid" />,
|
|
236
|
+
);
|
|
228
237
|
const legend = screen.getByTestId('legend');
|
|
229
238
|
expect(legend).toHaveTextContent('7');
|
|
230
239
|
expect(container.querySelector('.lk-stage')).toContainElement(legend);
|
package/src/canvas/Stage.tsx
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Yoke } from '@weasel-js/core';
|
|
2
|
+
import { useLatest } from '@weasel-js/core';
|
|
2
3
|
import { type CSSProperties, type ReactNode, useEffect, useMemo, useRef } from 'react';
|
|
3
4
|
import type { ViewTransform } from '../instrument/types';
|
|
4
5
|
import { normalize2DView } from '../state/view';
|
|
@@ -85,8 +86,7 @@ export function Stage({
|
|
|
85
86
|
[camera, size.width, size.height],
|
|
86
87
|
);
|
|
87
88
|
|
|
88
|
-
const onResizeRef =
|
|
89
|
-
onResizeRef.current = onResize;
|
|
89
|
+
const onResizeRef = useLatest(onResize);
|
|
90
90
|
useEffect(() => {
|
|
91
91
|
const el = host.current;
|
|
92
92
|
if (!el) return;
|
|
@@ -101,7 +101,7 @@ export function Stage({
|
|
|
101
101
|
const ro = new ResizeObserver(measure);
|
|
102
102
|
ro.observe(el);
|
|
103
103
|
return () => ro.disconnect();
|
|
104
|
-
}, []);
|
|
104
|
+
}, [onResizeRef]);
|
|
105
105
|
|
|
106
106
|
// A transform moves every tile inside the content without resizing anything,
|
|
107
107
|
// and a ResizeObserver reports only resizes.
|
|
@@ -15,6 +15,7 @@ export interface CameraRegistry {
|
|
|
15
15
|
subscribe(listener: () => void): () => void;
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
+
/** An empty in-memory `CameraRegistry`. */
|
|
18
19
|
export function createCameraRegistry(): CameraRegistry {
|
|
19
20
|
const stacks = new Map<string, CameraView[]>();
|
|
20
21
|
const listeners = new Set<() => void>();
|
package/src/canvas/worldSpec.ts
CHANGED
|
@@ -5,7 +5,7 @@ import type { Point, ViewTransform } from '../instrument/types';
|
|
|
5
5
|
* world (0,0) at the element's top-left, y growing downward. */
|
|
6
6
|
export interface WorldSpec {
|
|
7
7
|
/** Where world (0,0) sits at `pan` zero, as a fraction of the viewport.
|
|
8
|
-
* `{x:0,y:0}` top-left (default); `{x:0.5,y:0.5}`
|
|
8
|
+
* `{x:0,y:0}` top-left (default); `{x:0.5,y:0.5}` center. */
|
|
9
9
|
origin?: Point;
|
|
10
10
|
/** Which way the world y axis runs on screen. Default `'down'`. */
|
|
11
11
|
yAxis?: 'down' | 'up';
|
|
@@ -25,8 +25,10 @@ export interface WorldFrame {
|
|
|
25
25
|
yDir: 1 | -1;
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
+
/** The frame an absent `WorldSpec` resolves to: origin top-left, y down. */
|
|
28
29
|
export const DEFAULT_FRAME: WorldFrame = { originPx: { x: 0, y: 0 }, yDir: 1 };
|
|
29
30
|
|
|
31
|
+
/** Resolve `spec` against a viewport of `size`; `undefined` gives `DEFAULT_FRAME`. */
|
|
30
32
|
export function resolveFrame(spec: WorldSpec | undefined, size: ViewportSize): WorldFrame {
|
|
31
33
|
if (!spec) return DEFAULT_FRAME;
|
|
32
34
|
const origin = spec.origin ?? { x: 0, y: 0 };
|
|
@@ -23,6 +23,7 @@ const ctx: TrialChromeContext = {
|
|
|
23
23
|
canRedo: false,
|
|
24
24
|
undo: () => {},
|
|
25
25
|
redo: () => {},
|
|
26
|
+
hasLoupe: false,
|
|
26
27
|
loupeOn: false,
|
|
27
28
|
toggleLoupe: () => {},
|
|
28
29
|
configFields: [],
|
|
@@ -72,21 +73,21 @@ describe('builtinContributions', () => {
|
|
|
72
73
|
expect(ids({ ...bare, undo: {} })).toContain('redo');
|
|
73
74
|
});
|
|
74
75
|
|
|
75
|
-
it('contributes the loupe toggle only
|
|
76
|
+
it('contributes the loupe toggle only while a lens is mounted', () => {
|
|
76
77
|
expect(ids(bare)).not.toContain('loupe');
|
|
77
|
-
|
|
78
|
-
expect(
|
|
78
|
+
const mounted = builtinContributions(bare, { ...ctx, hasLoupe: true });
|
|
79
|
+
expect(mounted.map((c) => c.id)).toContain('loupe');
|
|
79
80
|
});
|
|
80
81
|
|
|
81
82
|
it('offers the loupe to a DOM instrument, which declares no canvas', () => {
|
|
82
|
-
const contributions = builtinContributions({ ...
|
|
83
|
+
const contributions = builtinContributions(bare, { ...ctx, hasLoupe: true });
|
|
83
84
|
const loupe = contributions.find((c) => c.id === 'loupe');
|
|
84
85
|
expect(loupe?.region).toBe('toolbar');
|
|
85
86
|
});
|
|
86
87
|
|
|
87
88
|
it('reports the loupe switch position, so the button reads as held', () => {
|
|
88
|
-
const off = builtinContributions({ ...
|
|
89
|
-
const on = builtinContributions({ ...
|
|
89
|
+
const off = builtinContributions(bare, { ...ctx, hasLoupe: true });
|
|
90
|
+
const on = builtinContributions(bare, { ...ctx, hasLoupe: true, loupeOn: true });
|
|
90
91
|
expect(off.find((c) => c.id === 'loupe')?.item).toMatchObject({ pressed: false });
|
|
91
92
|
expect(on.find((c) => c.id === 'loupe')?.item).toMatchObject({ pressed: true });
|
|
92
93
|
});
|
package/src/chrome/builtins.tsx
CHANGED
|
@@ -39,6 +39,10 @@ function groupFolds(
|
|
|
39
39
|
return out;
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
+
/** Built-in ids contributed only while runtime state says so — the loupe
|
|
43
|
+
* toggle appears once a lens mounts, after the trial's first render. */
|
|
44
|
+
export const TRANSIENT_BUILTINS: ReadonlySet<string> = new Set(['loupe']);
|
|
45
|
+
|
|
42
46
|
/**
|
|
43
47
|
* The contributions a trial gets from what its instrument declared. This is
|
|
44
48
|
* the whole "declaring a capability provides the chrome" rule — it replaces
|
|
@@ -93,7 +97,7 @@ export function builtinContributions(
|
|
|
93
97
|
});
|
|
94
98
|
}
|
|
95
99
|
|
|
96
|
-
if (
|
|
100
|
+
if (ctx.hasLoupe) {
|
|
97
101
|
out.push({
|
|
98
102
|
id: 'loupe',
|
|
99
103
|
region: 'toolbar',
|
package/src/chrome/index.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { fireEvent, render, screen } from '@testing-library/react';
|
|
1
|
+
import { act, fireEvent, render, screen } from '@testing-library/react';
|
|
2
|
+
import { renderSettled } from '@weasel-js/react/testing/renderSettled';
|
|
2
3
|
import { describe, expect, it, vi } from 'vitest';
|
|
3
4
|
import type { Instrument } from '../instrument/types';
|
|
4
5
|
import { Lab } from '../lab/Lab';
|
|
@@ -19,19 +20,19 @@ function silenceRenderError(): void {
|
|
|
19
20
|
}
|
|
20
21
|
|
|
21
22
|
describe('lab-level chrome', () => {
|
|
22
|
-
it('renders a header contribution beside the lab header', () => {
|
|
23
|
+
it('renders a header contribution beside the lab header', async () => {
|
|
23
24
|
const contribution: LabContribution = {
|
|
24
25
|
id: 'publish',
|
|
25
26
|
region: 'header',
|
|
26
27
|
item: { icon: Glyph, label: 'Publish', onActivate: () => {} },
|
|
27
28
|
};
|
|
28
|
-
|
|
29
|
+
await renderSettled(
|
|
29
30
|
<Lab title="T" instruments={[bare]} defaultInstrument="Bare" labChrome={[contribution]} />,
|
|
30
31
|
);
|
|
31
32
|
expect(screen.getByRole('button', { name: 'Publish' })).toBeInTheDocument();
|
|
32
33
|
});
|
|
33
34
|
|
|
34
|
-
it('hands a header contribution the lab, not a trial', () => {
|
|
35
|
+
it('hands a header contribution the lab, not a trial', async () => {
|
|
35
36
|
const contribution: LabContribution = {
|
|
36
37
|
id: 'another',
|
|
37
38
|
region: 'header',
|
|
@@ -41,21 +42,23 @@ describe('lab-level chrome', () => {
|
|
|
41
42
|
onActivate: (ctx) => ctx.addTrial('Bare'),
|
|
42
43
|
},
|
|
43
44
|
};
|
|
44
|
-
|
|
45
|
+
await renderSettled(
|
|
45
46
|
<Lab title="T" instruments={[bare]} defaultInstrument="Bare" labChrome={[contribution]} />,
|
|
46
47
|
);
|
|
47
48
|
expect(screen.getAllByLabelText(/^Trial /)).toHaveLength(1);
|
|
48
|
-
|
|
49
|
+
await act(async () => {
|
|
50
|
+
fireEvent.click(screen.getByRole('button', { name: 'Another' }));
|
|
51
|
+
});
|
|
49
52
|
expect(screen.getAllByLabelText(/^Trial /)).toHaveLength(2);
|
|
50
53
|
});
|
|
51
54
|
|
|
52
|
-
it('renders a palette contribution in the lab rail and selects it', () => {
|
|
55
|
+
it('renders a palette contribution in the lab rail and selects it', async () => {
|
|
53
56
|
const contribution: LabContribution = {
|
|
54
57
|
id: 'measure',
|
|
55
58
|
region: 'palette',
|
|
56
59
|
item: { icon: Glyph, label: 'Measure' },
|
|
57
60
|
};
|
|
58
|
-
|
|
61
|
+
await renderSettled(
|
|
59
62
|
<Lab title="T" instruments={[bare]} defaultInstrument="Bare" labChrome={[contribution]} />,
|
|
60
63
|
);
|
|
61
64
|
const button = screen.getByRole('button', { name: 'Measure' });
|
|
@@ -63,7 +66,7 @@ describe('lab-level chrome', () => {
|
|
|
63
66
|
expect(screen.getByRole('button', { name: 'Measure' })).toHaveAttribute('aria-current', 'true');
|
|
64
67
|
});
|
|
65
68
|
|
|
66
|
-
it('gives a palette contribution the whole lab context, not a tool slot alone', () => {
|
|
69
|
+
it('gives a palette contribution the whole lab context, not a tool slot alone', async () => {
|
|
67
70
|
// The shape the `as unknown as TrialChromeContext` cast used to fake: a
|
|
68
71
|
// contribution reading anything past the tool slot got `undefined`.
|
|
69
72
|
const seen: LabChromeContext[] = [];
|
|
@@ -75,7 +78,7 @@ describe('lab-level chrome', () => {
|
|
|
75
78
|
return <span>{`${ctx.trials.length} trial(s), mode ${ctx.mode}`}</span>;
|
|
76
79
|
},
|
|
77
80
|
};
|
|
78
|
-
|
|
81
|
+
await renderSettled(
|
|
79
82
|
<Lab title="T" instruments={[bare]} defaultInstrument="Bare" labChrome={[contribution]} />,
|
|
80
83
|
);
|
|
81
84
|
expect(screen.getByText('1 trial(s), mode auto')).toBeInTheDocument();
|
|
@@ -83,19 +86,19 @@ describe('lab-level chrome', () => {
|
|
|
83
86
|
expect(typeof seen[0]?.saveSnapshot).toBe('function');
|
|
84
87
|
});
|
|
85
88
|
|
|
86
|
-
it('renders a footer contribution', () => {
|
|
89
|
+
it('renders a footer contribution', async () => {
|
|
87
90
|
const contribution: LabContribution = {
|
|
88
91
|
id: 'count',
|
|
89
92
|
region: 'footer',
|
|
90
93
|
item: { text: 'idle' },
|
|
91
94
|
};
|
|
92
|
-
|
|
95
|
+
await renderSettled(
|
|
93
96
|
<Lab title="T" instruments={[bare]} defaultInstrument="Bare" labChrome={[contribution]} />,
|
|
94
97
|
);
|
|
95
98
|
expect(screen.getByText('idle')).toBeInTheDocument();
|
|
96
99
|
});
|
|
97
100
|
|
|
98
|
-
it('throws when two lab contributions share an id', () => {
|
|
101
|
+
it('throws when two lab contributions share an id', async () => {
|
|
99
102
|
silenceRenderError();
|
|
100
103
|
const clash: LabContribution[] = [
|
|
101
104
|
{ id: 'same', region: 'header', item: { icon: Glyph, label: 'One', onActivate: () => {} } },
|
|
@@ -106,7 +109,7 @@ describe('lab-level chrome', () => {
|
|
|
106
109
|
).toThrow(/duplicate contribution id "same"/);
|
|
107
110
|
});
|
|
108
111
|
|
|
109
|
-
it('keeps the lab tool slot and lab palette contributions in one namespace', () => {
|
|
112
|
+
it('keeps the lab tool slot and lab palette contributions in one namespace', async () => {
|
|
110
113
|
silenceRenderError();
|
|
111
114
|
const clash: LabContribution = {
|
|
112
115
|
id: 'pick',
|
|
@@ -134,8 +137,8 @@ describe('lab sidebar region', () => {
|
|
|
134
137
|
item: { title: 'Stories', body: <p>story list</p> },
|
|
135
138
|
};
|
|
136
139
|
|
|
137
|
-
it('renders a section beside the workspace', () => {
|
|
138
|
-
const { container } =
|
|
140
|
+
it('renders a section beside the workspace', async () => {
|
|
141
|
+
const { container } = await renderSettled(
|
|
139
142
|
<Lab title="T" instruments={[bare]} defaultInstrument="Bare" labChrome={[tree]} />,
|
|
140
143
|
);
|
|
141
144
|
expect(screen.getByText('story list')).toBeInTheDocument();
|
|
@@ -143,18 +146,22 @@ describe('lab sidebar region', () => {
|
|
|
143
146
|
expect(screen.getByRole('separator', { name: /lab sidebar/i })).toBeInTheDocument();
|
|
144
147
|
});
|
|
145
148
|
|
|
146
|
-
it('folds a section', () => {
|
|
147
|
-
|
|
149
|
+
it('folds a section', async () => {
|
|
150
|
+
await renderSettled(
|
|
151
|
+
<Lab title="T" instruments={[bare]} defaultInstrument="Bare" labChrome={[tree]} />,
|
|
152
|
+
);
|
|
148
153
|
fireEvent.click(screen.getByRole('button', { name: 'Stories' }));
|
|
149
154
|
expect(screen.queryByText('story list')).toBeNull();
|
|
150
155
|
});
|
|
151
156
|
|
|
152
|
-
it('offers no tear-out at lab level', () => {
|
|
153
|
-
|
|
157
|
+
it('offers no tear-out at lab level', async () => {
|
|
158
|
+
await renderSettled(
|
|
159
|
+
<Lab title="T" instruments={[bare]} defaultInstrument="Bare" labChrome={[tree]} />,
|
|
160
|
+
);
|
|
154
161
|
expect(screen.queryByRole('button', { name: 'Undock Stories' })).toBeNull();
|
|
155
162
|
});
|
|
156
163
|
|
|
157
|
-
it('hands a render contribution the lab context', () => {
|
|
164
|
+
it('hands a render contribution the lab context', async () => {
|
|
158
165
|
const seen: LabChromeContext[] = [];
|
|
159
166
|
const probe: LabContribution = {
|
|
160
167
|
id: 'probe',
|
|
@@ -164,13 +171,17 @@ describe('lab sidebar region', () => {
|
|
|
164
171
|
return <p>probe</p>;
|
|
165
172
|
},
|
|
166
173
|
};
|
|
167
|
-
|
|
174
|
+
await renderSettled(
|
|
175
|
+
<Lab title="T" instruments={[bare]} defaultInstrument="Bare" labChrome={[probe]} />,
|
|
176
|
+
);
|
|
168
177
|
expect(screen.getByText('probe')).toBeInTheDocument();
|
|
169
178
|
expect(seen[0]?.trials).toHaveLength(1);
|
|
170
179
|
});
|
|
171
180
|
|
|
172
|
-
it('adds no strip to a lab without sidebar chrome', () => {
|
|
173
|
-
const { container } =
|
|
181
|
+
it('adds no strip to a lab without sidebar chrome', async () => {
|
|
182
|
+
const { container } = await renderSettled(
|
|
183
|
+
<Lab title="T" instruments={[bare]} defaultInstrument="Bare" />,
|
|
184
|
+
);
|
|
174
185
|
expect(container.querySelector('.lk-lab__panes')).toBeNull();
|
|
175
186
|
});
|
|
176
187
|
});
|
|
@@ -182,7 +193,7 @@ describe('lab chrome without <Lab>', () => {
|
|
|
182
193
|
undo: () => void;
|
|
183
194
|
}
|
|
184
195
|
|
|
185
|
-
it('renders a contribution typed against a consumer context and hands it back', () => {
|
|
196
|
+
it('renders a contribution typed against a consumer context and hands it back', async () => {
|
|
186
197
|
const undo = vi.fn();
|
|
187
198
|
const ctx: RailCtx = { undo };
|
|
188
199
|
const rail: readonly LabContribution<RailCtx>[] = [
|
|
@@ -192,7 +203,7 @@ describe('lab chrome without <Lab>', () => {
|
|
|
192
203
|
item: { icon: Glyph, label: 'Undo', showLabel: true, onActivate: (c) => c.undo() },
|
|
193
204
|
},
|
|
194
205
|
];
|
|
195
|
-
|
|
206
|
+
await renderSettled(
|
|
196
207
|
<LabShell
|
|
197
208
|
title="Palette lab"
|
|
198
209
|
header={
|
package/src/chrome/merge.test.ts
CHANGED
|
@@ -44,6 +44,11 @@ describe('suppressContributions', () => {
|
|
|
44
44
|
);
|
|
45
45
|
});
|
|
46
46
|
|
|
47
|
+
it('passes over an absent id it was told may come and go', () => {
|
|
48
|
+
const out = suppressContributions([toolbarItem('a')], ['late'], new Set(['late']));
|
|
49
|
+
expect(out.map((c) => c.id)).toEqual(['a']);
|
|
50
|
+
});
|
|
51
|
+
|
|
47
52
|
it('is a no-op for an empty suppress list', () => {
|
|
48
53
|
const bundle = [toolbarItem('a')];
|
|
49
54
|
expect(suppressContributions(bundle, [])).toEqual(bundle);
|
package/src/chrome/merge.ts
CHANGED
|
@@ -30,15 +30,17 @@ export function mergeContributions<T extends { id: string }>(
|
|
|
30
30
|
/**
|
|
31
31
|
* Drop contributions by id. Throws when an id is not present: a typo that
|
|
32
32
|
* silently suppresses nothing is the same class of bug as a duplicate id
|
|
33
|
-
* silently winning.
|
|
33
|
+
* silently winning. `transient` names ids that are legitimately absent at
|
|
34
|
+
* times — contributed only while some runtime state holds.
|
|
34
35
|
*/
|
|
35
36
|
export function suppressContributions<T extends { id: string }>(
|
|
36
37
|
bundle: readonly T[],
|
|
37
38
|
ids: readonly string[],
|
|
39
|
+
transient: ReadonlySet<string> = new Set(),
|
|
38
40
|
): T[] {
|
|
39
41
|
const present = new Set(bundle.map((c) => c.id));
|
|
40
42
|
for (const id of ids) {
|
|
41
|
-
if (!present.has(id)) {
|
|
43
|
+
if (!present.has(id) && !transient.has(id)) {
|
|
42
44
|
throw new Error(
|
|
43
45
|
`[labkit] cannot suppress "${id}": no contribution with that id. ` +
|
|
44
46
|
`Present ids: ${[...present].join(', ')}`,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { fireEvent, render, screen } from '@testing-library/react';
|
|
1
|
+
import { act, fireEvent, render, screen } from '@testing-library/react';
|
|
2
2
|
import { describe, expect, it, vi } from 'vitest';
|
|
3
3
|
import type { RegionContribution, ToolSlotContext } from '../types';
|
|
4
4
|
import { PaletteRegion } from './PaletteRegion';
|
|
@@ -62,16 +62,15 @@ describe('PaletteRegion', () => {
|
|
|
62
62
|
|
|
63
63
|
it('walks the vertical strip with ArrowDown, and leaves ArrowRight to the page', () => {
|
|
64
64
|
render(<PaletteRegion contributions={[tool('brush'), tool('eraser')]} ctx={ctxWith(null)} />);
|
|
65
|
-
const strip = screen.getByRole('toolbar', { name: 'Tools' });
|
|
66
65
|
const brush = screen.getByRole('button', { name: 'brush' });
|
|
67
66
|
const eraser = screen.getByRole('button', { name: 'eraser' });
|
|
68
67
|
|
|
69
|
-
brush.focus();
|
|
70
|
-
fireEvent.keyDown(
|
|
68
|
+
act(() => brush.focus());
|
|
69
|
+
fireEvent.keyDown(brush, { key: 'ArrowDown' });
|
|
71
70
|
expect(document.activeElement).toBe(eraser);
|
|
72
71
|
expect(eraser).toHaveAttribute('tabindex', '0');
|
|
73
72
|
|
|
74
|
-
fireEvent.keyDown(
|
|
73
|
+
fireEvent.keyDown(eraser, { key: 'ArrowRight' });
|
|
75
74
|
expect(document.activeElement).toBe(eraser);
|
|
76
75
|
});
|
|
77
76
|
});
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import { ToolButton, ToolGroup } from '../../passthrough/weasel-ui';
|
|
2
|
-
import { useRovingTabIndex } from '../../primitives/useRovingTabIndex';
|
|
1
|
+
import { ToolButton, ToolGroup, useRovingTabIndex } from '../../passthrough/weasel-ui';
|
|
3
2
|
import type { RegionContribution, ToolItem, ToolSlotContext, TrialChromeContext } from '../types';
|
|
4
3
|
|
|
5
4
|
/** Props for `<PaletteRegion>`. */
|
|
@@ -19,11 +18,11 @@ export function PaletteRegion<TCtx extends ToolSlotContext = TrialChromeContext>
|
|
|
19
18
|
ctx,
|
|
20
19
|
region = 'palette',
|
|
21
20
|
}: PaletteRegionProps<TCtx>) {
|
|
22
|
-
const {
|
|
21
|
+
const { rootRef, onKeyDown } = useRovingTabIndex<HTMLDivElement>({ orientation: 'vertical' });
|
|
23
22
|
if (contributions.length === 0) return null;
|
|
24
23
|
return (
|
|
25
24
|
<div
|
|
26
|
-
ref={
|
|
25
|
+
ref={rootRef}
|
|
27
26
|
className="lk-palette-region"
|
|
28
27
|
role="toolbar"
|
|
29
28
|
aria-label="Tools"
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import '@weasel-js/theme/tokens.css';
|
|
2
|
+
import '../../styles.less';
|
|
3
|
+
import './ViewportRegion.browser.test.less';
|
|
4
|
+
import { cleanup, render } from '@testing-library/react';
|
|
5
|
+
import { afterEach, expect, test } from 'vitest';
|
|
6
|
+
import { ZoomControl } from '../../primitives/ZoomControl';
|
|
7
|
+
import type { TrialChromeContext, TrialContribution } from '../types';
|
|
8
|
+
import { ViewportRegion } from './ViewportRegion';
|
|
9
|
+
|
|
10
|
+
// Whether a rendered control shrinks to the cluster or runs out past it is
|
|
11
|
+
// layout, which only a real browser has.
|
|
12
|
+
|
|
13
|
+
afterEach(cleanup);
|
|
14
|
+
|
|
15
|
+
const ctx = { zoom: 1, setZoom: () => {} } as unknown as TrialChromeContext;
|
|
16
|
+
|
|
17
|
+
const contributions: TrialContribution[] = [
|
|
18
|
+
{
|
|
19
|
+
id: 'zoom-control',
|
|
20
|
+
region: 'viewport',
|
|
21
|
+
render: (c) => <ZoomControl zoom={c.zoom ?? 1} onZoomChange={c.setZoom} />,
|
|
22
|
+
},
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
test('a zoom control on its own row shrinks its slider to fit a narrow well', () => {
|
|
26
|
+
const { container } = render(
|
|
27
|
+
<div className="lk-root">
|
|
28
|
+
<div className="lk-viewport-test-well">
|
|
29
|
+
<ViewportRegion contributions={contributions} ctx={ctx} />
|
|
30
|
+
</div>
|
|
31
|
+
</div>,
|
|
32
|
+
);
|
|
33
|
+
const cluster = container.querySelector('.lk-viewport-controls')?.getBoundingClientRect();
|
|
34
|
+
const zoom = container.querySelector('.lk-zoom')?.getBoundingClientRect();
|
|
35
|
+
if (!cluster || !zoom) throw new Error('nothing rendered');
|
|
36
|
+
expect(zoom.left).toBeGreaterThanOrEqual(cluster.left);
|
|
37
|
+
expect(zoom.right).toBeLessThanOrEqual(cluster.right);
|
|
38
|
+
});
|
|
@@ -17,6 +17,12 @@
|
|
|
17
17
|
backdrop-filter: blur(var(--wzl-glass-blur));
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
+
// Without this a rendered control's floor is its content's min-content, and the
|
|
21
|
+
// zoom slider's fixed width then runs it out past the cluster on a narrow well.
|
|
22
|
+
.lk-viewport-controls__item {
|
|
23
|
+
min-width: 0;
|
|
24
|
+
}
|
|
25
|
+
|
|
20
26
|
.lk-viewport-controls__button {
|
|
21
27
|
display: inline-flex;
|
|
22
28
|
align-items: center;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useRovingTabIndex } from '../../
|
|
1
|
+
import { useRovingTabIndex } from '../../passthrough/weasel-ui';
|
|
2
2
|
import type { TrialChromeContext, TrialContribution } from '../types';
|
|
3
3
|
|
|
4
4
|
/** Props for `<ViewportRegion>`. */
|
|
@@ -12,18 +12,24 @@ export interface ViewportRegionProps {
|
|
|
12
12
|
* rather than in the toolbar, which acts on the trial itself.
|
|
13
13
|
*/
|
|
14
14
|
export function ViewportRegion({ contributions, ctx }: ViewportRegionProps) {
|
|
15
|
-
const {
|
|
15
|
+
const { rootRef, onKeyDown } = useRovingTabIndex<HTMLDivElement>({ orientation: 'horizontal' });
|
|
16
16
|
if (contributions.length === 0) return null;
|
|
17
17
|
return (
|
|
18
18
|
<div
|
|
19
|
-
ref={
|
|
19
|
+
ref={rootRef}
|
|
20
20
|
className="lk-viewport-controls"
|
|
21
21
|
role="toolbar"
|
|
22
22
|
aria-label="View"
|
|
23
23
|
onKeyDown={onKeyDown}
|
|
24
24
|
>
|
|
25
25
|
{contributions.map((c) => {
|
|
26
|
-
if (c.render)
|
|
26
|
+
if (c.render) {
|
|
27
|
+
return (
|
|
28
|
+
<span key={c.id} className="lk-viewport-controls__item">
|
|
29
|
+
{c.render(ctx)}
|
|
30
|
+
</span>
|
|
31
|
+
);
|
|
32
|
+
}
|
|
27
33
|
if (c.region !== 'viewport' || !c.item) return null;
|
|
28
34
|
const { icon: Icon, label, disabled, onActivate } = c.item;
|
|
29
35
|
return (
|
package/src/chrome/types.ts
CHANGED
|
@@ -162,8 +162,10 @@ export interface TrialChromeContext extends ToolSlotContext, SidebarSlotContext
|
|
|
162
162
|
undo: () => void;
|
|
163
163
|
redo: () => void;
|
|
164
164
|
|
|
165
|
-
/** Whether the trial's
|
|
166
|
-
*
|
|
165
|
+
/** Whether a `<TrialLoupe>` following the trial's toggle is mounted in it.
|
|
166
|
+
* The loupe toggle is offered only while one is. */
|
|
167
|
+
hasLoupe: boolean;
|
|
168
|
+
/** Whether the trial's loupe is turned on. */
|
|
167
169
|
loupeOn: boolean;
|
|
168
170
|
toggleLoupe: () => void;
|
|
169
171
|
|