@carto/ps-react-ui 4.12.2 → 4.14.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/{category-Dnd2_j0x.js → category-CGS_eHr4.js} +2 -2
- package/dist/{category-Dnd2_j0x.js.map → category-CGS_eHr4.js.map} +1 -1
- package/dist/{change-column-DjjwoPt1.js → change-column-CiVAjOUB.js} +2 -2
- package/dist/{change-column-DjjwoPt1.js.map → change-column-CiVAjOUB.js.map} +1 -1
- package/dist/components.js +1232 -777
- package/dist/components.js.map +1 -1
- package/dist/{data-zoom-layout-CkVnm6ej.js → data-zoom-layout-D_FYdnap.js} +2 -2
- package/dist/{data-zoom-layout-CkVnm6ej.js.map → data-zoom-layout-D_FYdnap.js.map} +1 -1
- package/dist/{download-config-oJIFZ2WC.js → download-config-OKlLLSW_.js} +2 -2
- package/dist/{download-config-oJIFZ2WC.js.map → download-config-OKlLLSW_.js.map} +1 -1
- package/dist/{lasso-tool-CDFj4zKY.js → lasso-tool-YLFRgb-S.js} +182 -154
- package/dist/lasso-tool-YLFRgb-S.js.map +1 -0
- package/dist/legend/stores.js +20 -0
- package/dist/legend/stores.js.map +1 -0
- package/dist/legend.js +1536 -0
- package/dist/legend.js.map +1 -0
- package/dist/selectors-DwHj6mE2.js +388 -0
- package/dist/selectors-DwHj6mE2.js.map +1 -0
- package/dist/{spread-DYNpzgh_.js → spread-4MYPOntd.js} +2 -2
- package/dist/{spread-DYNpzgh_.js.map → spread-4MYPOntd.js.map} +1 -1
- package/dist/{table-B3ZWWhJt.js → table-CTcLOycx.js} +2 -2
- package/dist/{table-B3ZWWhJt.js.map → table-CTcLOycx.js.map} +1 -1
- package/dist/types/components/index.d.ts +5 -2
- package/dist/types/components/lasso-tool/geometry-toolbar.d.ts +13 -0
- package/dist/types/components/lasso-tool/icons.d.ts +6 -0
- package/dist/types/components/lasso-tool/lasso-tool.d.ts +1 -1
- package/dist/types/components/lasso-tool/layer-filters.d.ts +10 -0
- package/dist/types/components/lasso-tool/styles.d.ts +1 -1
- package/dist/types/components/lasso-tool/types.d.ts +62 -1
- package/dist/types/components/responsive-drawer/responsive-drawer.d.ts +35 -3
- package/dist/types/legend/components/contexts.d.ts +20 -0
- package/dist/types/legend/components/icons.d.ts +12 -0
- package/dist/types/legend/components/index.d.ts +26 -0
- package/dist/types/legend/components/legend-actions/legend-actions.d.ts +24 -0
- package/dist/types/legend/components/legend-actions/styles.d.ts +7 -0
- package/dist/types/legend/components/legend-category/legend-category-ui.d.ts +13 -0
- package/dist/types/legend/components/legend-category/styles.d.ts +52 -0
- package/dist/types/legend/components/legend-config-select/legend-config-select.d.ts +14 -0
- package/dist/types/legend/components/legend-config-select/styles.d.ts +44 -0
- package/dist/types/legend/components/legend-group/legend-group.d.ts +21 -0
- package/dist/types/legend/components/legend-group/styles.d.ts +63 -0
- package/dist/types/legend/components/legend-icon/legend-icon-ui.d.ts +10 -0
- package/dist/types/legend/components/legend-icon/styles.d.ts +52 -0
- package/dist/types/legend/components/legend-item/legend-item-ui.d.ts +14 -0
- package/dist/types/legend/components/legend-item/legend-item.d.ts +10 -0
- package/dist/types/legend/components/legend-item/styles.d.ts +7 -0
- package/dist/types/legend/components/legend-opacity/legend-opacity.d.ts +32 -0
- package/dist/types/legend/components/legend-opacity/styles.d.ts +23 -0
- package/dist/types/legend/components/legend-proportion/legend-proportion-ui.d.ts +16 -0
- package/dist/types/legend/components/legend-proportion/styles.d.ts +37 -0
- package/dist/types/legend/components/legend-ramp/legend-ramp-ui.d.ts +20 -0
- package/dist/types/legend/components/legend-ramp/styles.d.ts +53 -0
- package/dist/types/legend/components/legend-row/legend-row.d.ts +23 -0
- package/dist/types/legend/components/legend-row/styles.d.ts +77 -0
- package/dist/types/legend/components/legend-row-menu/legend-row-menu.d.ts +18 -0
- package/dist/types/legend/components/legend-row-menu/legend-zoom-to.d.ts +15 -0
- package/dist/types/legend/components/legend-swatch/legend-swatch.d.ts +26 -0
- package/dist/types/legend/components/legend-visibility-toggle/legend-visibility-toggle.d.ts +35 -0
- package/dist/types/legend/index.d.ts +88 -0
- package/dist/types/legend/provider/index.d.ts +6 -0
- package/dist/types/legend/provider/labels.d.ts +27 -0
- package/dist/types/legend/provider/legend-config-context.d.ts +7 -0
- package/dist/types/legend/provider/legend-provider.d.ts +38 -0
- package/dist/types/legend/stores/index.d.ts +5 -0
- package/dist/types/legend/stores/legend-context.d.ts +4 -0
- package/dist/types/legend/stores/legend-store-registry.d.ts +39 -0
- package/dist/types/legend/stores/legend-store.test.d.ts +1 -0
- package/dist/types/legend/stores/selectors.d.ts +33 -0
- package/dist/types/legend/stores/types.d.ts +275 -0
- package/dist/widgets/actions.js +1 -1
- package/dist/widgets/bar.js +1 -1
- package/dist/widgets/category.js +1 -1
- package/dist/widgets/formula.js +1 -1
- package/dist/widgets/histogram.js +1 -1
- package/dist/widgets/markdown.js +1 -1
- package/dist/widgets/pie.js +1 -1
- package/dist/widgets/scatterplot.js +1 -1
- package/dist/widgets/spread.js +1 -1
- package/dist/widgets/table.js +1 -1
- package/dist/widgets/timeseries.js +1 -1
- package/dist/widgets/utils.js +1 -1
- package/dist/widgets/wrapper.js +1 -1
- package/dist/widgets-v2/actions.js +1 -1
- package/dist/widgets-v2/bar.js +2 -2
- package/dist/widgets-v2/category.js +2 -2
- package/dist/widgets-v2/formula.js +1 -1
- package/dist/widgets-v2/histogram.js +2 -2
- package/dist/widgets-v2/markdown.js +1 -1
- package/dist/widgets-v2/pie.js +1 -1
- package/dist/widgets-v2/scatterplot.js +1 -1
- package/dist/widgets-v2/spread.js +2 -2
- package/dist/widgets-v2/table.js +3 -3
- package/dist/widgets-v2/timeseries.js +2 -2
- package/dist/widgets-v2/utils.js +1 -1
- package/dist/widgets-v2.js +5 -5
- package/package.json +11 -3
- package/src/components/geolocation-controls/geolocation-controls.test.tsx +15 -0
- package/src/components/geolocation-controls/geolocation-controls.tsx +19 -3
- package/src/components/index.ts +10 -0
- package/src/components/lasso-tool/const.tsx +1 -0
- package/src/components/lasso-tool/geometry-toolbar.test.tsx +138 -0
- package/src/components/lasso-tool/geometry-toolbar.tsx +258 -0
- package/src/components/lasso-tool/icons.tsx +40 -0
- package/src/components/lasso-tool/lasso-tool.tsx +25 -1
- package/src/components/lasso-tool/layer-filters.test.tsx +90 -0
- package/src/components/lasso-tool/layer-filters.tsx +236 -0
- package/src/components/lasso-tool/styles.ts +1 -1
- package/src/components/lasso-tool/types.ts +66 -1
- package/src/components/responsive-drawer/responsive-drawer.test.tsx +129 -25
- package/src/components/responsive-drawer/responsive-drawer.tsx +149 -16
- package/src/legend/components/contexts.ts +32 -0
- package/src/legend/components/icons.tsx +22 -0
- package/src/legend/components/index.ts +39 -0
- package/src/legend/components/legend-actions/legend-actions.tsx +57 -0
- package/src/legend/components/legend-actions/styles.ts +9 -0
- package/src/legend/components/legend-category/legend-category-ui.tsx +91 -0
- package/src/legend/components/legend-category/styles.ts +61 -0
- package/src/legend/components/legend-config-select/legend-config-select.tsx +96 -0
- package/src/legend/components/legend-config-select/styles.ts +48 -0
- package/src/legend/components/legend-group/legend-group.test.tsx +115 -0
- package/src/legend/components/legend-group/legend-group.tsx +88 -0
- package/src/legend/components/legend-group/styles.ts +82 -0
- package/src/legend/components/legend-icon/legend-icon-ui.tsx +56 -0
- package/src/legend/components/legend-icon/styles.ts +56 -0
- package/src/legend/components/legend-item/legend-item-ui.test.tsx +362 -0
- package/src/legend/components/legend-item/legend-item-ui.tsx +43 -0
- package/src/legend/components/legend-item/legend-item.tsx +34 -0
- package/src/legend/components/legend-item/styles.ts +11 -0
- package/src/legend/components/legend-opacity/legend-opacity.tsx +179 -0
- package/src/legend/components/legend-opacity/styles.ts +28 -0
- package/src/legend/components/legend-proportion/legend-proportion-ui.tsx +122 -0
- package/src/legend/components/legend-proportion/styles.ts +43 -0
- package/src/legend/components/legend-ramp/legend-ramp-ui.tsx +159 -0
- package/src/legend/components/legend-ramp/styles.ts +62 -0
- package/src/legend/components/legend-row/legend-row.test.tsx +498 -0
- package/src/legend/components/legend-row/legend-row.tsx +129 -0
- package/src/legend/components/legend-row/styles.ts +111 -0
- package/src/legend/components/legend-row-menu/legend-row-menu.tsx +68 -0
- package/src/legend/components/legend-row-menu/legend-zoom-to.tsx +35 -0
- package/src/legend/components/legend-swatch/legend-swatch.test.tsx +109 -0
- package/src/legend/components/legend-swatch/legend-swatch.tsx +237 -0
- package/src/legend/components/legend-visibility-toggle/legend-visibility-toggle.tsx +127 -0
- package/src/legend/index.ts +121 -0
- package/src/legend/provider/index.ts +6 -0
- package/src/legend/provider/labels.ts +59 -0
- package/src/legend/provider/legend-config-context.ts +15 -0
- package/src/legend/provider/legend-provider.tsx +102 -0
- package/src/legend/stores/index.ts +46 -0
- package/src/legend/stores/legend-context.ts +17 -0
- package/src/legend/stores/legend-store-registry.ts +639 -0
- package/src/legend/stores/legend-store.test.ts +457 -0
- package/src/legend/stores/selectors.ts +65 -0
- package/src/legend/stores/types.ts +347 -0
- package/dist/lasso-tool-CDFj4zKY.js.map +0 -1
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
import { DeleteOutlined, LockOutlined } from '@mui/icons-material'
|
|
2
|
+
import {
|
|
3
|
+
Box,
|
|
4
|
+
Divider,
|
|
5
|
+
IconButton,
|
|
6
|
+
Paper,
|
|
7
|
+
Popover,
|
|
8
|
+
SvgIcon,
|
|
9
|
+
TextField,
|
|
10
|
+
type SxProps,
|
|
11
|
+
type Theme,
|
|
12
|
+
} from '@mui/material'
|
|
13
|
+
import {
|
|
14
|
+
useRef,
|
|
15
|
+
useState,
|
|
16
|
+
type JSX,
|
|
17
|
+
type KeyboardEvent,
|
|
18
|
+
type MouseEvent,
|
|
19
|
+
type ReactNode,
|
|
20
|
+
} from 'react'
|
|
21
|
+
import { Tooltip } from '../tooltip/tooltip'
|
|
22
|
+
import { SpatialMaskIcon, TextAaIcon } from './icons'
|
|
23
|
+
import type { LassoGeometryToolbarUIProps } from './types'
|
|
24
|
+
|
|
25
|
+
const ICON_SIZE = 18
|
|
26
|
+
|
|
27
|
+
const containerSx = {
|
|
28
|
+
display: 'flex',
|
|
29
|
+
flexDirection: 'row',
|
|
30
|
+
alignItems: 'center',
|
|
31
|
+
width: 'fit-content',
|
|
32
|
+
gap: ({ spacing }) => spacing(0.5),
|
|
33
|
+
p: ({ spacing }) => spacing(0.5),
|
|
34
|
+
borderRadius: ({ spacing }) => spacing(0.5),
|
|
35
|
+
backgroundColor: ({ palette }) => palette.background.paper,
|
|
36
|
+
} satisfies SxProps<Theme>
|
|
37
|
+
|
|
38
|
+
const baseButtonSx = {
|
|
39
|
+
p: ({ spacing }) => spacing(0.375),
|
|
40
|
+
borderRadius: ({ spacing }) => spacing(0.5),
|
|
41
|
+
'.MuiTouchRipple-ripple .MuiTouchRipple-child': {
|
|
42
|
+
borderRadius: ({ spacing }) => spacing(0.5),
|
|
43
|
+
},
|
|
44
|
+
} satisfies SxProps<Theme>
|
|
45
|
+
|
|
46
|
+
/** Active = Figma toggle "selected" treatment (primary tint); inactive = gray. */
|
|
47
|
+
const toggleButtonSx = (active: boolean): SxProps<Theme> => ({
|
|
48
|
+
...baseButtonSx,
|
|
49
|
+
color: ({ palette }) =>
|
|
50
|
+
active ? palette.primary.main : palette.text.secondary,
|
|
51
|
+
backgroundColor: active ? 'rgba(3, 111, 226, 0.08)' : 'transparent',
|
|
52
|
+
'&:hover': {
|
|
53
|
+
backgroundColor: ({ palette }) =>
|
|
54
|
+
active ? 'rgba(3, 111, 226, 0.12)' : palette.action.hover,
|
|
55
|
+
},
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Contextual toolbar for a single lasso geometry: toggle mask/draw, rename,
|
|
60
|
+
* lock, and delete. Purely presentational — position it over the map and wire
|
|
61
|
+
* the callbacks to the lasso store (see `LassoTools.GeometryToolbar` in
|
|
62
|
+
* `@carto/ps-react-maps`).
|
|
63
|
+
*
|
|
64
|
+
* Each button keeps a fixed icon and toggles an active highlight: the mask
|
|
65
|
+
* button is active when `type === 'mask'`, the lock button is active when
|
|
66
|
+
* `locked`. When `locked`, every action but the lock toggle is disabled.
|
|
67
|
+
*/
|
|
68
|
+
export function LassoGeometryToolbarUI({
|
|
69
|
+
type,
|
|
70
|
+
label,
|
|
71
|
+
locked = false,
|
|
72
|
+
onToggleType,
|
|
73
|
+
onRename,
|
|
74
|
+
onToggleLock,
|
|
75
|
+
onDelete,
|
|
76
|
+
labels,
|
|
77
|
+
PaperProps: { sx, ...PaperProps } = {},
|
|
78
|
+
}: LassoGeometryToolbarUIProps): JSX.Element {
|
|
79
|
+
const paperRef = useRef<HTMLDivElement>(null)
|
|
80
|
+
// Guards `onRename` to once per rename session — the Popover `onClose` and the
|
|
81
|
+
// TextField `onBlur` can both fire for a single close (and Enter blurs on unmount).
|
|
82
|
+
const committedRef = useRef(false)
|
|
83
|
+
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null)
|
|
84
|
+
const [barWidth, setBarWidth] = useState<number>()
|
|
85
|
+
const [draft, setDraft] = useState(label)
|
|
86
|
+
|
|
87
|
+
const renaming = Boolean(anchorEl)
|
|
88
|
+
|
|
89
|
+
const toggleTypeLabel =
|
|
90
|
+
type === 'mask'
|
|
91
|
+
? (labels?.toggleType?.draw ?? 'Switch to drawing')
|
|
92
|
+
: (labels?.toggleType?.mask ?? 'Switch to mask')
|
|
93
|
+
const renameLabel = labels?.rename ?? 'Rename'
|
|
94
|
+
const renameInputLabel = labels?.renameInput ?? renameLabel
|
|
95
|
+
const lockLabel = locked
|
|
96
|
+
? (labels?.lock?.unlock ?? 'Unlock')
|
|
97
|
+
: (labels?.lock?.lock ?? 'Lock')
|
|
98
|
+
const deleteLabel = labels?.delete ?? 'Delete'
|
|
99
|
+
|
|
100
|
+
const openRename = () => {
|
|
101
|
+
committedRef.current = false
|
|
102
|
+
setDraft(label)
|
|
103
|
+
setBarWidth(paperRef.current?.clientWidth)
|
|
104
|
+
setAnchorEl(paperRef.current)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const commitRename = () => {
|
|
108
|
+
if (!committedRef.current) {
|
|
109
|
+
committedRef.current = true
|
|
110
|
+
const next = draft.trim()
|
|
111
|
+
if (next && next !== label) {
|
|
112
|
+
onRename(next)
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
setAnchorEl(null)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Escape discards the edit: mark the session done so the unmount blur / onClose
|
|
119
|
+
// don't fire `onRename`.
|
|
120
|
+
const cancelRename = () => {
|
|
121
|
+
committedRef.current = true
|
|
122
|
+
setAnchorEl(null)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const handleRenameKeyDown = (event: KeyboardEvent<HTMLInputElement>) => {
|
|
126
|
+
if (event.key === 'Enter') {
|
|
127
|
+
event.preventDefault()
|
|
128
|
+
commitRename()
|
|
129
|
+
}
|
|
130
|
+
if (event.key === 'Escape') {
|
|
131
|
+
event.preventDefault()
|
|
132
|
+
cancelRename()
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return (
|
|
137
|
+
<Paper
|
|
138
|
+
ref={paperRef}
|
|
139
|
+
elevation={6}
|
|
140
|
+
sx={{ ...containerSx, ...sx }}
|
|
141
|
+
{...PaperProps}
|
|
142
|
+
>
|
|
143
|
+
<ToolbarButton
|
|
144
|
+
label={toggleTypeLabel}
|
|
145
|
+
active={type === 'mask'}
|
|
146
|
+
pressed={type === 'mask'}
|
|
147
|
+
disabled={locked}
|
|
148
|
+
onClick={() => onToggleType(type === 'mask' ? 'draw' : 'mask')}
|
|
149
|
+
icon={
|
|
150
|
+
<SvgIcon
|
|
151
|
+
component={SpatialMaskIcon}
|
|
152
|
+
inheritViewBox
|
|
153
|
+
sx={{ fontSize: ICON_SIZE }}
|
|
154
|
+
/>
|
|
155
|
+
}
|
|
156
|
+
/>
|
|
157
|
+
|
|
158
|
+
<Divider orientation='vertical' flexItem />
|
|
159
|
+
|
|
160
|
+
<ToolbarButton
|
|
161
|
+
label={renameLabel}
|
|
162
|
+
active={renaming}
|
|
163
|
+
disabled={locked}
|
|
164
|
+
onClick={openRename}
|
|
165
|
+
icon={
|
|
166
|
+
<SvgIcon
|
|
167
|
+
component={TextAaIcon}
|
|
168
|
+
inheritViewBox
|
|
169
|
+
sx={{ fontSize: ICON_SIZE }}
|
|
170
|
+
/>
|
|
171
|
+
}
|
|
172
|
+
/>
|
|
173
|
+
|
|
174
|
+
<Divider orientation='vertical' flexItem />
|
|
175
|
+
|
|
176
|
+
<ToolbarButton
|
|
177
|
+
label={lockLabel}
|
|
178
|
+
active={locked}
|
|
179
|
+
pressed={locked}
|
|
180
|
+
onClick={() => onToggleLock(!locked)}
|
|
181
|
+
icon={<LockOutlined sx={{ fontSize: ICON_SIZE }} />}
|
|
182
|
+
/>
|
|
183
|
+
|
|
184
|
+
<Divider orientation='vertical' flexItem />
|
|
185
|
+
|
|
186
|
+
<ToolbarButton
|
|
187
|
+
label={deleteLabel}
|
|
188
|
+
disabled={locked}
|
|
189
|
+
onClick={onDelete}
|
|
190
|
+
icon={<DeleteOutlined sx={{ fontSize: ICON_SIZE }} />}
|
|
191
|
+
color='error'
|
|
192
|
+
/>
|
|
193
|
+
|
|
194
|
+
<Popover
|
|
195
|
+
open={renaming}
|
|
196
|
+
anchorEl={anchorEl}
|
|
197
|
+
onClose={commitRename}
|
|
198
|
+
anchorOrigin={{ vertical: 'bottom', horizontal: 'left' }}
|
|
199
|
+
transformOrigin={{ vertical: 'top', horizontal: 'left' }}
|
|
200
|
+
slotProps={{
|
|
201
|
+
paper: {
|
|
202
|
+
sx: { width: barWidth, minWidth: 180 },
|
|
203
|
+
},
|
|
204
|
+
}}
|
|
205
|
+
>
|
|
206
|
+
<Box sx={{ p: 1 }}>
|
|
207
|
+
<TextField
|
|
208
|
+
inputRef={(node: HTMLInputElement | null) => node?.focus()}
|
|
209
|
+
fullWidth
|
|
210
|
+
label={renameInputLabel}
|
|
211
|
+
size='small'
|
|
212
|
+
variant='outlined'
|
|
213
|
+
value={draft}
|
|
214
|
+
onChange={(e) => setDraft(e.target.value)}
|
|
215
|
+
onKeyDown={handleRenameKeyDown}
|
|
216
|
+
onBlur={commitRename}
|
|
217
|
+
/>
|
|
218
|
+
</Box>
|
|
219
|
+
</Popover>
|
|
220
|
+
</Paper>
|
|
221
|
+
)
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function ToolbarButton({
|
|
225
|
+
label,
|
|
226
|
+
icon,
|
|
227
|
+
active = false,
|
|
228
|
+
pressed,
|
|
229
|
+
disabled = false,
|
|
230
|
+
color,
|
|
231
|
+
onClick,
|
|
232
|
+
}: {
|
|
233
|
+
label: string
|
|
234
|
+
icon: ReactNode
|
|
235
|
+
active?: boolean
|
|
236
|
+
/** When set, exposes `aria-pressed` so the button reads as a toggle. */
|
|
237
|
+
pressed?: boolean
|
|
238
|
+
disabled?: boolean
|
|
239
|
+
color?: 'error'
|
|
240
|
+
onClick: (event: MouseEvent<HTMLButtonElement>) => void
|
|
241
|
+
}): ReactNode {
|
|
242
|
+
return (
|
|
243
|
+
<Tooltip title={label} placement='top'>
|
|
244
|
+
<span>
|
|
245
|
+
<IconButton
|
|
246
|
+
aria-label={label}
|
|
247
|
+
aria-pressed={pressed}
|
|
248
|
+
disabled={disabled}
|
|
249
|
+
color={color}
|
|
250
|
+
onClick={onClick}
|
|
251
|
+
sx={color ? baseButtonSx : toggleButtonSx(active)}
|
|
252
|
+
>
|
|
253
|
+
{icon}
|
|
254
|
+
</IconButton>
|
|
255
|
+
</span>
|
|
256
|
+
</Tooltip>
|
|
257
|
+
)
|
|
258
|
+
}
|
|
@@ -89,3 +89,43 @@ export function SelectToolIcon(props: SVGProps<SVGSVGElement>) {
|
|
|
89
89
|
</svg>
|
|
90
90
|
)
|
|
91
91
|
}
|
|
92
|
+
|
|
93
|
+
/** Spatial-mask glyph (circle with horizontal "globe" stripes) for the lasso
|
|
94
|
+
* geometry toolbar's mask/draw toggle. From Figma node 5960:14347. */
|
|
95
|
+
export function SpatialMaskIcon(props: SVGProps<SVGSVGElement>) {
|
|
96
|
+
return (
|
|
97
|
+
<svg
|
|
98
|
+
fill='none'
|
|
99
|
+
xmlns='http://www.w3.org/2000/svg'
|
|
100
|
+
viewBox='0 0 15 15'
|
|
101
|
+
{...props}
|
|
102
|
+
>
|
|
103
|
+
<path
|
|
104
|
+
fillRule='evenodd'
|
|
105
|
+
clipRule='evenodd'
|
|
106
|
+
d='M15 7.5C15 11.6421 11.6421 15 7.5 15C3.35786 15 0 11.6421 0 7.5C0 3.35786 3.35786 0 7.5 0C11.6421 0 15 3.35786 15 7.5ZM13.5 7.5C13.5 10.8137 10.8137 13.5 7.5 13.5C6.44531 13.5 5.45418 13.2279 4.59299 12.75H6.85607C7.29596 12.3013 7.67991 11.7975 7.99663 11.25H2.81597C2.62675 11.014 2.45501 10.7633 2.30269 10.5H8.37593C8.58402 10.0237 8.74407 9.52171 8.84997 9H1.68903C1.62614 8.75568 1.57826 8.50533 1.54642 8.25H8.96297C8.98746 8.00332 9 7.75312 9 7.5C9 7.24688 8.98746 6.99668 8.96297 6.75H1.54642C1.57826 6.49467 1.62614 6.24432 1.68903 6H8.84997C8.74407 5.47829 8.58402 4.97625 8.37593 4.5H2.30269C2.45501 4.2367 2.62675 3.98604 2.81597 3.75H7.99663C7.67991 3.20248 7.29596 2.69872 6.85607 2.25H4.59299C5.45418 1.77213 6.44531 1.5 7.5 1.5C10.8137 1.5 13.5 4.18629 13.5 7.5Z'
|
|
107
|
+
fill='currentColor'
|
|
108
|
+
/>
|
|
109
|
+
</svg>
|
|
110
|
+
)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** "Aa" glyph for the lasso geometry toolbar's rename action. From Figma node
|
|
114
|
+
* 5960:14347. */
|
|
115
|
+
export function TextAaIcon(props: SVGProps<SVGSVGElement>) {
|
|
116
|
+
return (
|
|
117
|
+
<svg
|
|
118
|
+
fill='none'
|
|
119
|
+
xmlns='http://www.w3.org/2000/svg'
|
|
120
|
+
viewBox='0 0 15.877 10.6875'
|
|
121
|
+
{...props}
|
|
122
|
+
>
|
|
123
|
+
<path
|
|
124
|
+
fillRule='evenodd'
|
|
125
|
+
clipRule='evenodd'
|
|
126
|
+
d='M5.4 0L9.18 10.5H7.56L6.72 8.085H2.475L1.62 10.5H0L3.795 0H5.4ZM4.605 1.95C4.515 2.31 4.395 2.705 4.245 3.135L2.955 6.705H6.24L4.965 3.135C4.785 2.645 4.665 2.25 4.605 1.95ZM12.9247 2.5875C13.3083 2.5875 13.6514 2.6203 13.9542 2.68591C14.257 2.75152 14.5649 2.87769 14.8778 3.06442C15.1907 3.25114 15.4355 3.55143 15.6121 3.96526C15.7887 4.37909 15.877 4.88881 15.877 5.49442V10.5058H14.363V9.83965C13.818 10.4049 13.1114 10.6875 12.2434 10.6875C11.4258 10.6875 10.7622 10.4629 10.2525 10.0138C9.74275 9.5646 9.48789 8.92619 9.48789 8.09853C9.48789 7.26077 9.77555 6.62488 10.3509 6.19086C10.9262 5.75685 11.6277 5.53984 12.4554 5.53984C13.2124 5.53984 13.8483 5.69124 14.363 5.99404V5.57012C14.363 4.99479 14.2318 4.586 13.9694 4.34376C13.707 4.10152 13.2881 3.9804 12.7127 3.9804C11.8649 3.9804 11.1281 4.21255 10.5023 4.67685L10.0329 3.38993C10.8101 2.85498 11.7741 2.5875 12.9247 2.5875ZM12.7127 6.90245C12.2081 6.90245 11.7968 7.00086 11.4788 7.19769C11.1609 7.39451 11.0019 7.69479 11.0019 8.09853C11.0019 8.47199 11.1331 8.7647 11.3955 8.97666C11.658 9.18862 12.0365 9.2946 12.5311 9.2946C13.2578 9.2946 13.8684 9.01703 14.363 8.46189V7.28096C13.8886 7.02862 13.3385 6.90245 12.7127 6.90245Z'
|
|
127
|
+
fill='currentColor'
|
|
128
|
+
/>
|
|
129
|
+
</svg>
|
|
130
|
+
)
|
|
131
|
+
}
|
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
ArrowDropDown,
|
|
3
|
+
LayersOutlined,
|
|
4
|
+
MoreVertOutlined,
|
|
5
|
+
} from '@mui/icons-material'
|
|
2
6
|
import {
|
|
3
7
|
Box,
|
|
4
8
|
Divider,
|
|
@@ -73,6 +77,7 @@ export function LassoToolsUI({
|
|
|
73
77
|
onChangeMode,
|
|
74
78
|
onAllChipToggle,
|
|
75
79
|
onAllDelete,
|
|
80
|
+
onLayerFilters,
|
|
76
81
|
ChipsSlot = LassoToolsUI.Chips,
|
|
77
82
|
ActionSlot = LassoToolsUI.Action,
|
|
78
83
|
SecondaryActionsSlot = LassoToolsUI.SecondaryActions,
|
|
@@ -94,6 +99,7 @@ export function LassoToolsUI({
|
|
|
94
99
|
onChangeMode,
|
|
95
100
|
onAllChipToggle,
|
|
96
101
|
onAllDelete,
|
|
102
|
+
onLayerFilters,
|
|
97
103
|
}
|
|
98
104
|
return (
|
|
99
105
|
<Paper
|
|
@@ -506,6 +512,24 @@ LassoToolsUI.Options = function OptionsWrapper({
|
|
|
506
512
|
/>
|
|
507
513
|
</>
|
|
508
514
|
)}
|
|
515
|
+
{!!props.onLayerFilters && (
|
|
516
|
+
<>
|
|
517
|
+
<Divider />
|
|
518
|
+
<MenuItem
|
|
519
|
+
onClick={() => {
|
|
520
|
+
props.onLayerFilters?.()
|
|
521
|
+
props.onClose()
|
|
522
|
+
}}
|
|
523
|
+
>
|
|
524
|
+
<ListItemIcon sx={styles.options.icons}>
|
|
525
|
+
<LayersOutlined />
|
|
526
|
+
</ListItemIcon>
|
|
527
|
+
<ListItemText>
|
|
528
|
+
{labels?.options?.layerFilters ?? 'Layer filters'}
|
|
529
|
+
</ListItemText>
|
|
530
|
+
</MenuItem>
|
|
531
|
+
</>
|
|
532
|
+
)}
|
|
509
533
|
</>
|
|
510
534
|
)
|
|
511
535
|
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for the LassoLayerFiltersUI dialog — renders layer checkboxes from the
|
|
3
|
+
* committed selection, edits a draft, commits only on Apply (Cancel/close
|
|
4
|
+
* discards), supports "All selected" and search.
|
|
5
|
+
*/
|
|
6
|
+
import { describe, it, expect, vi } from 'vitest'
|
|
7
|
+
import { render, fireEvent, screen, within } from '@testing-library/react'
|
|
8
|
+
import { LassoLayerFiltersUI } from './layer-filters'
|
|
9
|
+
|
|
10
|
+
const layers = [
|
|
11
|
+
{ id: 'a', label: 'Blocks', checked: true },
|
|
12
|
+
{ id: 'b', label: 'Roads', checked: false },
|
|
13
|
+
]
|
|
14
|
+
|
|
15
|
+
function setup(props: Partial<Parameters<typeof LassoLayerFiltersUI>[0]> = {}) {
|
|
16
|
+
const handlers = { onClose: vi.fn(), onApply: vi.fn() }
|
|
17
|
+
render(<LassoLayerFiltersUI open layers={layers} {...handlers} {...props} />)
|
|
18
|
+
return handlers
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const dialog = () => screen.getByRole('dialog')
|
|
22
|
+
|
|
23
|
+
describe('LassoLayerFiltersUI', () => {
|
|
24
|
+
it('renders the title and a checkbox per layer (+ All selected)', () => {
|
|
25
|
+
setup()
|
|
26
|
+
expect(screen.getByText('Layer Filters')).toBeDefined()
|
|
27
|
+
// 2 layers + the "All selected" master = 3 checkboxes.
|
|
28
|
+
expect(within(dialog()).getAllByRole('checkbox')).toHaveLength(3)
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
it('reflects the committed selection in the draft', () => {
|
|
32
|
+
setup()
|
|
33
|
+
expect(
|
|
34
|
+
within(dialog()).getByRole<HTMLInputElement>('checkbox', {
|
|
35
|
+
name: 'Blocks',
|
|
36
|
+
}).checked,
|
|
37
|
+
).toBe(true)
|
|
38
|
+
expect(
|
|
39
|
+
within(dialog()).getByRole<HTMLInputElement>('checkbox', {
|
|
40
|
+
name: 'Roads',
|
|
41
|
+
}).checked,
|
|
42
|
+
).toBe(false)
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
it('commits the edited selection only on Apply', () => {
|
|
46
|
+
const { onApply, onClose } = setup()
|
|
47
|
+
fireEvent.click(within(dialog()).getByRole('checkbox', { name: 'Roads' }))
|
|
48
|
+
// Not committed yet.
|
|
49
|
+
expect(onApply).not.toHaveBeenCalled()
|
|
50
|
+
fireEvent.click(screen.getByRole('button', { name: 'Apply' }))
|
|
51
|
+
expect(onApply).toHaveBeenCalledWith(['a', 'b'])
|
|
52
|
+
expect(onClose).toHaveBeenCalled()
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
it('discards the draft on Cancel', () => {
|
|
56
|
+
const { onApply, onClose } = setup()
|
|
57
|
+
fireEvent.click(within(dialog()).getByRole('checkbox', { name: 'Blocks' }))
|
|
58
|
+
fireEvent.click(screen.getByRole('button', { name: 'Cancel' }))
|
|
59
|
+
expect(onApply).not.toHaveBeenCalled()
|
|
60
|
+
expect(onClose).toHaveBeenCalled()
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
it('"All selected" toggles every layer', () => {
|
|
64
|
+
const { onApply } = setup()
|
|
65
|
+
// Currently only "Blocks" checked → master is indeterminate; clicking selects all.
|
|
66
|
+
fireEvent.click(
|
|
67
|
+
within(dialog()).getByRole('checkbox', { name: 'All selected' }),
|
|
68
|
+
)
|
|
69
|
+
fireEvent.click(screen.getByRole('button', { name: 'Apply' }))
|
|
70
|
+
expect(onApply).toHaveBeenCalledWith(['a', 'b'])
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
it('filters the list by search text', () => {
|
|
74
|
+
setup()
|
|
75
|
+
fireEvent.change(within(dialog()).getByRole('textbox'), {
|
|
76
|
+
target: { value: 'road' },
|
|
77
|
+
})
|
|
78
|
+
expect(
|
|
79
|
+
within(dialog()).queryByRole('checkbox', { name: 'Blocks' }),
|
|
80
|
+
).toBeNull()
|
|
81
|
+
expect(
|
|
82
|
+
within(dialog()).getByRole('checkbox', { name: 'Roads' }),
|
|
83
|
+
).toBeDefined()
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
it('does not render when closed', () => {
|
|
87
|
+
setup({ open: false })
|
|
88
|
+
expect(screen.queryByRole('dialog')).toBeNull()
|
|
89
|
+
})
|
|
90
|
+
})
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
import { Close, Search } from '@mui/icons-material'
|
|
2
|
+
import {
|
|
3
|
+
Box,
|
|
4
|
+
Button,
|
|
5
|
+
Checkbox,
|
|
6
|
+
Dialog,
|
|
7
|
+
DialogActions,
|
|
8
|
+
DialogContent,
|
|
9
|
+
DialogTitle,
|
|
10
|
+
Divider,
|
|
11
|
+
IconButton,
|
|
12
|
+
InputBase,
|
|
13
|
+
List,
|
|
14
|
+
ListItemButton,
|
|
15
|
+
ListItemIcon,
|
|
16
|
+
ListItemText,
|
|
17
|
+
Typography,
|
|
18
|
+
type SxProps,
|
|
19
|
+
type Theme,
|
|
20
|
+
} from '@mui/material'
|
|
21
|
+
import { useId, useState, type JSX } from 'react'
|
|
22
|
+
import type { LassoLayerFiltersUIProps } from './types'
|
|
23
|
+
|
|
24
|
+
const titleSx = {
|
|
25
|
+
display: 'flex',
|
|
26
|
+
alignItems: 'center',
|
|
27
|
+
justifyContent: 'space-between',
|
|
28
|
+
gap: 1,
|
|
29
|
+
p: 2,
|
|
30
|
+
} satisfies SxProps<Theme>
|
|
31
|
+
|
|
32
|
+
// One rounded group on the grey "default" background, holding the search row,
|
|
33
|
+
// a divider, and the scrollable list (Figma node 5960-14780).
|
|
34
|
+
const groupSx = {
|
|
35
|
+
borderRadius: ({ spacing }) => spacing(0.5),
|
|
36
|
+
overflow: 'hidden',
|
|
37
|
+
backgroundColor: ({ palette }) => palette.background.default,
|
|
38
|
+
} satisfies SxProps<Theme>
|
|
39
|
+
|
|
40
|
+
const searchSx = {
|
|
41
|
+
display: 'flex',
|
|
42
|
+
alignItems: 'center',
|
|
43
|
+
gap: 1,
|
|
44
|
+
height: 32,
|
|
45
|
+
px: 1.5,
|
|
46
|
+
color: ({ palette }) => palette.text.secondary,
|
|
47
|
+
} satisfies SxProps<Theme>
|
|
48
|
+
|
|
49
|
+
const rowSx = {
|
|
50
|
+
px: 2,
|
|
51
|
+
py: 0.5,
|
|
52
|
+
} satisfies SxProps<Theme>
|
|
53
|
+
|
|
54
|
+
const checkboxSlotSx = {
|
|
55
|
+
minWidth: 0,
|
|
56
|
+
mr: 1,
|
|
57
|
+
} satisfies SxProps<Theme>
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* "Layer filters" dialog: choose which map layers the lasso mask clips. The
|
|
61
|
+
* selection is a draft committed on Apply (Cancel/close discards it). Wire it
|
|
62
|
+
* to the lasso store via `LassoTools.LayerFilters` in `@carto/ps-react-maps`,
|
|
63
|
+
* and open it from the drawing-tool menu's "Layer filters" item
|
|
64
|
+
* (`LassoToolsUI` `onLayerFilters`).
|
|
65
|
+
*/
|
|
66
|
+
export function LassoLayerFiltersUI({
|
|
67
|
+
open,
|
|
68
|
+
onClose,
|
|
69
|
+
layers,
|
|
70
|
+
onApply,
|
|
71
|
+
labels,
|
|
72
|
+
DialogProps,
|
|
73
|
+
}: LassoLayerFiltersUIProps): JSX.Element {
|
|
74
|
+
const labelId = useId()
|
|
75
|
+
const [draft, setDraft] = useState<Set<string>>(() => new Set())
|
|
76
|
+
const [search, setSearch] = useState('')
|
|
77
|
+
|
|
78
|
+
// Reset the draft from the committed selection when the dialog opens, by
|
|
79
|
+
// adjusting state during render (the React-recommended pattern — no effect,
|
|
80
|
+
// and later `layers` changes don't discard in-progress edits).
|
|
81
|
+
const [wasOpen, setWasOpen] = useState(false)
|
|
82
|
+
if (open !== wasOpen) {
|
|
83
|
+
setWasOpen(open)
|
|
84
|
+
if (open) {
|
|
85
|
+
setDraft(new Set(layers.filter((l) => l.checked).map((l) => l.id)))
|
|
86
|
+
setSearch('')
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const title = labels?.title ?? 'Layer Filters'
|
|
91
|
+
const description =
|
|
92
|
+
labels?.description ??
|
|
93
|
+
'Select the layers you want to filter with this polygon'
|
|
94
|
+
const allSelectedLabel = labels?.allSelected ?? 'All selected'
|
|
95
|
+
const empty = labels?.empty ?? 'No layers to filter'
|
|
96
|
+
|
|
97
|
+
const allChecked = layers.length > 0 && layers.every((l) => draft.has(l.id))
|
|
98
|
+
const someChecked = layers.some((l) => draft.has(l.id))
|
|
99
|
+
|
|
100
|
+
const toggle = (id: string) =>
|
|
101
|
+
setDraft((prev) => {
|
|
102
|
+
const next = new Set(prev)
|
|
103
|
+
if (next.has(id)) next.delete(id)
|
|
104
|
+
else next.add(id)
|
|
105
|
+
return next
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
const toggleAll = () =>
|
|
109
|
+
setDraft(allChecked ? new Set() : new Set(layers.map((l) => l.id)))
|
|
110
|
+
|
|
111
|
+
const handleApply = () => {
|
|
112
|
+
// Emit ids in `layers` order (stable, diff-friendly), not Set-insertion order.
|
|
113
|
+
onApply(layers.filter((l) => draft.has(l.id)).map((l) => l.id))
|
|
114
|
+
onClose()
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const normalized = search.trim().toLowerCase()
|
|
118
|
+
const filtered = normalized
|
|
119
|
+
? layers.filter((l) => l.label.toLowerCase().includes(normalized))
|
|
120
|
+
: layers
|
|
121
|
+
|
|
122
|
+
return (
|
|
123
|
+
<Dialog
|
|
124
|
+
open={open}
|
|
125
|
+
onClose={onClose}
|
|
126
|
+
maxWidth='xs'
|
|
127
|
+
fullWidth
|
|
128
|
+
{...DialogProps}
|
|
129
|
+
>
|
|
130
|
+
<DialogTitle component='div' sx={titleSx}>
|
|
131
|
+
<Typography variant='subtitle1' fontWeight={500}>
|
|
132
|
+
{title}
|
|
133
|
+
</Typography>
|
|
134
|
+
<IconButton aria-label='close' size='small' onClick={onClose}>
|
|
135
|
+
<Close fontSize='small' />
|
|
136
|
+
</IconButton>
|
|
137
|
+
</DialogTitle>
|
|
138
|
+
|
|
139
|
+
<DialogContent sx={{ pt: 1, px: 2, pb: 3 }}>
|
|
140
|
+
<Typography variant='body2' color='text.secondary' sx={{ mb: 2 }}>
|
|
141
|
+
{description}
|
|
142
|
+
</Typography>
|
|
143
|
+
|
|
144
|
+
<Box sx={groupSx}>
|
|
145
|
+
<Box sx={searchSx}>
|
|
146
|
+
<Search sx={{ fontSize: 18 }} />
|
|
147
|
+
<InputBase
|
|
148
|
+
fullWidth
|
|
149
|
+
placeholder={labels?.search ?? 'Search layers'}
|
|
150
|
+
inputProps={{ 'aria-label': labels?.search ?? 'Search layers' }}
|
|
151
|
+
value={search}
|
|
152
|
+
onChange={(e) => setSearch(e.target.value)}
|
|
153
|
+
sx={{ fontSize: 13, color: 'text.primary' }}
|
|
154
|
+
/>
|
|
155
|
+
</Box>
|
|
156
|
+
<Divider />
|
|
157
|
+
<List
|
|
158
|
+
component='div'
|
|
159
|
+
role='list'
|
|
160
|
+
disablePadding
|
|
161
|
+
sx={{
|
|
162
|
+
maxHeight: 240,
|
|
163
|
+
overflowY: 'auto',
|
|
164
|
+
// MUI gives dividers inside a List an 8px vertical margin; the
|
|
165
|
+
// "All selected" separator should sit flush like the rows.
|
|
166
|
+
'& .MuiDivider-root': { my: 0 },
|
|
167
|
+
}}
|
|
168
|
+
>
|
|
169
|
+
<ListItemButton dense sx={rowSx} onClick={toggleAll}>
|
|
170
|
+
<ListItemIcon sx={checkboxSlotSx}>
|
|
171
|
+
<Checkbox
|
|
172
|
+
edge='start'
|
|
173
|
+
tabIndex={-1}
|
|
174
|
+
disableRipple
|
|
175
|
+
sx={{ p: 0.5 }}
|
|
176
|
+
checked={allChecked}
|
|
177
|
+
indeterminate={!allChecked && someChecked}
|
|
178
|
+
inputProps={{ 'aria-labelledby': `${labelId}-all` }}
|
|
179
|
+
/>
|
|
180
|
+
</ListItemIcon>
|
|
181
|
+
<ListItemText
|
|
182
|
+
id={`${labelId}-all`}
|
|
183
|
+
primary={allSelectedLabel}
|
|
184
|
+
primaryTypographyProps={{ variant: 'body2' }}
|
|
185
|
+
/>
|
|
186
|
+
</ListItemButton>
|
|
187
|
+
<Divider />
|
|
188
|
+
{filtered.length === 0 ? (
|
|
189
|
+
<Typography variant='body2' color='text.secondary' sx={{ p: 2 }}>
|
|
190
|
+
{empty}
|
|
191
|
+
</Typography>
|
|
192
|
+
) : (
|
|
193
|
+
filtered.map((layer) => (
|
|
194
|
+
<ListItemButton
|
|
195
|
+
key={layer.id}
|
|
196
|
+
dense
|
|
197
|
+
sx={rowSx}
|
|
198
|
+
onClick={() => toggle(layer.id)}
|
|
199
|
+
>
|
|
200
|
+
<ListItemIcon sx={checkboxSlotSx}>
|
|
201
|
+
<Checkbox
|
|
202
|
+
edge='start'
|
|
203
|
+
tabIndex={-1}
|
|
204
|
+
disableRipple
|
|
205
|
+
sx={{ p: 0.5 }}
|
|
206
|
+
checked={draft.has(layer.id)}
|
|
207
|
+
inputProps={{
|
|
208
|
+
'aria-labelledby': `${labelId}-${layer.id}`,
|
|
209
|
+
}}
|
|
210
|
+
/>
|
|
211
|
+
</ListItemIcon>
|
|
212
|
+
<ListItemText
|
|
213
|
+
id={`${labelId}-${layer.id}`}
|
|
214
|
+
primary={layer.label}
|
|
215
|
+
primaryTypographyProps={{
|
|
216
|
+
variant: 'body2',
|
|
217
|
+
noWrap: true,
|
|
218
|
+
title: layer.label,
|
|
219
|
+
}}
|
|
220
|
+
/>
|
|
221
|
+
</ListItemButton>
|
|
222
|
+
))
|
|
223
|
+
)}
|
|
224
|
+
</List>
|
|
225
|
+
</Box>
|
|
226
|
+
</DialogContent>
|
|
227
|
+
|
|
228
|
+
<DialogActions sx={{ p: 2 }}>
|
|
229
|
+
<Button onClick={onClose}>{labels?.cancel ?? 'Cancel'}</Button>
|
|
230
|
+
<Button variant='contained' onClick={handleApply}>
|
|
231
|
+
{labels?.apply ?? 'Apply'}
|
|
232
|
+
</Button>
|
|
233
|
+
</DialogActions>
|
|
234
|
+
</Dialog>
|
|
235
|
+
)
|
|
236
|
+
}
|