@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.
Files changed (92) hide show
  1. package/dist/components/CodeEditor/types.d.ts +22 -2
  2. package/dist/components/CodeEditorV2/CodeEditorV2.d.ts +1 -1
  3. package/dist/components/CodeEditorV2/codeEditorV2.types.d.ts +23 -2
  4. package/dist/components/DataTable/TableBody/types.d.ts +1 -0
  5. package/dist/components/DataTable/TableCell/types.d.ts +1 -0
  6. package/dist/components/DataTable/columnTypes.d.ts +5 -3
  7. package/dist/components/DataTable/dataTable.tokens.d.ts +4 -0
  8. package/dist/components/DataTable/types.d.ts +20 -2
  9. package/dist/components/DataTable/utils.d.ts +4 -3
  10. package/dist/components/MenuV2/MenuV2Content.d.ts +1 -0
  11. package/dist/components/MenuV2/menuV2.types.d.ts +5 -0
  12. package/dist/components/MenuV2/menuV2.utils.d.ts +10 -2
  13. package/dist/components/MultiSelect/MultiSelect.d.ts +1 -1
  14. package/dist/components/MultiSelect/MultiSelectMenu.d.ts +1 -1
  15. package/dist/components/MultiSelect/types.d.ts +2 -0
  16. package/dist/components/MultiSelectV2/MultiSelectV2.d.ts +1 -1
  17. package/dist/components/MultiSelectV2/MultiSelectV2Menu.d.ts +1 -1
  18. package/dist/components/MultiSelectV2/multiSelectV2.types.d.ts +2 -0
  19. package/dist/components/SingleSelect/SingleSelect.d.ts +1 -1
  20. package/dist/components/SingleSelect/SingleSelectMenu.d.ts +2 -1
  21. package/dist/components/SingleSelect/types.d.ts +1 -0
  22. package/dist/components/SingleSelectV2/SingleSelectV2.d.ts +1 -1
  23. package/dist/components/SingleSelectV2/SingleSelectV2Menu.d.ts +1 -1
  24. package/dist/components/SingleSelectV2/singleSelectV2.tokens.types.d.ts +8 -0
  25. package/dist/components/SingleSelectV2/singleSelectV2.types.d.ts +2 -0
  26. package/dist/components/Skeleton/hooks/useSkeletonBase.d.ts +7 -11
  27. package/dist/components/Snackbar/Snackbar.d.ts +1 -1
  28. package/dist/components/Snackbar/snackbar.tokens.d.ts +1 -0
  29. package/dist/components/Snackbar/types.d.ts +2 -0
  30. package/dist/components/StatCardV2/StatCardV2.d.ts +1 -0
  31. package/dist/components/StatCardV2/StatCardV2NoData.d.ts +2 -2
  32. package/dist/components/StatCardV2/statcardV2.types.d.ts +1 -0
  33. package/dist/components/TabsV2/tabsV2.tokens.types.d.ts +3 -0
  34. package/dist/global-utils/GlobalUtils.d.ts +2 -1
  35. package/dist/main.js +27446 -27271
  36. package/dist/{node-CJ_Tft0g.js → node-CovJEwot.js} +65 -25
  37. package/dist/node.js +1 -1
  38. package/dist/{tokens.js → token-engine.js} +1 -1
  39. package/lib/components/Card/Card.tsx +1 -0
  40. package/lib/components/Charts/ChartHeader.tsx +4 -1
  41. package/lib/components/CodeBlock/CodeBlock.tsx +8 -4
  42. package/lib/components/CodeEditor/MonacoEditorWrapper.tsx +6 -2
  43. package/lib/components/CodeEditor/types.ts +52 -2
  44. package/lib/components/CodeEditorV2/MonacoEditor/MonacoEditorWrapper.tsx +6 -2
  45. package/lib/components/CodeEditorV2/codeEditorV2.types.ts +44 -5
  46. package/lib/components/DataTable/DataTable.tsx +2 -0
  47. package/lib/components/DataTable/TableBody/index.tsx +2 -0
  48. package/lib/components/DataTable/TableBody/types.ts +1 -0
  49. package/lib/components/DataTable/TableCell/index.tsx +45 -20
  50. package/lib/components/DataTable/TableCell/types.ts +1 -0
  51. package/lib/components/DataTable/columnTypes.ts +5 -3
  52. package/lib/components/DataTable/dataTable.tokens.ts +15 -0
  53. package/lib/components/DataTable/types.ts +33 -2
  54. package/lib/components/DataTable/utils.ts +91 -5
  55. package/lib/components/DateRangePicker/CalendarGrid.tsx +3 -0
  56. package/lib/components/DateRangePicker/DateRangePicker.tsx +5 -1
  57. package/lib/components/MenuV2/MenuV2.tsx +9 -2
  58. package/lib/components/MenuV2/MenuV2Content.tsx +6 -0
  59. package/lib/components/MenuV2/MenuV2SubMenu.tsx +16 -2
  60. package/lib/components/MenuV2/menuV2.types.ts +11 -0
  61. package/lib/components/MenuV2/menuV2.utils.ts +45 -3
  62. package/lib/components/MultiSelect/MultiSelect.tsx +2 -0
  63. package/lib/components/MultiSelect/MultiSelectMenu.tsx +8 -0
  64. package/lib/components/MultiSelect/types.ts +2 -0
  65. package/lib/components/MultiSelectV2/MultiSelectV2.tsx +2 -0
  66. package/lib/components/MultiSelectV2/MultiSelectV2Menu.tsx +8 -0
  67. package/lib/components/MultiSelectV2/multiSelectV2.types.ts +2 -0
  68. package/lib/components/SingleSelect/SingleSelect.tsx +2 -0
  69. package/lib/components/SingleSelect/SingleSelectMenu.tsx +171 -138
  70. package/lib/components/SingleSelect/types.ts +1 -0
  71. package/lib/components/SingleSelectV2/SingleSelectV2.tsx +2 -0
  72. package/lib/components/SingleSelectV2/SingleSelectV2Menu.tsx +84 -58
  73. package/lib/components/SingleSelectV2/SingleSelectV2Search.tsx +1 -0
  74. package/lib/components/SingleSelectV2/singleSelectV2.dark.tokens.ts +8 -0
  75. package/lib/components/SingleSelectV2/singleSelectV2.light.tokens.ts +8 -0
  76. package/lib/components/SingleSelectV2/singleSelectV2.tokens.types.ts +8 -0
  77. package/lib/components/SingleSelectV2/singleSelectV2.types.ts +2 -0
  78. package/lib/components/Skeleton/hooks/useSkeletonBase.ts +11 -1
  79. package/lib/components/Snackbar/Snackbar.tsx +18 -8
  80. package/lib/components/Snackbar/snackbar.tokens.ts +6 -0
  81. package/lib/components/Snackbar/types.ts +2 -0
  82. package/lib/components/StatCard/StatCard.tsx +39 -2
  83. package/lib/components/StatCardV2/StatCardV2.tsx +8 -6
  84. package/lib/components/StatCardV2/StatCardV2NoData.tsx +8 -6
  85. package/lib/components/StatCardV2/statcardV2.types.ts +1 -0
  86. package/lib/components/TabsV2/TabsV2List.tsx +5 -1
  87. package/lib/components/TabsV2/tabsV2.dark.tokens.ts +12 -0
  88. package/lib/components/TabsV2/tabsV2.light.tokens.ts +12 -0
  89. package/lib/components/TabsV2/tabsV2.tokens.types.ts +3 -0
  90. package/lib/global-utils/GlobalUtils.ts +10 -1
  91. package/package.json +5 -4
  92. 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?: string
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?: string
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?: string
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?: string
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?: string
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?: string
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?: string
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?: string } or Date or string'
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?: string }'
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
- return item.subMenu.reduce<MenuV2ItemType[]>((acc, sub) => {
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
- }, [item.subMenu, searchText])
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 { MenuV2GroupType, MenuV2ItemType } from './menuV2.types'
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