react-core-ts 2.1.31 → 2.1.32

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.
@@ -24,6 +24,26 @@ type valueProps = {
24
24
  param4?: string | number | null;
25
25
  from?: number;
26
26
  };
27
+
28
+ /** Add new theme ids here when introducing layouts */
29
+ export type DropdownThemeId = 'default' | 'splitDetail';
30
+
31
+ /** Options for `dropdownTheme: 'splitDetail'` (title + subtitle + optional badge pill) */
32
+ export type SplitDetailThemeConfig = {
33
+ subtitleKey?: string;
34
+ badgeKey?: string;
35
+ /** Shown before badge value, e.g. "Acc. No.: " */
36
+ badgePrefix?: string;
37
+ };
38
+
39
+ /**
40
+ * Per-theme configuration. Extend this type with new keys when adding themes
41
+ * (e.g. `compact?: CompactThemeConfig`).
42
+ */
43
+ export type DropdownThemeConfig = {
44
+ splitDetail?: SplitDetailThemeConfig;
45
+ };
46
+
27
47
  interface AutoSuggestionInputProps {
28
48
  id?: string;
29
49
  label?: string;
@@ -68,6 +88,13 @@ interface AutoSuggestionInputProps {
68
88
  key: string; // Field name in the suggestion object
69
89
  label: string; // Label for the column
70
90
  }>; // Column header to display
91
+ dropdownTheme?: DropdownThemeId;
92
+ /** Theme-specific field mapping and options; see `DropdownThemeConfig` */
93
+ dropdownThemeConfig?: DropdownThemeConfig;
94
+ /**
95
+ * When true, typing updates the input only; fetching/opening the list runs from the search control (and Enter), not while typing.
96
+ */
97
+ searchOnButtonOnly?: boolean;
71
98
  }
72
99
 
