@carto/ps-react-ui 4.15.0 → 4.17.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-Bo8U_qWM.js +468 -0
- package/dist/legend-store-registry-Bo8U_qWM.js.map +1 -0
- package/dist/legend.js +1485 -798
- 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 +19 -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 +191 -9
- package/src/legend/components/legend-group/legend-group.tsx +88 -52
- 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 +82 -2
- package/src/legend/stores/legend-store.test.ts +168 -0
- package/src/legend/stores/selectors.ts +64 -2
- package/src/legend/stores/types.ts +19 -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
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import type { MouseEvent } from 'react'
|
|
2
|
+
import { CircularProgress, MenuItem } from '@mui/material'
|
|
3
|
+
import {
|
|
4
|
+
getLegendStore,
|
|
5
|
+
selectAllGroupsCollapsed,
|
|
6
|
+
selectGroupVisibility,
|
|
7
|
+
selectHasGroups,
|
|
8
|
+
selectLayersByGroup,
|
|
9
|
+
selectOrderedGroups,
|
|
10
|
+
useLegendId,
|
|
11
|
+
useLegendStore,
|
|
12
|
+
} from '../../stores'
|
|
13
|
+
import { useLegendConfig } from '../../provider'
|
|
14
|
+
import { useLegendGroupId, useLegendMenu } from '../contexts'
|
|
15
|
+
import { runAsyncMenuAction } from '../run-async-menu-action'
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Show only this group (hide all other layers).
|
|
19
|
+
*
|
|
20
|
+
* @experimental This API is new and may change in a future release.
|
|
21
|
+
*/
|
|
22
|
+
export function LegendShowOnlyGroup() {
|
|
23
|
+
const id = useLegendId()
|
|
24
|
+
const groupId = useLegendGroupId()
|
|
25
|
+
const { labels } = useLegendConfig()
|
|
26
|
+
|
|
27
|
+
if (groupId === null) return null
|
|
28
|
+
|
|
29
|
+
const showOnly = () => {
|
|
30
|
+
const state = getLegendStore(id).getState()
|
|
31
|
+
for (const group of selectOrderedGroups(state)) {
|
|
32
|
+
state.setGroupVisibility(group.id, group.id === groupId)
|
|
33
|
+
}
|
|
34
|
+
// Exclusive: the ungrouped layers hide too.
|
|
35
|
+
for (const layer of selectLayersByGroup(state, undefined)) {
|
|
36
|
+
state.setVisibility(layer.id, false)
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return <MenuItem onClick={showOnly}>{labels.showOnlyGroup}</MenuItem>
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Show every layer (panel-level `Legend.PanelMenu` only).
|
|
45
|
+
*
|
|
46
|
+
* @experimental This API is new and may change in a future release.
|
|
47
|
+
*/
|
|
48
|
+
export function LegendShowAllGroups() {
|
|
49
|
+
const id = useLegendId()
|
|
50
|
+
const hasGroups = useLegendStore(id, selectHasGroups)
|
|
51
|
+
const { labels } = useLegendConfig()
|
|
52
|
+
|
|
53
|
+
if (!hasGroups) return null
|
|
54
|
+
|
|
55
|
+
const showAll = () => {
|
|
56
|
+
const state = getLegendStore(id).getState()
|
|
57
|
+
for (const group of selectOrderedGroups(state)) {
|
|
58
|
+
state.setGroupVisibility(group.id, true)
|
|
59
|
+
}
|
|
60
|
+
for (const layer of selectLayersByGroup(state, undefined)) {
|
|
61
|
+
state.setVisibility(layer.id, true)
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return <MenuItem onClick={showAll}>{labels.showAllGroups}</MenuItem>
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Collapse / expand all groups (panel-level `Legend.PanelMenu` only).
|
|
70
|
+
*
|
|
71
|
+
* @experimental This API is new and may change in a future release.
|
|
72
|
+
*/
|
|
73
|
+
export function LegendCollapseAllGroups() {
|
|
74
|
+
const id = useLegendId()
|
|
75
|
+
const hasGroups = useLegendStore(id, selectHasGroups)
|
|
76
|
+
const allCollapsed = useLegendStore(id, selectAllGroupsCollapsed)
|
|
77
|
+
const { labels } = useLegendConfig()
|
|
78
|
+
|
|
79
|
+
if (!hasGroups) return null
|
|
80
|
+
|
|
81
|
+
const toggleAll = () => {
|
|
82
|
+
const state = getLegendStore(id).getState()
|
|
83
|
+
const collapsed = selectAllGroupsCollapsed(state)
|
|
84
|
+
for (const group of selectOrderedGroups(state)) {
|
|
85
|
+
state.setGroupCollapsed(group.id, !collapsed)
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return (
|
|
90
|
+
<MenuItem onClick={toggleAll}>
|
|
91
|
+
{allCollapsed ? labels.expandAllGroups : labels.collapseAllGroups}
|
|
92
|
+
</MenuItem>
|
|
93
|
+
)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export interface LegendZoomToGroupProps {
|
|
97
|
+
/** Fit viewport to the group; may return a Promise for async menu feedback. */
|
|
98
|
+
onZoomTo: (groupId: string) => void | Promise<void>
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Zoom-to-group menu item for `Legend.GroupMenu`. Self-hides when all members are hidden.
|
|
103
|
+
*
|
|
104
|
+
* @experimental This API is new and may change in a future release.
|
|
105
|
+
*/
|
|
106
|
+
export function LegendZoomToGroup({ onZoomTo }: LegendZoomToGroupProps) {
|
|
107
|
+
const id = useLegendId()
|
|
108
|
+
const groupId = useLegendGroupId()
|
|
109
|
+
const menu = useLegendMenu()
|
|
110
|
+
const hidden = useLegendStore(
|
|
111
|
+
id,
|
|
112
|
+
(s) => groupId !== null && selectGroupVisibility(s, groupId) === 'hidden',
|
|
113
|
+
)
|
|
114
|
+
const { labels } = useLegendConfig()
|
|
115
|
+
|
|
116
|
+
if (groupId === null || hidden) return null
|
|
117
|
+
|
|
118
|
+
const busy = menu?.busy ?? false
|
|
119
|
+
|
|
120
|
+
const handleClick = (e: MouseEvent) => {
|
|
121
|
+
e.stopPropagation()
|
|
122
|
+
if (!menu) return
|
|
123
|
+
void runAsyncMenuAction(menu, () => onZoomTo(groupId))
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return (
|
|
127
|
+
<MenuItem disabled={busy} onClick={handleClick}>
|
|
128
|
+
{busy && <CircularProgress size={16} sx={{ mr: 1 }} />}
|
|
129
|
+
{busy ? labels.loading : labels.zoomToGroup}
|
|
130
|
+
</MenuItem>
|
|
131
|
+
)
|
|
132
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { ReactNode } from 'react'
|
|
2
|
+
import { useLegendConfig } from '../../provider'
|
|
3
|
+
import { useLegendGroupId } from '../contexts'
|
|
4
|
+
import { LegendOverflowMenu } from '../legend-overflow-menu/legend-overflow-menu'
|
|
5
|
+
|
|
6
|
+
export interface LegendGroupMenuProps {
|
|
7
|
+
/** Menu items; async items use `stopPropagation` + `runAsyncMenuAction`. */
|
|
8
|
+
children?: ReactNode
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Overflow (⋮) menu for a group header. Renders nothing outside a `Legend.Group`.
|
|
13
|
+
*
|
|
14
|
+
* @experimental This API is new and may change in a future release.
|
|
15
|
+
*/
|
|
16
|
+
export function LegendGroupMenu({ children }: LegendGroupMenuProps) {
|
|
17
|
+
const groupId = useLegendGroupId()
|
|
18
|
+
const { labels } = useLegendConfig()
|
|
19
|
+
|
|
20
|
+
return (
|
|
21
|
+
<LegendOverflowMenu gate={groupId !== null} ariaLabel={labels.groupOptions}>
|
|
22
|
+
{children}
|
|
23
|
+
</LegendOverflowMenu>
|
|
24
|
+
)
|
|
25
|
+
}
|
|
@@ -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
|
-
|
|
9
|
+
Typography,
|
|
10
10
|
} from '@mui/material'
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
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
|
|
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
|
|
26
|
+
function OpacityItem() {
|
|
29
27
|
const id = useLegendId()
|
|
30
28
|
const row = useLegendRow()
|
|
31
|
-
const
|
|
32
|
-
|
|
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
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
-
*
|
|
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
|
|
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
|
-
|
|
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
|
-
<
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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
|
-
</
|
|
140
|
+
</Box>
|
|
166
141
|
)
|
|
167
142
|
}
|
|
168
143
|
|
|
169
144
|
/**
|
|
170
|
-
* Compound opacity control
|
|
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
|
-
|
|
178
|
-
|
|
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
|
-
//
|
|
5
|
-
//
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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
|
-
|
|
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:
|
|
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,
|
|
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'
|