@carto/ps-react-ui 4.14.1 → 4.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/chat.js +494 -466
- package/dist/chat.js.map +1 -1
- package/dist/components.js +837 -774
- package/dist/components.js.map +1 -1
- package/dist/legend/stores.js +20 -16
- package/dist/legend-store-registry-CVYzhR1-.js +464 -0
- package/dist/legend-store-registry-CVYzhR1-.js.map +1 -0
- package/dist/legend.js +1478 -794
- package/dist/legend.js.map +1 -1
- package/dist/markdown-DdqANCiN.js +102 -0
- package/dist/markdown-DdqANCiN.js.map +1 -0
- package/dist/markdown-content-Dk2DSgbf.js +10287 -0
- package/dist/markdown-content-Dk2DSgbf.js.map +1 -0
- package/dist/types/chat/bubbles/chat-agent-message-markdown.d.ts +12 -0
- package/dist/types/chat/bubbles/chat-agent-message.d.ts +9 -0
- package/dist/types/chat/bubbles/index.d.ts +1 -0
- package/dist/types/chat/index.d.ts +2 -1
- package/dist/types/chat/types.d.ts +9 -0
- package/dist/types/components/index.d.ts +2 -1
- package/dist/types/components/lasso-tool/message.d.ts +7 -0
- package/dist/types/components/lasso-tool/types.d.ts +17 -2
- package/dist/types/components/responsive-drawer/responsive-drawer.d.ts +2 -9
- package/dist/types/legend/components/a11y.d.ts +6 -0
- package/dist/types/legend/components/contexts.d.ts +22 -4
- package/dist/types/legend/components/index.d.ts +2 -2
- package/dist/types/legend/components/legend-actions/legend-actions.d.ts +4 -3
- package/dist/types/legend/components/legend-group/legend-group.d.ts +2 -11
- package/dist/types/legend/components/legend-group/styles.d.ts +40 -2
- package/dist/types/legend/components/legend-group-menu/legend-group-menu-items.d.ts +28 -0
- package/dist/types/legend/components/legend-group-menu/legend-group-menu.d.ts +11 -0
- package/dist/types/legend/components/legend-opacity/legend-opacity.d.ts +7 -17
- package/dist/types/legend/components/legend-opacity/styles.d.ts +11 -6
- package/dist/types/legend/components/legend-overflow-menu/legend-overflow-menu.d.ts +12 -0
- package/dist/types/legend/components/legend-panel-menu/legend-panel-menu.d.ts +11 -0
- package/dist/types/legend/components/legend-row/legend-row.d.ts +4 -11
- package/dist/types/legend/components/legend-row/styles.d.ts +36 -1
- package/dist/types/legend/components/legend-row-menu/legend-row-menu-items.d.ts +12 -0
- package/dist/types/legend/components/legend-row-menu/legend-row-menu.d.ts +4 -10
- package/dist/types/legend/components/legend-row-menu/legend-zoom-to.d.ts +3 -8
- package/dist/types/legend/components/legend-sortable/contexts.d.ts +7 -0
- package/dist/types/legend/components/legend-sortable/legend-sortable.d.ts +11 -0
- package/dist/types/legend/components/legend-sortable/sortable-entity.d.ts +27 -0
- package/dist/types/legend/components/legend-sortable/sortable-ids.d.ts +6 -0
- package/dist/types/legend/components/legend-sortable/styles.d.ts +54 -0
- package/dist/types/legend/components/legend-visible-layers/legend-visible-layers.d.ts +7 -0
- package/dist/types/legend/components/legend-visible-layers/styles.d.ts +48 -0
- package/dist/types/legend/components/run-async-menu-action.d.ts +7 -0
- package/dist/types/legend/index.d.ts +53 -6
- package/dist/types/legend/provider/labels.d.ts +21 -0
- package/dist/types/legend/stores/index.d.ts +1 -1
- package/dist/types/legend/stores/selectors.d.ts +31 -0
- package/dist/types/legend/stores/types.d.ts +11 -0
- package/dist/types/widgets-v2/markdown/markdown-content.d.ts +7 -1
- package/dist/widgets-v2/markdown.js +9 -8
- package/dist/widgets-v2/markdown.js.map +1 -1
- package/dist/widgets-v2.js +23 -22
- package/dist/widgets-v2.js.map +1 -1
- package/package.json +5 -4
- package/src/chat/bubbles/chat-agent-message-markdown.test.tsx +38 -0
- package/src/chat/bubbles/chat-agent-message-markdown.tsx +34 -0
- package/src/chat/bubbles/chat-agent-message.test.tsx +12 -0
- package/src/chat/bubbles/chat-agent-message.tsx +21 -1
- package/src/chat/bubbles/index.ts +1 -0
- package/src/chat/index.ts +2 -0
- package/src/chat/types.ts +10 -0
- package/src/components/index.ts +2 -0
- package/src/components/lasso-tool/message.tsx +36 -0
- package/src/components/lasso-tool/types.ts +20 -1
- package/src/components/responsive-drawer/responsive-drawer.test.tsx +138 -0
- package/src/components/responsive-drawer/responsive-drawer.tsx +77 -51
- package/src/legend/components/a11y.ts +14 -0
- package/src/legend/components/contexts.ts +29 -4
- package/src/legend/components/index.ts +3 -4
- package/src/legend/components/legend-actions/legend-actions.tsx +4 -3
- package/src/legend/components/legend-group/legend-group.test.tsx +166 -9
- package/src/legend/components/legend-group/legend-group.tsx +88 -49
- package/src/legend/components/legend-group/styles.ts +59 -2
- package/src/legend/components/legend-group-menu/legend-group-menu-items.tsx +132 -0
- package/src/legend/components/legend-group-menu/legend-group-menu.tsx +25 -0
- package/src/legend/components/legend-opacity/legend-opacity.tsx +43 -70
- package/src/legend/components/legend-opacity/styles.ts +14 -9
- package/src/legend/components/legend-overflow-menu/legend-overflow-menu.tsx +84 -0
- package/src/legend/components/legend-panel-menu/legend-panel-menu.tsx +23 -0
- package/src/legend/components/legend-ramp/legend-ramp-ui.tsx +2 -1
- package/src/legend/components/legend-row/legend-row.test.tsx +304 -85
- package/src/legend/components/legend-row/legend-row.tsx +73 -50
- package/src/legend/components/legend-row/styles.ts +54 -1
- package/src/legend/components/legend-row-menu/legend-row-menu-items.tsx +57 -0
- package/src/legend/components/legend-row-menu/legend-row-menu.tsx +12 -50
- package/src/legend/components/legend-row-menu/legend-zoom-to.tsx +25 -17
- package/src/legend/components/legend-sortable/contexts.ts +12 -0
- package/src/legend/components/legend-sortable/legend-sortable.test.tsx +144 -0
- package/src/legend/components/legend-sortable/legend-sortable.tsx +195 -0
- package/src/legend/components/legend-sortable/sortable-entity.tsx +91 -0
- package/src/legend/components/legend-sortable/sortable-ids.ts +17 -0
- package/src/legend/components/legend-sortable/styles.ts +56 -0
- package/src/legend/components/legend-visibility-toggle/legend-visibility-toggle.tsx +14 -8
- package/src/legend/components/legend-visible-layers/legend-visible-layers.test.tsx +86 -0
- package/src/legend/components/legend-visible-layers/legend-visible-layers.tsx +135 -0
- package/src/legend/components/legend-visible-layers/styles.ts +60 -0
- package/src/legend/components/run-async-menu-action.ts +20 -0
- package/src/legend/index.ts +59 -4
- package/src/legend/provider/labels.ts +43 -1
- package/src/legend/stores/index.ts +4 -0
- package/src/legend/stores/legend-store-registry.ts +60 -0
- package/src/legend/stores/legend-store.test.ts +116 -0
- package/src/legend/stores/selectors.ts +64 -2
- package/src/legend/stores/types.ts +11 -0
- package/src/widgets/actions/download/exports.test.tsx +14 -1
- package/src/widgets-v2/markdown/markdown-content.test.tsx +10 -0
- package/src/widgets-v2/markdown/markdown-content.tsx +9 -0
- package/src/widgets-v2/wrapper/widget-wrapper.tsx +4 -1
- package/dist/markdown-BD1jcknS.js +0 -8326
- package/dist/markdown-BD1jcknS.js.map +0 -1
- package/dist/selectors-DwHj6mE2.js +0 -388
- package/dist/selectors-DwHj6mE2.js.map +0 -1
|
@@ -1,11 +1,31 @@
|
|
|
1
|
+
import { forwardRef } from 'react'
|
|
1
2
|
import { Box, styled, type BoxProps } from '@mui/material'
|
|
3
|
+
import type { ChatAgentMessageProps } from '../types'
|
|
2
4
|
import { styles } from './styles'
|
|
3
5
|
|
|
4
6
|
const InnerBox = ({ className, ...props }: BoxProps) => (
|
|
5
7
|
<Box className={`PsChat--agent-message ${className ?? ''}`} {...props} />
|
|
6
8
|
)
|
|
7
9
|
|
|
8
|
-
|
|
10
|
+
const StyledAgentMessage = styled(InnerBox)(({ theme }) =>
|
|
9
11
|
styles.agentMessageContainer(theme),
|
|
10
12
|
)
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Agent reply bubble. A styles container that positions the reply and its
|
|
16
|
+
* composed pieces (text, loaders, tool traces, …); it renders `children`
|
|
17
|
+
* verbatim and forwards its `ref` to the underlying element. For Markdown
|
|
18
|
+
* replies, compose `ChatAgentMessageMarkdown` instead of teaching this
|
|
19
|
+
* container about the markdown engine.
|
|
20
|
+
*/
|
|
21
|
+
export const ChatAgentMessage = forwardRef<
|
|
22
|
+
HTMLDivElement,
|
|
23
|
+
ChatAgentMessageProps
|
|
24
|
+
>(function ChatAgentMessage({ children, ...props }, ref) {
|
|
25
|
+
return (
|
|
26
|
+
<StyledAgentMessage ref={ref} {...props}>
|
|
27
|
+
{children}
|
|
28
|
+
</StyledAgentMessage>
|
|
29
|
+
)
|
|
30
|
+
})
|
|
11
31
|
ChatAgentMessage.displayName = 'ChatAgentMessage'
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export { ChatUserMessage } from './chat-user-message'
|
|
2
2
|
export { ChatAgentMessage } from './chat-agent-message'
|
|
3
|
+
export { ChatAgentMessageMarkdown } from './chat-agent-message-markdown'
|
|
3
4
|
export { ChatErrorMessage } from './chat-error-message'
|
|
4
5
|
export { ChatSuggestionButton } from './chat-suggestion-button'
|
package/src/chat/index.ts
CHANGED
|
@@ -4,6 +4,7 @@ export type {
|
|
|
4
4
|
ChatErrorAction,
|
|
5
5
|
ChatUserMessageProps,
|
|
6
6
|
ChatAgentMessageProps,
|
|
7
|
+
ChatAgentMessageMarkdownProps,
|
|
7
8
|
ChatErrorMessageProps,
|
|
8
9
|
ChatSuggestionButtonProps,
|
|
9
10
|
ChatThinkingProps,
|
|
@@ -37,6 +38,7 @@ export { useTypewriter } from './use-typewriter'
|
|
|
37
38
|
// Messages
|
|
38
39
|
export { ChatUserMessage } from './bubbles/chat-user-message'
|
|
39
40
|
export { ChatAgentMessage } from './bubbles/chat-agent-message'
|
|
41
|
+
export { ChatAgentMessageMarkdown } from './bubbles/chat-agent-message-markdown'
|
|
40
42
|
export { ChatErrorMessage } from './bubbles/chat-error-message'
|
|
41
43
|
export { ChatSuggestionButton } from './bubbles/chat-suggestion-button'
|
|
42
44
|
export { ChatMessageOverflow } from './bubbles/styles'
|
package/src/chat/types.ts
CHANGED
|
@@ -25,6 +25,16 @@ export interface ChatAgentMessageProps extends ChatSxProps {
|
|
|
25
25
|
children: ReactNode
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
+
export interface ChatAgentMessageMarkdownProps extends ChatSxProps {
|
|
29
|
+
/**
|
|
30
|
+
* Markdown source — rendered as GitHub-Flavored Markdown (tables, task lists,
|
|
31
|
+
* strikethrough) through the library's sanitized renderer, inside a
|
|
32
|
+
* `ChatAgentMessage`. Raw HTML is skipped and images are stripped, since the
|
|
33
|
+
* content is model output.
|
|
34
|
+
*/
|
|
35
|
+
children: string
|
|
36
|
+
}
|
|
37
|
+
|
|
28
38
|
export interface ChatErrorMessageProps extends ChatSxProps {
|
|
29
39
|
errors: string[]
|
|
30
40
|
icon?: ReactNode
|
package/src/components/index.ts
CHANGED
|
@@ -28,11 +28,13 @@ export type {
|
|
|
28
28
|
LassoGeometryType,
|
|
29
29
|
LassoLayerFiltersUIProps,
|
|
30
30
|
LassoLayerFilterItem,
|
|
31
|
+
LassoMessageUIProps,
|
|
31
32
|
} from './lasso-tool/types'
|
|
32
33
|
export { LassoToolsUI } from './lasso-tool/lasso-tool'
|
|
33
34
|
export { LassoToolsInlineUI } from './lasso-tool/lasso-tool-inline'
|
|
34
35
|
export { LassoGeometryToolbarUI } from './lasso-tool/geometry-toolbar'
|
|
35
36
|
export { LassoLayerFiltersUI } from './lasso-tool/layer-filters'
|
|
37
|
+
export { LassoMessageUI } from './lasso-tool/message'
|
|
36
38
|
export {
|
|
37
39
|
DEFAULT_LASSO_TOOLS_MODES_MAPPING,
|
|
38
40
|
LASSO_TOOLS_LABELS,
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { Alert, AlertTitle, Snackbar } from '@mui/material'
|
|
2
|
+
import type { LassoMessageUIProps } from './types'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Presentational counterpart of `LassoTools.Message` (`@carto/ps-react-maps`),
|
|
6
|
+
* positioned by that render-props component. Auto-hides after 5 seconds via
|
|
7
|
+
* MUI `Snackbar` (calls `onDismiss`, which removes the message from the store).
|
|
8
|
+
*/
|
|
9
|
+
export function LassoMessageUI({
|
|
10
|
+
message,
|
|
11
|
+
onDismiss,
|
|
12
|
+
AlertProps,
|
|
13
|
+
SnackbarProps,
|
|
14
|
+
}: LassoMessageUIProps) {
|
|
15
|
+
return (
|
|
16
|
+
<Snackbar
|
|
17
|
+
open
|
|
18
|
+
autoHideDuration={5000}
|
|
19
|
+
// 'clickaway' is ignored so map interactions don't dismiss the message.
|
|
20
|
+
onClose={(_, reason) => reason !== 'clickaway' && onDismiss()}
|
|
21
|
+
// Stay in flow — the maps-side `LassoTools.Message` owns positioning.
|
|
22
|
+
sx={{ position: 'static', transform: 'none' }}
|
|
23
|
+
{...SnackbarProps}
|
|
24
|
+
>
|
|
25
|
+
<Alert
|
|
26
|
+
severity={message.severity ?? 'info'}
|
|
27
|
+
action={message.action}
|
|
28
|
+
onClose={onDismiss}
|
|
29
|
+
{...AlertProps}
|
|
30
|
+
>
|
|
31
|
+
<AlertTitle>{message.title}</AlertTitle>
|
|
32
|
+
{message.content}
|
|
33
|
+
</Alert>
|
|
34
|
+
</Snackbar>
|
|
35
|
+
)
|
|
36
|
+
}
|
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
import type {
|
|
2
|
+
AlertColor,
|
|
3
|
+
AlertProps,
|
|
2
4
|
Box,
|
|
3
5
|
ChipProps,
|
|
4
6
|
DialogProps,
|
|
5
7
|
Paper,
|
|
8
|
+
SnackbarProps,
|
|
6
9
|
TooltipProps,
|
|
7
10
|
} from '@mui/material'
|
|
8
|
-
import type { ComponentProps, ComponentType } from 'react'
|
|
11
|
+
import type { ComponentProps, ComponentType, ReactNode } from 'react'
|
|
9
12
|
import type {
|
|
10
13
|
LassoToolsMode,
|
|
11
14
|
LassoToolsModes,
|
|
@@ -153,6 +156,22 @@ export interface LassoGeometryToolbarUIProps {
|
|
|
153
156
|
PaperProps?: ComponentProps<typeof Paper>
|
|
154
157
|
}
|
|
155
158
|
|
|
159
|
+
export interface LassoMessageUIProps {
|
|
160
|
+
/** Message from the lasso tools store (`LassoToolsMessage` in `@carto/ps-react-maps`). */
|
|
161
|
+
message: {
|
|
162
|
+
id: string
|
|
163
|
+
title: string
|
|
164
|
+
/** Body rendered under the title. */
|
|
165
|
+
content?: ReactNode
|
|
166
|
+
/** @default 'info' */
|
|
167
|
+
severity?: AlertColor
|
|
168
|
+
action?: ReactNode
|
|
169
|
+
}
|
|
170
|
+
onDismiss: () => void
|
|
171
|
+
AlertProps?: Omit<AlertProps, 'severity' | 'onClose' | 'children' | 'action'>
|
|
172
|
+
SnackbarProps?: Omit<SnackbarProps, 'open' | 'onClose' | 'children'>
|
|
173
|
+
}
|
|
174
|
+
|
|
156
175
|
export interface LassoLayerFilterItem {
|
|
157
176
|
id: string
|
|
158
177
|
label: string
|
|
@@ -154,6 +154,144 @@ describe('ResponsiveDrawer', () => {
|
|
|
154
154
|
).not.toThrow()
|
|
155
155
|
})
|
|
156
156
|
|
|
157
|
+
test('defaults to portaling into document.body when container is omitted', () => {
|
|
158
|
+
render(
|
|
159
|
+
<ThemeProvider theme={theme}>
|
|
160
|
+
<ResponsiveDrawer ref={anchor} {...defaultProps} />
|
|
161
|
+
</ThemeProvider>,
|
|
162
|
+
)
|
|
163
|
+
const content = screen.getByTestId('drawer-content')
|
|
164
|
+
const modalRoot = content.closest('.MuiModal-root')
|
|
165
|
+
expect(modalRoot?.parentElement).toBe(document.body)
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
test('portals into the given container instead of document.body', () => {
|
|
169
|
+
const customContainer = document.createElement('div')
|
|
170
|
+
document.body.appendChild(customContainer)
|
|
171
|
+
try {
|
|
172
|
+
render(
|
|
173
|
+
<ThemeProvider theme={theme}>
|
|
174
|
+
<ResponsiveDrawer
|
|
175
|
+
ref={anchor}
|
|
176
|
+
{...defaultProps}
|
|
177
|
+
container={customContainer}
|
|
178
|
+
/>
|
|
179
|
+
</ThemeProvider>,
|
|
180
|
+
)
|
|
181
|
+
const content = screen.getByTestId('drawer-content')
|
|
182
|
+
const modalRoot = content.closest('.MuiModal-root')
|
|
183
|
+
expect(modalRoot?.parentElement).toBe(customContainer)
|
|
184
|
+
} finally {
|
|
185
|
+
customContainer.remove()
|
|
186
|
+
}
|
|
187
|
+
})
|
|
188
|
+
|
|
189
|
+
test('caps the popover paper height at 90% of a measured container element', () => {
|
|
190
|
+
const customContainer = document.createElement('div')
|
|
191
|
+
Object.defineProperty(customContainer, 'clientHeight', {
|
|
192
|
+
value: 500,
|
|
193
|
+
configurable: true,
|
|
194
|
+
})
|
|
195
|
+
document.body.appendChild(customContainer)
|
|
196
|
+
try {
|
|
197
|
+
render(
|
|
198
|
+
<ThemeProvider theme={theme}>
|
|
199
|
+
<ResponsiveDrawer
|
|
200
|
+
ref={anchor}
|
|
201
|
+
{...defaultProps}
|
|
202
|
+
container={customContainer}
|
|
203
|
+
/>
|
|
204
|
+
</ThemeProvider>,
|
|
205
|
+
)
|
|
206
|
+
const paper = document.querySelector<HTMLElement>('.MuiPopover-paper')!
|
|
207
|
+
expect(paper.style.maxHeight).toBe('450px')
|
|
208
|
+
expect(paper.style.overflow).toBe('hidden')
|
|
209
|
+
expect(paper.style.display).toBe('flex')
|
|
210
|
+
expect(paper.style.flexDirection).toBe('column')
|
|
211
|
+
|
|
212
|
+
const content = screen.getByTestId('drawer-content')
|
|
213
|
+
const wrapper = content.parentElement!
|
|
214
|
+
expect(wrapper.style.display).toBe('flex')
|
|
215
|
+
expect(wrapper.style.flexDirection).toBe('column')
|
|
216
|
+
expect(wrapper.style.flex).toBe('1 1 0%')
|
|
217
|
+
expect(wrapper.style.minHeight).toBe('0')
|
|
218
|
+
expect(wrapper.style.maxHeight).toBe('100%')
|
|
219
|
+
expect(wrapper.style.overflow).toBe('hidden')
|
|
220
|
+
} finally {
|
|
221
|
+
customContainer.remove()
|
|
222
|
+
}
|
|
223
|
+
})
|
|
224
|
+
|
|
225
|
+
test('leaves paper maxHeight unset when no container is given', () => {
|
|
226
|
+
render(
|
|
227
|
+
<ThemeProvider theme={theme}>
|
|
228
|
+
<ResponsiveDrawer ref={anchor} {...defaultProps} />
|
|
229
|
+
</ThemeProvider>,
|
|
230
|
+
)
|
|
231
|
+
const paper = document.querySelector<HTMLElement>('.MuiPopover-paper')!
|
|
232
|
+
expect(paper.style.maxHeight).toBe('')
|
|
233
|
+
expect(paper.style.display).toBe('')
|
|
234
|
+
|
|
235
|
+
const content = screen.getByTestId('drawer-content')
|
|
236
|
+
expect(content.parentElement!.style.display).toBe('')
|
|
237
|
+
})
|
|
238
|
+
|
|
239
|
+
test('leaves paper maxHeight unset when the container reports height 0', () => {
|
|
240
|
+
const customContainer = document.createElement('div')
|
|
241
|
+
Object.defineProperty(customContainer, 'clientHeight', {
|
|
242
|
+
value: 0,
|
|
243
|
+
configurable: true,
|
|
244
|
+
})
|
|
245
|
+
document.body.appendChild(customContainer)
|
|
246
|
+
try {
|
|
247
|
+
render(
|
|
248
|
+
<ThemeProvider theme={theme}>
|
|
249
|
+
<ResponsiveDrawer
|
|
250
|
+
ref={anchor}
|
|
251
|
+
{...defaultProps}
|
|
252
|
+
container={customContainer}
|
|
253
|
+
/>
|
|
254
|
+
</ThemeProvider>,
|
|
255
|
+
)
|
|
256
|
+
const paper = document.querySelector<HTMLElement>('.MuiPopover-paper')!
|
|
257
|
+
expect(paper.style.maxHeight).toBe('')
|
|
258
|
+
} finally {
|
|
259
|
+
customContainer.remove()
|
|
260
|
+
}
|
|
261
|
+
})
|
|
262
|
+
|
|
263
|
+
test('preserves slotProps.root when capping paper height under modal=true', () => {
|
|
264
|
+
const customContainer = document.createElement('div')
|
|
265
|
+
Object.defineProperty(customContainer, 'clientHeight', {
|
|
266
|
+
value: 500,
|
|
267
|
+
configurable: true,
|
|
268
|
+
})
|
|
269
|
+
document.body.appendChild(customContainer)
|
|
270
|
+
try {
|
|
271
|
+
render(
|
|
272
|
+
<ThemeProvider theme={theme}>
|
|
273
|
+
<ResponsiveDrawer
|
|
274
|
+
ref={anchor}
|
|
275
|
+
{...defaultProps}
|
|
276
|
+
container={customContainer}
|
|
277
|
+
slotProps={
|
|
278
|
+
{
|
|
279
|
+
root: { 'data-testid': 'drawer-root-slot' },
|
|
280
|
+
paper: { 'data-testid': 'drawer-paper-slot' },
|
|
281
|
+
} as never
|
|
282
|
+
}
|
|
283
|
+
/>
|
|
284
|
+
</ThemeProvider>,
|
|
285
|
+
)
|
|
286
|
+
expect(screen.getByTestId('drawer-root-slot')).toBeDefined()
|
|
287
|
+
expect(screen.getByTestId('drawer-paper-slot')).toBeDefined()
|
|
288
|
+
const paper = document.querySelector<HTMLElement>('.MuiPopover-paper')!
|
|
289
|
+
expect(paper.style.maxHeight).toBe('450px')
|
|
290
|
+
} finally {
|
|
291
|
+
customContainer.remove()
|
|
292
|
+
}
|
|
293
|
+
})
|
|
294
|
+
|
|
157
295
|
test('respects sx prop on the Popover', () => {
|
|
158
296
|
expect(() =>
|
|
159
297
|
render(
|
|
@@ -5,7 +5,13 @@ import {
|
|
|
5
5
|
type PopoverOrigin,
|
|
6
6
|
type PopoverProps,
|
|
7
7
|
} from '@mui/material'
|
|
8
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
useCallback,
|
|
10
|
+
useRef,
|
|
11
|
+
useSyncExternalStore,
|
|
12
|
+
type CSSProperties,
|
|
13
|
+
type ReactNode,
|
|
14
|
+
} from 'react'
|
|
9
15
|
|
|
10
16
|
type ResponsiveDrawerPosition =
|
|
11
17
|
| 'top-left'
|
|
@@ -17,14 +23,7 @@ type ResponsiveDrawerPosition =
|
|
|
17
23
|
export type ResponsiveDrawerPlacement = 'side' | 'overlay'
|
|
18
24
|
|
|
19
25
|
/**
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
* `transformOrigin` (the panel corner that pins to the anchor point) is the same
|
|
23
|
-
* for both placements: the panel's own corner matching `position`. `anchorOrigin`
|
|
24
|
-
* (the point on the anchor the panel pins to) differs:
|
|
25
|
-
* - `'side'` flips horizontally → the panel opens beside the trigger.
|
|
26
|
-
* - `'overlay'` uses the same corner → the panel sits **on top of** the anchor
|
|
27
|
-
* (e.g. a trigger that's hidden while the panel is open), pinned corner-to-corner.
|
|
26
|
+
* Popover anchor/transform origins. `'overlay'` pins corner-to-corner on the anchor.
|
|
28
27
|
*/
|
|
29
28
|
export function resolveOrigins(
|
|
30
29
|
position: ResponsiveDrawerPosition,
|
|
@@ -37,8 +36,6 @@ export function resolveOrigins(
|
|
|
37
36
|
return {
|
|
38
37
|
anchorOrigin: {
|
|
39
38
|
vertical: isBottom ? 'bottom' : 'top',
|
|
40
|
-
// `'side'` flips horizontally to open beside the anchor; `'overlay'` keeps
|
|
41
|
-
// the same edge so the panel lands on top of it.
|
|
42
39
|
horizontal: overlay
|
|
43
40
|
? isRight
|
|
44
41
|
? 'right'
|
|
@@ -69,11 +66,12 @@ export function ResponsiveDrawer<
|
|
|
69
66
|
position,
|
|
70
67
|
sx,
|
|
71
68
|
slotProps,
|
|
69
|
+
container,
|
|
72
70
|
onChangeCollapsed,
|
|
73
71
|
modal = true,
|
|
74
72
|
placement = 'side',
|
|
75
73
|
}: Required<Pick<T, 'collapsed' | 'onChangeCollapsed' | 'position'>> &
|
|
76
|
-
Pick<PopoverProps, 'slotProps' | 'sx'> & {
|
|
74
|
+
Pick<PopoverProps, 'slotProps' | 'sx' | 'container'> & {
|
|
77
75
|
ref: HTMLElement | null
|
|
78
76
|
isMobile: boolean
|
|
79
77
|
children: ReactNode
|
|
@@ -92,18 +90,7 @@ export function ResponsiveDrawer<
|
|
|
92
90
|
*/
|
|
93
91
|
placement?: ResponsiveDrawerPlacement
|
|
94
92
|
}) {
|
|
95
|
-
//
|
|
96
|
-
// its content resizes — so a panel that grows/shrinks (e.g. collapsing legend
|
|
97
|
-
// groups) keeps its frozen `top` and detaches from the anchored edge (the
|
|
98
|
-
// "blink"). Re-run the Popover's own positioning (`action.updatePosition`)
|
|
99
|
-
// whenever the content changes size: with `transformOrigin` matching the
|
|
100
|
-
// anchor edge, that edge stays pinned (bottom for `bottom-*`, top for
|
|
101
|
-
// `top-*`) — and because updatePosition is synchronous, it tracks every
|
|
102
|
-
// animation frame smoothly rather than snapping after a debounce.
|
|
103
|
-
//
|
|
104
|
-
// A callback ref (not a `useEffect`) attaches the observer the moment the
|
|
105
|
-
// content mounts — the Popover renders into a portal behind a transition, so
|
|
106
|
-
// the element isn't available on the effect tick that follows `open`.
|
|
93
|
+
// Popover position is frozen on open — re-measure on content resize (callback ref: portal mounts after open).
|
|
107
94
|
const popoverActions = useRef<PopoverActions>(null)
|
|
108
95
|
const observerRef = useRef<ResizeObserver | null>(null)
|
|
109
96
|
const observeContent = useCallback((el: HTMLDivElement | null) => {
|
|
@@ -118,8 +105,27 @@ export function ResponsiveDrawer<
|
|
|
118
105
|
}
|
|
119
106
|
}, [])
|
|
120
107
|
|
|
108
|
+
// Popover Paper is auto-height — `%` max-height needs a measured px cap from `container`.
|
|
109
|
+
const measurableContainer =
|
|
110
|
+
container && typeof container !== 'function' ? container : null
|
|
111
|
+
const subscribe = useCallback(
|
|
112
|
+
(onStoreChange: () => void) => {
|
|
113
|
+
if (!measurableContainer || typeof ResizeObserver === 'undefined') {
|
|
114
|
+
return () => undefined
|
|
115
|
+
}
|
|
116
|
+
const observer = new ResizeObserver(onStoreChange)
|
|
117
|
+
observer.observe(measurableContainer)
|
|
118
|
+
return () => observer.disconnect()
|
|
119
|
+
},
|
|
120
|
+
[measurableContainer],
|
|
121
|
+
)
|
|
122
|
+
const getSnapshot = useCallback(
|
|
123
|
+
() => measurableContainer?.clientHeight,
|
|
124
|
+
[measurableContainer],
|
|
125
|
+
)
|
|
126
|
+
const containerHeight = useSyncExternalStore(subscribe, getSnapshot)
|
|
127
|
+
|
|
121
128
|
if (isMobile) {
|
|
122
|
-
// MUI calls `onClose(event, reason)`; adapt it to the boolean contract.
|
|
123
129
|
return (
|
|
124
130
|
<Drawer
|
|
125
131
|
anchor='bottom'
|
|
@@ -133,41 +139,60 @@ export function ResponsiveDrawer<
|
|
|
133
139
|
|
|
134
140
|
const { anchorOrigin, transformOrigin } = resolveOrigins(position, placement)
|
|
135
141
|
|
|
136
|
-
// Non-modal mode injects pointer-through `style` into the root/paper slots so
|
|
137
|
-
// clicks fall through to the content behind (the map) while the panel stays
|
|
138
|
-
// interactive. Only the legend uses it and it passes no `slotProps`, so the
|
|
139
|
-
// common path (modal, incl. Basemaps' callback/object slotProps) is passed
|
|
140
|
-
// straight through untouched.
|
|
141
142
|
const objectSlotProps = slotProps as
|
|
142
143
|
| { root?: Record<string, unknown>; paper?: { style?: CSSProperties } }
|
|
143
144
|
| undefined
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
145
|
+
// Capped Paper: disable outer scroll + flex so inner scroll regions can shrink.
|
|
146
|
+
const paperMaxHeightStyle: CSSProperties | undefined =
|
|
147
|
+
containerHeight != null && containerHeight > 0
|
|
148
|
+
? {
|
|
149
|
+
maxHeight: containerHeight * 0.9,
|
|
150
|
+
overflow: 'hidden',
|
|
151
|
+
display: 'flex',
|
|
152
|
+
flexDirection: 'column',
|
|
153
|
+
}
|
|
154
|
+
: undefined
|
|
155
|
+
const contentWrapperStyle: CSSProperties | undefined = paperMaxHeightStyle
|
|
156
|
+
? {
|
|
157
|
+
display: 'flex',
|
|
158
|
+
flexDirection: 'column',
|
|
159
|
+
flex: 1,
|
|
160
|
+
minHeight: 0,
|
|
161
|
+
maxHeight: '100%',
|
|
162
|
+
overflow: 'hidden',
|
|
163
|
+
width: '100%',
|
|
158
164
|
}
|
|
165
|
+
: undefined
|
|
166
|
+
const mergedSlotProps: PopoverProps['slotProps'] =
|
|
167
|
+
modal && !paperMaxHeightStyle
|
|
168
|
+
? slotProps
|
|
169
|
+
: {
|
|
170
|
+
...(typeof slotProps === 'object' && slotProps != null
|
|
171
|
+
? slotProps
|
|
172
|
+
: {}),
|
|
173
|
+
...(!modal && {
|
|
174
|
+
root: {
|
|
175
|
+
style: { pointerEvents: 'none' },
|
|
176
|
+
...objectSlotProps?.root,
|
|
177
|
+
},
|
|
178
|
+
}),
|
|
179
|
+
paper: {
|
|
180
|
+
...objectSlotProps?.paper,
|
|
181
|
+
style: {
|
|
182
|
+
...(!modal && { pointerEvents: 'auto' }),
|
|
183
|
+
...paperMaxHeightStyle,
|
|
184
|
+
...objectSlotProps?.paper?.style,
|
|
185
|
+
},
|
|
186
|
+
},
|
|
187
|
+
}
|
|
159
188
|
|
|
160
189
|
return (
|
|
161
190
|
<Popover
|
|
162
191
|
action={popoverActions}
|
|
163
192
|
sx={sx}
|
|
193
|
+
container={container}
|
|
164
194
|
anchorEl={ref}
|
|
165
|
-
// Guard on `ref` so an open-by-default panel doesn't render before the
|
|
166
|
-
// anchor callback-ref has resolved (avoids the null-anchorEl warning +
|
|
167
|
-
// one-frame misposition; it re-renders non-null immediately after mount).
|
|
168
195
|
open={!collapsed && ref != null}
|
|
169
|
-
// In non-modal mode neither backdrop nor Escape dismisses — close via
|
|
170
|
-
// `onChangeCollapsed` (e.g. the panel's own ✕).
|
|
171
196
|
onClose={modal ? () => onChangeCollapsed(true) : undefined}
|
|
172
197
|
hideBackdrop={!modal}
|
|
173
198
|
disableScrollLock={!modal}
|
|
@@ -179,8 +204,9 @@ export function ResponsiveDrawer<
|
|
|
179
204
|
transformOrigin={transformOrigin}
|
|
180
205
|
slotProps={mergedSlotProps}
|
|
181
206
|
>
|
|
182
|
-
{
|
|
183
|
-
|
|
207
|
+
<div ref={observeContent} style={contentWrapperStyle}>
|
|
208
|
+
{children}
|
|
209
|
+
</div>
|
|
184
210
|
</Popover>
|
|
185
211
|
)
|
|
186
212
|
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { KeyboardEvent } from 'react'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Enter/Space activate a non-native `role='button'` element (the accordion
|
|
5
|
+
* title areas of `Legend.Row` / `Legend.Group`), like a real button.
|
|
6
|
+
*/
|
|
7
|
+
export function accordionKeyDown(toggle: () => void) {
|
|
8
|
+
return (event: KeyboardEvent<HTMLElement>) => {
|
|
9
|
+
if (event.key === 'Enter' || event.key === ' ') {
|
|
10
|
+
event.preventDefault()
|
|
11
|
+
toggle()
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
}
|
|
@@ -3,13 +3,11 @@ import { createContext, useContext } from 'react'
|
|
|
3
3
|
/**
|
|
4
4
|
* Per-row composition context provided by `Legend.Row`. Actions placed inside
|
|
5
5
|
* the row resolve their target layer from here instead of taking a `layerId`
|
|
6
|
-
* prop, and the opacity compound
|
|
7
|
-
*
|
|
6
|
+
* prop, and the opacity compound (`Legend.Opacity.Item` / `.Inline`) shares
|
|
7
|
+
* its open state through it.
|
|
8
8
|
*/
|
|
9
9
|
export interface LegendRowContextValue {
|
|
10
10
|
layerId: string
|
|
11
|
-
/** The row's sticky header element — anchor for `Legend.Opacity.Popover`. */
|
|
12
|
-
headerEl: HTMLElement | null
|
|
13
11
|
opacityOpen: boolean
|
|
14
12
|
setOpacityOpen: (open: boolean) => void
|
|
15
13
|
}
|
|
@@ -30,3 +28,30 @@ export const LegendGroupContext = createContext<string | null>(null)
|
|
|
30
28
|
export function useLegendGroupId(): string | null {
|
|
31
29
|
return useContext(LegendGroupContext)
|
|
32
30
|
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Overflow-menu controller for `Legend.RowMenu` / `Legend.GroupMenu` /
|
|
34
|
+
* `Legend.PanelMenu`. Async items (e.g. `Legend.ZoomTo`) use `setBusy` to keep
|
|
35
|
+
* the menu open and show in-item loading feedback, then `close()` when the
|
|
36
|
+
* handler settles. Prefer `runAsyncMenuAction` over calling these by hand.
|
|
37
|
+
*/
|
|
38
|
+
export interface LegendMenuController {
|
|
39
|
+
/** Reactive busy flag for UI (spinner / disabled). */
|
|
40
|
+
busy: boolean
|
|
41
|
+
/**
|
|
42
|
+
* Synchronous busy check (ref-backed) — use this to gate clicks so a second
|
|
43
|
+
* press before React re-renders cannot start another async action.
|
|
44
|
+
*/
|
|
45
|
+
isBusy: () => boolean
|
|
46
|
+
setBusy: (busy: boolean) => void
|
|
47
|
+
close: () => void
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export const LegendMenuContext = createContext<LegendMenuController | null>(
|
|
51
|
+
null,
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
/** Nearest overflow-menu controller, or `null` outside a row/group menu. */
|
|
55
|
+
export function useLegendMenu(): LegendMenuController | null {
|
|
56
|
+
return useContext(LegendMenuContext)
|
|
57
|
+
}
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
// Composition shell
|
|
2
1
|
export { LegendGroup } from './legend-group/legend-group'
|
|
3
2
|
export type { LegendGroupProps } from './legend-group/legend-group'
|
|
4
3
|
export { LegendRow } from './legend-row/legend-row'
|
|
@@ -8,12 +7,13 @@ export type { LegendActionsProps } from './legend-actions/legend-actions'
|
|
|
8
7
|
export {
|
|
9
8
|
LegendRowContext,
|
|
10
9
|
LegendGroupContext,
|
|
10
|
+
LegendMenuContext,
|
|
11
11
|
useLegendRow,
|
|
12
12
|
useLegendGroupId,
|
|
13
|
+
useLegendMenu,
|
|
13
14
|
} from './contexts'
|
|
14
|
-
export type { LegendRowContextValue } from './contexts'
|
|
15
|
+
export type { LegendRowContextValue, LegendMenuController } from './contexts'
|
|
15
16
|
|
|
16
|
-
// Store-connected actions & controls (context-driven)
|
|
17
17
|
export { LegendVisibilityToggle } from './legend-visibility-toggle/legend-visibility-toggle'
|
|
18
18
|
export { LegendRowMenu } from './legend-row-menu/legend-row-menu'
|
|
19
19
|
export { LegendZoomTo } from './legend-row-menu/legend-zoom-to'
|
|
@@ -21,7 +21,6 @@ export { LegendOpacity } from './legend-opacity/legend-opacity'
|
|
|
21
21
|
export { LegendConfigSelect } from './legend-config-select/legend-config-select'
|
|
22
22
|
export { LegendItem } from './legend-item/legend-item'
|
|
23
23
|
|
|
24
|
-
// Item renderers (pure UI)
|
|
25
24
|
export { LegendItemUI } from './legend-item/legend-item-ui'
|
|
26
25
|
export type { LegendItemUIProps } from './legend-item/legend-item-ui'
|
|
27
26
|
export {
|
|
@@ -15,9 +15,10 @@ export interface LegendActionsProps {
|
|
|
15
15
|
/**
|
|
16
16
|
* Header actions slot for `Legend.Row` / `Legend.Group` (widgets-v2 `Actions`
|
|
17
17
|
* parity): the wrapper partitions its children by component identity and
|
|
18
|
-
* renders this slot inside the header,
|
|
19
|
-
*
|
|
20
|
-
*
|
|
18
|
+
* renders this slot inside the header, wrapped in the hover-fade group
|
|
19
|
+
* (`PsLegend-rowFade` in rows, `PsLegend-groupFade` in group headers —
|
|
20
|
+
* resolved from the nearest context). Compose the ⋮ menu last so it sits
|
|
21
|
+
* rightmost, per design.
|
|
21
22
|
*
|
|
22
23
|
* @experimental This API is new and may change in a future release.
|
|
23
24
|
*/
|