@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.
Files changed (82) hide show
  1. package/dist/components.js +786 -749
  2. package/dist/components.js.map +1 -1
  3. package/dist/legend/stores.js +20 -16
  4. package/dist/legend-store-registry-CVYzhR1-.js +464 -0
  5. package/dist/legend-store-registry-CVYzhR1-.js.map +1 -0
  6. package/dist/legend.js +1478 -794
  7. package/dist/legend.js.map +1 -1
  8. package/dist/types/components/responsive-drawer/responsive-drawer.d.ts +2 -9
  9. package/dist/types/legend/components/a11y.d.ts +6 -0
  10. package/dist/types/legend/components/contexts.d.ts +22 -4
  11. package/dist/types/legend/components/index.d.ts +2 -2
  12. package/dist/types/legend/components/legend-actions/legend-actions.d.ts +4 -3
  13. package/dist/types/legend/components/legend-group/legend-group.d.ts +2 -11
  14. package/dist/types/legend/components/legend-group/styles.d.ts +40 -2
  15. package/dist/types/legend/components/legend-group-menu/legend-group-menu-items.d.ts +28 -0
  16. package/dist/types/legend/components/legend-group-menu/legend-group-menu.d.ts +11 -0
  17. package/dist/types/legend/components/legend-opacity/legend-opacity.d.ts +7 -17
  18. package/dist/types/legend/components/legend-opacity/styles.d.ts +11 -6
  19. package/dist/types/legend/components/legend-overflow-menu/legend-overflow-menu.d.ts +12 -0
  20. package/dist/types/legend/components/legend-panel-menu/legend-panel-menu.d.ts +11 -0
  21. package/dist/types/legend/components/legend-row/legend-row.d.ts +4 -11
  22. package/dist/types/legend/components/legend-row/styles.d.ts +36 -1
  23. package/dist/types/legend/components/legend-row-menu/legend-row-menu-items.d.ts +12 -0
  24. package/dist/types/legend/components/legend-row-menu/legend-row-menu.d.ts +4 -10
  25. package/dist/types/legend/components/legend-row-menu/legend-zoom-to.d.ts +3 -8
  26. package/dist/types/legend/components/legend-sortable/contexts.d.ts +7 -0
  27. package/dist/types/legend/components/legend-sortable/legend-sortable.d.ts +11 -0
  28. package/dist/types/legend/components/legend-sortable/sortable-entity.d.ts +27 -0
  29. package/dist/types/legend/components/legend-sortable/sortable-ids.d.ts +6 -0
  30. package/dist/types/legend/components/legend-sortable/styles.d.ts +54 -0
  31. package/dist/types/legend/components/legend-visible-layers/legend-visible-layers.d.ts +7 -0
  32. package/dist/types/legend/components/legend-visible-layers/styles.d.ts +48 -0
  33. package/dist/types/legend/components/run-async-menu-action.d.ts +7 -0
  34. package/dist/types/legend/index.d.ts +53 -6
  35. package/dist/types/legend/provider/labels.d.ts +21 -0
  36. package/dist/types/legend/stores/index.d.ts +1 -1
  37. package/dist/types/legend/stores/selectors.d.ts +31 -0
  38. package/dist/types/legend/stores/types.d.ts +11 -0
  39. package/package.json +3 -3
  40. package/src/components/responsive-drawer/responsive-drawer.test.tsx +138 -0
  41. package/src/components/responsive-drawer/responsive-drawer.tsx +77 -51
  42. package/src/legend/components/a11y.ts +14 -0
  43. package/src/legend/components/contexts.ts +29 -4
  44. package/src/legend/components/index.ts +3 -4
  45. package/src/legend/components/legend-actions/legend-actions.tsx +4 -3
  46. package/src/legend/components/legend-group/legend-group.test.tsx +166 -9
  47. package/src/legend/components/legend-group/legend-group.tsx +88 -49
  48. package/src/legend/components/legend-group/styles.ts +59 -2
  49. package/src/legend/components/legend-group-menu/legend-group-menu-items.tsx +132 -0
  50. package/src/legend/components/legend-group-menu/legend-group-menu.tsx +25 -0
  51. package/src/legend/components/legend-opacity/legend-opacity.tsx +43 -70
  52. package/src/legend/components/legend-opacity/styles.ts +14 -9
  53. package/src/legend/components/legend-overflow-menu/legend-overflow-menu.tsx +84 -0
  54. package/src/legend/components/legend-panel-menu/legend-panel-menu.tsx +23 -0
  55. package/src/legend/components/legend-ramp/legend-ramp-ui.tsx +2 -1
  56. package/src/legend/components/legend-row/legend-row.test.tsx +304 -85
  57. package/src/legend/components/legend-row/legend-row.tsx +73 -50
  58. package/src/legend/components/legend-row/styles.ts +54 -1
  59. package/src/legend/components/legend-row-menu/legend-row-menu-items.tsx +57 -0
  60. package/src/legend/components/legend-row-menu/legend-row-menu.tsx +12 -50
  61. package/src/legend/components/legend-row-menu/legend-zoom-to.tsx +25 -17
  62. package/src/legend/components/legend-sortable/contexts.ts +12 -0
  63. package/src/legend/components/legend-sortable/legend-sortable.test.tsx +144 -0
  64. package/src/legend/components/legend-sortable/legend-sortable.tsx +195 -0
  65. package/src/legend/components/legend-sortable/sortable-entity.tsx +91 -0
  66. package/src/legend/components/legend-sortable/sortable-ids.ts +17 -0
  67. package/src/legend/components/legend-sortable/styles.ts +56 -0
  68. package/src/legend/components/legend-visibility-toggle/legend-visibility-toggle.tsx +14 -8
  69. package/src/legend/components/legend-visible-layers/legend-visible-layers.test.tsx +86 -0
  70. package/src/legend/components/legend-visible-layers/legend-visible-layers.tsx +135 -0
  71. package/src/legend/components/legend-visible-layers/styles.ts +60 -0
  72. package/src/legend/components/run-async-menu-action.ts +20 -0
  73. package/src/legend/index.ts +59 -4
  74. package/src/legend/provider/labels.ts +43 -1
  75. package/src/legend/stores/index.ts +4 -0
  76. package/src/legend/stores/legend-store-registry.ts +60 -0
  77. package/src/legend/stores/legend-store.test.ts +116 -0
  78. package/src/legend/stores/selectors.ts +64 -2
  79. package/src/legend/stores/types.ts +11 -0
  80. package/src/widgets/actions/download/exports.test.tsx +14 -1
  81. package/dist/selectors-DwHj6mE2.js +0 -388
  82. package/dist/selectors-DwHj6mE2.js.map +0 -1