73
100
  const ReactAutoCompleteTableView: React.FC<AutoSuggestionInputProps> = ({
@@ -106,7 +133,16 @@ const ReactAutoCompleteTableView: React.FC<AutoSuggestionInputProps> = ({
106
133
  tableView = false,
107
134
  additionalColumns = [],
108
135
  columnHeader = [],
136
+ dropdownTheme = 'default',
137
+ dropdownThemeConfig,
138
+ searchOnButtonOnly = false,
109
139
  }) => {
140
+ const splitDetailOptions = dropdownThemeConfig?.splitDetail;
141
+ const showSearchOnButtonChrome =
142
+ searchOnButtonOnly &&
143
+ (type === 'auto_complete' || type === 'custom_search_select') &&
144
+ !disabled &&
145
+ !readOnly;
110
146
  const [inputValue, setInputValue] = useState<any>(value?.name ?? "");
111
147
  const [isHovered, setIsHovered] = useState<boolean>(false);
112
148
  const [isLoading, setIsLoading] = useState<boolean>(false);
@@ -199,13 +235,52 @@ const ReactAutoCompleteTableView: React.FC<AutoSuggestionInputProps> = ({
199
235
  timerRef.current = 0;
200
236
 
201
237
  setInputValue(value);
202
- handleValChange(value);
238
+ if (!searchOnButtonOnly) {
239
+ handleValChange(value);
240
+ }
203
241
  if (!value) {
204
242
  setInputValue('');
205
243
  onChange({ id: undefined, name: '', from: 2 });
206
244
  }
207
245
  };
208
246
 
247
+ const runSearchFromInput = (value: string) => {
248
+ setDropOpen(true);
249
+ onChange({ id: undefined, name: '', from: 1 });
250
+ if (value.trim() === '' && type === 'auto_complete') {
251
+ setSuggestions([]);
252
+ if (autoFilter) {
253
+ handleDropData('*');
254
+ } else {
255
+ setDropOpen(false);
256
+ }
257
+ } else {
258
+ handleDropData(value);
259
+ }
260
+ setTimeout(() => {
261
+ timerRef.current = 1;
262
+ }, 200);
263
+ };
264
+
265
+ const handleSearchButtonClick = (e: React.MouseEvent) => {
266
+ e.preventDefault();
267
+ e.stopPropagation();
268
+ if (disabled || readOnly) return;
269
+ timerRef.current = 0;
270
+ const q = inputRef.current?.value ?? inputValue ?? '';
271
+ runSearchFromInput(q);
272
+ };
273
+
274
+ const handleSearchInputKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
275
+ if (!searchOnButtonOnly || disabled || readOnly) return;
276
+ if (e.key === 'Enter') {
277
+ e.preventDefault();
278
+ timerRef.current = 0;
279
+ const q = inputRef.current?.value ?? inputValue ?? '';
280
+ runSearchFromInput(q);
281
+ }
282
+ };
283
+
209
284
  const handleValChange = useCallback(
210
285
  debounce(
211
286
  (value: string) => {
@@ -289,6 +364,9 @@ const ReactAutoCompleteTableView: React.FC<AutoSuggestionInputProps> = ({
289
364
  const onLabelClick = () => {
290
365
  if (!isDisabled) {
291
366
  inputRef?.current?.focus();
367
+ if (searchOnButtonOnly) {
368
+ return;
369
+ }
292
370
  if (autoFilter && inputValue === '') {
293
371
  handleValChange('*');
294
372
  } else if (
@@ -350,6 +428,9 @@ const ReactAutoCompleteTableView: React.FC<AutoSuggestionInputProps> = ({
350
428
  }, [autoFocus]);
351
429
  const onInputFocus = () => {
352
430
  if (!isDisabled) {
431
+ if (searchOnButtonOnly) {
432
+ return;
433
+ }
353
434
  if (autoFilter && inputValue === '') {
354
435
  handleValChange('*');
355
436
  } else if (
@@ -385,6 +466,12 @@ const ReactAutoCompleteTableView: React.FC<AutoSuggestionInputProps> = ({
385
466
  };
386
467
 
387
468
  const handleOpenDropdown = (e: any) => {
469
+ if (searchOnButtonOnly) {
470
+ if (suggestions && suggestions.length > 0 && !isLoading) {
471
+ setDropOpen((open) => !open);
472
+ }
473
+ return;
474
+ }
388
475
  if (!suggestions || suggestions?.length === 0 || refetchData) {
389
476
  if (autoDropdown && (inputValue === '' || inputValue.trim() === '')) {
390
477
  setSuggestions([]);
@@ -560,21 +647,50 @@ const ReactAutoCompleteTableView: React.FC<AutoSuggestionInputProps> = ({
560
647
  // const getPosition = () => {
561
648
  // return 'bottom'
562
649
  // }
650
+ const itemMatchesLocalFilter = (item: any): boolean => {
651
+ const baseMatch = checkIncludes(
652
+ item.name,
653
+ inputValue,
654
+ item.param1 ?? '',
655
+ item.param2 ?? '',
656
+ item.param3 ?? '',
657
+ item.param4 ?? ''
658
+ );
659
+ if (
660
+ dropdownTheme !== 'splitDetail' ||
661
+ !splitDetailOptions ||
662
+ !inputValue
663
+ ) {
664
+ return baseMatch;
665
+ }
666
+ const q = inputValue.trim().toLowerCase();
667
+ if (!q) return baseMatch;
668
+
669
+ const hay = (v: unknown) =>
670
+ v != null && String(v).toLowerCase().includes(q);
671
+
672
+ if (item.label != null && hay(item.label)) return true;
673
+ if (
674
+ splitDetailOptions.subtitleKey &&
675
+ hay(item[splitDetailOptions.subtitleKey])
676
+ ) {
677
+ return true;
678
+ }
679
+ if (
680
+ splitDetailOptions.badgeKey &&
681
+ hay(item[splitDetailOptions.badgeKey])
682
+ ) {
683
+ return true;
684
+ }
685
+ return baseMatch;
686
+ };
687
+
563
688
  const filteredData =
564
689
  inputValue !== '*' &&
565
690
  inputValue !== '' &&
566
691
  type !== 'custom_select' &&
567
692
  !noLocalFilter
568
- ? suggestions?.filter((item: valueProps) =>
569
- checkIncludes(
570
- item.name,
571
- inputValue,
572
- item.param1 ?? '',
573
- item.param2 ?? '',
574
- item.param3 ?? '',
575
- item.param4 ?? ''
576
- )
577
- )
693
+ ? suggestions?.filter((item: valueProps) => itemMatchesLocalFilter(item))
578
694
  : suggestions;
579
695
 
580
696
  const handleError = (data: any) => {
@@ -770,7 +886,11 @@ const ReactAutoCompleteTableView: React.FC<AutoSuggestionInputProps> = ({
770
886
  <div
771
887
  ref={dropdownContentRef}
772
888
  style={{ ...dropPosition, overflow: 'hidden' }}
773
- className="autocomplete-suggections autocomplete-suggections-tableview absolute bg-white shadow-gray-300 shadow-md border border-grey-light z-50 mt-9"
889
+ className={`autocomplete-suggections autocomplete-suggections-tableview absolute bg-white shadow-gray-300 shadow-md border border-grey-light z-50 mt-9${
890
+ dropdownTheme === 'splitDetail'
891
+ ? ' autocomplete-suggections--split-detail'
892
+ : ''
893
+ }`}
774
894
  >
775
895
  <ul
776
896
  className={`h-auto overflow-auto w-full ${tableView ? '' : 'py-1.5'}`}
@@ -779,7 +899,9 @@ const ReactAutoCompleteTableView: React.FC<AutoSuggestionInputProps> = ({
779
899
  >
780
900
  {filteredData?.length > 0 ? (
781
901
  <>
782
- {columnHeader && columnHeader.length > 0 && (
902
+ {dropdownTheme === 'default' &&
903
+ columnHeader &&
904
+ columnHeader.length > 0 && (
783
905
  <li className="sticky top-0 auto-suggections-tableview-header z-10 bg-gray-100 border-b border-grey-light">
784
906
  <ul className="flex items-stretch w-full list-none p-0 m-0">
785
907
  {(() => {
@@ -861,9 +983,9 @@ const ReactAutoCompleteTableView: React.FC<AutoSuggestionInputProps> = ({
861
983
  index === selectedIndex ? 'is-selected' : ''
862
984
  } hover:bg-table-hover`
863
985
  } cursor-pointer text-xxs qbs-autocomplete-suggections-items ${
864
- tableView
865
- ? "border-b border-grey-light last:border-b-0"
866
- : "p-1 ps-3.5 pl-[10px]"
986
+ dropdownTheme === 'splitDetail' || tableView
987
+ ? 'border-b border-grey-light last:border-b-0'
988
+ : 'p-1 ps-3.5 pl-[10px]'
867
989
  }`}
868
990
  key={suggestion?.id}
869
991
  data-testid={suggestion.name}
@@ -871,70 +993,144 @@ const ReactAutoCompleteTableView: React.FC<AutoSuggestionInputProps> = ({
871
993
  tabIndex={index}
872
994
  ref={(el) => (itemRefs.current[index] = el)}
873
995
  >
874
- <ul className="flex items-stretch w-full list-none p-0 m-0">
875
- {(() => {
876
- // Sort columns by order if specified, otherwise maintain array order
877
- const sortedColumns = additionalColumns
878
- ? [...additionalColumns].sort((a, b) => {
879
- const orderA = a.order !== undefined ? a.order : Infinity;
880
- const orderB = b.order !== undefined ? b.order : Infinity;
881
- return orderA - orderB;
882
- })
883
- : [];
884
-
885
- // Separate columns before first column (order < 0) and after (order >= 0 or undefined)
886
- const columnsBefore = sortedColumns.filter(col => col.order !== undefined && col.order < 0);
887
- const columnsAfter = sortedColumns.filter(col => col.order === undefined || col.order >= 0);
888
-
889
- // Determine if first column needs a separator
890
- const hasColumnsAfter = columnsAfter.length > 0;
891
-
892
- return (
893
- <>
894
- {/* Columns before first column */}
895
- {columnsBefore.map((column, colIndex) => {
896
- const hasValue = suggestion?.[column.key];
897
- const isLastBefore = false;
898
- return hasValue ? (
899
- <li
900
- key={column.key}
901
- className={`${column.width ? 'flex-shrink-0' : 'flex-1'} min-w-0 px-3 ${tableView ? 'py-2' : ''} ${!isLastBefore ? 'border-r border-grey-light' : ''} break-words flex flex-col`}
902
- style={column.width ? { width: `${column.width}px` } : undefined}
903
- >
904
- <span className="block whitespace-normal">
905
- {suggestion?.[column.key]}
906
- </span>
907
- </li>
908
- ) : null;
909
- })}
910
-
911
- {/* First column (label/name) */}
912
- <li className={`flex-1 min-w-0 px-3 ${tableView ? 'py-2' : ''} ${hasColumnsAfter ? 'border-r border-grey-light' : ''} break-words flex flex-col`}>
913
- <span className="block whitespace-normal">
914
- {suggestion?.label ? suggestion?.label : suggestion.name}
996
+ {dropdownTheme === 'splitDetail' ? (
997
+ <div className="qbs-split-detail-row">
998
+ <div className="qbs-split-detail-main">
999
+ <div className="qbs-split-detail-title">
1000
+ {suggestion?.label ? suggestion?.label : suggestion.name}
1001
+ </div>
1002
+ {splitDetailOptions?.subtitleKey &&
1003
+ suggestion?.[splitDetailOptions.subtitleKey] != null &&
1004
+ String(
1005
+ suggestion[splitDetailOptions.subtitleKey]
1006
+ ).trim() !== '' && (
1007
+ <div className="qbs-split-detail-subtitle">
1008
+ {String(
1009
+ suggestion[splitDetailOptions.subtitleKey]
1010
+ )}
1011
+ </div>
1012
+ )}
1013
+ </div>
1014
+ {splitDetailOptions?.badgeKey &&
1015
+ suggestion?.[splitDetailOptions.badgeKey] != null &&
1016
+ String(suggestion[splitDetailOptions.badgeKey]).trim() !==
1017
+ '' && (
1018
+ <div className="qbs-split-detail-badge-wrap">
1019
+ <span className="qbs-split-detail-badge">
1020
+ {splitDetailOptions.badgePrefix ?? ''}
1021
+ {String(suggestion[splitDetailOptions.badgeKey])}
915
1022
  </span>
916
- </li>
917
-
918
- {/* Columns after first column */}
919
- {columnsAfter.map((column, colIndex) => {
920
- const hasValue = suggestion?.[column.key];
921
- const isLastColumn = colIndex === columnsAfter.length - 1;
922
- return hasValue ? (
923
- <li
924
- key={column.key}
925
- className={`${column.width ? 'flex-shrink-0' : 'flex-1'} min-w-0 px-3 ${tableView ? 'py-2' : ''} ${!isLastColumn ? 'border-r border-grey-light' : ''} break-words flex flex-col`}
926
- style={column.width ? { width: `${column.width}px` } : undefined}
927
- >
928
- <span className="block whitespace-normal">
929
- {suggestion?.[column.key]}
930
- </span>
931
- </li>
932
- ) : null;
933
- })}
934
- </>
935
- );
936
- })()}
937
- </ul>
1023
+ </div>
1024
+ )}
1025
+ </div>
1026
+ ) : (
1027
+ <ul className="flex items-stretch w-full list-none p-0 m-0">
1028
+ {(() => {
1029
+ // Sort columns by order if specified, otherwise maintain array order
1030
+ const sortedColumns = additionalColumns
1031
+ ? [...additionalColumns].sort((a, b) => {
1032
+ const orderA =
1033
+ a.order !== undefined ? a.order : Infinity;
1034
+ const orderB =
1035
+ b.order !== undefined ? b.order : Infinity;
1036
+ return orderA - orderB;
1037
+ })
1038
+ : [];
1039
+
1040
+ // Separate columns before first column (order < 0) and after (order >= 0 or undefined)
1041
+ const columnsBefore = sortedColumns.filter(
1042
+ (col) => col.order !== undefined && col.order < 0
1043
+ );
1044
+ const columnsAfter = sortedColumns.filter(
1045
+ (col) =>
1046
+ col.order === undefined || col.order >= 0
1047
+ );
1048
+
1049
+ // Determine if first column needs a separator
1050
+ const hasColumnsAfter = columnsAfter.length > 0;
1051
+
1052
+ return (
1053
+ <>
1054
+ {/* Columns before first column */}
1055
+ {columnsBefore.map((column, colIndex) => {
1056
+ const hasValue = suggestion?.[column.key];
1057
+ const isLastBefore = false;
1058
+ return hasValue ? (
1059
+ <li
1060
+ key={column.key}
1061
+ className={`${
1062
+ column.width ? 'flex-shrink-0' : 'flex-1'
1063
+ } min-w-0 px-3 ${
1064
+ tableView ? 'py-2' : ''
1065
+ } ${
1066
+ !isLastBefore
1067
+ ? 'border-r border-grey-light'
1068
+ : ''
1069
+ } break-words flex flex-col`}
1070
+ style={
1071
+ column.width
1072
+ ? { width: `${column.width}px` }
1073
+ : undefined
1074
+ }
1075
+ >
1076
+ <span className="block whitespace-normal">
1077
+ {suggestion?.[column.key]}
1078
+ </span>
1079
+ </li>
1080
+ ) : null;
1081
+ })}
1082
+
1083
+ {/* First column (label/name) */}
1084
+ <li
1085
+ className={`flex-1 min-w-0 px-3 ${
1086
+ tableView ? 'py-2' : ''
1087
+ } ${
1088
+ hasColumnsAfter
1089
+ ? 'border-r border-grey-light'
1090
+ : ''
1091
+ } break-words flex flex-col`}
1092
+ >
1093
+ <span className="block whitespace-normal">
1094
+ {suggestion?.label
1095
+ ? suggestion?.label
1096
+ : suggestion.name}
1097
+ </span>
1098
+ </li>
1099
+
1100
+ {/* Columns after first column */}
1101
+ {columnsAfter.map((column, colIndex) => {
1102
+ const hasValue = suggestion?.[column.key];
1103
+ const isLastColumn =
1104
+ colIndex === columnsAfter.length - 1;
1105
+ return hasValue ? (
1106
+ <li
1107
+ key={column.key}
1108
+ className={`${
1109
+ column.width ? 'flex-shrink-0' : 'flex-1'
1110
+ } min-w-0 px-3 ${
1111
+ tableView ? 'py-2' : ''
1112
+ } ${
1113
+ !isLastColumn
1114
+ ? 'border-r border-grey-light'
1115
+ : ''
1116
+ } break-words flex flex-col`}
1117
+ style={
1118
+ column.width
1119
+ ? { width: `${column.width}px` }
1120
+ : undefined
1121
+ }
1122
+ >
1123
+ <span className="block whitespace-normal">
1124
+ {suggestion?.[column.key]}
1125
+ </span>
1126
+ </li>
1127
+ ) : null;
1128
+ })}
1129
+ </>
1130
+ );
1131
+ })()}
1132
+ </ul>
1133
+ )}
938
1134
  </li>
939
1135
  ))}
940
1136
  </>
@@ -1185,10 +1381,13 @@ const ReactAutoCompleteTableView: React.FC<AutoSuggestionInputProps> = ({
1185
1381
  : placeholder ?? '--Select--'
1186
1382
  }
1187
1383
  onFocus={onInputFocus}
1384
+ onKeyDown={handleSearchInputKeyDown}
1188
1385
  onClick={(e) => {
1189
1386
  if (type === 'custom_select') {
1190
1387
  setDropOpen(!dropOpen);
1191
1388
  handleOpen(e);
1389
+ } else if (searchOnButtonOnly) {
1390
+ return;
1192
1391
  } else {
1193
1392
  if (dropOpen || filteredData?.length > 0)
1194
1393
  setDropOpen(!dropOpen);
@@ -1226,7 +1425,26 @@ const ReactAutoCompleteTableView: React.FC<AutoSuggestionInputProps> = ({
1226
1425
  <CustomIcons name="close" type="large-m" />
1227
1426
  </button>
1228
1427
  )}
1229
- {isLoading && <Spinner />}
1428
+ {showSearchOnButtonChrome && (
1429
+ <button
1430
+ type="button"
1431
+ aria-label={isLoading ? 'Loading' : 'Search'}
1432
+ aria-busy={isLoading}
1433
+ data-testid="autocomplete-search-button"
1434
+ onClick={handleSearchButtonClick}
1435
+ disabled={isLoading}
1436
+ className="text-table-bodyColor text-[#667085] focus-visible:outline-slate-100 p-0.5 inline-flex items-center justify-center min-w-[28px] min-h-[28px] disabled:opacity-70"
1437
+ >
1438
+ {isLoading ? (
1439
+ <span className="inline-flex items-center justify-center [&_svg]:mr-0 [&_svg]:h-5 [&_svg]:w-5">
1440
+ <Spinner />
1441
+ </span>
1442
+ ) : (
1443
+ <CustomIcons name="search" type="medium" />
1444
+ )}
1445
+ </button>
1446
+ )}
1447
+ {isLoading && !showSearchOnButtonChrome && <Spinner />}
1230
1448
  {(type !== 'auto_complete' || autoDropdown) &&
1231
1449
  !disabled &&
1232
1450
  !readOnly && (
package/src/index.tsx CHANGED
@@ -21,3 +21,8 @@ export { default as ExpandableToolTip } from './utilities/expandableTootltip';
21
21
  export { default as ToolTip } from './utilities/tooltip';
22
22
  export { default as ModernAutoCompleteDropdown } from './ReactAutoCompleteDropdown';
23
23
  export { default as ModernAutoCompleteTableView } from './ReactAutoCompleteTableView';
24
+ export type {
25
+ DropdownThemeConfig,
26
+ DropdownThemeId,
27
+ SplitDetailThemeConfig,
28
+ } from './ReactAutoCompleteTableView';
@@ -748,6 +748,75 @@ span.dropdown-search-icon {
748
748
  flex: 0 1 auto;
749
749
  }
750
750
 
751
+ /* Split-detail dropdown theme (bank-style row: title + subtitle | badge) */
752
+ .autocomplete-suggections--split-detail .qbs-split-detail-row {
753
+ display: flex;
754
+ align-items: center;
755
+ gap: 12px;
756
+ width: 100%;
757
+ padding: 10px 12px;
758
+ box-sizing: border-box;
759
+ }
760
+
761
+ .autocomplete-suggections--split-detail .qbs-split-detail-main {
762
+ flex: 1;
763
+ min-width: 0;
764
+ }
765
+
766
+ .autocomplete-suggections--split-detail .qbs-split-detail-title {
767
+ font-size: 14px;
768
+ font-weight: 600;
769
+ color: #101828;
770
+ line-height: 1.35;
771
+ }
772
+
773
+ .autocomplete-suggections--split-detail .qbs-split-detail-subtitle {
774
+ margin-top: 4px;
775
+ font-size: 12px;
776
+ font-weight: 400;
777
+ color: #667085;
778
+ line-height: 1.45;
779
+ white-space: normal;
780
+ word-break: break-word;
781
+ }
782
+
783
+ .autocomplete-suggections--split-detail .qbs-split-detail-badge-wrap {
784
+ flex-shrink: 0;
785
+ align-self: center;
786
+ max-width: 45%;
787
+ }
788
+
789
+ .autocomplete-suggections--split-detail .qbs-split-detail-badge {
790
+ display: inline-block;
791
+ padding: 6px 10px;
792
+ font-size: 12px;
793
+ font-weight: 600;
794
+ line-height: 1.3;
795
+ color: #194185;
796
+ background: #d1e9ff;
797
+ border: 1px solid #b2ddff;
798
+ border-radius: 6px;
799
+ white-space: normal;
800
+ word-break: break-word;
801
+ }
802
+
803
+ .autocomplete-suggections--split-detail
804
+ .qbs-autocomplete-suggections-items.bg-blue-navy
805
+ .qbs-split-detail-title,
806
+ .autocomplete-suggections--split-detail
807
+ .qbs-autocomplete-suggections-items.bg-blue-navy
808
+ .qbs-split-detail-subtitle {
809
+ color: #fff;
810
+ }
811
+
812
+ .autocomplete-suggections--split-detail
813
+ .qbs-autocomplete-suggections-items.bg-blue-navy
814
+ .qbs-split-detail-badge {
815
+ color: #194185;
816
+ background: #eff8ff;
817
+ border-color: #b2ddff;
818
+ }
819
+
751
820
  /* TextField Inner Search Button */
752
821
  .inner-search-button {
753
822
  background: #f8f8f8;