@carto/ps-react-ui 4.17.2 → 4.17.3
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/legend/stores.js +1 -1
- package/dist/{legend-store-registry-Bo8U_qWM.js → legend-store-registry-p1tSwJXH.js} +151 -147
- package/dist/legend-store-registry-p1tSwJXH.js.map +1 -0
- package/dist/legend.js +473 -415
- package/dist/legend.js.map +1 -1
- package/dist/types/legend/components/legend-actions/legend-actions.d.ts +2 -2
- package/dist/types/legend/components/legend-group/styles.d.ts +31 -20
- package/dist/types/legend/components/legend-row/styles.d.ts +24 -4
- package/dist/types/legend/components/legend-sortable/styles.d.ts +4 -3
- package/package.json +3 -3
- package/src/legend/components/legend-actions/legend-actions.tsx +2 -2
- package/src/legend/components/legend-group/styles.ts +59 -36
- package/src/legend/components/legend-row/legend-row.tsx +10 -7
- package/src/legend/components/legend-row/styles.ts +41 -10
- package/src/legend/components/legend-sortable/styles.ts +13 -4
- package/src/legend/stores/legend-store-registry.ts +28 -4
- package/src/legend/stores/legend-store.test.ts +23 -0
- package/dist/legend-store-registry-Bo8U_qWM.js.map +0 -1
|
@@ -7,8 +7,8 @@ export interface LegendActionsProps {
|
|
|
7
7
|
* parity): the wrapper partitions its children by component identity and
|
|
8
8
|
* renders this slot inside the header, wrapped in the hover-fade group
|
|
9
9
|
* (`PsLegend-rowFade` in rows, `PsLegend-groupFade` in group headers —
|
|
10
|
-
* resolved from the nearest context). Compose the
|
|
11
|
-
* rightmost, per design.
|
|
10
|
+
* resolved from the nearest context). Compose the visibility toggle last so
|
|
11
|
+
* it sits rightmost, per design.
|
|
12
12
|
*
|
|
13
13
|
* @experimental This API is new and may change in a future release.
|
|
14
14
|
*/
|
|
@@ -13,6 +13,22 @@ export declare const styles: {
|
|
|
13
13
|
borderTop: string;
|
|
14
14
|
borderColor: "divider";
|
|
15
15
|
};
|
|
16
|
+
};
|
|
17
|
+
groupDragging: {
|
|
18
|
+
opacity: number;
|
|
19
|
+
};
|
|
20
|
+
header: {
|
|
21
|
+
display: "flex";
|
|
22
|
+
alignItems: "center";
|
|
23
|
+
gap: number;
|
|
24
|
+
padding: ({ spacing }: Theme) => string;
|
|
25
|
+
minHeight: number;
|
|
26
|
+
borderBottom: string;
|
|
27
|
+
borderColor: "divider";
|
|
28
|
+
position: "sticky";
|
|
29
|
+
top: number;
|
|
30
|
+
zIndex: number;
|
|
31
|
+
backgroundColor: "background.default";
|
|
16
32
|
'& .PsLegend-groupFade > *': {
|
|
17
33
|
opacity: number;
|
|
18
34
|
transition: ({ transitions }: Theme) => string;
|
|
@@ -23,10 +39,14 @@ export declare const styles: {
|
|
|
23
39
|
opacity: number;
|
|
24
40
|
};
|
|
25
41
|
};
|
|
26
|
-
'&:hover .PsLegend-groupFade > *, &:focus-within .PsLegend-groupFade > *': {
|
|
27
|
-
opacity: number;
|
|
28
|
-
};
|
|
29
42
|
'@media (hover: hover)': {
|
|
43
|
+
'& .PsLegend-groupFade > *:not(.active)': {
|
|
44
|
+
width: number;
|
|
45
|
+
minWidth: number;
|
|
46
|
+
padding: number;
|
|
47
|
+
overflow: "hidden";
|
|
48
|
+
pointerEvents: "none";
|
|
49
|
+
};
|
|
30
50
|
'& .PsLegend-groupActions:not(:has(.active))': {
|
|
31
51
|
width: number;
|
|
32
52
|
minWidth: number;
|
|
@@ -35,6 +55,14 @@ export declare const styles: {
|
|
|
35
55
|
pointerEvents: "none";
|
|
36
56
|
};
|
|
37
57
|
};
|
|
58
|
+
'&:hover .PsLegend-groupFade > *, &:focus-within .PsLegend-groupFade > *': {
|
|
59
|
+
opacity: number;
|
|
60
|
+
width: string;
|
|
61
|
+
minWidth: number;
|
|
62
|
+
padding: string;
|
|
63
|
+
overflow: "visible";
|
|
64
|
+
pointerEvents: "auto";
|
|
65
|
+
};
|
|
38
66
|
'&:hover .PsLegend-groupActions, &:focus-within .PsLegend-groupActions': {
|
|
39
67
|
width: string;
|
|
40
68
|
minWidth: number;
|
|
@@ -53,22 +81,6 @@ export declare const styles: {
|
|
|
53
81
|
opacity: number;
|
|
54
82
|
};
|
|
55
83
|
};
|
|
56
|
-
groupDragging: {
|
|
57
|
-
opacity: number;
|
|
58
|
-
};
|
|
59
|
-
header: {
|
|
60
|
-
display: "flex";
|
|
61
|
-
alignItems: "center";
|
|
62
|
-
gap: number;
|
|
63
|
-
padding: ({ spacing }: Theme) => string;
|
|
64
|
-
minHeight: number;
|
|
65
|
-
borderBottom: string;
|
|
66
|
-
borderColor: "divider";
|
|
67
|
-
position: "sticky";
|
|
68
|
-
top: number;
|
|
69
|
-
zIndex: number;
|
|
70
|
-
backgroundColor: "background.default";
|
|
71
|
-
};
|
|
72
84
|
titleArea: {
|
|
73
85
|
flex: number;
|
|
74
86
|
minWidth: number;
|
|
@@ -88,7 +100,6 @@ export declare const styles: {
|
|
|
88
100
|
color: "text.secondary";
|
|
89
101
|
};
|
|
90
102
|
label: {
|
|
91
|
-
flex: number;
|
|
92
103
|
minWidth: number;
|
|
93
104
|
overflow: "hidden";
|
|
94
105
|
textOverflow: "ellipsis";
|
|
@@ -18,10 +18,14 @@ export declare const styles: {
|
|
|
18
18
|
opacity: number;
|
|
19
19
|
};
|
|
20
20
|
};
|
|
21
|
-
'&:hover .PsLegend-rowFade > *, &:focus-within .PsLegend-rowFade > *': {
|
|
22
|
-
opacity: number;
|
|
23
|
-
};
|
|
24
21
|
'@media (hover: hover)': {
|
|
22
|
+
'& .PsLegend-rowFade > *:not(.active)': {
|
|
23
|
+
width: number;
|
|
24
|
+
minWidth: number;
|
|
25
|
+
padding: number;
|
|
26
|
+
overflow: "hidden";
|
|
27
|
+
pointerEvents: "none";
|
|
28
|
+
};
|
|
25
29
|
'& .PsLegend-rowActions:not(:has(.active))': {
|
|
26
30
|
width: number;
|
|
27
31
|
minWidth: number;
|
|
@@ -30,6 +34,14 @@ export declare const styles: {
|
|
|
30
34
|
pointerEvents: "none";
|
|
31
35
|
};
|
|
32
36
|
};
|
|
37
|
+
'&:hover .PsLegend-rowFade > *, &:focus-within .PsLegend-rowFade > *': {
|
|
38
|
+
opacity: number;
|
|
39
|
+
width: string;
|
|
40
|
+
minWidth: number;
|
|
41
|
+
padding: string;
|
|
42
|
+
overflow: "visible";
|
|
43
|
+
pointerEvents: "auto";
|
|
44
|
+
};
|
|
33
45
|
'&:hover .PsLegend-rowActions, &:focus-within .PsLegend-rowActions': {
|
|
34
46
|
width: string;
|
|
35
47
|
minWidth: number;
|
|
@@ -64,6 +76,9 @@ export declare const styles: {
|
|
|
64
76
|
zIndex: number;
|
|
65
77
|
backgroundColor: "background.paper";
|
|
66
78
|
};
|
|
79
|
+
headerGrouped: {
|
|
80
|
+
paddingLeft: number;
|
|
81
|
+
};
|
|
67
82
|
titleBox: {
|
|
68
83
|
flex: number;
|
|
69
84
|
minWidth: number;
|
|
@@ -86,7 +101,6 @@ export declare const styles: {
|
|
|
86
101
|
gap: number;
|
|
87
102
|
};
|
|
88
103
|
title: {
|
|
89
|
-
flex: number;
|
|
90
104
|
minWidth: number;
|
|
91
105
|
overflow: "hidden";
|
|
92
106
|
textOverflow: "ellipsis";
|
|
@@ -120,9 +134,15 @@ export declare const styles: {
|
|
|
120
134
|
gap: number;
|
|
121
135
|
padding: ({ spacing }: Theme) => string;
|
|
122
136
|
};
|
|
137
|
+
contentGrouped: {
|
|
138
|
+
paddingLeft: number;
|
|
139
|
+
};
|
|
123
140
|
footer: {
|
|
124
141
|
padding: ({ spacing }: Theme) => string;
|
|
125
142
|
};
|
|
143
|
+
footerGrouped: {
|
|
144
|
+
paddingLeft: number;
|
|
145
|
+
};
|
|
126
146
|
dimmed: {
|
|
127
147
|
opacity: number;
|
|
128
148
|
};
|
|
@@ -6,13 +6,14 @@ export declare const styles: {
|
|
|
6
6
|
display: "flex";
|
|
7
7
|
alignItems: "center";
|
|
8
8
|
justifyContent: "center";
|
|
9
|
-
|
|
9
|
+
left: number;
|
|
10
|
+
top: number;
|
|
10
11
|
};
|
|
11
12
|
handleSlotLayer: {
|
|
12
|
-
|
|
13
|
+
height: number;
|
|
13
14
|
};
|
|
14
15
|
handleSlotGroup: {
|
|
15
|
-
|
|
16
|
+
height: number;
|
|
16
17
|
};
|
|
17
18
|
handle: {
|
|
18
19
|
padding: string;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@carto/ps-react-ui",
|
|
3
|
-
"version": "4.17.
|
|
3
|
+
"version": "4.17.3",
|
|
4
4
|
"description": "CARTO's Professional Service React Material library",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"devDependencies": {
|
|
@@ -17,8 +17,8 @@
|
|
|
17
17
|
"react-dom": "19.2.4",
|
|
18
18
|
"react-markdown": "10.1.0",
|
|
19
19
|
"zustand": "5.0.12",
|
|
20
|
-
"@carto/ps-
|
|
21
|
-
"@carto/ps-
|
|
20
|
+
"@carto/ps-common-types": "1.0.0",
|
|
21
|
+
"@carto/ps-utils": "2.1.0"
|
|
22
22
|
},
|
|
23
23
|
"peerDependencies": {
|
|
24
24
|
"@carto/meridian-ds": "^2.0.0",
|
|
@@ -17,8 +17,8 @@ export interface LegendActionsProps {
|
|
|
17
17
|
* parity): the wrapper partitions its children by component identity and
|
|
18
18
|
* renders this slot inside the header, wrapped in the hover-fade group
|
|
19
19
|
* (`PsLegend-rowFade` in rows, `PsLegend-groupFade` in group headers —
|
|
20
|
-
* resolved from the nearest context). Compose the
|
|
21
|
-
* rightmost, per design.
|
|
20
|
+
* resolved from the nearest context). Compose the visibility toggle last so
|
|
21
|
+
* it sits rightmost, per design.
|
|
22
22
|
*
|
|
23
23
|
* @experimental This API is new and may change in a future release.
|
|
24
24
|
*/
|
|
@@ -15,16 +15,46 @@ export const styles = {
|
|
|
15
15
|
borderTop: '1px solid',
|
|
16
16
|
borderColor: 'divider',
|
|
17
17
|
},
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
18
|
+
},
|
|
19
|
+
// The original stays in place, dimmed, while its overlay clone is dragged.
|
|
20
|
+
groupDragging: {
|
|
21
|
+
opacity: 0.4,
|
|
22
|
+
},
|
|
23
|
+
// Figma: 48px tall, 16px left inset (matches the row's own left inset),
|
|
24
|
+
// 8px gap to the label, actions right-aligned with ~8px edge inset (the
|
|
25
|
+
// small icon-button padding brings the glyphs to the design's 16px optical
|
|
26
|
+
// inset), hairline divider below. Sticky: pins to the top of the panel's
|
|
27
|
+
// scroll container.
|
|
28
|
+
header: {
|
|
29
|
+
display: 'flex',
|
|
30
|
+
alignItems: 'center',
|
|
31
|
+
gap: 1,
|
|
32
|
+
padding: ({ spacing }) => spacing(1.5, 2, 1.5, 2),
|
|
33
|
+
minHeight: LEGEND_GROUP_HEADER_HEIGHT,
|
|
34
|
+
borderBottom: '1px solid',
|
|
35
|
+
borderColor: 'divider',
|
|
36
|
+
position: 'sticky',
|
|
37
|
+
top: 0,
|
|
38
|
+
// Above the sticky row headers (2), which in turn sit above MUI's
|
|
39
|
+
// z-index-1 form internals in row content.
|
|
40
|
+
zIndex: 3,
|
|
41
|
+
// Distinct from the row header / panel body (both `background.paper`) so
|
|
42
|
+
// the group header visually separates from the rest.
|
|
43
|
+
backgroundColor: 'background.default',
|
|
44
|
+
// Hover-fade group actions (widgets-v2 Wrapper parity). Scoped to the
|
|
45
|
+
// header itself (not the group root) so hovering a member row's body
|
|
46
|
+
// below never lights up the group's own actions/handle — only hovering
|
|
47
|
+
// the header (title/handle/actions) does. A control carrying `.active`
|
|
48
|
+
// (hidden group) stays visible. Touch devices always show them. The
|
|
49
|
+
// collapse chevron sits outside the fade group. Each non-active child
|
|
50
|
+
// also collapses to zero width on its own (not just the whole actions
|
|
51
|
+
// slot) so a sibling's `.active` pin (e.g. the eye on a mixed-visibility
|
|
52
|
+
// group) doesn't leave a same-sized invisible hole where the still-faded
|
|
53
|
+
// ⋮ menu would've been.
|
|
24
54
|
'& .PsLegend-groupFade > *': {
|
|
25
55
|
opacity: 1,
|
|
26
56
|
transition: ({ transitions }: Theme) =>
|
|
27
|
-
transitions.create('opacity', {
|
|
57
|
+
transitions.create(['opacity', 'width'], {
|
|
28
58
|
duration: transitions.duration.standard,
|
|
29
59
|
easing: transitions.easing.easeInOut,
|
|
30
60
|
}),
|
|
@@ -35,14 +65,21 @@ export const styles = {
|
|
|
35
65
|
opacity: 1,
|
|
36
66
|
},
|
|
37
67
|
},
|
|
38
|
-
'&:hover .PsLegend-groupFade > *, &:focus-within .PsLegend-groupFade > *': {
|
|
39
|
-
opacity: 1,
|
|
40
|
-
},
|
|
41
68
|
// Faded actions don't reserve flex space on fine-pointer devices — the
|
|
42
69
|
// label grows to the trailing edge. Hover/focus-within restores the slot
|
|
43
70
|
// so the label reflows with the buttons; a dirty `.active` child keeps
|
|
44
71
|
// the slot open even outside hover.
|
|
45
72
|
'@media (hover: hover)': {
|
|
73
|
+
'& .PsLegend-groupFade > *:not(.active)': {
|
|
74
|
+
width: 0,
|
|
75
|
+
minWidth: 0,
|
|
76
|
+
// border-box IconButtons keep their padding inside `width` — zero it
|
|
77
|
+
// too, otherwise the fixed 2px+2px padding survives as a tiny but
|
|
78
|
+
// needless residual gap.
|
|
79
|
+
padding: 0,
|
|
80
|
+
overflow: 'hidden',
|
|
81
|
+
pointerEvents: 'none',
|
|
82
|
+
},
|
|
46
83
|
'& .PsLegend-groupActions:not(:has(.active))': {
|
|
47
84
|
width: 0,
|
|
48
85
|
minWidth: 0,
|
|
@@ -52,6 +89,14 @@ export const styles = {
|
|
|
52
89
|
pointerEvents: 'none',
|
|
53
90
|
},
|
|
54
91
|
},
|
|
92
|
+
'&:hover .PsLegend-groupFade > *, &:focus-within .PsLegend-groupFade > *': {
|
|
93
|
+
opacity: 1,
|
|
94
|
+
width: 'auto',
|
|
95
|
+
minWidth: 0,
|
|
96
|
+
padding: '2px',
|
|
97
|
+
overflow: 'visible',
|
|
98
|
+
pointerEvents: 'auto',
|
|
99
|
+
},
|
|
55
100
|
'&:hover .PsLegend-groupActions, &:focus-within .PsLegend-groupActions': {
|
|
56
101
|
width: 'auto',
|
|
57
102
|
minWidth: 0,
|
|
@@ -78,31 +123,6 @@ export const styles = {
|
|
|
78
123
|
opacity: 1,
|
|
79
124
|
},
|
|
80
125
|
},
|
|
81
|
-
// The original stays in place, dimmed, while its overlay clone is dragged.
|
|
82
|
-
groupDragging: {
|
|
83
|
-
opacity: 0.4,
|
|
84
|
-
},
|
|
85
|
-
// Figma: 48px tall, icon at 12px from the left, 8px gap to the label,
|
|
86
|
-
// actions right-aligned with ~8px edge inset (the small icon-button padding
|
|
87
|
-
// brings the glyphs to the design's 16px optical inset), hairline divider
|
|
88
|
-
// below. Sticky: pins to the top of the panel's scroll container.
|
|
89
|
-
header: {
|
|
90
|
-
display: 'flex',
|
|
91
|
-
alignItems: 'center',
|
|
92
|
-
gap: 1,
|
|
93
|
-
padding: ({ spacing }) => spacing(1.5, 2, 1.5, 1),
|
|
94
|
-
minHeight: LEGEND_GROUP_HEADER_HEIGHT,
|
|
95
|
-
borderBottom: '1px solid',
|
|
96
|
-
borderColor: 'divider',
|
|
97
|
-
position: 'sticky',
|
|
98
|
-
top: 0,
|
|
99
|
-
// Above the sticky row headers (2), which in turn sit above MUI's
|
|
100
|
-
// z-index-1 form internals in row content.
|
|
101
|
-
zIndex: 3,
|
|
102
|
-
// Distinct from the row header / panel body (both `background.paper`) so
|
|
103
|
-
// the group header visually separates from the rest.
|
|
104
|
-
backgroundColor: 'background.default',
|
|
105
|
-
},
|
|
106
126
|
// Accordion click target: everything in the header but the actions box.
|
|
107
127
|
// Non-native button (role='button'), so it carries its own focus ring.
|
|
108
128
|
titleArea: {
|
|
@@ -123,8 +143,11 @@ export const styles = {
|
|
|
123
143
|
display: 'flex',
|
|
124
144
|
color: 'text.secondary',
|
|
125
145
|
},
|
|
146
|
+
// No `flex:1` here — the label sizes to its own text (shrinking to
|
|
147
|
+
// ellipsis when too long) so the chevron sits right after it, instead of
|
|
148
|
+
// being dragged to the trailing edge of `titleArea` (which keeps its own
|
|
149
|
+
// `flex:1` to fill the header up to the actions).
|
|
126
150
|
label: {
|
|
127
|
-
flex: 1,
|
|
128
151
|
minWidth: 0,
|
|
129
152
|
overflow: 'hidden',
|
|
130
153
|
textOverflow: 'ellipsis',
|
|
@@ -74,7 +74,10 @@ export function LegendRow({ layerId, children }: LegendRowProps) {
|
|
|
74
74
|
sortableProps?.dragging === true && styles.rowDragging,
|
|
75
75
|
]}
|
|
76
76
|
>
|
|
77
|
-
<Box
|
|
77
|
+
<Box
|
|
78
|
+
sx={[styles.header, inGroup && styles.headerGrouped]}
|
|
79
|
+
style={{ top: stickyTop }}
|
|
80
|
+
>
|
|
78
81
|
{sortableProps?.handle}
|
|
79
82
|
<Box
|
|
80
83
|
sx={[styles.titleBox, canToggle && styles.titleBoxInteractive]}
|
|
@@ -122,16 +125,16 @@ export function LegendRow({ layerId, children }: LegendRowProps) {
|
|
|
122
125
|
|
|
123
126
|
<Collapse in={!effectivelyCollapsed}>
|
|
124
127
|
<Box
|
|
125
|
-
sx={
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
}
|
|
128
|
+
sx={[
|
|
129
|
+
styles.content,
|
|
130
|
+
inGroup && styles.contentGrouped,
|
|
131
|
+
!layer.visible && styles.dimmed,
|
|
132
|
+
]}
|
|
130
133
|
>
|
|
131
134
|
{body}
|
|
132
135
|
</Box>
|
|
133
136
|
{layer.helperText && (
|
|
134
|
-
<Box sx={styles.footer}>
|
|
137
|
+
<Box sx={[styles.footer, inGroup && styles.footerGrouped]}>
|
|
135
138
|
{typeof layer.helperText === 'string' ? (
|
|
136
139
|
<Typography variant='caption' color='text.secondary'>
|
|
137
140
|
{layer.helperText}
|
|
@@ -16,14 +16,16 @@ export const styles = {
|
|
|
16
16
|
// `.PsLegend-rowFade` are hidden on fine-pointer devices and fade in when
|
|
17
17
|
// the row is hovered or holds focus; a control carrying `.active` (dirty
|
|
18
18
|
// state — hidden layer, open menu, opacity ≠ 1) stays visible. Coarse-
|
|
19
|
-
// pointer (touch) devices always show them.
|
|
20
|
-
// collapses to zero width
|
|
21
|
-
//
|
|
22
|
-
//
|
|
19
|
+
// pointer (touch) devices always show them. Each non-active child also
|
|
20
|
+
// collapses to zero width on its own (not just the whole actions slot —
|
|
21
|
+
// see `PsLegend-rowActions` below) so a sibling's `.active` pin (e.g. the
|
|
22
|
+
// eye while hidden) doesn't leave a same-sized invisible hole where the
|
|
23
|
+
// still-faded ⋮ menu would've been; the title grows into that space. The
|
|
24
|
+
// collapse chevron sits outside the fade group and never fades.
|
|
23
25
|
'& .PsLegend-rowFade > *': {
|
|
24
26
|
opacity: 1,
|
|
25
27
|
transition: ({ transitions }: Theme) =>
|
|
26
|
-
transitions.create('opacity', {
|
|
28
|
+
transitions.create(['opacity', 'width'], {
|
|
27
29
|
duration: transitions.duration.standard,
|
|
28
30
|
easing: transitions.easing.easeInOut,
|
|
29
31
|
}),
|
|
@@ -34,14 +36,21 @@ export const styles = {
|
|
|
34
36
|
opacity: 1,
|
|
35
37
|
},
|
|
36
38
|
},
|
|
37
|
-
'&:hover .PsLegend-rowFade > *, &:focus-within .PsLegend-rowFade > *': {
|
|
38
|
-
opacity: 1,
|
|
39
|
-
},
|
|
40
39
|
// Faded actions don't reserve flex space on fine-pointer devices — the
|
|
41
40
|
// title grows to the trailing edge. Hover/focus-within restores the slot
|
|
42
41
|
// so the title reflows with the buttons; a dirty `.active` child keeps
|
|
43
42
|
// the slot open even outside hover.
|
|
44
43
|
'@media (hover: hover)': {
|
|
44
|
+
'& .PsLegend-rowFade > *:not(.active)': {
|
|
45
|
+
width: 0,
|
|
46
|
+
minWidth: 0,
|
|
47
|
+
// border-box IconButtons keep their padding inside `width` — zero it
|
|
48
|
+
// too, otherwise the fixed 2px+2px padding survives as a tiny but
|
|
49
|
+
// needless residual gap.
|
|
50
|
+
padding: 0,
|
|
51
|
+
overflow: 'hidden',
|
|
52
|
+
pointerEvents: 'none',
|
|
53
|
+
},
|
|
45
54
|
'& .PsLegend-rowActions:not(:has(.active))': {
|
|
46
55
|
width: 0,
|
|
47
56
|
minWidth: 0,
|
|
@@ -51,6 +60,14 @@ export const styles = {
|
|
|
51
60
|
pointerEvents: 'none',
|
|
52
61
|
},
|
|
53
62
|
},
|
|
63
|
+
'&:hover .PsLegend-rowFade > *, &:focus-within .PsLegend-rowFade > *': {
|
|
64
|
+
opacity: 1,
|
|
65
|
+
width: 'auto',
|
|
66
|
+
minWidth: 0,
|
|
67
|
+
padding: '2px',
|
|
68
|
+
overflow: 'visible',
|
|
69
|
+
pointerEvents: 'auto',
|
|
70
|
+
},
|
|
54
71
|
'&:hover .PsLegend-rowActions, &:focus-within .PsLegend-rowActions': {
|
|
55
72
|
width: 'auto',
|
|
56
73
|
minWidth: 0,
|
|
@@ -91,7 +108,9 @@ export const styles = {
|
|
|
91
108
|
// band (subtitle flows below them) — hence `flex-start` with the title
|
|
92
109
|
// optically centered against the 30px icon-button row. Sticky: pins inside
|
|
93
110
|
// the panel's scroll container; `top` is applied inline per row (0, or the
|
|
94
|
-
// group-header height for grouped rows).
|
|
111
|
+
// group-header height for grouped rows). Grouped rows get an extra 20px
|
|
112
|
+
// left inset (36px total) so member layers read as nested under the group
|
|
113
|
+
// and line up with the group's own icon+label text start.
|
|
95
114
|
header: {
|
|
96
115
|
display: 'flex',
|
|
97
116
|
alignItems: 'flex-start',
|
|
@@ -104,6 +123,9 @@ export const styles = {
|
|
|
104
123
|
zIndex: 2,
|
|
105
124
|
backgroundColor: 'background.paper',
|
|
106
125
|
},
|
|
126
|
+
headerGrouped: {
|
|
127
|
+
paddingLeft: 4.5,
|
|
128
|
+
},
|
|
107
129
|
titleBox: {
|
|
108
130
|
flex: 1,
|
|
109
131
|
minWidth: 0,
|
|
@@ -130,8 +152,11 @@ export const styles = {
|
|
|
130
152
|
alignItems: 'center',
|
|
131
153
|
gap: 0.5,
|
|
132
154
|
},
|
|
155
|
+
// No `flex:1` here — the title sizes to its own text (shrinking to
|
|
156
|
+
// ellipsis when too long) so the chevron sits right after it, instead of
|
|
157
|
+
// being dragged to the trailing edge of `titleLine` (which spans the full
|
|
158
|
+
// width of `titleBox`, itself `flex:1` to fill the header up to actions).
|
|
133
159
|
title: {
|
|
134
|
-
flex: 1,
|
|
135
160
|
minWidth: 0,
|
|
136
161
|
overflow: 'hidden',
|
|
137
162
|
textOverflow: 'ellipsis',
|
|
@@ -178,10 +203,16 @@ export const styles = {
|
|
|
178
203
|
gap: 1,
|
|
179
204
|
padding: ({ spacing }) => spacing(0.5, 2, 1, 2),
|
|
180
205
|
},
|
|
206
|
+
contentGrouped: {
|
|
207
|
+
paddingLeft: 4.5,
|
|
208
|
+
},
|
|
181
209
|
// ~16px above the note (content's 8px bottom inset + 8px here), per design.
|
|
182
210
|
footer: {
|
|
183
211
|
padding: ({ spacing }) => spacing(1, 2, 1, 2),
|
|
184
212
|
},
|
|
213
|
+
footerGrouped: {
|
|
214
|
+
paddingLeft: 4.5,
|
|
215
|
+
},
|
|
185
216
|
dimmed: {
|
|
186
217
|
opacity: 0.5,
|
|
187
218
|
},
|
|
@@ -1,20 +1,29 @@
|
|
|
1
1
|
import type { SxProps, Theme } from '@mui/material'
|
|
2
|
+
import { LEGEND_GROUP_HEADER_HEIGHT } from '../legend-group/styles'
|
|
2
3
|
|
|
3
4
|
export const styles = {
|
|
4
|
-
// Absolute handle
|
|
5
|
+
// Absolute handle, flush against the header's top-left corner — no flex
|
|
6
|
+
// slot; sticky header is the containing block.
|
|
5
7
|
handleSlot: {
|
|
6
8
|
position: 'absolute',
|
|
7
9
|
zIndex: 1,
|
|
8
10
|
display: 'flex',
|
|
9
11
|
alignItems: 'center',
|
|
10
12
|
justifyContent: 'center',
|
|
11
|
-
|
|
13
|
+
left: 0,
|
|
14
|
+
top: 0,
|
|
12
15
|
},
|
|
16
|
+
// Both headers center their title on the same 24px band from the header's
|
|
17
|
+
// top edge (group: 48px header, `alignItems:'center'`; row: 12px top
|
|
18
|
+
// padding + 24px `titleLine`, `alignItems:'flex-start'`) — so `height:48`
|
|
19
|
+
// centers the icon on the title in both cases, even though the row's own
|
|
20
|
+
// header is only 32px tall (a 32px-tall slot would center at 16px, 8px
|
|
21
|
+
// short of the title).
|
|
13
22
|
handleSlotLayer: {
|
|
14
|
-
|
|
23
|
+
height: LEGEND_GROUP_HEADER_HEIGHT,
|
|
15
24
|
},
|
|
16
25
|
handleSlotGroup: {
|
|
17
|
-
|
|
26
|
+
height: LEGEND_GROUP_HEADER_HEIGHT,
|
|
18
27
|
},
|
|
19
28
|
handle: {
|
|
20
29
|
padding: '2px',
|
|
@@ -238,15 +238,21 @@ function mergeLayerEntity(
|
|
|
238
238
|
} as LegendLayer
|
|
239
239
|
}
|
|
240
240
|
|
|
241
|
-
/**
|
|
241
|
+
/**
|
|
242
|
+
* Group counterpart of {@link mergeLayerEntity}. A group with no `prev`
|
|
243
|
+
* state (first sync) defaults collapsed when none of its member layers is
|
|
244
|
+
* visible yet — `hasVisibleLayer` is ignored once the group exists so a
|
|
245
|
+
* user's manual expand/collapse always sticks.
|
|
246
|
+
*/
|
|
242
247
|
function mergeGroupEntity(
|
|
243
248
|
prev: LegendGroup | undefined,
|
|
244
249
|
input: LegendGroupInput,
|
|
245
250
|
fallbackOrder: number,
|
|
251
|
+
hasVisibleLayer: boolean,
|
|
246
252
|
): LegendGroup {
|
|
247
253
|
return {
|
|
248
254
|
...input,
|
|
249
|
-
collapsed: prev?.collapsed ?? input.collapsed ??
|
|
255
|
+
collapsed: prev?.collapsed ?? input.collapsed ?? !hasVisibleLayer,
|
|
250
256
|
collapsedBeforeHide: prev?.collapsedBeforeHide,
|
|
251
257
|
order: prev?.order ?? input.order ?? fallbackOrder,
|
|
252
258
|
} as LegendGroup
|
|
@@ -274,9 +280,19 @@ function syncState(
|
|
|
274
280
|
}))
|
|
275
281
|
normalizeOrders(layerEntries, (l) => l.groupId)
|
|
276
282
|
|
|
283
|
+
const groupsWithVisibleLayer = new Set(
|
|
284
|
+
layerEntries
|
|
285
|
+
.filter(({ merged }) => merged.visible && merged.groupId != null)
|
|
286
|
+
.map(({ merged }) => merged.groupId!),
|
|
287
|
+
)
|
|
277
288
|
const groupEntries = groupInputs.map((input, index) => ({
|
|
278
289
|
index,
|
|
279
|
-
merged: mergeGroupEntity(
|
|
290
|
+
merged: mergeGroupEntity(
|
|
291
|
+
state.groups[input.id],
|
|
292
|
+
input,
|
|
293
|
+
index,
|
|
294
|
+
groupsWithVisibleLayer.has(input.id),
|
|
295
|
+
),
|
|
280
296
|
}))
|
|
281
297
|
normalizeOrders(groupEntries, () => undefined)
|
|
282
298
|
|
|
@@ -382,7 +398,15 @@ function upsertGroupState(
|
|
|
382
398
|
const count = Object.values(state.groups).filter(
|
|
383
399
|
(g) => g.id !== input.id,
|
|
384
400
|
).length
|
|
385
|
-
const
|
|
401
|
+
const hasVisibleLayer = Object.values(state.layers).some(
|
|
402
|
+
(l) => l.groupId === input.id && l.visible,
|
|
403
|
+
)
|
|
404
|
+
const merged = mergeGroupEntity(
|
|
405
|
+
state.groups[input.id],
|
|
406
|
+
input,
|
|
407
|
+
count,
|
|
408
|
+
hasVisibleLayer,
|
|
409
|
+
)
|
|
386
410
|
const groups = rebuildRecord(
|
|
387
411
|
state.groups,
|
|
388
412
|
{ ...state.groups, [input.id]: merged },
|
|
@@ -148,6 +148,29 @@ describe('legend store', () => {
|
|
|
148
148
|
expect(store.getState().groups.g1!.collapsed).toBe(true)
|
|
149
149
|
})
|
|
150
150
|
|
|
151
|
+
it('a group with no visible member starts collapsed', () => {
|
|
152
|
+
const store = createLegendStore('s', {
|
|
153
|
+
layers: layers.map((l) =>
|
|
154
|
+
l.groupId === 'g1' ? { ...l, visible: false } : l,
|
|
155
|
+
),
|
|
156
|
+
groups: [
|
|
157
|
+
{ id: 'g1', label: 'One' },
|
|
158
|
+
{ id: 'g2', label: 'Empty' },
|
|
159
|
+
],
|
|
160
|
+
})
|
|
161
|
+
expect(store.getState().groups.g1!.collapsed).toBe(true)
|
|
162
|
+
// No members at all also counts as "nothing enabled".
|
|
163
|
+
expect(store.getState().groups.g2!.collapsed).toBe(true)
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
it('a group with at least one visible member starts expanded', () => {
|
|
167
|
+
const store = createLegendStore('s', {
|
|
168
|
+
layers,
|
|
169
|
+
groups: [{ id: 'g1', label: 'One' }],
|
|
170
|
+
})
|
|
171
|
+
expect(store.getState().groups.g1!.collapsed).toBe(false)
|
|
172
|
+
})
|
|
173
|
+
|
|
151
174
|
it('selectGroupVisibility derives the tri-state from member layers', () => {
|
|
152
175
|
const store = createLegendStore('s', {
|
|
153
176
|
layers,
|