@@ -3,13 +3,14 @@ import {
3
3
  Box,
4
4
  IconButton,
5
5
  InputAdornment,
6
+ MenuItem,
6
7
  OutlinedInput,
7
- Popover,
8
8
  Slider,
9
- Tooltip,
9
+ Typography,
10
10
  } from '@mui/material'
11
- import { Opacity as OpacityIcon } from '@carto/meridian-ds/custom-icons'
12
- import { useLegendId, useLegendStore } from '../../stores'
11
+ import { Close as CloseIcon } from '@mui/icons-material'
12
+ import { Tooltip } from '../../../components/tooltip/tooltip'
13
+ import { useLegendId, useLegendShallow, useLegendStore } from '../../stores'
13
14
  import { useLegendConfig } from '../../provider'
14
15
  import { useLegendRow } from '../contexts'
15
16
  import { styles } from './styles'
@@ -18,82 +19,53 @@ const clampPercent = (value: number): number =>
18
19
  Math.min(100, Math.max(0, Math.round(value)))
19
20
 
20
21
  /**
21
- * Opacity header action for a layer row: a checkerboard icon button, pinned
22
- * `.active` in the hover-fade group while opacity ≠ 1 or the popover is open,
23
- * disabled while the layer is hidden. Opens `Legend.Opacity.Popover` through
24
- * the row context. Place it inside `<Legend.Actions>` of a `Legend.Row`.
22
+ * Opacity menu item toggles the row's inline bar; self-hides when hidden or no `opacityControl`.
25
23
  *
26
24
  * @experimental This API is new and may change in a future release.
27
25
  */
