@carto/ps-react-ui 4.9.1 → 4.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/category-Dnd2_j0x.js +719 -0
- package/dist/category-Dnd2_j0x.js.map +1 -0
- package/dist/change-column-BiuuHCDN.js +1156 -0
- package/dist/change-column-BiuuHCDN.js.map +1 -0
- package/dist/chat.js +1507 -0
- package/dist/chat.js.map +1 -0
- package/dist/components.js +122 -120
- package/dist/components.js.map +1 -1
- package/dist/copy-button-DGL1tyli.js +26 -0
- package/dist/copy-button-DGL1tyli.js.map +1 -0
- package/dist/{data-zoom-layout-0QSptXG_.js → data-zoom-layout--YiY6ko_.js} +4 -3
- package/dist/{data-zoom-layout-0QSptXG_.js.map → data-zoom-layout--YiY6ko_.js.map} +1 -1
- package/dist/{download-config-CzmjOT2T.js → download-config-oJIFZ2WC.js} +9 -8
- package/dist/{download-config-CzmjOT2T.js.map → download-config-oJIFZ2WC.js.map} +1 -1
- package/dist/{spread-Y9R1f5dm.js → spread-CPis22AE.js} +4 -3
- package/dist/{spread-Y9R1f5dm.js.map → spread-CPis22AE.js.map} +1 -1
- package/dist/types/chat/bubbles/chat-error-message.d.ts +2 -0
- package/dist/types/chat/bubbles/chat-suggestion-button.d.ts +2 -0
- package/dist/types/chat/bubbles/chat-user-message.d.ts +2 -0
- package/dist/types/chat/bubbles/index.d.ts +4 -0
- package/dist/types/chat/const.d.ts +4 -0
- package/dist/types/chat/containers/chat-content.d.ts +2 -0
- package/dist/types/chat/containers/chat-footer.d.ts +2 -0
- package/dist/types/chat/containers/chat-header.d.ts +2 -0
- package/dist/types/chat/containers/chat-starter.d.ts +2 -0
- package/dist/types/chat/containers/index.d.ts +4 -0
- package/dist/types/chat/containers/styles.d.ts +93 -0
- package/dist/types/chat/feedback/chat-loader.d.ts +2 -0
- package/dist/types/chat/feedback/chat-rating-action.d.ts +2 -0
- package/dist/types/chat/feedback/chat-thinking.d.ts +2 -0
- package/dist/types/chat/feedback/chat-tool-code-area.d.ts +2 -0
- package/dist/types/chat/feedback/chat-tool-full-view-dialog.d.ts +2 -0
- package/dist/types/chat/feedback/chat-tool-group.d.ts +2 -0
- package/dist/types/chat/feedback/chat-tool-trace.d.ts +3 -0
- package/dist/types/chat/feedback/get-tool-label.d.ts +2 -0
- package/dist/types/chat/feedback/index.d.ts +8 -0
- package/dist/types/chat/feedback/styles.d.ts +211 -0
- package/dist/types/chat/index.d.ts +20 -0
- package/dist/types/chat/types.d.ts +184 -0
- package/dist/types/chat/use-typewriter.d.ts +30 -0
- package/dist/types/components/copy-button/copy-button.d.ts +2 -0
- package/dist/types/components/copy-button/types.d.ts +6 -0
- package/dist/types/components/index.d.ts +2 -0
- package/dist/types/widgets/actions/brush-toggle/style.d.ts +1 -1
- package/dist/types/widgets/actions/shared/styles.d.ts +1 -1
- package/dist/types/widgets/actions/zoom-toggle/style.d.ts +1 -1
- package/dist/types/widgets/echart/types.d.ts +1 -1
- package/dist/types/widgets/toolbar-actions/styles.d.ts +1 -1
- package/dist/types/widgets-v2/actions/brush-toggle/style.d.ts +1 -1
- package/dist/types/widgets-v2/actions/change-column/style.d.ts +1 -1
- package/dist/types/widgets-v2/actions/fullscreen/style.d.ts +1 -1
- package/dist/types/widgets-v2/actions/index.d.ts +1 -0
- package/dist/types/widgets-v2/actions/lock-selection/style.d.ts +1 -1
- package/dist/types/widgets-v2/actions/relative-data/style.d.ts +1 -1
- package/dist/types/widgets-v2/actions/searcher/style.d.ts +1 -1
- package/dist/types/widgets-v2/actions/show-all/index.d.ts +2 -0
- package/dist/types/widgets-v2/actions/show-all/labels.d.ts +5 -0
- package/dist/types/widgets-v2/actions/show-all/show-all.d.ts +33 -0
- package/dist/types/widgets-v2/actions/show-all/style.d.ts +8 -0
- package/dist/types/widgets-v2/actions/stack-toggle/style.d.ts +1 -1
- package/dist/types/widgets-v2/actions/zoom-toggle/style.d.ts +1 -1
- package/dist/types/widgets-v2/category/category-ui.d.ts +9 -2
- package/dist/types/widgets-v2/category/category.d.ts +9 -2
- package/dist/types/widgets-v2/category/components/category-row-other.d.ts +19 -6
- package/dist/types/widgets-v2/category/style.d.ts +21 -2
- package/dist/types/widgets-v2/category/types.d.ts +2 -0
- package/dist/types/widgets-v2/index.d.ts +3 -2
- package/dist/types/widgets-v2/selection-summary/labels.d.ts +7 -2
- package/dist/types/widgets-v2/selection-summary/selection-summary.d.ts +13 -6
- package/dist/types/widgets-v2/selection-summary/style.d.ts +15 -0
- package/dist/widgets/actions.js +115 -114
- package/dist/widgets/actions.js.map +1 -1
- package/dist/widgets/bar.js +1 -1
- package/dist/widgets/category.js +9 -8
- package/dist/widgets/category.js.map +1 -1
- package/dist/widgets/formula.js +11 -10
- package/dist/widgets/formula.js.map +1 -1
- package/dist/widgets/histogram.js +7 -6
- package/dist/widgets/histogram.js.map +1 -1
- package/dist/widgets/markdown.js +9 -8
- package/dist/widgets/markdown.js.map +1 -1
- package/dist/widgets/pie.js +1 -1
- package/dist/widgets/scatterplot.js +1 -1
- package/dist/widgets/spread.js +9 -8
- package/dist/widgets/spread.js.map +1 -1
- package/dist/widgets/table.js +17 -16
- package/dist/widgets/table.js.map +1 -1
- package/dist/widgets/timeseries.js +1 -1
- package/dist/widgets/utils.js +1 -1
- package/dist/widgets/wrapper.js +3 -2
- package/dist/widgets/wrapper.js.map +1 -1
- package/dist/widgets-v2/actions.js +41 -37
- package/dist/widgets-v2/bar.js +8 -7
- package/dist/widgets-v2/bar.js.map +1 -1
- package/dist/widgets-v2/category.js +22 -21
- package/dist/widgets-v2/category.js.map +1 -1
- package/dist/widgets-v2/formula.js +23 -22
- package/dist/widgets-v2/formula.js.map +1 -1
- package/dist/widgets-v2/histogram.js +10 -9
- package/dist/widgets-v2/histogram.js.map +1 -1
- package/dist/widgets-v2/markdown.js +9 -8
- package/dist/widgets-v2/markdown.js.map +1 -1
- package/dist/widgets-v2/pie.js +7 -6
- package/dist/widgets-v2/pie.js.map +1 -1
- package/dist/widgets-v2/scatterplot.js +9 -8
- package/dist/widgets-v2/scatterplot.js.map +1 -1
- package/dist/widgets-v2/spread.js +9 -8
- package/dist/widgets-v2/spread.js.map +1 -1
- package/dist/widgets-v2/table.js +16 -15
- package/dist/widgets-v2/table.js.map +1 -1
- package/dist/widgets-v2/timeseries.js +8 -7
- package/dist/widgets-v2/timeseries.js.map +1 -1
- package/dist/widgets-v2/utils.js +1 -1
- package/dist/widgets-v2.js +276 -271
- package/dist/widgets-v2.js.map +1 -1
- package/package.json +7 -3
- package/src/chat/bubbles/chat-agent-message.test.tsx +30 -0
- package/src/chat/bubbles/chat-agent-message.tsx +11 -0
- package/src/chat/bubbles/chat-error-message.test.tsx +40 -0
- package/src/chat/bubbles/chat-error-message.tsx +47 -0
- package/src/chat/bubbles/chat-suggestion-button.test.tsx +24 -0
- package/src/chat/bubbles/chat-suggestion-button.tsx +27 -0
- package/src/chat/bubbles/chat-user-message.test.tsx +27 -0
- package/src/chat/bubbles/chat-user-message.tsx +27 -0
- package/src/chat/bubbles/index.ts +4 -0
- package/src/chat/bubbles/styles.ts +148 -0
- package/src/chat/const.ts +4 -0
- package/src/chat/containers/chat-content.test.tsx +269 -0
- package/src/chat/containers/chat-content.tsx +142 -0
- package/src/chat/containers/chat-footer.test.tsx +34 -0
- package/src/chat/containers/chat-footer.tsx +78 -0
- package/src/chat/containers/chat-header.test.tsx +28 -0
- package/src/chat/containers/chat-header.tsx +29 -0
- package/src/chat/containers/chat-starter.test.tsx +32 -0
- package/src/chat/containers/chat-starter.tsx +75 -0
- package/src/chat/containers/index.ts +4 -0
- package/src/chat/containers/styles.ts +96 -0
- package/src/chat/feedback/chat-actions-container.test.tsx +64 -0
- package/src/chat/feedback/chat-actions-container.tsx +7 -0
- package/src/chat/feedback/chat-loader.test.tsx +10 -0
- package/src/chat/feedback/chat-loader.tsx +31 -0
- package/src/chat/feedback/chat-rating-action.tsx +43 -0
- package/src/chat/feedback/chat-thinking.test.tsx +15 -0
- package/src/chat/feedback/chat-thinking.tsx +23 -0
- package/src/chat/feedback/chat-tool-code-area.test.tsx +23 -0
- package/src/chat/feedback/chat-tool-code-area.tsx +71 -0
- package/src/chat/feedback/chat-tool-full-view-dialog.test.tsx +39 -0
- package/src/chat/feedback/chat-tool-full-view-dialog.tsx +121 -0
- package/src/chat/feedback/chat-tool-group.test.tsx +84 -0
- package/src/chat/feedback/chat-tool-group.tsx +156 -0
- package/src/chat/feedback/chat-tool-trace.test.tsx +81 -0
- package/src/chat/feedback/chat-tool-trace.tsx +192 -0
- package/src/chat/feedback/get-tool-label.test.tsx +91 -0
- package/src/chat/feedback/get-tool-label.ts +13 -0
- package/src/chat/feedback/index.ts +8 -0
- package/src/chat/feedback/styles.ts +229 -0
- package/src/chat/index.ts +59 -0
- package/src/chat/types.ts +215 -0
- package/src/chat/use-typewriter.test.tsx +38 -0
- package/src/chat/use-typewriter.ts +82 -0
- package/src/components/copy-button/copy-button.test.tsx +41 -0
- package/src/components/copy-button/copy-button.tsx +31 -0
- package/src/components/copy-button/types.ts +10 -0
- package/src/components/index.ts +3 -0
- package/src/widgets/echart/types.ts +1 -1
- package/src/widgets-v2/actions/index.ts +8 -0
- package/src/widgets-v2/actions/show-all/index.ts +7 -0
- package/src/widgets-v2/actions/show-all/labels.ts +8 -0
- package/src/widgets-v2/actions/show-all/show-all.test.tsx +50 -0
- package/src/widgets-v2/actions/show-all/show-all.tsx +72 -0
- package/src/widgets-v2/actions/show-all/style.ts +8 -0
- package/src/widgets-v2/category/category-ui.test.tsx +26 -10
- package/src/widgets-v2/category/category-ui.tsx +13 -3
- package/src/widgets-v2/category/category.test.tsx +4 -4
- package/src/widgets-v2/category/category.tsx +10 -1
- package/src/widgets-v2/category/components/category-row-other.test.tsx +36 -7
- package/src/widgets-v2/category/components/category-row-other.tsx +64 -13
- package/src/widgets-v2/category/style.ts +35 -4
- package/src/widgets-v2/category/types.ts +2 -0
- package/src/widgets-v2/index.ts +3 -0
- package/src/widgets-v2/selection-summary/labels.ts +8 -4
- package/src/widgets-v2/selection-summary/selection-summary.test.tsx +15 -9
- package/src/widgets-v2/selection-summary/selection-summary.tsx +42 -22
- package/src/widgets-v2/selection-summary/style.ts +15 -0
- package/dist/category-DwaeYjpX.js +0 -656
- package/dist/category-DwaeYjpX.js.map +0 -1
- package/dist/change-column-B4IT0rh6.js +0 -1110
- package/dist/change-column-B4IT0rh6.js.map +0 -1
|
@@ -50,7 +50,7 @@ export interface CategoryUIProps {
|
|
|
50
50
|
* Caps the number of visible category rows.
|
|
51
51
|
*
|
|
52
52
|
* - `undefined` (omitted) — caps at {@link DEFAULT_MAX_ITEMS} (20).
|
|
53
|
-
* Surplus rows fold into a single
|
|
53
|
+
* Surplus rows fold into a single "Others <count>" footer row.
|
|
54
54
|
* `undefined` cannot mean "no cap" because it's consumed by the
|
|
55
55
|
* default-parameter syntax — pass `null` instead.
|
|
56
56
|
* - positive finite N — same as the default but with a custom cap.
|
|
@@ -71,6 +71,13 @@ export interface CategoryUIProps {
|
|
|
71
71
|
maxItems?: number | null
|
|
72
72
|
/** Labels for the "Other" overflow row. `{count}` placeholder is replaced. */
|
|
73
73
|
labels?: CategoryLabels
|
|
74
|
+
/**
|
|
75
|
+
* When provided, the "Other" overflow row becomes a button — clicking it
|
|
76
|
+
* fires `onShowAll`, which a composer typically wires to expand the widget
|
|
77
|
+
* (drop the row cap) so every category becomes reachable. When omitted the
|
|
78
|
+
* overflow row stays a static summary.
|
|
79
|
+
*/
|
|
80
|
+
onShowAll?: () => void
|
|
74
81
|
/** Manual override for the bar-width denominator. */
|
|
75
82
|
maxOverride?: number
|
|
76
83
|
/**
|
|
@@ -156,6 +163,7 @@ export function CategoryUI({
|
|
|
156
163
|
series,
|
|
157
164
|
maxItems = DEFAULT_MAX_ITEMS,
|
|
158
165
|
labels,
|
|
166
|
+
onShowAll,
|
|
159
167
|
maxOverride,
|
|
160
168
|
size = 'small',
|
|
161
169
|
stacked = false,
|
|
@@ -224,7 +232,7 @@ export function CategoryUI({
|
|
|
224
232
|
|
|
225
233
|
// Three modes:
|
|
226
234
|
// * positive finite N (default `DEFAULT_MAX_ITEMS = 20` when prop
|
|
227
|
-
// omitted): cap at N rows; surplus folds into "
|
|
235
|
+
// omitted): cap at N rows; surplus folds into "Others <count>".
|
|
228
236
|
// * `0`: no cap WITH scroll — every row renders, the list locks to a
|
|
229
237
|
// fixed viewport and scrolls internally. Composers use this to
|
|
230
238
|
// bypass pagination while the user is searching:
|
|
@@ -296,7 +304,7 @@ export function CategoryUI({
|
|
|
296
304
|
<Box sx={styles.root}>
|
|
297
305
|
<Box
|
|
298
306
|
ref={listRefCallback}
|
|
299
|
-
sx={styles.list}
|
|
307
|
+
sx={scrollMode ? { ...styles.list, ...styles.listScroll } : styles.list}
|
|
300
308
|
style={
|
|
301
309
|
scrollMaxHeight !== undefined
|
|
302
310
|
? { maxHeight: scrollMaxHeight, overflowY: 'auto' }
|
|
@@ -378,6 +386,8 @@ export function CategoryUI({
|
|
|
378
386
|
hiddenCount={hiddenCount}
|
|
379
387
|
otherLabel={labels?.other}
|
|
380
388
|
otherCountLabel={labels?.otherCount}
|
|
389
|
+
showAllLabel={labels?.showAll}
|
|
390
|
+
onShowAll={onShowAll}
|
|
381
391
|
/>
|
|
382
392
|
)}
|
|
383
393
|
</Box>
|
|
@@ -96,7 +96,7 @@ describe('<Category> bridge', () => {
|
|
|
96
96
|
// 3 visible rows + Other footer.
|
|
97
97
|
expect(screen.getAllByRole('button')).toHaveLength(3)
|
|
98
98
|
expect(screen.getByText('Rest')).toBeTruthy()
|
|
99
|
-
expect(screen.getByText('(
|
|
99
|
+
expect(screen.getByText('(3 hidden)')).toBeTruthy()
|
|
100
100
|
})
|
|
101
101
|
|
|
102
102
|
it('does NOT read `transformStates.searcher.enabled` from the store (composer-mediation contract)', () => {
|
|
@@ -119,7 +119,7 @@ describe('<Category> bridge', () => {
|
|
|
119
119
|
</Provider>,
|
|
120
120
|
)
|
|
121
121
|
expect(screen.getAllByRole('button')).toHaveLength(3)
|
|
122
|
-
expect(screen.getByText('
|
|
122
|
+
expect(screen.getByText('Others')).toBeTruthy()
|
|
123
123
|
|
|
124
124
|
// Flip the searcher flag in the store — bridge should NOT react.
|
|
125
125
|
act(() => {
|
|
@@ -134,7 +134,7 @@ describe('<Category> bridge', () => {
|
|
|
134
134
|
|
|
135
135
|
// Still capped, Other still there.
|
|
136
136
|
expect(screen.getAllByRole('button')).toHaveLength(3)
|
|
137
|
-
expect(screen.getByText('
|
|
137
|
+
expect(screen.getByText('Others')).toBeTruthy()
|
|
138
138
|
})
|
|
139
139
|
|
|
140
140
|
it('respects maxItems=0 (no-cap sentinel) from the consumer', () => {
|
|
@@ -153,7 +153,7 @@ describe('<Category> bridge', () => {
|
|
|
153
153
|
)
|
|
154
154
|
// No cap → all rows render, Other footer is suppressed.
|
|
155
155
|
expect(screen.getAllByRole('button')).toHaveLength(10)
|
|
156
|
-
expect(screen.queryByText('
|
|
156
|
+
expect(screen.queryByText('Others')).toBeNull()
|
|
157
157
|
})
|
|
158
158
|
|
|
159
159
|
it('forwards size="medium" through to the rendered bars', () => {
|
|
@@ -40,7 +40,7 @@ export interface CategoryProps {
|
|
|
40
40
|
/** Per-series metadata. Enables the legend + overrides palette per index. */
|
|
41
41
|
series?: readonly CategorySeriesConfig[]
|
|
42
42
|
/**
|
|
43
|
-
* Cap visible rows; overflow folds into "
|
|
43
|
+
* Cap visible rows; overflow folds into an "Others <count>" row. Default 20
|
|
44
44
|
* (when omitted). Pass `0` to swap the cap for a scrollable viewport
|
|
45
45
|
* (composers use this when the user opens the SearcherToggle —
|
|
46
46
|
* `maxItems = searcherOpen ? 0 : userMaxItems`). Pass `null` to disable
|
|
@@ -49,6 +49,13 @@ export interface CategoryProps {
|
|
|
49
49
|
maxItems?: number | null
|
|
50
50
|
/** Labels for the "Other" overflow row. */
|
|
51
51
|
labels?: CategoryLabels
|
|
52
|
+
/**
|
|
53
|
+
* When provided, the "Other" overflow row becomes a button that fires this
|
|
54
|
+
* callback. Composers wire it to expand the widget (e.g. flip the
|
|
55
|
+
* `show-all` flag) so every category is shown. Forwarded to
|
|
56
|
+
* {@link CategoryUI}.
|
|
57
|
+
*/
|
|
58
|
+
onShowAll?: () => void
|
|
52
59
|
/**
|
|
53
60
|
* Manual override for the bar-width denominator. When omitted, the
|
|
54
61
|
* bridge auto-fills from the widget store's `rawData` so bar widths
|
|
@@ -85,6 +92,7 @@ export function Category({
|
|
|
85
92
|
series,
|
|
86
93
|
maxItems,
|
|
87
94
|
labels,
|
|
95
|
+
onShowAll,
|
|
88
96
|
maxOverride,
|
|
89
97
|
size,
|
|
90
98
|
stacked,
|
|
@@ -113,6 +121,7 @@ export function Category({
|
|
|
113
121
|
series={series}
|
|
114
122
|
maxItems={maxItems}
|
|
115
123
|
labels={labels}
|
|
124
|
+
onShowAll={onShowAll}
|
|
116
125
|
maxOverride={effectiveMaxOverride}
|
|
117
126
|
size={size}
|
|
118
127
|
stacked={stacked}
|
|
@@ -1,12 +1,13 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest'
|
|
2
|
-
import { render, screen } from '@testing-library/react'
|
|
1
|
+
import { describe, it, expect, vi } from 'vitest'
|
|
2
|
+
import { fireEvent, render, screen } from '@testing-library/react'
|
|
3
3
|
import { CategoryRowOther } from './category-row-other'
|
|
4
4
|
|
|
5
5
|
describe('<CategoryRowOther>', () => {
|
|
6
|
-
it('renders the default
|
|
6
|
+
it('renders the default label + bare count (no parentheses)', () => {
|
|
7
7
|
render(<CategoryRowOther hiddenCount={7} />)
|
|
8
|
-
expect(screen.getByText('
|
|
9
|
-
expect(screen.getByText('
|
|
8
|
+
expect(screen.getByText('Others')).toBeTruthy()
|
|
9
|
+
expect(screen.getByText('7')).toBeTruthy()
|
|
10
|
+
expect(screen.queryByText('(7 more)')).toBeNull()
|
|
10
11
|
})
|
|
11
12
|
|
|
12
13
|
it('honors custom labels with {count} placeholder', () => {
|
|
@@ -18,11 +19,39 @@ describe('<CategoryRowOther>', () => {
|
|
|
18
19
|
/>,
|
|
19
20
|
)
|
|
20
21
|
expect(screen.getByText('Hidden')).toBeTruthy()
|
|
21
|
-
expect(screen.getByText('
|
|
22
|
+
expect(screen.getByText('+3 not shown')).toBeTruthy()
|
|
22
23
|
})
|
|
23
24
|
|
|
24
|
-
it('is not a button (
|
|
25
|
+
it('is not a button when onShowAll is omitted (static fallback)', () => {
|
|
25
26
|
const { container } = render(<CategoryRowOther hiddenCount={1} />)
|
|
26
27
|
expect(container.querySelectorAll('[role="button"]').length).toBe(0)
|
|
27
28
|
})
|
|
29
|
+
|
|
30
|
+
describe('when onShowAll is provided', () => {
|
|
31
|
+
it('renders as a button', () => {
|
|
32
|
+
render(<CategoryRowOther hiddenCount={4} onShowAll={vi.fn()} />)
|
|
33
|
+
expect(screen.getByRole('button')).toBeTruthy()
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
it('fires onShowAll on click', () => {
|
|
37
|
+
const onShowAll = vi.fn()
|
|
38
|
+
render(<CategoryRowOther hiddenCount={4} onShowAll={onShowAll} />)
|
|
39
|
+
fireEvent.click(screen.getByRole('button'))
|
|
40
|
+
expect(onShowAll).toHaveBeenCalledTimes(1)
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
it('fires onShowAll on Enter and Space', () => {
|
|
44
|
+
const onShowAll = vi.fn()
|
|
45
|
+
render(<CategoryRowOther hiddenCount={4} onShowAll={onShowAll} />)
|
|
46
|
+
const btn = screen.getByRole('button')
|
|
47
|
+
fireEvent.keyDown(btn, { key: 'Enter' })
|
|
48
|
+
fireEvent.keyDown(btn, { key: ' ' })
|
|
49
|
+
expect(onShowAll).toHaveBeenCalledTimes(2)
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
it('is keyboard-focusable', () => {
|
|
53
|
+
render(<CategoryRowOther hiddenCount={4} onShowAll={vi.fn()} />)
|
|
54
|
+
expect(screen.getByRole('button').getAttribute('tabindex')).toBe('0')
|
|
55
|
+
})
|
|
56
|
+
})
|
|
28
57
|
})
|
|
@@ -1,33 +1,84 @@
|
|
|
1
|
+
import type { KeyboardEvent } from 'react'
|
|
1
2
|
import { Box, Typography } from '@mui/material'
|
|
3
|
+
import { Tooltip } from '../../../components'
|
|
2
4
|
import { styles } from '../style'
|
|
3
5
|
|
|
4
6
|
export interface CategoryRowOtherProps {
|
|
5
7
|
hiddenCount: number
|
|
6
|
-
/** Label for the overflow row. Defaults to `'
|
|
8
|
+
/** Label for the overflow row. Defaults to `'Others'`. */
|
|
7
9
|
otherLabel?: string
|
|
8
|
-
/** Count text with `{count}` placeholder. Defaults to `'{count}
|
|
10
|
+
/** Count text with `{count}` placeholder. Defaults to `'{count}'`. */
|
|
9
11
|
otherCountLabel?: string
|
|
12
|
+
/**
|
|
13
|
+
* When provided, the row becomes a button: clicking it (or Enter/Space)
|
|
14
|
+
* fires `onShowAll`, expanding the widget to reveal every category. When
|
|
15
|
+
* omitted the row renders as a static, non-interactive summary.
|
|
16
|
+
*/
|
|
17
|
+
onShowAll?: () => void
|
|
18
|
+
/** Tooltip shown over the button form. Defaults to `'Show all'`. */
|
|
19
|
+
showAllLabel?: string
|
|
10
20
|
}
|
|
11
21
|
|
|
12
22
|
/**
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
23
|
+
* Overflow summary row rendered after the last visible category row when
|
|
24
|
+
* `data.length > maxItems`. Shows `Others <count>` (count muted, no
|
|
25
|
+
* parentheses).
|
|
26
|
+
*
|
|
27
|
+
* When `onShowAll` is supplied the row is an interactive button — clicking
|
|
28
|
+
* it (or Enter/Space) expands the widget to show all categories, mirroring
|
|
29
|
+
* the Searcher's full-list view. Without `onShowAll` it stays a static,
|
|
30
|
+
* non-interactive summary (backward-compatible default).
|
|
16
31
|
*/
|
|
17
32
|
export function CategoryRowOther({
|
|
18
33
|
hiddenCount,
|
|
19
|
-
otherLabel = '
|
|
20
|
-
otherCountLabel = '{count}
|
|
34
|
+
otherLabel = 'Others',
|
|
35
|
+
otherCountLabel = '{count}',
|
|
36
|
+
onShowAll,
|
|
37
|
+
showAllLabel = 'Show all',
|
|
21
38
|
}: CategoryRowOtherProps) {
|
|
22
39
|
const countText = otherCountLabel.replace('{count}', String(hiddenCount))
|
|
40
|
+
|
|
41
|
+
const count = (
|
|
42
|
+
<Typography variant='body2' sx={styles.otherCount}>
|
|
43
|
+
{countText}
|
|
44
|
+
</Typography>
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
if (!onShowAll) {
|
|
48
|
+
return (
|
|
49
|
+
<Box sx={styles.otherRow}>
|
|
50
|
+
<Typography variant='body2' sx={styles.otherLabel}>
|
|
51
|
+
{otherLabel}
|
|
52
|
+
</Typography>
|
|
53
|
+
{count}
|
|
54
|
+
</Box>
|
|
55
|
+
)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const handleKey = (e: KeyboardEvent<HTMLSpanElement>): void => {
|
|
59
|
+
if (e.key === 'Enter' || e.key === ' ') {
|
|
60
|
+
e.preventDefault()
|
|
61
|
+
onShowAll()
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Only the label is the click target; the count stays static beside it.
|
|
23
66
|
return (
|
|
24
67
|
<Box sx={styles.otherRow}>
|
|
25
|
-
<
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
68
|
+
<Tooltip title={showAllLabel}>
|
|
69
|
+
<Typography
|
|
70
|
+
variant='body2'
|
|
71
|
+
component='span'
|
|
72
|
+
role='button'
|
|
73
|
+
tabIndex={0}
|
|
74
|
+
onClick={onShowAll}
|
|
75
|
+
onKeyDown={handleKey}
|
|
76
|
+
sx={{ ...styles.otherLabel, ...styles.otherLabelButton }}
|
|
77
|
+
>
|
|
78
|
+
{otherLabel}
|
|
79
|
+
</Typography>
|
|
80
|
+
</Tooltip>
|
|
81
|
+
{count}
|
|
31
82
|
</Box>
|
|
32
83
|
)
|
|
33
84
|
}
|
|
@@ -78,6 +78,13 @@ export const styles = {
|
|
|
78
78
|
gap: 1,
|
|
79
79
|
py: 0.5,
|
|
80
80
|
},
|
|
81
|
+
// Layered on `list` only in scroll mode (`maxItems === 0`): right padding
|
|
82
|
+
// so the bars + right-aligned values clear the scrollbar. Right side only
|
|
83
|
+
// — the left edge stays at 0 so content stays aligned with the capped
|
|
84
|
+
// view. `overflowY` + the dynamic `maxHeight` remain on the inline style.
|
|
85
|
+
listScroll: {
|
|
86
|
+
pr: 1,
|
|
87
|
+
},
|
|
81
88
|
|
|
82
89
|
// ── Single-series row ─────────────────────────────────────────────
|
|
83
90
|
// v1 layout: label + value share a top flex row (justify-between); the
|
|
@@ -266,20 +273,44 @@ export const styles = {
|
|
|
266
273
|
},
|
|
267
274
|
|
|
268
275
|
// ── "Other" overflow row ──────────────────────────────────────────
|
|
276
|
+
// Left-aligned `Others <count>` — the label sits next to a muted count
|
|
277
|
+
// (no parentheses). Only the label is the click target when interactive;
|
|
278
|
+
// the count is always static text beside it.
|
|
269
279
|
otherRow: {
|
|
270
280
|
display: 'flex',
|
|
271
|
-
justifyContent: 'space-between',
|
|
272
281
|
alignItems: 'center',
|
|
282
|
+
gap: 1,
|
|
273
283
|
py: 0.5,
|
|
274
284
|
px: 0.5,
|
|
275
285
|
},
|
|
286
|
+
// Interactive affordances layered onto the "Others" label only (cursor,
|
|
287
|
+
// hover tint, focus ring). A negative LEFT margin + matching padding keep
|
|
288
|
+
// the hover pill breathing without shifting the label's text edge (it
|
|
289
|
+
// stays aligned with the category rows above). No right margin, so the
|
|
290
|
+
// row `gap` is preserved between the pill and the count.
|
|
291
|
+
otherLabelButton: {
|
|
292
|
+
cursor: 'pointer',
|
|
293
|
+
userSelect: 'none',
|
|
294
|
+
borderRadius: 1,
|
|
295
|
+
px: 0.5,
|
|
296
|
+
ml: -0.5,
|
|
297
|
+
transition: 'background-color 80ms ease',
|
|
298
|
+
'&:hover': {
|
|
299
|
+
bgcolor: 'action.hover',
|
|
300
|
+
},
|
|
301
|
+
'&:focus-visible': {
|
|
302
|
+
outline: '2px solid',
|
|
303
|
+
outlineColor: 'primary.main',
|
|
304
|
+
outlineOffset: 2,
|
|
305
|
+
},
|
|
306
|
+
},
|
|
276
307
|
otherLabel: {
|
|
277
|
-
|
|
278
|
-
color: 'text.secondary',
|
|
308
|
+
color: 'text.primary',
|
|
279
309
|
fontWeight: 'medium',
|
|
280
310
|
},
|
|
281
311
|
otherCount: {
|
|
282
|
-
color: 'text.
|
|
312
|
+
color: 'text.secondary',
|
|
313
|
+
fontVariantNumeric: 'tabular-nums',
|
|
283
314
|
},
|
|
284
315
|
// `bar` / `barFill` are size-keyed (`Record<CategorySize, SxProps>`);
|
|
285
316
|
// every other entry is a single `SxProps<Theme>`. The mixed shape means
|
package/src/widgets-v2/index.ts
CHANGED
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
Searcher,
|
|
23
23
|
SearcherToggle,
|
|
24
24
|
StackToggle,
|
|
25
|
+
ShowAllToggle,
|
|
25
26
|
ZoomToggle,
|
|
26
27
|
BrushToggle,
|
|
27
28
|
RelativeData,
|
|
@@ -83,6 +84,7 @@ export const Widget = {
|
|
|
83
84
|
Searcher,
|
|
84
85
|
SearcherToggle,
|
|
85
86
|
StackToggle,
|
|
87
|
+
ShowAllToggle,
|
|
86
88
|
ZoomToggle,
|
|
87
89
|
BrushToggle,
|
|
88
90
|
RelativeData,
|
|
@@ -149,6 +151,7 @@ export type {
|
|
|
149
151
|
SearcherProps,
|
|
150
152
|
SearcherToggleProps,
|
|
151
153
|
StackToggleProps,
|
|
154
|
+
ShowAllToggleProps,
|
|
152
155
|
ZoomToggleProps,
|
|
153
156
|
BrushToggleProps,
|
|
154
157
|
RelativeDataProps,
|
|
@@ -1,11 +1,15 @@
|
|
|
1
|
+
import type { ReactNode } from 'react'
|
|
2
|
+
|
|
1
3
|
export interface SelectionSummaryLabels {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
+
/**
|
|
5
|
+
* Optional custom renderer for the count. Defaults to the built-in two-tone
|
|
6
|
+
* `selected / total` rendering. Receives the already-resolved displayed
|
|
7
|
+
* number (which equals `total` when nothing is selected).
|
|
8
|
+
*/
|
|
9
|
+
summary?: (selected: number, total: number) => ReactNode
|
|
4
10
|
clear: string
|
|
5
11
|
}
|
|
6
12
|
|
|
7
13
|
export const DEFAULT_SELECTION_SUMMARY_LABELS: SelectionSummaryLabels = {
|
|
8
|
-
allSelected: 'All selected',
|
|
9
|
-
selections: (count) => `${count} selected`,
|
|
10
14
|
clear: 'Clear',
|
|
11
15
|
}
|
|
@@ -9,15 +9,22 @@ describe('<SelectionSummary>', () => {
|
|
|
9
9
|
expect(container.textContent).toBe('')
|
|
10
10
|
})
|
|
11
11
|
|
|
12
|
-
it('renders
|
|
12
|
+
it('renders nothing when total is omitted (undefined)', () => {
|
|
13
|
+
const { container } = render(<SelectionSummary count={0} />)
|
|
14
|
+
expect(container.textContent).toBe('')
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
it('renders total / total when nothing is selected (count === 0)', () => {
|
|
13
18
|
render(<SelectionSummary count={0} total={10} />)
|
|
14
|
-
expect(screen.
|
|
19
|
+
expect(screen.getByLabelText('10 / 10')).toBeTruthy()
|
|
20
|
+
// No Clear button when nothing is selected.
|
|
21
|
+
expect(screen.queryByText('Clear')).toBeNull()
|
|
15
22
|
})
|
|
16
23
|
|
|
17
|
-
it('renders
|
|
24
|
+
it('renders count / total + Clear when count > 0', () => {
|
|
18
25
|
const onClear = vi.fn()
|
|
19
26
|
render(<SelectionSummary count={3} total={10} onClear={onClear} />)
|
|
20
|
-
expect(screen.
|
|
27
|
+
expect(screen.getByLabelText('3 / 10')).toBeTruthy()
|
|
21
28
|
fireEvent.click(screen.getByText('Clear'))
|
|
22
29
|
expect(onClear).toHaveBeenCalledTimes(1)
|
|
23
30
|
})
|
|
@@ -27,24 +34,23 @@ describe('<SelectionSummary>', () => {
|
|
|
27
34
|
expect(screen.queryByText('Clear')).toBeNull()
|
|
28
35
|
})
|
|
29
36
|
|
|
30
|
-
it('honors custom
|
|
37
|
+
it('honors a custom summary renderer and clear label', () => {
|
|
31
38
|
render(
|
|
32
39
|
<SelectionSummary
|
|
33
40
|
count={3}
|
|
34
41
|
total={10}
|
|
35
42
|
labels={{
|
|
36
|
-
|
|
37
|
-
selections: (n) => `${n} pinned`,
|
|
43
|
+
summary: (selected, total) => `${selected} of ${total} pinned`,
|
|
38
44
|
clear: 'Reset',
|
|
39
45
|
}}
|
|
40
46
|
onClear={() => undefined}
|
|
41
47
|
/>,
|
|
42
48
|
)
|
|
43
|
-
expect(screen.getByText('3 pinned')).toBeTruthy()
|
|
49
|
+
expect(screen.getByText('3 of 10 pinned')).toBeTruthy()
|
|
44
50
|
expect(screen.getByText('Reset')).toBeTruthy()
|
|
45
51
|
})
|
|
46
52
|
|
|
47
|
-
it('renders the custom icon component when provided
|
|
53
|
+
it('renders the custom icon component when provided', () => {
|
|
48
54
|
const { container } = render(
|
|
49
55
|
<SelectionSummary count={0} total={10} icon={StarIcon} />,
|
|
50
56
|
)
|
|
@@ -9,9 +9,9 @@ import { styles } from './style'
|
|
|
9
9
|
export interface SelectionSummaryProps {
|
|
10
10
|
/** Number of currently selected items. */
|
|
11
11
|
count: number
|
|
12
|
-
/** Optional total — when 0 the component renders nothing (no data
|
|
12
|
+
/** Optional total — when `undefined` or `0` the component renders nothing (no data). */
|
|
13
13
|
total?: number
|
|
14
|
-
/** Clear callback.
|
|
14
|
+
/** Clear callback. The Clear control is shown only when `count > 0` and `onClear` is provided. */
|
|
15
15
|
onClear?: () => void
|
|
16
16
|
labels?: Partial<SelectionSummaryLabels>
|
|
17
17
|
icon?: ComponentType<SvgIconProps>
|
|
@@ -19,11 +19,18 @@ export interface SelectionSummaryProps {
|
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
/**
|
|
22
|
+
* Renders a `selected / total` count.
|
|
23
|
+
*
|
|
22
24
|
* Render rules:
|
|
23
|
-
* - `
|
|
24
|
-
* - `count === 0` →
|
|
25
|
-
*
|
|
26
|
-
* - `count > 0`
|
|
25
|
+
* - `!total` (undefined or 0) → render nothing (no data to summarize).
|
|
26
|
+
* - `count === 0` → display `total / total` (everything is implicitly in
|
|
27
|
+
* scope); no Clear button.
|
|
28
|
+
* - `count > 0` → display `count / total` + Clear (when `onClear` is given).
|
|
29
|
+
*
|
|
30
|
+
* The count is two-tone by default (selected number bold/`text.primary`,
|
|
31
|
+
* ` / total` in `text.secondary`). Pass `labels.summary` to render a custom
|
|
32
|
+
* node instead — it receives the already-resolved displayed number (which
|
|
33
|
+
* equals `total` when nothing is selected).
|
|
27
34
|
*/
|
|
28
35
|
export function SelectionSummary({
|
|
29
36
|
count,
|
|
@@ -33,26 +40,39 @@ export function SelectionSummary({
|
|
|
33
40
|
icon: Icon,
|
|
34
41
|
iconProps,
|
|
35
42
|
}: SelectionSummaryProps) {
|
|
36
|
-
if (total
|
|
43
|
+
if (!total) return null
|
|
37
44
|
const _labels = { ...DEFAULT_SELECTION_SUMMARY_LABELS, ...labels }
|
|
38
|
-
|
|
39
|
-
if (count === 0) {
|
|
40
|
-
return (
|
|
41
|
-
<Box sx={styles.root}>
|
|
42
|
-
{Icon ? <Icon fontSize='small' {...iconProps} /> : null}
|
|
43
|
-
<Typography variant='caption' sx={styles.label}>
|
|
44
|
-
{_labels.allSelected}
|
|
45
|
-
</Typography>
|
|
46
|
-
</Box>
|
|
47
|
-
)
|
|
48
|
-
}
|
|
45
|
+
const shown = count === 0 ? total : count
|
|
49
46
|
|
|
50
47
|
return (
|
|
51
48
|
<Box sx={styles.root}>
|
|
52
|
-
<
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
49
|
+
{Icon ? <Icon fontSize='small' {...iconProps} /> : null}
|
|
50
|
+
<Box
|
|
51
|
+
component='span'
|
|
52
|
+
sx={styles.count}
|
|
53
|
+
aria-label={`${shown} / ${total}`}
|
|
54
|
+
>
|
|
55
|
+
{_labels.summary ? (
|
|
56
|
+
_labels.summary(shown, total)
|
|
57
|
+
) : (
|
|
58
|
+
<>
|
|
59
|
+
<Typography component='span' variant='caption' sx={styles.selected}>
|
|
60
|
+
{shown}
|
|
61
|
+
</Typography>
|
|
62
|
+
<Typography
|
|
63
|
+
component='span'
|
|
64
|
+
variant='caption'
|
|
65
|
+
sx={styles.separator}
|
|
66
|
+
>
|
|
67
|
+
/
|
|
68
|
+
</Typography>
|
|
69
|
+
<Typography component='span' variant='caption' sx={styles.total}>
|
|
70
|
+
{total}
|
|
71
|
+
</Typography>
|
|
72
|
+
</>
|
|
73
|
+
)}
|
|
74
|
+
</Box>
|
|
75
|
+
{count > 0 && onClear && (
|
|
56
76
|
<Button size='small' variant='text' onClick={onClear}>
|
|
57
77
|
{_labels.clear}
|
|
58
78
|
</Button>
|
|
@@ -10,6 +10,21 @@ export const styles = {
|
|
|
10
10
|
label: {
|
|
11
11
|
color: 'text.secondary',
|
|
12
12
|
},
|
|
13
|
+
count: {
|
|
14
|
+
display: 'inline-flex',
|
|
15
|
+
alignItems: 'baseline',
|
|
16
|
+
gap: 0.5,
|
|
17
|
+
},
|
|
18
|
+
selected: {
|
|
19
|
+
color: 'text.primary',
|
|
20
|
+
fontWeight: 600,
|
|
21
|
+
},
|
|
22
|
+
separator: {
|
|
23
|
+
color: 'text.secondary',
|
|
24
|
+
},
|
|
25
|
+
total: {
|
|
26
|
+
color: 'text.secondary',
|
|
27
|
+
},
|
|
13
28
|
clear: {
|
|
14
29
|
color: 'primary.main',
|
|
15
30
|
cursor: 'pointer',
|