@carto/ps-react-ui 4.15.0 → 4.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components.js +786 -749
- package/dist/components.js.map +1 -1
- package/dist/legend/stores.js +20 -16
- package/dist/legend-store-registry-CVYzhR1-.js +464 -0
- package/dist/legend-store-registry-CVYzhR1-.js.map +1 -0
- package/dist/legend.js +1478 -794
- package/dist/legend.js.map +1 -1
- package/dist/types/components/responsive-drawer/responsive-drawer.d.ts +2 -9
- package/dist/types/legend/components/a11y.d.ts +6 -0
- package/dist/types/legend/components/contexts.d.ts +22 -4
- package/dist/types/legend/components/index.d.ts +2 -2
- package/dist/types/legend/components/legend-actions/legend-actions.d.ts +4 -3
- package/dist/types/legend/components/legend-group/legend-group.d.ts +2 -11
- package/dist/types/legend/components/legend-group/styles.d.ts +40 -2
- package/dist/types/legend/components/legend-group-menu/legend-group-menu-items.d.ts +28 -0
- package/dist/types/legend/components/legend-group-menu/legend-group-menu.d.ts +11 -0
- package/dist/types/legend/components/legend-opacity/legend-opacity.d.ts +7 -17
- package/dist/types/legend/components/legend-opacity/styles.d.ts +11 -6
- package/dist/types/legend/components/legend-overflow-menu/legend-overflow-menu.d.ts +12 -0
- package/dist/types/legend/components/legend-panel-menu/legend-panel-menu.d.ts +11 -0
- package/dist/types/legend/components/legend-row/legend-row.d.ts +4 -11
- package/dist/types/legend/components/legend-row/styles.d.ts +36 -1
- package/dist/types/legend/components/legend-row-menu/legend-row-menu-items.d.ts +12 -0
- package/dist/types/legend/components/legend-row-menu/legend-row-menu.d.ts +4 -10
- package/dist/types/legend/components/legend-row-menu/legend-zoom-to.d.ts +3 -8
- package/dist/types/legend/components/legend-sortable/contexts.d.ts +7 -0
- package/dist/types/legend/components/legend-sortable/legend-sortable.d.ts +11 -0
- package/dist/types/legend/components/legend-sortable/sortable-entity.d.ts +27 -0
- package/dist/types/legend/components/legend-sortable/sortable-ids.d.ts +6 -0
- package/dist/types/legend/components/legend-sortable/styles.d.ts +54 -0
- package/dist/types/legend/components/legend-visible-layers/legend-visible-layers.d.ts +7 -0
- package/dist/types/legend/components/legend-visible-layers/styles.d.ts +48 -0
- package/dist/types/legend/components/run-async-menu-action.d.ts +7 -0
- package/dist/types/legend/index.d.ts +53 -6
- package/dist/types/legend/provider/labels.d.ts +21 -0
- package/dist/types/legend/stores/index.d.ts +1 -1
- package/dist/types/legend/stores/selectors.d.ts +31 -0
- package/dist/types/legend/stores/types.d.ts +11 -0
- package/package.json +3 -3
- package/src/components/responsive-drawer/responsive-drawer.test.tsx +138 -0
- package/src/components/responsive-drawer/responsive-drawer.tsx +77 -51
- package/src/legend/components/a11y.ts +14 -0
- package/src/legend/components/contexts.ts +29 -4
- package/src/legend/components/index.ts +3 -4
- package/src/legend/components/legend-actions/legend-actions.tsx +4 -3
- package/src/legend/components/legend-group/legend-group.test.tsx +166 -9
- package/src/legend/components/legend-group/legend-group.tsx +88 -49
- package/src/legend/components/legend-group/styles.ts +59 -2
- package/src/legend/components/legend-group-menu/legend-group-menu-items.tsx +132 -0
- package/src/legend/components/legend-group-menu/legend-group-menu.tsx +25 -0
- package/src/legend/components/legend-opacity/legend-opacity.tsx +43 -70
- package/src/legend/components/legend-opacity/styles.ts +14 -9
- package/src/legend/components/legend-overflow-menu/legend-overflow-menu.tsx +84 -0
- package/src/legend/components/legend-panel-menu/legend-panel-menu.tsx +23 -0
- package/src/legend/components/legend-ramp/legend-ramp-ui.tsx +2 -1
- package/src/legend/components/legend-row/legend-row.test.tsx +304 -85
- package/src/legend/components/legend-row/legend-row.tsx +73 -50
- package/src/legend/components/legend-row/styles.ts +54 -1
- package/src/legend/components/legend-row-menu/legend-row-menu-items.tsx +57 -0
- package/src/legend/components/legend-row-menu/legend-row-menu.tsx +12 -50
- package/src/legend/components/legend-row-menu/legend-zoom-to.tsx +25 -17
- package/src/legend/components/legend-sortable/contexts.ts +12 -0
- package/src/legend/components/legend-sortable/legend-sortable.test.tsx +144 -0
- package/src/legend/components/legend-sortable/legend-sortable.tsx +195 -0
- package/src/legend/components/legend-sortable/sortable-entity.tsx +91 -0
- package/src/legend/components/legend-sortable/sortable-ids.ts +17 -0
- package/src/legend/components/legend-sortable/styles.ts +56 -0
- package/src/legend/components/legend-visibility-toggle/legend-visibility-toggle.tsx +14 -8
- package/src/legend/components/legend-visible-layers/legend-visible-layers.test.tsx +86 -0
- package/src/legend/components/legend-visible-layers/legend-visible-layers.tsx +135 -0
- package/src/legend/components/legend-visible-layers/styles.ts +60 -0
- package/src/legend/components/run-async-menu-action.ts +20 -0
- package/src/legend/index.ts +59 -4
- package/src/legend/provider/labels.ts +43 -1
- package/src/legend/stores/index.ts +4 -0
- package/src/legend/stores/legend-store-registry.ts +60 -0
- package/src/legend/stores/legend-store.test.ts +116 -0
- package/src/legend/stores/selectors.ts +64 -2
- package/src/legend/stores/types.ts +11 -0
- package/src/widgets/actions/download/exports.test.tsx +14 -1
- package/dist/selectors-DwHj6mE2.js +0 -388
- package/dist/selectors-DwHj6mE2.js.map +0 -1
|
@@ -3,10 +3,15 @@ import { fireEvent, render, screen } from '@testing-library/react'
|
|
|
3
3
|
import { MenuItem } from '@mui/material'
|
|
4
4
|
import { LegendProvider } from '../../provider'
|
|
5
5
|
import { LegendRow } from './legend-row'
|
|
6
|
+
import { LegendGroup } from '../legend-group/legend-group'
|
|
6
7
|
import { LegendActions } from '../legend-actions/legend-actions'
|
|
7
8
|
import { LegendVisibilityToggle } from '../legend-visibility-toggle/legend-visibility-toggle'
|
|
8
9
|
import { LegendRowMenu } from '../legend-row-menu/legend-row-menu'
|
|
9
10
|
import { LegendZoomTo } from '../legend-row-menu/legend-zoom-to'
|
|
11
|
+
import {
|
|
12
|
+
LegendShowOnlyLayer,
|
|
13
|
+
LegendShowAllLayers,
|
|
14
|
+
} from '../legend-row-menu/legend-row-menu-items'
|
|
10
15
|
import { LegendOpacity } from '../legend-opacity/legend-opacity'
|
|
11
16
|
import { LegendConfigSelect } from '../legend-config-select/legend-config-select'
|
|
12
17
|
import { LegendItem } from '../legend-item/legend-item'
|
|
@@ -25,37 +30,42 @@ const cat: LegendVariable = {
|
|
|
25
30
|
afterEach(() => clearAllLegendStores())
|
|
26
31
|
|
|
27
32
|
/**
|
|
28
|
-
* Full row composition (the module shape):
|
|
29
|
-
*
|
|
30
|
-
*
|
|
33
|
+
* Full row composition (the module shape): eye + ⋮ menu in the header slot
|
|
34
|
+
* (menu last → rightmost), the inline opacity bar as first body child, then
|
|
35
|
+
* config-select + item. Menu items: built-ins first, ZoomTo (app extra) last.
|
|
31
36
|
*/
|
|
32
37
|
function renderRow(
|
|
33
38
|
id: string,
|
|
34
39
|
layers: LegendLayerInput[],
|
|
35
|
-
callbacks: { onZoomTo?: (id: string) => void } = {},
|
|
40
|
+
callbacks: { onZoomTo?: (id: string) => void | Promise<void> } = {},
|
|
36
41
|
layerId = layers[0]!.id,
|
|
37
42
|
) {
|
|
38
43
|
return render(
|
|
39
44
|
<LegendProvider id={id} layers={layers}>
|
|
40
45
|
<LegendRow layerId={layerId}>
|
|
41
|
-
{/* L→R: options · opacity · visibility (chevron appended after). */}
|
|
42
46
|
<LegendActions>
|
|
43
|
-
{callbacks.onZoomTo && (
|
|
44
|
-
<LegendRowMenu>
|
|
45
|
-
<LegendZoomTo onZoomTo={callbacks.onZoomTo} />
|
|
46
|
-
</LegendRowMenu>
|
|
47
|
-
)}
|
|
48
|
-
<LegendOpacity.Trigger />
|
|
49
47
|
<LegendVisibilityToggle />
|
|
48
|
+
<LegendRowMenu>
|
|
49
|
+
<LegendShowOnlyLayer />
|
|
50
|
+
<LegendShowAllLayers />
|
|
51
|
+
<LegendOpacity.Item />
|
|
52
|
+
{callbacks.onZoomTo && (
|
|
53
|
+
<LegendZoomTo onZoomTo={callbacks.onZoomTo} />
|
|
54
|
+
)}
|
|
55
|
+
</LegendRowMenu>
|
|
50
56
|
</LegendActions>
|
|
57
|
+
<LegendOpacity.Inline />
|
|
51
58
|
<LegendConfigSelect />
|
|
52
59
|
<LegendItem />
|
|
53
|
-
<LegendOpacity.Popover />
|
|
54
60
|
</LegendRow>
|
|
55
61
|
</LegendProvider>,
|
|
56
62
|
)
|
|
57
63
|
}
|
|
58
64
|
|
|
65
|
+
/** The row's accordion header (title area), named by its content. */
|
|
66
|
+
const titleButton = (name: string | RegExp) =>
|
|
67
|
+
screen.getByRole('button', { name })
|
|
68
|
+
|
|
59
69
|
describe('<LegendRow> composition', () => {
|
|
60
70
|
it('partitions Actions into the header and other children into the body', () => {
|
|
61
71
|
const { container } = renderRow('comp1', [
|
|
@@ -66,6 +76,8 @@ describe('<LegendRow> composition', () => {
|
|
|
66
76
|
// The visibility action landed inside the header fade group…
|
|
67
77
|
const fade = container.querySelector('.PsLegend-rowFade')!
|
|
68
78
|
expect(fade.contains(screen.getByLabelText('Hide layer'))).toBe(true)
|
|
79
|
+
// …the ⋮ too (it fades like the rest)…
|
|
80
|
+
expect(fade.contains(screen.getByLabelText('Layer options'))).toBe(true)
|
|
69
81
|
// …while the item renderer landed in the body, outside the fade group.
|
|
70
82
|
expect(fade.contains(screen.getByText('A'))).toBe(false)
|
|
71
83
|
})
|
|
@@ -78,29 +90,60 @@ describe('<LegendRow> composition', () => {
|
|
|
78
90
|
expect(getLegendStore('r1').getState().layers.a!.visible).toBe(true)
|
|
79
91
|
})
|
|
80
92
|
|
|
81
|
-
it('toggles collapse
|
|
93
|
+
it('clicking the title row toggles collapse (accordion header)', () => {
|
|
82
94
|
renderRow('r2', [{ id: 'a', name: 'A', variables: [cat] }])
|
|
83
|
-
|
|
95
|
+
const header = titleButton(/A/)
|
|
96
|
+
expect(header.getAttribute('aria-expanded')).toBe('true')
|
|
97
|
+
fireEvent.click(header)
|
|
84
98
|
expect(getLegendStore('r2').getState().layers.a!.collapsed).toBe(true)
|
|
99
|
+
expect(header.getAttribute('aria-expanded')).toBe('false')
|
|
100
|
+
// Keyboard parity: Space re-expands.
|
|
101
|
+
fireEvent.keyDown(header, { key: ' ' })
|
|
102
|
+
expect(getLegendStore('r2').getState().layers.a!.collapsed).toBe(false)
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
it('offers no collapse affordance when the layer has nothing to collapse', () => {
|
|
106
|
+
renderRow('r3', [{ id: 'a', name: 'Bodiless', variables: [] }])
|
|
107
|
+
expect(screen.queryByRole('button', { name: /Bodiless/ })).toBeNull()
|
|
85
108
|
})
|
|
86
109
|
|
|
87
|
-
it('eye toggle drives collapse; a manual
|
|
110
|
+
it('eye toggle drives collapse; a manual title-click collapse persists', () => {
|
|
88
111
|
renderRow('r6', [{ id: 'a', name: 'A', variables: [cat] }])
|
|
89
|
-
// Hide → collapses, and the
|
|
112
|
+
// Hide → collapses, and the title button disappears while hidden.
|
|
90
113
|
fireEvent.click(screen.getByLabelText('Hide layer'))
|
|
91
114
|
expect(getLegendStore('r6').getState().layers.a!.collapsed).toBe(true)
|
|
92
|
-
|
|
93
|
-
expect(chevron.disabled).toBe(true)
|
|
115
|
+
expect(screen.queryByRole('button', { name: /A/ })).toBeNull()
|
|
94
116
|
fireEvent.click(screen.getByLabelText('Show layer'))
|
|
95
117
|
expect(getLegendStore('r6').getState().layers.a!.collapsed).toBe(false)
|
|
96
118
|
|
|
97
119
|
// Manual collapse first → hide → show → stays collapsed.
|
|
98
|
-
fireEvent.click(
|
|
120
|
+
fireEvent.click(titleButton(/A/))
|
|
99
121
|
fireEvent.click(screen.getByLabelText('Hide layer'))
|
|
100
122
|
fireEvent.click(screen.getByLabelText('Show layer'))
|
|
101
123
|
expect(getLegendStore('r6').getState().layers.a!.collapsed).toBe(true)
|
|
102
124
|
})
|
|
103
125
|
|
|
126
|
+
it('hides the subtitle while collapsed, restores it on expand', () => {
|
|
127
|
+
renderRow('sub1', [
|
|
128
|
+
{ id: 'a', name: 'A', subtitle: 'Zoom 0–12', variables: [cat] },
|
|
129
|
+
])
|
|
130
|
+
expect(screen.getByText('Zoom 0–12')).toBeTruthy()
|
|
131
|
+
fireEvent.click(titleButton(/A/))
|
|
132
|
+
expect(screen.queryByText('Zoom 0–12')).toBeNull()
|
|
133
|
+
fireEvent.click(titleButton(/A/))
|
|
134
|
+
expect(screen.getByText('Zoom 0–12')).toBeTruthy()
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
it('hides the subtitle while the layer is hidden (force-collapsed)', () => {
|
|
138
|
+
renderRow('sub2', [
|
|
139
|
+
{ id: 'a', name: 'A', subtitle: 'Zoom 0–12', variables: [cat] },
|
|
140
|
+
])
|
|
141
|
+
fireEvent.click(screen.getByLabelText('Hide layer'))
|
|
142
|
+
expect(screen.queryByText('Zoom 0–12')).toBeNull()
|
|
143
|
+
fireEvent.click(screen.getByLabelText('Show layer'))
|
|
144
|
+
expect(screen.getByText('Zoom 0–12')).toBeTruthy()
|
|
145
|
+
})
|
|
146
|
+
|
|
104
147
|
it('renders the config select when the layer declares sections', () => {
|
|
105
148
|
renderRow('r5', [
|
|
106
149
|
{
|
|
@@ -136,15 +179,160 @@ describe('<LegendRow> composition', () => {
|
|
|
136
179
|
})
|
|
137
180
|
})
|
|
138
181
|
|
|
139
|
-
describe('<
|
|
140
|
-
it('
|
|
182
|
+
describe('<LegendRow> grouped (inside Legend.Group)', () => {
|
|
183
|
+
it('renders inside a group and collapses via its own title row', () => {
|
|
184
|
+
render(
|
|
185
|
+
<LegendProvider
|
|
186
|
+
id='g1-row'
|
|
187
|
+
groups={[{ id: 'g1', label: 'G' }]}
|
|
188
|
+
layers={[{ id: 'a', name: 'Alpha', groupId: 'g1', variables: [cat] }]}
|
|
189
|
+
>
|
|
190
|
+
<LegendGroup groupId='g1'>
|
|
191
|
+
<LegendRow layerId='a'>
|
|
192
|
+
<LegendActions>
|
|
193
|
+
<LegendVisibilityToggle />
|
|
194
|
+
</LegendActions>
|
|
195
|
+
<LegendItem />
|
|
196
|
+
</LegendRow>
|
|
197
|
+
</LegendGroup>
|
|
198
|
+
</LegendProvider>,
|
|
199
|
+
)
|
|
200
|
+
fireEvent.click(screen.getByRole('button', { name: /Alpha/ }))
|
|
201
|
+
expect(getLegendStore('g1-row').getState().layers.a!.collapsed).toBe(true)
|
|
202
|
+
})
|
|
203
|
+
})
|
|
204
|
+
|
|
205
|
+
describe('<LegendRowMenu> (agnostic shell) + built-in items', () => {
|
|
206
|
+
it('Legend.ZoomTo fires the composed handler with the layer id; the menu closes', async () => {
|
|
141
207
|
const onZoomTo = vi.fn()
|
|
142
208
|
renderRow('m3', [{ id: 'a', name: 'A', variables: [cat] }], { onZoomTo })
|
|
143
209
|
fireEvent.click(screen.getByLabelText('Layer options'))
|
|
144
|
-
fireEvent.click(screen.getByText('Zoom to'))
|
|
210
|
+
fireEvent.click(screen.getByText('Zoom to layer'))
|
|
145
211
|
expect(onZoomTo).toHaveBeenCalledWith('a')
|
|
146
|
-
//
|
|
147
|
-
|
|
212
|
+
// Sync handler settles on the next microtask → item closes the menu.
|
|
213
|
+
await vi.waitFor(() => {
|
|
214
|
+
expect(screen.queryByRole('menuitem')).toBeNull()
|
|
215
|
+
})
|
|
216
|
+
})
|
|
217
|
+
|
|
218
|
+
it('Legend.ZoomTo keeps the menu open with loading feedback while the handler is pending', async () => {
|
|
219
|
+
let resolveZoom!: () => void
|
|
220
|
+
const onZoomTo = vi.fn(
|
|
221
|
+
() =>
|
|
222
|
+
new Promise<void>((resolve) => {
|
|
223
|
+
resolveZoom = resolve
|
|
224
|
+
}),
|
|
225
|
+
)
|
|
226
|
+
renderRow('m3-busy', [{ id: 'a', name: 'A', variables: [cat] }], {
|
|
227
|
+
onZoomTo,
|
|
228
|
+
})
|
|
229
|
+
fireEvent.click(screen.getByLabelText('Layer options'))
|
|
230
|
+
fireEvent.click(screen.getByText('Zoom to layer'))
|
|
231
|
+
|
|
232
|
+
expect(onZoomTo).toHaveBeenCalledWith('a')
|
|
233
|
+
// Menu stays open; item shows the in-flight label and is disabled.
|
|
234
|
+
const item = await screen.findByRole('menuitem', { name: /Loading/ })
|
|
235
|
+
expect(item.getAttribute('aria-disabled')).toBe('true')
|
|
236
|
+
expect(screen.getByRole('progressbar')).toBeDefined()
|
|
237
|
+
|
|
238
|
+
resolveZoom()
|
|
239
|
+
await vi.waitFor(() => {
|
|
240
|
+
expect(screen.queryByRole('menuitem')).toBeNull()
|
|
241
|
+
})
|
|
242
|
+
})
|
|
243
|
+
|
|
244
|
+
it('Legend.ZoomTo ignores a second click before the busy re-render', async () => {
|
|
245
|
+
let resolveZoom!: () => void
|
|
246
|
+
const onZoomTo = vi.fn(
|
|
247
|
+
() =>
|
|
248
|
+
new Promise<void>((resolve) => {
|
|
249
|
+
resolveZoom = resolve
|
|
250
|
+
}),
|
|
251
|
+
)
|
|
252
|
+
renderRow('m3-dbl', [{ id: 'a', name: 'A', variables: [cat] }], {
|
|
253
|
+
onZoomTo,
|
|
254
|
+
})
|
|
255
|
+
fireEvent.click(screen.getByLabelText('Layer options'))
|
|
256
|
+
const zoomItem = screen.getByText('Zoom to layer')
|
|
257
|
+
// Two clicks in the same tick — before React applies setBusy(true).
|
|
258
|
+
fireEvent.click(zoomItem)
|
|
259
|
+
fireEvent.click(zoomItem)
|
|
260
|
+
|
|
261
|
+
expect(onZoomTo).toHaveBeenCalledTimes(1)
|
|
262
|
+
|
|
263
|
+
resolveZoom()
|
|
264
|
+
await vi.waitFor(() => {
|
|
265
|
+
expect(screen.queryByRole('menuitem')).toBeNull()
|
|
266
|
+
})
|
|
267
|
+
})
|
|
268
|
+
|
|
269
|
+
it('Show only this layer / Show all layers dispatch the sibling actions', () => {
|
|
270
|
+
render(
|
|
271
|
+
<LegendProvider
|
|
272
|
+
id='m7'
|
|
273
|
+
layers={[
|
|
274
|
+
{ id: 'a', name: 'A', variables: [cat] },
|
|
275
|
+
{ id: 'b', name: 'B', variables: [cat] },
|
|
276
|
+
]}
|
|
277
|
+
>
|
|
278
|
+
<LegendRow layerId='a'>
|
|
279
|
+
<LegendActions>
|
|
280
|
+
<LegendRowMenu>
|
|
281
|
+
<LegendShowOnlyLayer />
|
|
282
|
+
<LegendShowAllLayers />
|
|
283
|
+
</LegendRowMenu>
|
|
284
|
+
</LegendActions>
|
|
285
|
+
<LegendItem />
|
|
286
|
+
</LegendRow>
|
|
287
|
+
</LegendProvider>,
|
|
288
|
+
)
|
|
289
|
+
fireEvent.click(screen.getByLabelText('Layer options'))
|
|
290
|
+
fireEvent.click(screen.getByText('Show only this layer'))
|
|
291
|
+
expect(getLegendStore('m7').getState().layers.b!.visible).toBe(false)
|
|
292
|
+
|
|
293
|
+
fireEvent.click(screen.getByLabelText('Layer options'))
|
|
294
|
+
fireEvent.click(screen.getByText('Show all layers'))
|
|
295
|
+
expect(getLegendStore('m7').getState().layers.b!.visible).toBe(true)
|
|
296
|
+
})
|
|
297
|
+
|
|
298
|
+
it('show only/all scope to the group bucket and restore collapse snapshots', () => {
|
|
299
|
+
render(
|
|
300
|
+
<LegendProvider
|
|
301
|
+
id='m9'
|
|
302
|
+
groups={[{ id: 'g1', label: 'G' }]}
|
|
303
|
+
layers={[
|
|
304
|
+
{ id: 'a', name: 'A', groupId: 'g1', variables: [cat] },
|
|
305
|
+
{ id: 'b', name: 'B', groupId: 'g1', variables: [cat] },
|
|
306
|
+
{ id: 'loose', name: 'Loose', variables: [cat] },
|
|
307
|
+
]}
|
|
308
|
+
>
|
|
309
|
+
<LegendGroup groupId='g1'>
|
|
310
|
+
<LegendRow layerId='a'>
|
|
311
|
+
<LegendActions>
|
|
312
|
+
<LegendRowMenu>
|
|
313
|
+
<LegendShowOnlyLayer />
|
|
314
|
+
<LegendShowAllLayers />
|
|
315
|
+
</LegendRowMenu>
|
|
316
|
+
</LegendActions>
|
|
317
|
+
<LegendItem />
|
|
318
|
+
</LegendRow>
|
|
319
|
+
</LegendGroup>
|
|
320
|
+
</LegendProvider>,
|
|
321
|
+
)
|
|
322
|
+
// Manual collapse on the sibling: the snapshot must survive the cycle.
|
|
323
|
+
getLegendStore('m9').getState().setCollapsed('b', true)
|
|
324
|
+
|
|
325
|
+
fireEvent.click(screen.getByLabelText('Layer options'))
|
|
326
|
+
fireEvent.click(screen.getByText('Show only this layer'))
|
|
327
|
+
let st = getLegendStore('m9').getState()
|
|
328
|
+
expect(st.layers.b!.visible).toBe(false) // sibling in g1
|
|
329
|
+
expect(st.layers.loose!.visible).toBe(true) // other bucket untouched
|
|
330
|
+
|
|
331
|
+
fireEvent.click(screen.getByLabelText('Layer options'))
|
|
332
|
+
fireEvent.click(screen.getByText('Show all layers'))
|
|
333
|
+
st = getLegendStore('m9').getState()
|
|
334
|
+
expect(st.layers.b!.visible).toBe(true)
|
|
335
|
+
expect(st.layers.b!.collapsed).toBe(true) // manual collapse restored
|
|
148
336
|
})
|
|
149
337
|
|
|
150
338
|
it('renders any consumer MenuItem; clicks dispatch and close the menu', () => {
|
|
@@ -169,10 +357,7 @@ describe('<LegendRowMenu> (agnostic shell)', () => {
|
|
|
169
357
|
expect(screen.queryByRole('menuitem')).toBeNull()
|
|
170
358
|
})
|
|
171
359
|
|
|
172
|
-
it('renders nothing without children
|
|
173
|
-
renderRow('m4', [{ id: 'a', name: 'A', variables: [cat] }])
|
|
174
|
-
expect(screen.queryByLabelText('Layer options')).toBeNull()
|
|
175
|
-
|
|
360
|
+
it('renders nothing without children', () => {
|
|
176
361
|
render(
|
|
177
362
|
<LegendProvider
|
|
178
363
|
id='m6'
|
|
@@ -187,6 +372,24 @@ describe('<LegendRowMenu> (agnostic shell)', () => {
|
|
|
187
372
|
)
|
|
188
373
|
expect(screen.queryByLabelText('Layer options')).toBeNull()
|
|
189
374
|
})
|
|
375
|
+
|
|
376
|
+
it('stays available while hidden, with only the applicable items', () => {
|
|
377
|
+
renderRow(
|
|
378
|
+
'm8',
|
|
379
|
+
[{ id: 'a', name: 'A', variables: [cat], opacityControl: true }],
|
|
380
|
+
{ onZoomTo: vi.fn() },
|
|
381
|
+
)
|
|
382
|
+
fireEvent.click(screen.getByLabelText('Hide layer'))
|
|
383
|
+
// ⋮ still present (hover-revealed, not pinned).
|
|
384
|
+
const trigger = screen.getByLabelText('Layer options')
|
|
385
|
+
expect(trigger.className).not.toContain('active')
|
|
386
|
+
fireEvent.click(trigger)
|
|
387
|
+
// Show only/all still apply; Zoom + Opacity remove themselves.
|
|
388
|
+
expect(screen.getByText('Show only this layer')).toBeTruthy()
|
|
389
|
+
expect(screen.getByText('Show all layers')).toBeTruthy()
|
|
390
|
+
expect(screen.queryByText('Zoom to layer')).toBeNull()
|
|
391
|
+
expect(screen.queryByText('Opacity')).toBeNull()
|
|
392
|
+
})
|
|
190
393
|
})
|
|
191
394
|
|
|
192
395
|
describe('hover-fade dirty pinning (.active)', () => {
|
|
@@ -200,75 +403,80 @@ describe('hover-fade dirty pinning (.active)', () => {
|
|
|
200
403
|
)
|
|
201
404
|
})
|
|
202
405
|
|
|
203
|
-
it('marks the menu trigger active while its menu is open', () => {
|
|
204
|
-
renderRow('f2', [
|
|
205
|
-
|
|
206
|
-
|
|
406
|
+
it('marks the menu trigger active while its menu or the opacity bar is open', () => {
|
|
407
|
+
renderRow('f2', [
|
|
408
|
+
{ id: 'a', name: 'A', variables: [cat], opacityControl: true },
|
|
409
|
+
])
|
|
207
410
|
const trigger = screen.getByLabelText('Layer options')
|
|
208
411
|
expect(trigger.className).not.toContain('active')
|
|
209
412
|
fireEvent.click(trigger)
|
|
413
|
+
expect(trigger.className).toContain('active') // menu open
|
|
414
|
+
|
|
415
|
+
// Pick Opacity: the menu closes but the ⋮ stays pinned while the bar is
|
|
416
|
+
// open.
|
|
417
|
+
fireEvent.click(screen.getByRole('menuitem', { name: 'Opacity' }))
|
|
418
|
+
expect(screen.queryByRole('menuitem')).toBeNull()
|
|
210
419
|
expect(trigger.className).toContain('active')
|
|
211
420
|
})
|
|
421
|
+
})
|
|
212
422
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
const action = screen.getByRole('button', { name: 'Opacity' })
|
|
218
|
-
// The pin lives on the action's tooltip span — the fade group's child.
|
|
219
|
-
const pin = action.parentElement!
|
|
220
|
-
expect(pin.className).not.toContain('active')
|
|
423
|
+
describe('<LegendOpacity> Item + Inline', () => {
|
|
424
|
+
const layersWithOpacity: LegendLayerInput[] = [
|
|
425
|
+
{ id: 'a', name: 'A', variables: [cat], opacityControl: true },
|
|
426
|
+
]
|
|
221
427
|
|
|
222
|
-
|
|
223
|
-
|
|
428
|
+
const openBar = () => {
|
|
429
|
+
fireEvent.click(screen.getByLabelText('Layer options'))
|
|
430
|
+
fireEvent.click(screen.getByRole('menuitem', { name: 'Opacity' }))
|
|
431
|
+
}
|
|
224
432
|
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
// Close via the backdrop: stays pinned because opacity is dirty.
|
|
229
|
-
fireEvent.click(baseElement.querySelector('.MuiBackdrop-root')!)
|
|
230
|
-
expect(pin.className).toContain('active')
|
|
231
|
-
})
|
|
433
|
+
it('the menu item toggles the inline bar and stays selected while open', () => {
|
|
434
|
+
renderRow('o1', layersWithOpacity)
|
|
435
|
+
expect(screen.queryByRole('slider')).toBeNull()
|
|
232
436
|
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
})
|
|
237
|
-
expect(screen.getByRole('button', { name: 'Opacity' })).toBeTruthy()
|
|
238
|
-
expect(screen.getByLabelText('Layer options')).toBeTruthy()
|
|
437
|
+
openBar()
|
|
438
|
+
expect(screen.getByRole('slider', { name: 'Opacity' })).toBeTruthy()
|
|
439
|
+
expect(screen.getByRole('textbox', { name: 'Opacity' })).toBeTruthy()
|
|
239
440
|
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
expect(
|
|
244
|
-
|
|
245
|
-
expect(screen.
|
|
246
|
-
|
|
441
|
+
// Reopen the menu: the item reflects the open bar and toggles it off.
|
|
442
|
+
fireEvent.click(screen.getByLabelText('Layer options'))
|
|
443
|
+
const item = screen.getByRole('menuitem', { name: 'Opacity' })
|
|
444
|
+
expect(item.className).toContain('Mui-selected')
|
|
445
|
+
fireEvent.click(item)
|
|
446
|
+
expect(screen.queryByRole('slider')).toBeNull()
|
|
447
|
+
})
|
|
247
448
|
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
expect(screen.
|
|
449
|
+
it('removes the item without opacityControl', () => {
|
|
450
|
+
renderRow('o2', [{ id: 'a', name: 'A', variables: [cat] }])
|
|
451
|
+
fireEvent.click(screen.getByLabelText('Layer options'))
|
|
452
|
+
expect(screen.queryByRole('menuitem', { name: 'Opacity' })).toBeNull()
|
|
252
453
|
})
|
|
253
|
-
})
|
|
254
454
|
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
expect(
|
|
261
|
-
fireEvent.click(screen.getByRole('button', { name: 'Opacity' }))
|
|
262
|
-
// Trigger (header) and Popover (body) communicate through the row context.
|
|
455
|
+
it('opening on a collapsed row auto-expands it; folding keeps the state', () => {
|
|
456
|
+
renderRow('o3', layersWithOpacity)
|
|
457
|
+
fireEvent.click(titleButton(/A/)) // collapse
|
|
458
|
+
openBar()
|
|
459
|
+
// Auto-expanded so the bar is visible.
|
|
460
|
+
expect(getLegendStore('o3').getState().layers.a!.collapsed).toBe(false)
|
|
263
461
|
expect(screen.getByRole('slider', { name: 'Opacity' })).toBeTruthy()
|
|
264
|
-
|
|
265
|
-
|
|
462
|
+
|
|
463
|
+
// Folding hides the bar with the body but keeps opacityOpen: expanding
|
|
464
|
+
// brings it back without touching the menu.
|
|
465
|
+
fireEvent.click(titleButton(/A/))
|
|
466
|
+
fireEvent.click(titleButton(/A/))
|
|
467
|
+
expect(screen.getByRole('slider', { name: 'Opacity' })).toBeTruthy()
|
|
468
|
+
})
|
|
469
|
+
|
|
470
|
+
it('the ✕ closes the bar', () => {
|
|
471
|
+
renderRow('o4', layersWithOpacity)
|
|
472
|
+
openBar()
|
|
473
|
+
fireEvent.click(screen.getByLabelText('Close'))
|
|
266
474
|
expect(screen.queryByRole('slider')).toBeNull()
|
|
267
475
|
})
|
|
268
476
|
|
|
269
477
|
it('slider release commits setOpacity (0–1) to the store', () => {
|
|
270
|
-
renderRow('c1',
|
|
271
|
-
|
|
478
|
+
renderRow('c1', layersWithOpacity)
|
|
479
|
+
openBar()
|
|
272
480
|
const slider = screen.getByRole('slider', { name: 'Opacity' })
|
|
273
481
|
// The hidden-input change path fires MUI's onChangeCommitted too.
|
|
274
482
|
fireEvent.change(slider, { target: { value: '40' } })
|
|
@@ -276,8 +484,8 @@ describe('<LegendOpacity> / <LegendConfigSelect> dispatch', () => {
|
|
|
276
484
|
})
|
|
277
485
|
|
|
278
486
|
it('percent input commits live and clamps to 0–100', () => {
|
|
279
|
-
renderRow('c3',
|
|
280
|
-
|
|
487
|
+
renderRow('c3', layersWithOpacity)
|
|
488
|
+
openBar()
|
|
281
489
|
const input = screen.getByRole('textbox', { name: 'Opacity' })
|
|
282
490
|
|
|
283
491
|
fireEvent.change(input, { target: { value: '40' } })
|
|
@@ -294,7 +502,9 @@ describe('<LegendOpacity> / <LegendConfigSelect> dispatch', () => {
|
|
|
294
502
|
fireEvent.blur(input)
|
|
295
503
|
expect((input as HTMLInputElement).value).toBe('100')
|
|
296
504
|
})
|
|
505
|
+
})
|
|
297
506
|
|
|
507
|
+
describe('<LegendConfigSelect> dispatch', () => {
|
|
298
508
|
it('a single section renders without its title; a pick writes the store', () => {
|
|
299
509
|
renderRow('c2', [
|
|
300
510
|
{
|
|
@@ -433,20 +643,29 @@ describe('labels override', () => {
|
|
|
433
643
|
render(
|
|
434
644
|
<LegendProvider
|
|
435
645
|
id='l10n'
|
|
436
|
-
layers={[
|
|
646
|
+
layers={[
|
|
647
|
+
{ id: 'a', name: 'A', variables: [cat], opacityControl: true },
|
|
648
|
+
]}
|
|
437
649
|
labels={{ opacity: 'Opacidad', hideLayer: 'Ocultar capa' }}
|
|
438
650
|
>
|
|
439
651
|
<LegendRow layerId='a'>
|
|
440
652
|
<LegendActions>
|
|
441
|
-
<LegendOpacity.Trigger />
|
|
442
653
|
<LegendVisibilityToggle />
|
|
654
|
+
<LegendRowMenu>
|
|
655
|
+
<LegendOpacity.Item />
|
|
656
|
+
</LegendRowMenu>
|
|
443
657
|
</LegendActions>
|
|
658
|
+
<LegendOpacity.Inline />
|
|
659
|
+
<LegendItem />
|
|
444
660
|
</LegendRow>
|
|
445
661
|
</LegendProvider>,
|
|
446
662
|
)
|
|
447
|
-
// The opacity action carries the overridden label (aria + tooltip).
|
|
448
|
-
expect(screen.getByRole('button', { name: 'Opacidad' })).toBeTruthy()
|
|
449
663
|
expect(screen.getByLabelText('Ocultar capa')).toBeTruthy()
|
|
664
|
+
fireEvent.click(screen.getByLabelText('Layer options'))
|
|
665
|
+
// The opacity menu item carries the overridden label…
|
|
666
|
+
fireEvent.click(screen.getByRole('menuitem', { name: 'Opacidad' }))
|
|
667
|
+
// …and so does the inline bar's slider.
|
|
668
|
+
expect(screen.getByRole('slider', { name: 'Opacidad' })).toBeTruthy()
|
|
450
669
|
})
|
|
451
670
|
})
|
|
452
671
|
|