28
- function OpacityTrigger() {
26
+ function OpacityItem() {
29
27
  const id = useLegendId()
30
28
  const row = useLegendRow()
31
- const layerId = row?.layerId
32
- const opacity = useLegendStore(id, (s) =>
33
- layerId ? (s.layers[layerId]?.opacity ?? 1) : 1,
34
- )
35
- const visible = useLegendStore(id, (s) =>
36
- layerId ? (s.layers[layerId]?.visible ?? true) : true,
29
+ const layer = useLegendShallow(id, (s) =>
30
+ row ? s.layers[row.layerId] : undefined,
37
31
  )
32
+ const setCollapsed = useLegendShallow(id, (s) => s.setCollapsed)
38
33
  const { labels } = useLegendConfig()
39
34
 
40
- // A hidden layer shows only its visibility eye + collapse chevron — the
41
- // opacity action disappears entirely until the layer is shown again.
42
- if (!row || !visible) return null
43
- const dirty = opacity !== 1
35
+ if (!row || !layer?.visible || !layer.opacityControl) return null
36
+
37
+ const toggle = () => {
38
+ const next = !row.opacityOpen
39
+ row.setOpacityOpen(next)
40
+ if (next && layer.collapsed) setCollapsed(row.layerId, false)
41
+ }
44
42
 
45
43
  return (
46
- // The wrapper keeps the Tooltip working on the button; as the fade
47
- // group's direct child it also carries the `.active` pin.
48
- <Tooltip title={labels.opacity}>
49
- <Box
50
- component='span'
51
- className={dirty || row.opacityOpen ? 'active' : undefined}
52
- sx={styles.triggerWrap}
53
- >
54
- <IconButton
55
- size='small'
56
- onClick={() => row.setOpacityOpen(true)}
57
- aria-label={labels.opacity}
58
- >
59
- <OpacityIcon fontSize='small' />
60
- </IconButton>
61
- </Box>
62
- </Tooltip>
44
+ <MenuItem selected={row.opacityOpen} onClick={toggle}>
45
+ {labels.opacity}
46
+ </MenuItem>
63
47
  )
64
48
  }
65
49
 
66
50
  /**
67
- * The opacity editing surface a popover **anchored to the row's sticky
68
- * header, spanning its full width**, holding a slider (writes `setOpacity`
69
- * only on release, widgets-v2 parity) and a percent input (commits live as
70
- * soon as the draft parses, clamped 0–100; never ellipsizes "100 %").
71
- * Declare it anywhere inside the `Legend.Row` (it renders in a portal);
72
- * open it with `Legend.Opacity.Trigger`.
51
+ * Inline opacity bar (first body child of `Legend.Row`). Slider commits on release; input commits live.
73
52
  *
74
53
  * @experimental This API is new and may change in a future release.
75
54
  */
76
- function OpacityPopover() {
55
+ function OpacityInline() {
77
56
  const id = useLegendId()
78
57
  const row = useLegendRow()
79
58
  const layerId = row?.layerId
80
59
  const opacity = useLegendStore(id, (s) =>
81
60
  layerId ? (s.layers[layerId]?.opacity ?? 1) : 1,
82
61
  )
83
- const visible = useLegendStore(id, (s) =>
84
- layerId ? (s.layers[layerId]?.visible ?? true) : true,
85
- )
86
62
  const setOpacity = useLegendStore(id, (s) => s.setOpacity)
87
63
  const { labels } = useLegendConfig()
88
64
 
89
- // Drag and typing drafts: the thumb and the input track each other through
90
- // `percent` while only commits reach the store (and the map).
91
65
  const [dragPercent, setDragPercent] = useState<number | null>(null)
92
66
  const [draft, setDraft] = useState<string | null>(null)
93
67
 
94
- // The trigger is gone while hidden; also gate `open` so a popover left open
95
- // can't survive the layer being hidden elsewhere.
96
- if (!row || !layerId || !visible) return null
68
+ if (!row || !layerId || !row.opacityOpen) return null
97
69
 
98
70
  const percent = dragPercent ?? clampPercent(opacity * 100)
99
71
 
@@ -109,7 +81,6 @@ function OpacityPopover() {
109
81
  const parsed = Number(raw)
110
82
  if (!Number.isFinite(parsed)) return
111
83
  const clamped = commitPercent(parsed)
112
- // Out-of-range entries snap the visible draft to the committed value.
113
84
  if (clamped !== parsed) setDraft(String(clamped))
114
85
  }
115
86
 
@@ -120,16 +91,22 @@ function OpacityPopover() {
120
91
  }
121
92
 
122
93
  return (
123
- <Popover
124
- open={row.opacityOpen && Boolean(row.headerEl)}
125
- anchorEl={row.headerEl}
126
- onClose={handleClose}
127
- anchorOrigin={{ vertical: 'bottom', horizontal: 'left' }}
128
- transformOrigin={{ vertical: 'top', horizontal: 'left' }}
129
- // Full row width: the paper matches the sticky header it anchors to.
130
- slotProps={{ paper: { sx: { width: row.headerEl?.clientWidth } } }}
131
- >
132
- <Box sx={styles.popover}>
94
+ <Box sx={styles.inline}>
95
+ <Box sx={styles.inlineHeader}>
96
+ <Typography variant='caption' fontWeight='medium' color='text.primary'>
97
+ {labels.opacity}
98
+ </Typography>
99
+ <Tooltip title={labels.close}>
100
+ <IconButton
101
+ size='small'
102
+ onClick={handleClose}
103
+ aria-label={labels.close}
104
+ >
105
+ <CloseIcon fontSize='small' />
106
+ </IconButton>
107
+ </Tooltip>
108
+ </Box>
109
+ <Box sx={styles.inlineControls}>
133
110
  <Slider
134
111
  size='small'
135
112
  min={0}
@@ -151,8 +128,6 @@ function OpacityPopover() {
151
128
  size='small'
152
129
  value={draft ?? String(percent)}
153
130
  onChange={(event) => handleInputChange(event.target.value)}
154
- // An abandoned draft (empty / non-numeric) snaps back to the
155
- // store value on blur.
156
131
  onBlur={() => setDraft(null)}
157
132
  onKeyDown={(event) => {
158
133
  if (event.key === 'Enter') (event.target as HTMLElement).blur()
@@ -162,18 +137,16 @@ function OpacityPopover() {
162
137
  sx={styles.input}
163
138
  />
164
139
  </Box>
165
- </Popover>
140
+ </Box>
166
141
  )
167
142
  }
168
143
 
169
144
  /**
170
- * Compound opacity control for layer rows (widgets-v2 `FullScreen` parity):
171
- * `Trigger` lives in the header `<Legend.Actions>`, `Popover` anywhere in the
172
- * row body — they share state through the row context.
145
+ * Compound opacity control: `Item` in the row menu, `Inline` in the body.
173
146
  *
174
147
  * @experimental This API is new and may change in a future release.
175
148
  */
176
149
  export const LegendOpacity = {
177
- Trigger: OpacityTrigger,
178
- Popover: OpacityPopover,
150
+ Item: OpacityItem,
151
+ Inline: OpacityInline,
179
152
  }
@@ -1,25 +1,30 @@
1
1
  import type { SxProps, Theme } from '@mui/material'
2
2
 
3
3
  export const styles = {
4
- // Tooltip wrapper around the (possibly disabled) trigger button. A plain
5
- // inline span would be sized by the line box (baseline + descender) and
6
- // end up ~1px taller than the 24px button inside.
7
- triggerWrap: {
8
- display: 'inline-flex',
4
+ // Figma "Legend Opacity": a soft rounded bar between the row header and the
5
+ // body caption label + on the first line, slider + % input below.
6
+ inline: {
7
+ display: 'flex',
8
+ flexDirection: 'column',
9
+ gap: 0.5,
10
+ borderRadius: 1,
11
+ },
12
+ inlineHeader: {
13
+ display: 'flex',
14
+ alignItems: 'center',
15
+ justifyContent: 'space-between',
9
16
  },
10
- popover: {
17
+ inlineControls: {
11
18
  display: 'flex',
12
19
  alignItems: 'center',
13
20
  gap: 1.5,
14
- padding: ({ spacing }) => spacing(1, 1.5),
15
- width: 280,
16
21
  },
17
22
  slider: {
18
23
  flex: 1,
19
24
  },
20
25
  // Wide enough for "100 %" — the value must never ellipsize.
21
26
  input: {
22
- width: 76,
27
+ width: 68,
23
28
  flexShrink: 0,
24
29
  '& input': {
25
30
  textAlign: 'right',
@@ -0,0 +1,84 @@
1
+ import {
2
+ useCallback,
3
+ useMemo,
4
+ useRef,
5
+ useState,
6
+ type MouseEvent,
7
+ type ReactNode,
8
+ } from 'react'
9
+ import { IconButton, Menu } from '@mui/material'
10
+ import { MoreVert as MoreVertIcon } from '@mui/icons-material'
11
+ import { Tooltip } from '../../../components/tooltip/tooltip'
12
+ import { useLegendConfig } from '../../provider'
13
+ import { LegendMenuContext, type LegendMenuController } from '../contexts'
14
+
15
+ export interface LegendOverflowMenuProps {
16
+ /** Menu items; async items use `stopPropagation` + `runAsyncMenuAction`. */
17
+ children?: ReactNode
18
+ /** When `false`, render nothing. Defaults to `true`. */
19
+ gate?: boolean
20
+ /** `aria-label` for the ⋮ trigger. */
21
+ ariaLabel: string
22
+ /** Extra `.active` pin (e.g. inline opacity bar open). */
23
+ active?: boolean
24
+ }
25
+ export function LegendOverflowMenu({
26
+ children,
27
+ gate = true,
28
+ ariaLabel,
29
+ active = false,
30
+ }: LegendOverflowMenuProps) {
31
+ const [anchorEl, setAnchorEl] = useState<HTMLElement | null>(null)
32
+ const [busy, setBusyState] = useState(false)
33
+ // Ref so close-guards and click gates see the flag before the next render.
34
+ const busyRef = useRef(false)
35
+ const open = Boolean(anchorEl)
36
+ const { labels } = useLegendConfig()
37
+
38
+ const close = useCallback(() => setAnchorEl(null), [])
39
+ const setBusy = useCallback((next: boolean) => {
40
+ busyRef.current = next
41
+ setBusyState(next)
42
+ }, [])
43
+ const isBusy = useCallback(() => busyRef.current, [])
44
+ const handleClose = useCallback(() => {
45
+ if (busyRef.current) return
46
+ close()
47
+ }, [close])
48
+ const menu = useMemo<LegendMenuController>(
49
+ () => ({ busy, isBusy, setBusy, close }),
50
+ [busy, isBusy, setBusy, close],
51
+ )
52
+
53
+ if (!gate || !children) return null
54
+
55
+ const handleOpen = (e: MouseEvent<HTMLElement>) => {
56
+ e.stopPropagation()
57
+ setAnchorEl(e.currentTarget)
58
+ }
59
+
60
+ return (
61
+ <LegendMenuContext.Provider value={menu}>
62
+ <Tooltip title={labels.moreOptions}>
63
+ <IconButton
64
+ size='small'
65
+ className={open || busy || active ? 'active' : undefined}
66
+ onClick={handleOpen}
67
+ aria-label={ariaLabel}
68
+ aria-haspopup='menu'
69
+ aria-expanded={open}
70
+ >
71
+ <MoreVertIcon fontSize='small' />
72
+ </IconButton>
73
+ </Tooltip>
74
+ <Menu
75
+ anchorEl={anchorEl}
76
+ open={open}
77
+ onClose={handleClose}
78
+ onClick={handleClose}
79
+ >
80
+ {children}
81
+ </Menu>
82
+ </LegendMenuContext.Provider>
83
+ )
84
+ }
@@ -0,0 +1,23 @@
1
+ import type { ReactNode } from 'react'
2
+ import { useLegendConfig } from '../../provider'
3
+ import { LegendOverflowMenu } from '../legend-overflow-menu/legend-overflow-menu'
4
+
5
+ export interface LegendPanelMenuProps {
6
+ /** Menu items; async items use `stopPropagation` + `runAsyncMenuAction`. */
7
+ children?: ReactNode
8
+ }
9
+
10
+ /**
11
+ * Overflow (⋮) menu for the panel header (no row/group context).
12
+ *
13
+ * @experimental This API is new and may change in a future release.
14
+ */
15
+ export function LegendPanelMenu({ children }: LegendPanelMenuProps) {
16
+ const { labels } = useLegendConfig()
17
+
18
+ return (
19
+ <LegendOverflowMenu ariaLabel={labels.panelOptions}>
20
+ {children}
21
+ </LegendOverflowMenu>
22
+ )
23
+ }
@@ -1,4 +1,5 @@
1
- import { Box, Tooltip, Typography } from '@mui/material'
1
+ import { Box, Typography } from '@mui/material'
2
+ import { Tooltip } from '../../../components/tooltip/tooltip'
2
3
  import { ErrorOutline as ErrorIcon } from '@mui/icons-material'
3
4
  import { styles } from './styles'
4
5
  import { DEFAULT_LEGEND_LABELS, type LegendLabels } from '../../provider/labels'