@juspay/blend-design-system 0.0.37-beta.6 → 0.0.37-beta.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/CodeEditor/types.d.ts +22 -2
- package/dist/components/CodeEditorV2/CodeEditorV2.d.ts +1 -1
- package/dist/components/CodeEditorV2/codeEditorV2.types.d.ts +23 -2
- package/dist/components/DataTable/TableBody/types.d.ts +1 -0
- package/dist/components/DataTable/TableCell/types.d.ts +1 -0
- package/dist/components/DataTable/columnTypes.d.ts +5 -3
- package/dist/components/DataTable/dataTable.tokens.d.ts +4 -0
- package/dist/components/DataTable/types.d.ts +20 -2
- package/dist/components/DataTable/utils.d.ts +4 -3
- package/dist/components/MenuV2/MenuV2Content.d.ts +1 -0
- package/dist/components/MenuV2/menuV2.types.d.ts +5 -0
- package/dist/components/MenuV2/menuV2.utils.d.ts +10 -2
- package/dist/components/MultiSelect/MultiSelect.d.ts +1 -1
- package/dist/components/MultiSelect/MultiSelectMenu.d.ts +1 -1
- package/dist/components/MultiSelect/types.d.ts +2 -0
- package/dist/components/MultiSelectV2/MultiSelectV2.d.ts +1 -1
- package/dist/components/MultiSelectV2/MultiSelectV2Menu.d.ts +1 -1
- package/dist/components/MultiSelectV2/multiSelectV2.types.d.ts +2 -0
- package/dist/components/SingleSelect/SingleSelect.d.ts +1 -1
- package/dist/components/SingleSelect/SingleSelectMenu.d.ts +2 -1
- package/dist/components/SingleSelect/types.d.ts +1 -0
- package/dist/components/SingleSelectV2/SingleSelectV2.d.ts +1 -1
- package/dist/components/SingleSelectV2/SingleSelectV2Menu.d.ts +1 -1
- package/dist/components/SingleSelectV2/singleSelectV2.tokens.types.d.ts +8 -0
- package/dist/components/SingleSelectV2/singleSelectV2.types.d.ts +2 -0
- package/dist/components/Skeleton/hooks/useSkeletonBase.d.ts +7 -11
- package/dist/components/Snackbar/Snackbar.d.ts +1 -1
- package/dist/components/Snackbar/snackbar.tokens.d.ts +1 -0
- package/dist/components/Snackbar/types.d.ts +2 -0
- package/dist/components/StatCardV2/StatCardV2.d.ts +1 -0
- package/dist/components/StatCardV2/StatCardV2NoData.d.ts +2 -2
- package/dist/components/StatCardV2/statcardV2.types.d.ts +1 -0
- package/dist/components/TabsV2/tabsV2.tokens.types.d.ts +3 -0
- package/dist/global-utils/GlobalUtils.d.ts +2 -1
- package/dist/main.js +27446 -27271
- package/dist/{node-CJ_Tft0g.js → node-CovJEwot.js} +65 -25
- package/dist/node.js +1 -1
- package/dist/{tokens.js → token-engine.js} +1 -1
- package/lib/components/Card/Card.tsx +1 -0
- package/lib/components/Charts/ChartHeader.tsx +4 -1
- package/lib/components/CodeBlock/CodeBlock.tsx +8 -4
- package/lib/components/CodeEditor/MonacoEditorWrapper.tsx +6 -2
- package/lib/components/CodeEditor/types.ts +52 -2
- package/lib/components/CodeEditorV2/MonacoEditor/MonacoEditorWrapper.tsx +6 -2
- package/lib/components/CodeEditorV2/codeEditorV2.types.ts +44 -5
- package/lib/components/DataTable/DataTable.tsx +2 -0
- package/lib/components/DataTable/TableBody/index.tsx +2 -0
- package/lib/components/DataTable/TableBody/types.ts +1 -0
- package/lib/components/DataTable/TableCell/index.tsx +45 -20
- package/lib/components/DataTable/TableCell/types.ts +1 -0
- package/lib/components/DataTable/columnTypes.ts +5 -3
- package/lib/components/DataTable/dataTable.tokens.ts +15 -0
- package/lib/components/DataTable/types.ts +33 -2
- package/lib/components/DataTable/utils.ts +91 -5
- package/lib/components/DateRangePicker/CalendarGrid.tsx +3 -0
- package/lib/components/DateRangePicker/DateRangePicker.tsx +5 -1
- package/lib/components/MenuV2/MenuV2.tsx +9 -2
- package/lib/components/MenuV2/MenuV2Content.tsx +6 -0
- package/lib/components/MenuV2/MenuV2SubMenu.tsx +16 -2
- package/lib/components/MenuV2/menuV2.types.ts +11 -0
- package/lib/components/MenuV2/menuV2.utils.ts +45 -3
- package/lib/components/MultiSelect/MultiSelect.tsx +2 -0
- package/lib/components/MultiSelect/MultiSelectMenu.tsx +8 -0
- package/lib/components/MultiSelect/types.ts +2 -0
- package/lib/components/MultiSelectV2/MultiSelectV2.tsx +2 -0
- package/lib/components/MultiSelectV2/MultiSelectV2Menu.tsx +8 -0
- package/lib/components/MultiSelectV2/multiSelectV2.types.ts +2 -0
- package/lib/components/SingleSelect/SingleSelect.tsx +2 -0
- package/lib/components/SingleSelect/SingleSelectMenu.tsx +171 -138
- package/lib/components/SingleSelect/types.ts +1 -0
- package/lib/components/SingleSelectV2/SingleSelectV2.tsx +2 -0
- package/lib/components/SingleSelectV2/SingleSelectV2Menu.tsx +84 -58
- package/lib/components/SingleSelectV2/SingleSelectV2Search.tsx +1 -0
- package/lib/components/SingleSelectV2/singleSelectV2.dark.tokens.ts +8 -0
- package/lib/components/SingleSelectV2/singleSelectV2.light.tokens.ts +8 -0
- package/lib/components/SingleSelectV2/singleSelectV2.tokens.types.ts +8 -0
- package/lib/components/SingleSelectV2/singleSelectV2.types.ts +2 -0
- package/lib/components/Skeleton/hooks/useSkeletonBase.ts +11 -1
- package/lib/components/Snackbar/Snackbar.tsx +18 -8
- package/lib/components/Snackbar/snackbar.tokens.ts +6 -0
- package/lib/components/Snackbar/types.ts +2 -0
- package/lib/components/StatCard/StatCard.tsx +39 -2
- package/lib/components/StatCardV2/StatCardV2.tsx +8 -6
- package/lib/components/StatCardV2/StatCardV2NoData.tsx +8 -6
- package/lib/components/StatCardV2/statcardV2.types.ts +1 -0
- package/lib/components/TabsV2/TabsV2List.tsx +5 -1
- package/lib/components/TabsV2/tabsV2.dark.tokens.ts +12 -0
- package/lib/components/TabsV2/tabsV2.light.tokens.ts +12 -0
- package/lib/components/TabsV2/tabsV2.tokens.types.ts +3 -0
- package/lib/global-utils/GlobalUtils.ts +10 -1
- package/package.json +5 -4
- package/dist/tokens.d.ts +0 -2
|
@@ -15,6 +15,7 @@ export type TableCellProps<T extends Record<string, unknown>> = {
|
|
|
15
15
|
hasCustomBackground?: boolean
|
|
16
16
|
onFieldChange: (value: unknown) => void
|
|
17
17
|
getDisplayValue?: (value: unknown, column: ColumnDefinition<T>) => unknown
|
|
18
|
+
dateLabel?: string
|
|
18
19
|
'data-row-index'?: number
|
|
19
20
|
'data-col-index'?: number
|
|
20
21
|
tabIndex?: number
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { FilterType, ColumnType } from './types'
|
|
1
|
+
import { FilterType, ColumnType, DateFormat } from './types'
|
|
2
2
|
|
|
3
3
|
export type ColumnFilterOption = {
|
|
4
4
|
id: string
|
|
@@ -45,13 +45,15 @@ export type MultiSelectData = {
|
|
|
45
45
|
|
|
46
46
|
export type DateData = {
|
|
47
47
|
date: Date | string
|
|
48
|
-
format?:
|
|
48
|
+
format?: DateFormat
|
|
49
|
+
dateLabel?: string
|
|
49
50
|
}
|
|
50
51
|
|
|
51
52
|
export type DateRangeData = {
|
|
52
53
|
startDate: Date | string
|
|
53
54
|
endDate: Date | string
|
|
54
|
-
format?:
|
|
55
|
+
format?: DateFormat
|
|
56
|
+
dateLabel?: string
|
|
55
57
|
}
|
|
56
58
|
|
|
57
59
|
export type ColumnDataTypeMap = {
|
|
@@ -198,6 +198,11 @@ export type TableTokenType = BasicCSSProps & {
|
|
|
198
198
|
color: CSSObject['color']
|
|
199
199
|
fontSize: CSSObject['fontSize']
|
|
200
200
|
borderTop: CSSObject['borderTop']
|
|
201
|
+
|
|
202
|
+
dateLabel: {
|
|
203
|
+
fontSize: CSSObject['fontSize']
|
|
204
|
+
color: CSSObject['color']
|
|
205
|
+
}
|
|
201
206
|
expandable: {
|
|
202
207
|
padding: CSSObject['padding']
|
|
203
208
|
borderTop: CSSObject['borderTop']
|
|
@@ -467,6 +472,11 @@ export const getTableToken = (
|
|
|
467
472
|
fontSize:
|
|
468
473
|
foundationToken.font.size.body.md.fontSize,
|
|
469
474
|
borderTop: `1px solid ${foundationToken.colors.gray[150]}`,
|
|
475
|
+
dateLabel: {
|
|
476
|
+
fontSize:
|
|
477
|
+
foundationToken.font.size.body.xs.fontSize,
|
|
478
|
+
color: foundationToken.colors.gray[400],
|
|
479
|
+
},
|
|
470
480
|
expandable: {
|
|
471
481
|
padding: foundationToken.unit[16],
|
|
472
482
|
borderTop: `1px solid ${foundationToken.colors.gray[150]}`,
|
|
@@ -732,6 +742,11 @@ export const getTableToken = (
|
|
|
732
742
|
fontSize:
|
|
733
743
|
foundationToken.font.size.body.md.fontSize,
|
|
734
744
|
borderTop: `1px solid ${foundationToken.colors.gray[150]}`,
|
|
745
|
+
dateLabel: {
|
|
746
|
+
fontSize:
|
|
747
|
+
foundationToken.font.size.body.xs.fontSize,
|
|
748
|
+
color: foundationToken.colors.gray[400],
|
|
749
|
+
},
|
|
735
750
|
expandable: {
|
|
736
751
|
padding: foundationToken.unit[16],
|
|
737
752
|
borderTop: `1px solid ${foundationToken.colors.gray[150]}`,
|
|
@@ -88,10 +88,30 @@ export type DropdownColumnProps = {
|
|
|
88
88
|
onSelect?: (value: unknown) => void
|
|
89
89
|
}
|
|
90
90
|
|
|
91
|
+
/**
|
|
92
|
+
* Format string for date display in DataTable DATE columns.
|
|
93
|
+
* @example 'DD MMM YYYY' → "24 Jun 2026"
|
|
94
|
+
* @example 'DD/MM/YYYY' → "24/06/2026"
|
|
95
|
+
* @example 'DD MMM YYYY, hh:mm A' → "24 Jun 2026, 10:30 AM"
|
|
96
|
+
*/
|
|
97
|
+
|
|
98
|
+
export type DateFormat =
|
|
99
|
+
| 'DD MMM YYYY'
|
|
100
|
+
| 'DD/MM/YYYY'
|
|
101
|
+
| 'MM/DD/YYYY'
|
|
102
|
+
| 'YYYY-MM-DD'
|
|
103
|
+
| 'DD MMM YYYY, hh:mm A'
|
|
104
|
+
| 'DD MMM YYYY, HH:mm'
|
|
105
|
+
| 'MMM DD, YYYY'
|
|
106
|
+
| 'YYYY/MM/DD HH:mm'
|
|
107
|
+
| 'HH:mm:ss'
|
|
108
|
+
| (string & {})
|
|
109
|
+
|
|
91
110
|
export type DateColumnProps = {
|
|
92
111
|
date: Date | string
|
|
93
|
-
format?:
|
|
112
|
+
format?: DateFormat
|
|
94
113
|
showTime?: boolean
|
|
114
|
+
dateLabel?: string
|
|
95
115
|
}
|
|
96
116
|
|
|
97
117
|
export type SliderColumnProps = {
|
|
@@ -253,8 +273,13 @@ export type ColumnDefinition<T> =
|
|
|
253
273
|
row: T,
|
|
254
274
|
index: number
|
|
255
275
|
) => ReactNode
|
|
256
|
-
dateFormat?:
|
|
276
|
+
dateFormat?: DateFormat
|
|
257
277
|
showTime?: boolean
|
|
278
|
+
/**
|
|
279
|
+
* Optional label appended after the formatted date, e.g. "(IST)".
|
|
280
|
+
* Column-level value can be overridden per-cell via DateColumnProps.dateLabel.
|
|
281
|
+
*/
|
|
282
|
+
dateLabel?: string
|
|
258
283
|
})
|
|
259
284
|
| (BaseColumnDefinition<T> & {
|
|
260
285
|
type: ColumnType.SLIDER
|
|
@@ -457,6 +482,12 @@ export type DataTableProps<T extends Record<string, unknown>> = {
|
|
|
457
482
|
// Mobile configuration
|
|
458
483
|
mobileColumnsToShow?: number
|
|
459
484
|
|
|
485
|
+
/**
|
|
486
|
+
* Optional label appended after formatted dates in DATE columns,
|
|
487
|
+
* e.g. "(IST)". Can be overridden per-column or per-cell.
|
|
488
|
+
*/
|
|
489
|
+
dateLabel?: string
|
|
490
|
+
|
|
460
491
|
// Internal pivot modal configuration
|
|
461
492
|
enablePivotTable?: boolean
|
|
462
493
|
pivotTableConfig?: {
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
ColumnDefinition,
|
|
8
8
|
ColumnType,
|
|
9
9
|
PivotAggregationType,
|
|
10
|
+
DateFormat,
|
|
10
11
|
} from './types'
|
|
11
12
|
import {
|
|
12
13
|
validateColumnData,
|
|
@@ -904,6 +905,91 @@ export const formatDate = (dateString: string): string => {
|
|
|
904
905
|
}).format(date)
|
|
905
906
|
}
|
|
906
907
|
|
|
908
|
+
/**
|
|
909
|
+
* Token map for the lightweight format-string parser. Supports the common
|
|
910
|
+
* dashboard tokens; anything else is passed through literally.
|
|
911
|
+
*
|
|
912
|
+
* YYYY -> 4-digit year YY -> 2-digit year
|
|
913
|
+
* MM -> 2-digit month MMM -> short month name (Jan)
|
|
914
|
+
* DD -> 2-digit day dd -> 2-digit day (alias)
|
|
915
|
+
* HH -> 24h hour (2-digit) hh -> 12h hour (2-digit)
|
|
916
|
+
* mm -> minutes (2-digit) ss -> seconds (2-digit)
|
|
917
|
+
* A -> AM/PM a -> am/pm
|
|
918
|
+
*/
|
|
919
|
+
const FORMAT_TOKEN_PATTERN = /YYYY|YY|MMM|MM|dd|DD|HH|hh|mm|ss|A|a/g
|
|
920
|
+
|
|
921
|
+
const pad2 = (n: number): string => String(n).padStart(2, '0')
|
|
922
|
+
|
|
923
|
+
// Format a Date using a format string like "DD MMM YYYY, hh:mm A"
|
|
924
|
+
export const formatDateString = (date: Date, format: string): string => {
|
|
925
|
+
if (isNaN(date.getTime())) return '-'
|
|
926
|
+
|
|
927
|
+
const parts = new Intl.DateTimeFormat('en-US', {
|
|
928
|
+
year: 'numeric',
|
|
929
|
+
month: '2-digit',
|
|
930
|
+
day: '2-digit',
|
|
931
|
+
hour: '2-digit',
|
|
932
|
+
minute: '2-digit',
|
|
933
|
+
second: '2-digit',
|
|
934
|
+
hour12: false,
|
|
935
|
+
}).formatToParts(date)
|
|
936
|
+
|
|
937
|
+
const lookup: Record<string, string> = {}
|
|
938
|
+
for (const p of parts) {
|
|
939
|
+
if (p.type !== 'literal') lookup[p.type] = p.value
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
const year = lookup.year ?? ''
|
|
943
|
+
const monthNum = lookup.month ?? ''
|
|
944
|
+
const day = lookup.day ?? ''
|
|
945
|
+
const hour24 = lookup.hour ?? ''
|
|
946
|
+
const minute = lookup.minute ?? ''
|
|
947
|
+
const second = lookup.second ?? ''
|
|
948
|
+
|
|
949
|
+
// Short month name via a separate formatter (month: 'short' gives 'Jan' etc.)
|
|
950
|
+
const shortMonth = new Intl.DateTimeFormat('en-US', {
|
|
951
|
+
month: 'short',
|
|
952
|
+
}).format(date)
|
|
953
|
+
|
|
954
|
+
// Normalize the 24-hour value: some browsers/locales return "24" for
|
|
955
|
+
// midnight instead of "00". Convert 24 -> 0 so HH always prints 00.
|
|
956
|
+
const hourNumRaw = parseInt(hour24, 10)
|
|
957
|
+
const hourNum = hourNumRaw === 24 ? 0 : hourNumRaw
|
|
958
|
+
const hour12 = (hourNum % 12 || 12).toString()
|
|
959
|
+
const ampm = hourNum < 12 ? 'AM' : 'PM'
|
|
960
|
+
|
|
961
|
+
FORMAT_TOKEN_PATTERN.lastIndex = 0
|
|
962
|
+
return format.replace(FORMAT_TOKEN_PATTERN, (token) => {
|
|
963
|
+
switch (token) {
|
|
964
|
+
case 'YYYY':
|
|
965
|
+
return year
|
|
966
|
+
case 'YY':
|
|
967
|
+
return year.slice(-2)
|
|
968
|
+
case 'MMM':
|
|
969
|
+
return shortMonth
|
|
970
|
+
case 'MM':
|
|
971
|
+
return monthNum
|
|
972
|
+
case 'DD':
|
|
973
|
+
case 'dd':
|
|
974
|
+
return day
|
|
975
|
+
case 'HH':
|
|
976
|
+
return pad2(hourNum)
|
|
977
|
+
case 'hh':
|
|
978
|
+
return pad2(parseInt(hour12, 10))
|
|
979
|
+
case 'mm':
|
|
980
|
+
return minute
|
|
981
|
+
case 'ss':
|
|
982
|
+
return second
|
|
983
|
+
case 'A':
|
|
984
|
+
return ampm
|
|
985
|
+
case 'a':
|
|
986
|
+
return ampm.toLowerCase()
|
|
987
|
+
default:
|
|
988
|
+
return token
|
|
989
|
+
}
|
|
990
|
+
})
|
|
991
|
+
}
|
|
992
|
+
|
|
907
993
|
export const updateColumnFilter = (
|
|
908
994
|
currentFilters: ColumnFilter[],
|
|
909
995
|
field: keyof Record<string, unknown>,
|
|
@@ -994,7 +1080,7 @@ const getExportValue = <T extends Record<string, unknown>>(
|
|
|
994
1080
|
) {
|
|
995
1081
|
const dateData = value as {
|
|
996
1082
|
date: Date | string
|
|
997
|
-
format?:
|
|
1083
|
+
format?: DateFormat
|
|
998
1084
|
showTime?: boolean
|
|
999
1085
|
}
|
|
1000
1086
|
const date = new Date(dateData.date)
|
|
@@ -1267,7 +1353,7 @@ export const createMultiSelectData = (
|
|
|
1267
1353
|
|
|
1268
1354
|
export const createDateData = (
|
|
1269
1355
|
date: Date | string,
|
|
1270
|
-
format?:
|
|
1356
|
+
format?: DateFormat
|
|
1271
1357
|
): DateData => ({
|
|
1272
1358
|
date,
|
|
1273
1359
|
format,
|
|
@@ -1276,7 +1362,7 @@ export const createDateData = (
|
|
|
1276
1362
|
export const createDateRangeData = (
|
|
1277
1363
|
startDate: Date | string,
|
|
1278
1364
|
endDate: Date | string,
|
|
1279
|
-
format?:
|
|
1365
|
+
format?: DateFormat
|
|
1280
1366
|
): DateRangeData => ({
|
|
1281
1367
|
startDate,
|
|
1282
1368
|
endDate,
|
|
@@ -1317,9 +1403,9 @@ const getExpectedTypeDescription = (columnType: ColumnType): string => {
|
|
|
1317
1403
|
case ColumnType.MULTISELECT:
|
|
1318
1404
|
return 'MultiSelectData { values: string[], labels?: string[] } or string[]'
|
|
1319
1405
|
case ColumnType.DATE:
|
|
1320
|
-
return 'DateData { date: Date | string, format?:
|
|
1406
|
+
return 'DateData { date: Date | string, format?: DateFormat } or Date or string'
|
|
1321
1407
|
case ColumnType.DATE_RANGE:
|
|
1322
|
-
return 'DateRangeData { startDate: Date | string, endDate: Date | string, format?:
|
|
1408
|
+
return 'DateRangeData { startDate: Date | string, endDate: Date | string, format?: DateFormat }'
|
|
1323
1409
|
case ColumnType.TEXT:
|
|
1324
1410
|
return 'string or number'
|
|
1325
1411
|
case ColumnType.NUMBER:
|
|
@@ -129,6 +129,7 @@ const CalendarSkeleton = ({
|
|
|
129
129
|
<Block
|
|
130
130
|
style={{
|
|
131
131
|
maxHeight: CONTAINER_HEIGHT,
|
|
132
|
+
height: '100%',
|
|
132
133
|
padding: FOUNDATION_THEME.unit[16],
|
|
133
134
|
}}
|
|
134
135
|
>
|
|
@@ -918,6 +919,7 @@ const CalendarGrid = forwardRef<HTMLDivElement, CalendarGridProps>(
|
|
|
918
919
|
<MotionBlock
|
|
919
920
|
style={{
|
|
920
921
|
maxHeight: CONTAINER_HEIGHT,
|
|
922
|
+
height: '100%',
|
|
921
923
|
overflowY: 'auto',
|
|
922
924
|
overflow: 'auto',
|
|
923
925
|
position: 'relative',
|
|
@@ -985,6 +987,7 @@ const CalendarGrid = forwardRef<HTMLDivElement, CalendarGridProps>(
|
|
|
985
987
|
}}
|
|
986
988
|
style={{
|
|
987
989
|
maxHeight: CONTAINER_HEIGHT,
|
|
990
|
+
height: '100%',
|
|
988
991
|
overflowY: 'auto',
|
|
989
992
|
overflow: 'auto',
|
|
990
993
|
position: 'relative',
|
|
@@ -292,7 +292,7 @@ const CalendarSection: React.FC<
|
|
|
292
292
|
isSingleDatePicker,
|
|
293
293
|
maxYearOffset,
|
|
294
294
|
}) => (
|
|
295
|
-
<Block>
|
|
295
|
+
<Block flexGrow={1} minHeight={0} overflow="auto">
|
|
296
296
|
<CalendarGrid
|
|
297
297
|
selectedRange={selectedRange}
|
|
298
298
|
onDateSelect={onDateSelect}
|
|
@@ -1228,6 +1228,10 @@ const DateRangePicker = forwardRef<HTMLDivElement, DateRangePickerProps>(
|
|
|
1228
1228
|
style={{
|
|
1229
1229
|
...calendarToken.calendar,
|
|
1230
1230
|
}}
|
|
1231
|
+
maxHeight="var(--radix-popper-available-height)"
|
|
1232
|
+
display="flex"
|
|
1233
|
+
flexDirection="column"
|
|
1234
|
+
overflow="hidden"
|
|
1231
1235
|
>
|
|
1232
1236
|
{showDateInput && (
|
|
1233
1237
|
<DateInputsSection
|
|
@@ -23,6 +23,8 @@ const MenuV2 = React.forwardRef<HTMLDivElement, MenuV2Props>(
|
|
|
23
23
|
dimensions = {} as MenuV2Dimensions,
|
|
24
24
|
enableSearch = false,
|
|
25
25
|
searchPlaceholder = 'Search',
|
|
26
|
+
searchSortFn,
|
|
27
|
+
onEnter,
|
|
26
28
|
enableVirtualScrolling = false,
|
|
27
29
|
virtualScrolling,
|
|
28
30
|
open: controlledOpen,
|
|
@@ -61,10 +63,14 @@ const MenuV2 = React.forwardRef<HTMLDivElement, MenuV2Props>(
|
|
|
61
63
|
)
|
|
62
64
|
|
|
63
65
|
const filteredItems = useMemo(
|
|
64
|
-
() => filterMenuV2Groups(items, searchText),
|
|
65
|
-
[items, searchText]
|
|
66
|
+
() => filterMenuV2Groups(items, searchText, searchSortFn),
|
|
67
|
+
[items, searchText, searchSortFn]
|
|
66
68
|
)
|
|
67
69
|
|
|
70
|
+
const handleSearchEnter = useCallback(() => {
|
|
71
|
+
onEnter?.(searchText, filteredItems)
|
|
72
|
+
}, [onEnter, searchText, filteredItems])
|
|
73
|
+
|
|
68
74
|
const handleInteractOutside = useCallback((e: unknown) => {
|
|
69
75
|
const event = e as {
|
|
70
76
|
target: HTMLElement | null
|
|
@@ -104,6 +110,7 @@ const MenuV2 = React.forwardRef<HTMLDivElement, MenuV2Props>(
|
|
|
104
110
|
searchPlaceholder={searchPlaceholder}
|
|
105
111
|
searchText={searchText}
|
|
106
112
|
onSearchTextChange={setSearchText}
|
|
113
|
+
onEnter={handleSearchEnter}
|
|
107
114
|
maxHeight={
|
|
108
115
|
dimensions.maxHeight as CSSObject['maxHeight']
|
|
109
116
|
}
|
|
@@ -104,6 +104,7 @@ export type MenuV2ContentProps = {
|
|
|
104
104
|
searchPlaceholder: string
|
|
105
105
|
searchText: string
|
|
106
106
|
onSearchTextChange: (value: string) => void
|
|
107
|
+
onEnter?: () => void
|
|
107
108
|
maxHeight?: CSSObject['maxHeight']
|
|
108
109
|
minHeight?: CSSObject['minHeight']
|
|
109
110
|
minWidth?: CSSObject['minWidth']
|
|
@@ -130,6 +131,7 @@ const MenuV2Content = React.forwardRef<HTMLDivElement, MenuV2ContentProps>(
|
|
|
130
131
|
searchPlaceholder,
|
|
131
132
|
searchText,
|
|
132
133
|
onSearchTextChange,
|
|
134
|
+
onEnter,
|
|
133
135
|
maxHeight,
|
|
134
136
|
minHeight,
|
|
135
137
|
minWidth: minWidthProp,
|
|
@@ -250,6 +252,10 @@ const MenuV2Content = React.forwardRef<HTMLDivElement, MenuV2ContentProps>(
|
|
|
250
252
|
onChange={(e) => onSearchTextChange(e.target.value)}
|
|
251
253
|
onKeyDown={(e) => {
|
|
252
254
|
e.stopPropagation()
|
|
255
|
+
if (e.key === 'Enter') {
|
|
256
|
+
e.preventDefault()
|
|
257
|
+
onEnter?.()
|
|
258
|
+
}
|
|
253
259
|
}}
|
|
254
260
|
aria-label={
|
|
255
261
|
searchPlaceholder
|
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
getMenuItemDescriptionColor,
|
|
15
15
|
filterMenuV2Item,
|
|
16
16
|
getItemSlots,
|
|
17
|
+
defaultSearchSortFn,
|
|
17
18
|
} from './menuV2.utils'
|
|
18
19
|
import { menuV2SubmenuContentAnimations } from './menuV2.animations'
|
|
19
20
|
import { useResponsiveTokens } from '../../hooks/useResponsiveTokens'
|
|
@@ -64,12 +65,15 @@ const MenuV2SubMenu = ({ item, index, maxHeight }: MenuV2SubMenuProps) => {
|
|
|
64
65
|
|
|
65
66
|
const lower = searchText.toLowerCase()
|
|
66
67
|
|
|
67
|
-
|
|
68
|
+
const matched = item.subMenu.reduce<MenuV2ItemType[]>((acc, sub) => {
|
|
68
69
|
const result = filterMenuV2Item(sub, lower)
|
|
69
70
|
if (result) acc.push(result)
|
|
70
71
|
return acc
|
|
71
72
|
}, [])
|
|
72
|
-
|
|
73
|
+
|
|
74
|
+
const sortFn = item.subMenuSearchSortFn ?? defaultSearchSortFn
|
|
75
|
+
return sortFn(matched, searchText)
|
|
76
|
+
}, [item.subMenu, searchText, item.subMenuSearchSortFn])
|
|
73
77
|
|
|
74
78
|
const {
|
|
75
79
|
bgDefault,
|
|
@@ -262,6 +266,16 @@ const MenuV2SubMenu = ({ item, index, maxHeight }: MenuV2SubMenuProps) => {
|
|
|
262
266
|
}
|
|
263
267
|
value={searchText}
|
|
264
268
|
onChange={(e) => setSearchText(e.target.value)}
|
|
269
|
+
onKeyDown={(e) => {
|
|
270
|
+
e.stopPropagation()
|
|
271
|
+
if (e.key === 'Enter') {
|
|
272
|
+
e.preventDefault()
|
|
273
|
+
item.onSubMenuSearchEnter?.(
|
|
274
|
+
searchText,
|
|
275
|
+
filteredSubMenuItems
|
|
276
|
+
)
|
|
277
|
+
}
|
|
278
|
+
}}
|
|
265
279
|
aria-label="Search submenu"
|
|
266
280
|
/>
|
|
267
281
|
</Block>
|
|
@@ -38,6 +38,10 @@ export type MenuV2ItemLabel = {
|
|
|
38
38
|
text: string
|
|
39
39
|
leftSlot?: React.ReactElement
|
|
40
40
|
}
|
|
41
|
+
export type MenuV2SearchSortFn = (
|
|
42
|
+
items: MenuV2ItemType[],
|
|
43
|
+
searchText: string
|
|
44
|
+
) => MenuV2ItemType[]
|
|
41
45
|
export type MenuV2ItemType = {
|
|
42
46
|
id?: string
|
|
43
47
|
label: MenuV2ItemLabel
|
|
@@ -49,6 +53,11 @@ export type MenuV2ItemType = {
|
|
|
49
53
|
subMenu?: MenuV2ItemType[]
|
|
50
54
|
enableSubMenuSearch?: boolean
|
|
51
55
|
subMenuSearchPlaceholder?: string
|
|
56
|
+
subMenuSearchSortFn?: MenuV2SearchSortFn
|
|
57
|
+
onSubMenuSearchEnter?: (
|
|
58
|
+
searchText: string,
|
|
59
|
+
filteredResults: MenuV2ItemType[]
|
|
60
|
+
) => void
|
|
52
61
|
tooltip?: string | ReactNode
|
|
53
62
|
tooltipProps?: MenuV2ItemTooltipProps
|
|
54
63
|
}
|
|
@@ -82,6 +91,8 @@ export type MenuV2Props = {
|
|
|
82
91
|
dimensions?: MenuV2Dimensions
|
|
83
92
|
enableSearch?: boolean
|
|
84
93
|
searchPlaceholder?: string
|
|
94
|
+
searchSortFn?: MenuV2SearchSortFn
|
|
95
|
+
onEnter?: (searchText: string, filteredGroups: MenuV2GroupType[]) => void
|
|
85
96
|
enableVirtualScrolling?: boolean
|
|
86
97
|
virtualScrolling?: MenuV2VirtualScrollingConfig
|
|
87
98
|
open?: boolean
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import type { ReactNode } from 'react'
|
|
2
|
-
import type {
|
|
2
|
+
import type {
|
|
3
|
+
MenuV2GroupType,
|
|
4
|
+
MenuV2ItemType,
|
|
5
|
+
MenuV2SearchSortFn,
|
|
6
|
+
} from './menuV2.types'
|
|
3
7
|
import type { MenuV2TokensType } from './menuV2.tokens'
|
|
4
8
|
|
|
5
9
|
import { MenuV2ItemActionType, MenuV2ItemVariant } from './menuV2.types'
|
|
@@ -11,6 +15,43 @@ export const getItemSlots = (item: MenuV2ItemType): [ReactNode?] => {
|
|
|
11
15
|
return [item.label.leftSlot]
|
|
12
16
|
}
|
|
13
17
|
|
|
18
|
+
export enum MenuV2MatchRank {
|
|
19
|
+
EXACT = 0,
|
|
20
|
+
PREFIX = 1,
|
|
21
|
+
SUBSTRING = 2,
|
|
22
|
+
NONE = 3,
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const getFieldMatchRank = (
|
|
26
|
+
fieldValue: string | undefined,
|
|
27
|
+
lower: string
|
|
28
|
+
): MenuV2MatchRank => {
|
|
29
|
+
if (!fieldValue) return MenuV2MatchRank.NONE
|
|
30
|
+
const value = fieldValue.toLowerCase()
|
|
31
|
+
if (value === lower) return MenuV2MatchRank.EXACT
|
|
32
|
+
if (value.startsWith(lower)) return MenuV2MatchRank.PREFIX
|
|
33
|
+
if (value.includes(lower)) return MenuV2MatchRank.SUBSTRING
|
|
34
|
+
return MenuV2MatchRank.NONE
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export const getItemMatchRank = (
|
|
38
|
+
item: MenuV2ItemType,
|
|
39
|
+
lower: string
|
|
40
|
+
): MenuV2MatchRank => {
|
|
41
|
+
return Math.min(
|
|
42
|
+
getFieldMatchRank(item.label.text, lower),
|
|
43
|
+
getFieldMatchRank(item.subLabel, lower)
|
|
44
|
+
) as MenuV2MatchRank
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export const defaultSearchSortFn: MenuV2SearchSortFn = (items, searchText) => {
|
|
48
|
+
if (!searchText.trim()) return items
|
|
49
|
+
const lower = searchText.toLowerCase()
|
|
50
|
+
return [...items].sort(
|
|
51
|
+
(a, b) => getItemMatchRank(a, lower) - getItemMatchRank(b, lower)
|
|
52
|
+
)
|
|
53
|
+
}
|
|
54
|
+
|
|
14
55
|
export const filterMenuV2Item = (
|
|
15
56
|
item: MenuV2ItemType,
|
|
16
57
|
lower: string
|
|
@@ -140,7 +181,8 @@ export const flattenMenuV2Groups = (
|
|
|
140
181
|
|
|
141
182
|
export const filterMenuV2Groups = (
|
|
142
183
|
groups: MenuV2GroupType[],
|
|
143
|
-
searchText: string
|
|
184
|
+
searchText: string,
|
|
185
|
+
searchSortFn: MenuV2SearchSortFn = defaultSearchSortFn
|
|
144
186
|
): MenuV2GroupType[] => {
|
|
145
187
|
if (!searchText) return groups
|
|
146
188
|
const lower = searchText.toLowerCase()
|
|
@@ -150,7 +192,7 @@ export const filterMenuV2Groups = (
|
|
|
150
192
|
.map((item) => filterMenuV2Item(item, lower))
|
|
151
193
|
.filter(Boolean) as MenuV2ItemType[]
|
|
152
194
|
if (filteredItems.length === 0) return null
|
|
153
|
-
return { ...group, items: filteredItems }
|
|
195
|
+
return { ...group, items: searchSortFn(filteredItems, searchText) }
|
|
154
196
|
})
|
|
155
197
|
.filter(Boolean) as MenuV2GroupType[]
|
|
156
198
|
}
|
|
@@ -101,6 +101,7 @@ const MultiSelect = ({
|
|
|
101
101
|
onClearAllClick,
|
|
102
102
|
onOpenChange,
|
|
103
103
|
multiSelectGroupPosition,
|
|
104
|
+
menuFooter,
|
|
104
105
|
...rest
|
|
105
106
|
}: MultiSelectProps) => {
|
|
106
107
|
const { onFocus, onBlur, ...buttonRest } = rest
|
|
@@ -356,6 +357,7 @@ const MultiSelect = ({
|
|
|
356
357
|
hasMore={hasMore}
|
|
357
358
|
loadingComponent={loadingComponent}
|
|
358
359
|
menuId={menuId}
|
|
360
|
+
menuFooter={menuFooter}
|
|
359
361
|
trigger={
|
|
360
362
|
customTrigger || (
|
|
361
363
|
<PrimitiveButton
|
|
@@ -86,6 +86,10 @@ const FixedActionButtons = styled(Block)(() => ({
|
|
|
86
86
|
flexShrink: 0,
|
|
87
87
|
}))
|
|
88
88
|
|
|
89
|
+
const MenuFooter = styled(Block)(() => ({
|
|
90
|
+
flexShrink: 0,
|
|
91
|
+
}))
|
|
92
|
+
|
|
89
93
|
type FlattenedMultiSelectItem = VirtualListItem & {
|
|
90
94
|
type: 'item' | 'label' | 'separator'
|
|
91
95
|
item?: MultiSelectMenuItemType
|
|
@@ -171,6 +175,7 @@ const MultiSelectMenu = ({
|
|
|
171
175
|
allowCustomValue = false,
|
|
172
176
|
customValueLabel = 'Specify',
|
|
173
177
|
menuId,
|
|
178
|
+
menuFooter,
|
|
174
179
|
}: MultiSelectMenuProps) => {
|
|
175
180
|
const multiSelectTokens =
|
|
176
181
|
useResponsiveTokens<MultiSelectTokensType>('MULTI_SELECT')
|
|
@@ -892,6 +897,9 @@ const MultiSelectMenu = ({
|
|
|
892
897
|
)}
|
|
893
898
|
</FixedActionButtons>
|
|
894
899
|
)}
|
|
900
|
+
{menuFooter && (
|
|
901
|
+
<MenuFooter>{menuFooter}</MenuFooter>
|
|
902
|
+
)}
|
|
895
903
|
</>
|
|
896
904
|
)}
|
|
897
905
|
</Content>
|
|
@@ -157,6 +157,7 @@ export type MultiSelectProps = {
|
|
|
157
157
|
showClearButton?: boolean
|
|
158
158
|
onClearAllClick?: () => void
|
|
159
159
|
multiSelectGroupPosition?: 'center' | 'left' | 'right'
|
|
160
|
+
menuFooter?: React.ReactNode
|
|
160
161
|
} & Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, 'slot' | 'onChange'>
|
|
161
162
|
|
|
162
163
|
// Multi Select Menu Dropdpown
|
|
@@ -225,4 +226,5 @@ export type MultiSelectMenuProps = {
|
|
|
225
226
|
customValueLabel?: string
|
|
226
227
|
menuId?: string
|
|
227
228
|
collisionBoundary?: Element | null | Array<Element | null>
|
|
229
|
+
menuFooter?: React.ReactNode
|
|
228
230
|
}
|
|
@@ -91,6 +91,7 @@ const MultiSelectV2 = ({
|
|
|
91
91
|
onClearAllClick,
|
|
92
92
|
onOpenChange,
|
|
93
93
|
multiSelectGroupPosition,
|
|
94
|
+
menuFooter,
|
|
94
95
|
...rest
|
|
95
96
|
}: MultiSelectV2Props) => {
|
|
96
97
|
const { disabled, name, ...buttonRest } = rest as {
|
|
@@ -366,6 +367,7 @@ const MultiSelectV2 = ({
|
|
|
366
367
|
}
|
|
367
368
|
allowCustomValue={allowCustomValue}
|
|
368
369
|
customValueLabel={customValueLabel}
|
|
370
|
+
menuFooter={menuFooter}
|
|
369
371
|
/>
|
|
370
372
|
|
|
371
373
|
{shouldShowClearButton && (
|
|
@@ -69,6 +69,10 @@ const ScrollableContent = styled(Block)(() => ({
|
|
|
69
69
|
},
|
|
70
70
|
}))
|
|
71
71
|
|
|
72
|
+
const MenuFooter = styled(Block)`
|
|
73
|
+
flex-shrink: 0;
|
|
74
|
+
`
|
|
75
|
+
|
|
72
76
|
const MultiSelectV2Menu = ({
|
|
73
77
|
items,
|
|
74
78
|
selected,
|
|
@@ -104,6 +108,7 @@ const MultiSelectV2Menu = ({
|
|
|
104
108
|
allowCustomValue = false,
|
|
105
109
|
customValueLabel = 'Specify',
|
|
106
110
|
menuId,
|
|
111
|
+
menuFooter,
|
|
107
112
|
}: MultiSelectV2MenuProps) => {
|
|
108
113
|
const multiSelectTokens =
|
|
109
114
|
useResponsiveTokens<MultiSelectV2TokensType>('MULTI_SELECT_V2')
|
|
@@ -467,6 +472,9 @@ const MultiSelectV2Menu = ({
|
|
|
467
472
|
onClose={() => handleOpenChange(false)}
|
|
468
473
|
/>
|
|
469
474
|
)}
|
|
475
|
+
{menuFooter && (
|
|
476
|
+
<MenuFooter>{menuFooter}</MenuFooter>
|
|
477
|
+
)}
|
|
470
478
|
</>
|
|
471
479
|
)}
|
|
472
480
|
</Content>
|
|
@@ -97,6 +97,7 @@ export type MultiSelectV2MenuProps = {
|
|
|
97
97
|
allowCustomValue?: boolean
|
|
98
98
|
customValueLabel?: string
|
|
99
99
|
menuId?: string
|
|
100
|
+
menuFooter?: ReactNode
|
|
100
101
|
}
|
|
101
102
|
|
|
102
103
|
export type MultiSelectV2MenuRootProps = SelectV2MenuRootPropsBase & {
|
|
@@ -170,4 +171,5 @@ export type MultiSelectV2Props = Omit<
|
|
|
170
171
|
showClearButton?: boolean
|
|
171
172
|
onClearAllClick?: () => void
|
|
172
173
|
multiSelectGroupPosition?: 'center' | 'left' | 'right'
|
|
174
|
+
menuFooter?: ReactNode
|
|
173
175
|
}
|
|
@@ -104,6 +104,7 @@ const SingleSelect = ({
|
|
|
104
104
|
customValueLabel = 'Specify',
|
|
105
105
|
singleSelectGroupPosition,
|
|
106
106
|
allowDeselect = false,
|
|
107
|
+
menuFooter,
|
|
107
108
|
...rest
|
|
108
109
|
}: SingleSelectProps) => {
|
|
109
110
|
const { onFocus, onBlur, ...buttonRest } = rest
|
|
@@ -298,6 +299,7 @@ const SingleSelect = ({
|
|
|
298
299
|
allowCustomValue={allowCustomValue}
|
|
299
300
|
customValueLabel={customValueLabel}
|
|
300
301
|
menuId={menuId}
|
|
302
|
+
menuFooter={menuFooter}
|
|
301
303
|
trigger={
|
|
302
304
|
customTrigger || (
|
|
303
305
|
<PrimitiveButton
|