playbook_ui 16.12.0.pre.alpha.PLAY3072railstextinputrequiredaddonmaskingbugs18157 → 16.12.0.pre.alpha.PLAY3100adddarkborderprob18326

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 (77) hide show
  1. checksums.yaml +4 -4
  2. data/app/pb_kits/playbook/_playbook.scss +0 -1
  3. data/app/pb_kits/playbook/pb_dropdown/_dropdown.tsx +18 -3
  4. data/app/pb_kits/playbook/pb_dropdown/docs/_dropdown_default_value_with_autocomplete.html.erb +10 -0
  5. data/app/pb_kits/playbook/pb_dropdown/docs/_dropdown_default_value_with_autocomplete.jsx +35 -0
  6. data/app/pb_kits/playbook/pb_dropdown/docs/example.yml +2 -0
  7. data/app/pb_kits/playbook/pb_dropdown/docs/index.js +1 -0
  8. data/app/pb_kits/playbook/pb_dropdown/dropdown.test.jsx +35 -0
  9. data/app/pb_kits/playbook/pb_dropdown/hooks/useHandleOnKeydown.tsx +18 -1
  10. data/app/pb_kits/playbook/pb_dropdown/index.js +34 -0
  11. data/app/pb_kits/playbook/pb_dropdown/subcomponents/DropdownOption.tsx +8 -1
  12. data/app/pb_kits/playbook/pb_dropdown/subcomponents/DropdownTrigger.tsx +7 -6
  13. data/app/pb_kits/playbook/pb_form/docs/example.yml +0 -1
  14. data/app/pb_kits/playbook/pb_form_group/_form_group.scss +24 -10
  15. data/app/pb_kits/playbook/pb_form_group/docs/_form_group_typeahead.html.erb +19 -14
  16. data/app/pb_kits/playbook/pb_form_group/docs/example.yml +2 -2
  17. data/app/pb_kits/playbook/pb_highlight/_highlight.tsx +31 -13
  18. data/app/pb_kits/playbook/pb_highlight/_highlightChunks.ts +89 -0
  19. data/app/pb_kits/playbook/pb_highlight/highlight.test.jsx +263 -0
  20. data/app/pb_kits/playbook/pb_table/_table.tsx +4 -0
  21. data/app/pb_kits/playbook/pb_table/docs/_playground.json +2 -0
  22. data/app/pb_kits/playbook/pb_table/docs/_playground.overrides.json +1 -1
  23. data/app/pb_kits/playbook/pb_table/docs/_sections.yml +1 -0
  24. data/app/pb_kits/playbook/pb_table/docs/_table_contrast_border.html.erb +71 -0
  25. data/app/pb_kits/playbook/pb_table/docs/_table_contrast_border.jsx +93 -0
  26. data/app/pb_kits/playbook/pb_table/docs/_table_contrast_border.md +1 -0
  27. data/app/pb_kits/playbook/pb_table/docs/example.yml +2 -0
  28. data/app/pb_kits/playbook/pb_table/docs/index.js +1 -0
  29. data/app/pb_kits/playbook/pb_table/kit.schema.json +8 -0
  30. data/app/pb_kits/playbook/pb_table/styles/_all.scss +2 -1
  31. data/app/pb_kits/playbook/pb_table/styles/_contrast_border.scss +87 -0
  32. data/app/pb_kits/playbook/pb_table/styles/_desktop_collapse.scss +2 -2
  33. data/app/pb_kits/playbook/pb_table/styles/_headers.scss +3 -3
  34. data/app/pb_kits/playbook/pb_table/styles/_hover.scss +2 -2
  35. data/app/pb_kits/playbook/pb_table/styles/_mobile.scss +2 -2
  36. data/app/pb_kits/playbook/pb_table/styles/_mobile_collapse.scss +2 -2
  37. data/app/pb_kits/playbook/pb_table/styles/_scroll.scss +2 -2
  38. data/app/pb_kits/playbook/pb_table/styles/_sticky_columns.scss +2 -2
  39. data/app/pb_kits/playbook/pb_table/styles/_structure.scss +1 -1
  40. data/app/pb_kits/playbook/pb_table/styles/_table-card.scss +2 -2
  41. data/app/pb_kits/playbook/pb_table/styles/_tablet_collapse.scss +2 -2
  42. data/app/pb_kits/playbook/pb_table/styles/_vertical_border.scss +9 -9
  43. data/app/pb_kits/playbook/pb_table/table.rb +7 -1
  44. data/app/pb_kits/playbook/pb_table/table.test.js +5 -0
  45. data/app/pb_kits/playbook/pb_timestamp/_timestamp.tsx +2 -2
  46. data/app/pb_kits/playbook/pb_timestamp/docs/_timestamp_updated.html.erb +19 -0
  47. data/app/pb_kits/playbook/pb_timestamp/docs/_timestamp_updated.jsx +21 -0
  48. data/app/pb_kits/playbook/pb_timestamp/docs/_timestamp_updated.md +3 -1
  49. data/app/pb_kits/playbook/pb_timestamp/timestamp.test.js +42 -0
  50. data/app/pb_kits/playbook/pb_typeahead/_typeahead.scss +3 -1
  51. data/app/pb_kits/playbook/pb_typeahead/typeahead.html.erb +1 -0
  52. data/dist/chunks/{_typeahead-BrQbahtd.js → _typeahead-BcKnlPk3.js} +1 -1
  53. data/dist/chunks/vendor.js +2 -2
  54. data/dist/menu.yml +0 -6
  55. data/dist/playbook-rails-react-bindings.js +1 -1
  56. data/dist/playbook-rails.js +1 -1
  57. data/dist/playbook.css +1 -1
  58. data/lib/playbook/tokens/colors.json +117 -1
  59. data/lib/playbook/tokens.rb +5 -4
  60. data/lib/playbook/version.rb +1 -1
  61. metadata +11 -19
  62. data/app/pb_kits/playbook/pb_form/docs/_form_form_with_validate_addon_currency.html.erb +0 -123
  63. data/app/pb_kits/playbook/pb_icon_button/_icon_button.scss +0 -78
  64. data/app/pb_kits/playbook/pb_icon_button/_icon_button.tsx +0 -75
  65. data/app/pb_kits/playbook/pb_icon_button/docs/_icon_button_click.jsx +0 -13
  66. data/app/pb_kits/playbook/pb_icon_button/docs/_icon_button_default.html.erb +0 -2
  67. data/app/pb_kits/playbook/pb_icon_button/docs/_icon_button_default.jsx +0 -17
  68. data/app/pb_kits/playbook/pb_icon_button/docs/_icon_button_sizes.html.erb +0 -27
  69. data/app/pb_kits/playbook/pb_icon_button/docs/_icon_button_sizes.jsx +0 -63
  70. data/app/pb_kits/playbook/pb_icon_button/docs/_playground.json +0 -135
  71. data/app/pb_kits/playbook/pb_icon_button/docs/_playground.overrides.json +0 -131
  72. data/app/pb_kits/playbook/pb_icon_button/docs/example.yml +0 -10
  73. data/app/pb_kits/playbook/pb_icon_button/docs/index.js +0 -3
  74. data/app/pb_kits/playbook/pb_icon_button/icon_button.html.erb +0 -17
  75. data/app/pb_kits/playbook/pb_icon_button/icon_button.rb +0 -25
  76. data/app/pb_kits/playbook/pb_icon_button/icon_button.test.jsx +0 -39
  77. data/app/pb_kits/playbook/pb_icon_button/kit.schema.json +0 -116
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 808c3d5ab3e82537f24aeac0f1cb2e402a30d4afae94e25825bfcc0cbe8feda0
4
- data.tar.gz: '084bedeca9f2498f50b6cac05965995b7cc91021f4eebd6e5e149caf79583a25'
3
+ metadata.gz: a180c0a86cb56e3e65389829c05e8f2a80a188e736401dec91b92257b2ba8cc9
4
+ data.tar.gz: 616c39a14d5f72c03de3020fcc6a1ecdb4cffde6a452d922dcc8791a07b8fa3a
5
5
  SHA512:
6
- metadata.gz: bca00447e61c8e7ed070c1ea0fe1fa3ade2c78d3ac3a2ecc2a294d9fb25464575f299aa231f8c4808344197e741daed75e5cd7d6b408c10fe16a39327953af13
7
- data.tar.gz: 52fced873ada1be081e91b4b01dbf166ed0f41e17545e86d467a1168c0d0526660e0712c04921718db3201c540c19341403873f20fe816f0205f90ae445c1cbc
6
+ metadata.gz: 7d19e915b3c1c7f1b5c99307c7dcb402b30bb2852efe87a59642dc40ef56a38a305d98e97c03a9535868e077ade98df3278d73d1174af0b8b1fe46a04a19fc4d
7
+ data.tar.gz: bff090a80a6dc89f2d0097b38a14851d4b68f0ec417502324a144dc624c6852902b4c9072ef3710954fecf2803350d5f9fbc530dec4d2ef010ea8ccc83ab6b96
@@ -41,7 +41,6 @@
41
41
  @import 'pb_highlight/highlight';
42
42
  @import 'pb_home_address_street/home_address_street';
43
43
  @import 'pb_icon/icon';
44
- @import 'pb_icon_button/icon_button';
45
44
  @import 'pb_icon_circle/icon_circle';
46
45
  @import 'pb_icon_stat_value/icon_stat_value';
47
46
  @import 'pb_icon_value/icon_value';
@@ -203,7 +203,6 @@ let Dropdown = (props: DropdownProps, ref: any): React.ReactElement | null => {
203
203
  : undefined;
204
204
  const errorId = error ? `${selectId}-error` : undefined;
205
205
 
206
- const [filterItem, setFilterItem] = useState("");
207
206
  const initialSelected = useMemo(() => {
208
207
  // Handle quickpick variant with string defaultValue (e.g., "This Month")
209
208
  if (variant === "quickpick" && typeof defaultValue === "string" && defaultValue) {
@@ -226,6 +225,13 @@ let Dropdown = (props: DropdownProps, ref: any): React.ReactElement | null => {
226
225
  initialSelected
227
226
  );
228
227
 
228
+ // Autocomplete displays the selection in the input; seed from defaultValue
229
+ const [filterItem, setFilterItem] = useState(() => {
230
+ if (!autocomplete || multiSelect) return "";
231
+ if (Array.isArray(initialSelected)) return "";
232
+ return (initialSelected as GenericObject)?.label || "";
233
+ });
234
+
229
235
  const filterResetDefaultSerialized = useMemo(
230
236
  () => serializeDropdownFilterResetDefault(variant, multiSelect, defaultValue, dropdownOptions),
231
237
  [variant, multiSelect, defaultValue, dropdownOptions]
@@ -320,10 +326,17 @@ let Dropdown = (props: DropdownProps, ref: any): React.ReactElement | null => {
320
326
  }, [optionsWithBlankSelection, selectedArray, multiSelect]);
321
327
 
322
328
  const filteredOptions = useMemo(() => {
329
+ // When the input shows the selected label, do not filter the list down to that one option
330
+ const selectedLabel =
331
+ !multiSelect &&
332
+ !Array.isArray(selected) &&
333
+ (selected as GenericObject)?.label;
334
+ const filterText =
335
+ selectedLabel && filterItem === selectedLabel ? "" : filterItem;
323
336
  return availableOptions.filter((opt: GenericObject) =>
324
- String(opt.label).toLowerCase().includes(filterItem.toLowerCase())
337
+ String(opt.label).toLowerCase().includes(filterText.toLowerCase())
325
338
  );
326
- }, [availableOptions, filterItem]);
339
+ }, [availableOptions, filterItem, multiSelect, selected]);
327
340
 
328
341
  // For keyboard accessibility: Set focus within dropdown to selected item if it exists
329
342
  useEffect(() => {
@@ -460,6 +473,7 @@ let Dropdown = (props: DropdownProps, ref: any): React.ReactElement | null => {
460
473
  setSelected({});
461
474
  onSelect && onSelect(null);
462
475
  setFocusedOptionIndex(-1);
476
+ setFilterItem("");
463
477
 
464
478
  // Clear linked DatePickers as well if this is a quickpick variant with controls
465
479
  if (variant === "quickpick") {
@@ -593,6 +607,7 @@ let Dropdown = (props: DropdownProps, ref: any): React.ReactElement | null => {
593
607
  onSelect,
594
608
  optionsWithBlankSelection,
595
609
  selected,
610
+ setFilterItem,
596
611
  setFocusedOptionIndex,
597
612
  setIsDropDownClosed,
598
613
  setIsInputFocused,
@@ -0,0 +1,10 @@
1
+ <%
2
+ options = [
3
+ { label: 'United States', value: 'unitedStates', id: 'us' },
4
+ { label: 'Canada', value: 'canada', id: 'ca' },
5
+ { label: 'Pakistan', value: 'pakistan', id: 'pk' },
6
+ ]
7
+
8
+ %>
9
+
10
+ <%= pb_rails("dropdown", props: {options: options, default_value: options.last, autocomplete: true}) %>
@@ -0,0 +1,35 @@
1
+ import React from 'react'
2
+ import Dropdown from '../../pb_dropdown/_dropdown'
3
+
4
+ const DropdownDefaultValueWithAutocomplete = (props) => {
5
+ const options = [
6
+ {
7
+ label: "United States",
8
+ value: "unitedStates",
9
+ id: "us"
10
+ },
11
+ {
12
+ label: "Canada",
13
+ value: "canada",
14
+ id: "ca"
15
+ },
16
+ {
17
+ label: "Pakistan",
18
+ value: "pakistan",
19
+ id: "pk"
20
+ }
21
+ ];
22
+
23
+ return (
24
+ <>
25
+ <Dropdown
26
+ autocomplete
27
+ defaultValue={options[2]}
28
+ options={options}
29
+ {...props}
30
+ />
31
+ </>
32
+ )
33
+ }
34
+
35
+ export default DropdownDefaultValueWithAutocomplete
@@ -22,6 +22,7 @@ examples:
22
22
  - dropdown_error: Dropdown with Error
23
23
  - dropdown_default_value: Default Value
24
24
  - dropdown_multi_select_with_default: Multi Select Default Value
25
+ - dropdown_default_value_with_autocomplete: Default Value with Autocomplete
25
26
  - dropdown_blank_selection: Blank Selection
26
27
  - dropdown_with_placeholder: Placeholder
27
28
  - dropdown_separators_hidden: Separators Hidden
@@ -63,6 +64,7 @@ examples:
63
64
  - dropdown_error: Dropdown with Error
64
65
  - dropdown_default_value: Default Value
65
66
  - dropdown_multi_select_with_default: Multi Select Default Value
67
+ - dropdown_default_value_with_autocomplete: Default Value with Autocomplete
66
68
  - dropdown_blank_selection: Blank Selection
67
69
  - dropdown_with_placeholder: Placeholder
68
70
  - dropdown_clear_selection: Clear Selection
@@ -37,3 +37,4 @@ export { default as DropdownDisabledOption } from "./_dropdown_disabled_option.j
37
37
  export { default as DropdownCustomDisplayDisabledOption } from "./_dropdown_custom_display_disabled_option.jsx";
38
38
  export { default as DropdownDisabled } from "./_dropdown_disabled.jsx";
39
39
  export { default as DropdownGroupedOptions } from "./_dropdown_grouped_options.jsx";
40
+ export { default as DropdownDefaultValueWithAutocomplete } from './_dropdown_default_value_with_autocomplete.jsx'
@@ -350,6 +350,41 @@ test("autocomplete prop to render input", () => {
350
350
  expect(input).toBeInTheDocument()
351
351
  })
352
352
 
353
+ test("defaultValue renders in autocomplete input", () => {
354
+ render(
355
+ <Dropdown
356
+ autocomplete
357
+ data={{ testid: testId }}
358
+ defaultValue={options[2]}
359
+ options={options}
360
+ />
361
+ )
362
+
363
+ const kit = screen.getByTestId(testId)
364
+ const input = kit.querySelector('.dropdown_input')
365
+ expect(input).toBeInTheDocument()
366
+ expect(input).toHaveValue(options[2].label)
367
+ expect(input).toHaveAttribute("placeholder", "")
368
+ })
369
+
370
+ test("autocomplete defaultValue still shows all options when opened", () => {
371
+ render(
372
+ <Dropdown
373
+ autocomplete
374
+ data={{ testid: testId }}
375
+ defaultValue={options[2]}
376
+ options={options}
377
+ />
378
+ )
379
+
380
+ const kit = screen.getByTestId(testId)
381
+ const optionEls = kit.querySelectorAll('.pb_dropdown_option_list, .pb_dropdown_option_selected')
382
+ expect(optionEls).toHaveLength(options.length)
383
+ expect(kit).toHaveTextContent(options[0].label)
384
+ expect(kit).toHaveTextContent(options[1].label)
385
+ expect(kit).toHaveTextContent(options[2].label)
386
+ })
387
+
353
388
  test("searchbar prop to render TextInput in container", () => {
354
389
  render (
355
390
  <Dropdown
@@ -6,11 +6,13 @@ export const useHandleOnKeyDown = () => {
6
6
 
7
7
  const {
8
8
  autocomplete,
9
+ filterItem,
9
10
  filteredOptions,
10
11
  focusedOptionIndex,
11
12
  handleBackspace,
12
13
  handleOptionClick,
13
14
  selected,
15
+ setFilterItem,
14
16
  setFocusedOptionIndex,
15
17
  setIsDropDownClosed,
16
18
  }= useContext(DropdownContext)
@@ -40,7 +42,22 @@ const {
40
42
  return (e: React.KeyboardEvent) => {
41
43
 
42
44
  if (e.key !== "Tab" && autocomplete && selected && selected.label) {
45
+ // Only when the input is showing the selected label (e.g. seeded defaultValue)
46
+ const replacingSelectedLabel = filterItem === selected.label;
43
47
  handleBackspace();
48
+
49
+ if (replacingSelectedLabel && e.key.length === 1 && !e.ctrlKey && !e.metaKey && !e.altKey) {
50
+ e.preventDefault();
51
+ setFilterItem?.(e.key);
52
+ setIsDropDownClosed(false);
53
+ return;
54
+ }
55
+
56
+ if (replacingSelectedLabel && (e.key === "Backspace" || e.key === "Delete")) {
57
+ e.preventDefault();
58
+ setFilterItem?.("");
59
+ return;
60
+ }
44
61
  }
45
62
 
46
63
  switch (e.key) {
@@ -72,4 +89,4 @@ const {
72
89
  break;
73
90
  }
74
91
  }
75
- };
92
+ };
@@ -541,6 +541,30 @@ export default class PbDropdown extends PbEnhancedElement {
541
541
  }
542
542
  }
543
543
 
544
+ // Keep the selected label in the autocomplete input, but do not treat it as a search filter
545
+ resetOptionFilterForSelectedLabel() {
546
+ if (this.isMultiSelect || !this.searchInput) return;
547
+
548
+ const selectedOption = Array.from(this.queryAllOptions()).find((opt) =>
549
+ opt.classList.contains("pb_dropdown_option_selected"),
550
+ );
551
+ if (!selectedOption) return;
552
+
553
+ try {
554
+ const selectedLabel = JSON.parse(
555
+ selectedOption.dataset.dropdownOptionLabel,
556
+ ).label;
557
+ if (this.searchInput.value !== selectedLabel) return;
558
+
559
+ this.queryAllOptions().forEach((opt) => {
560
+ opt.style.display = "";
561
+ });
562
+ this.removeNoOptionsMessage();
563
+ } catch {
564
+ // ignore invalid option payloads
565
+ }
566
+ }
567
+
544
568
  showNoOptionsMessage() {
545
569
  if (this.target?.querySelector(".dropdown_no_options")) return;
546
570
 
@@ -923,6 +947,9 @@ export default class PbDropdown extends PbEnhancedElement {
923
947
 
924
948
  showElement(elem) {
925
949
  if (this.isDisabled) return;
950
+
951
+ // When autocomplete is showing the selected label (not an active search), show all options
952
+ this.resetOptionFilterForSelectedLabel();
926
953
 
927
954
  this.ensureFloatingPortalConfig();
928
955
  if (this.useMenuPortal) {
@@ -1096,6 +1123,13 @@ export default class PbDropdown extends PbEnhancedElement {
1096
1123
  const optionData = JSON.parse(selectedOption.dataset.dropdownOptionLabel);
1097
1124
  this.setTriggerElementText(optionData.label);
1098
1125
 
1126
+ // Autocomplete trigger has no [data-dropdown-trigger-display]; set the input value instead
1127
+ const autocompleteInput =
1128
+ this.searchInput || this.element.querySelector(SEARCH_INPUT_SELECTOR);
1129
+ if (autocompleteInput) {
1130
+ autocompleteInput.value = optionData.label;
1131
+ }
1132
+
1099
1133
  // Handle quickpick variant: populate start/end date hidden inputs and sync DatePickers
1100
1134
  if (optionData.formatted_start_date && optionData.formatted_end_date) {
1101
1135
  const startDateId = this.element.dataset.startDateId;
@@ -52,8 +52,15 @@ const DropdownOption = (props: DropdownOptionProps) => {
52
52
  } = useContext(DropdownContext);
53
53
 
54
54
  const isItemMatchingFilter = (option: GenericObject | undefined) => {
55
+ // When the input is only showing the selected label (e.g. seeded defaultValue), do not filter
56
+ const selectedLabel =
57
+ !multiSelect &&
58
+ !Array.isArray(selected) &&
59
+ (selected as GenericObject)?.label;
60
+ const filterText =
61
+ selectedLabel && filterItem === selectedLabel ? "" : filterItem;
55
62
  const label = typeof option?.label === 'string' ? option.label.toLowerCase() : option?.label;
56
- return String(label).toLowerCase().includes(filterItem.toLowerCase());
63
+ return String(label).toLowerCase().includes(filterText.toLowerCase());
57
64
  }
58
65
 
59
66
  // When multiSelect, then if an option is selected, remove from dropdown
@@ -103,8 +103,9 @@ const DropdownTrigger = (props: DropdownTriggerProps) => {
103
103
  "Select..."
104
104
  );
105
105
 
106
+ const hideJoinedLabelInBody = Boolean(autocomplete && filterItem === joinedLabels);
106
107
  const defaultDisplayPlaceholder = joinedLabels
107
- ? joinedLabels
108
+ ? (hideJoinedLabelInBody ? "" : joinedLabels)
108
109
  : autocomplete
109
110
  ? ""
110
111
  : placeholder
@@ -252,9 +253,9 @@ const DropdownTrigger = (props: DropdownTriggerProps) => {
252
253
  }}
253
254
  key={`${isDropDownClosed ? "chevron-down" : "chevron-up"}`}
254
255
  >
255
- {(!blankSelection || selected?.value !== optionsWithBlankSelection?.[0]?.value) && (
256
- <>
257
- {clearable !== false && selectedArray.length > 0 && (
256
+ {(!blankSelection || selected?.value !== optionsWithBlankSelection?.[0]?.value) &&
257
+ clearable !== false &&
258
+ selectedArray.length > 0 ? (
258
259
  <div onClick={(e)=>{e.stopPropagation(); !disabled && handleBackspace()}}>
259
260
  <Icon
260
261
  cursor={disabled ? "default" : "pointer"}
@@ -264,8 +265,8 @@ const DropdownTrigger = (props: DropdownTriggerProps) => {
264
265
  size="sm"
265
266
  />
266
267
  </div>
267
- )}
268
- </>
268
+ ) : (
269
+ <></>
269
270
  )}
270
271
  <Icon
271
272
  cursor={disabled ? "default" : "pointer"}
@@ -1,7 +1,6 @@
1
1
  examples:
2
2
 
3
3
  rails:
4
- - form_form_with_validate_addon_currency: Validation + Add On + Currency Mask
5
4
  - form_form_with: Default
6
5
  - form_form_with_validate: Default + Validation
7
6
  - form_form_with_validation_msg: Validation + Custom Validation Message
@@ -202,23 +202,37 @@
202
202
  }
203
203
  }
204
204
 
205
- & > [class^=pb_typeahead_kit] {
205
+ & > [class^=pb_typeahead_kit],
206
+ & > [data-pb-react-component] > [class^=pb_typeahead_kit] {
206
207
  min-width: 200px;
207
208
  }
208
209
 
209
- & > [class^=pb_typeahead_kit]:not(:last-child) {
210
- [class^=pb_text_input_kit] .text_input_wrapper input, [class^=pb_text_input_kit] .text_input_wrapper .text_input {
211
- border-bottom-right-radius: 0;
212
- border-top-right-radius: 0;
213
- border-right-width: 0;
210
+ & > [class^=pb_typeahead_kit]:not(:last-child),
211
+ & > [data-pb-react-component]:not(:last-child) > [class^=pb_typeahead_kit] {
212
+ [class^=pb_text_input_kit] .text_input_wrapper {
213
+ input,
214
+ .text_input,
215
+ .typeahead-kit-select__control {
216
+ border-bottom-right-radius: 0 !important;
217
+ border-top-right-radius: 0 !important;
218
+ border-right-width: 0 !important;
219
+ }
220
+ }
221
+
222
+ [class^=pb_text_input_kit] .text_input_wrapper div.text_input {
214
223
  min-height: 26px;
215
224
  }
216
225
  }
217
226
 
218
- & > [class^=pb_typeahead_kit]:not(:first-child) {
219
- [class^=pb_text_input_kit] .text_input_wrapper input, [class^=pb_text_input_kit] .text_input_wrapper .text_input {
220
- border-bottom-left-radius: 0;
221
- border-top-left-radius: 0;
227
+ & > [class^=pb_typeahead_kit]:not(:first-child),
228
+ & > [data-pb-react-component]:not(:first-child) > [class^=pb_typeahead_kit] {
229
+ [class^=pb_text_input_kit] .text_input_wrapper {
230
+ input,
231
+ .text_input,
232
+ .typeahead-kit-select__control {
233
+ border-bottom-left-radius: 0 !important;
234
+ border-top-left-radius: 0 !important;
235
+ }
222
236
  }
223
237
  }
224
238
 
@@ -1,19 +1,24 @@
1
1
  <div>
2
+
3
+ <%
4
+ options = [
5
+ { label: 'Orange', value: '#FFA500' },
6
+ { label: 'Red', value: '#FF0000' },
7
+ { label: 'Green', value: '#00FF00' },
8
+ { label: 'Blue', value: '#0000FF' },
9
+ ]
10
+ %>
2
11
  <%= pb_rails("form_group") do %>
3
- <%
4
- options = [
5
- { label: 'Windows', value: 'windows' },
6
- { label: 'Roof', value: 'roof' },
7
- { label: 'Siding', value: 'siding' },
8
- { label: 'Doors', value: 'doors' },
9
- ]
10
- %>
11
- <%= pb_rails("typeahead", props: {
12
- default_options: [options.first],
13
- id: "form-group-typeahead-example",
14
- label: "Products",
15
- options: options,
16
- }) %>
12
+
13
+ <%= pb_rails("typeahead", props: {
14
+ id: "typeahead-default",
15
+ placeholder: "All Colors",
16
+ options: options,
17
+ label: "Colors",
18
+ name: :foo,
19
+ is_multi: false,
20
+ })
21
+ %>
17
22
  <%= pb_rails("button", props: {
18
23
  id: "form-group-button",
19
24
  text: "Add",
@@ -5,7 +5,7 @@ examples:
5
5
  - form_group_button: Button
6
6
  - form_group_full_width: Full Width
7
7
  - form_group_date_picker: Date Picker
8
- # - form_group_typeahead: Typeahead
8
+ - form_group_typeahead: Typeahead
9
9
  - form_group_select: Select
10
10
  - form_group_selectable_card: Selectable Card
11
11
  - form_group_selectable_card_icon: Selectable Card Icon
@@ -16,7 +16,7 @@ examples:
16
16
  - form_group_button: Button
17
17
  - form_group_full_width: Full Width
18
18
  - form_group_date_picker: Date Picker
19
- # - form_group_typeahead: Typeahead
19
+ - form_group_typeahead: Typeahead
20
20
  - form_group_select: Select
21
21
  - form_group_selectable_card: Selectable Card
22
22
  - form_group_selectable_card_icon: Selectable Card Icon
@@ -1,9 +1,8 @@
1
- /* eslint-disable react/no-multi-comp, flowtype/space-before-type-colon */
2
- import Highlighter from 'react-highlight-words'
3
1
  import React from 'react'
4
2
  import classnames from 'classnames'
5
3
  import { globalProps, GlobalProps } from '../utilities/globalProps'
6
- import { buildHtmlProps } from '../utilities/props'
4
+ import { buildDataProps, buildHtmlProps } from '../utilities/props'
5
+ import { findHighlightChunks } from './_highlightChunks'
7
6
 
8
7
  type HighlightProps = {
9
8
  className?: string,
@@ -26,20 +25,39 @@ const Highlight = (props: HighlightProps): React.ReactElement => {
26
25
  text = '',
27
26
  } = props
28
27
 
28
+ const dataProps = buildDataProps(data)
29
29
  const htmlProps = buildHtmlProps(htmlOptions)
30
- const highlightContent: any = text || children;
30
+ const highlightContent: string = (text || children || '') as string
31
+ const highlightClassName = classnames(globalProps(props), className)
32
+ const chunks = findHighlightChunks(highlightContent, highlightedText)
31
33
 
32
34
  return (
33
- <Highlighter
34
- autoEscape
35
- data={data}
36
- highlightClassName={classnames(globalProps(props), className)}
37
- highlightTag="mark"
38
- id={id}
39
- searchWords={highlightedText}
40
- textToHighlight={highlightContent}
35
+ <span
36
+ {...dataProps}
41
37
  {...htmlProps}
42
- />
38
+ id={id}
39
+ >
40
+ {chunks.map((chunk, index) => {
41
+ const chunkText = highlightContent.substring(chunk.start, chunk.end)
42
+
43
+ if (chunk.highlight) {
44
+ return (
45
+ <mark
46
+ className={highlightClassName}
47
+ key={index}
48
+ >
49
+ {chunkText}
50
+ </mark>
51
+ )
52
+ }
53
+
54
+ return (
55
+ <span key={index}>
56
+ {chunkText}
57
+ </span>
58
+ )
59
+ })}
60
+ </span>
43
61
  )
44
62
  }
45
63
 
@@ -0,0 +1,89 @@
1
+ type HighlightChunk = {
2
+ start: number,
3
+ end: number,
4
+ highlight: boolean,
5
+ }
6
+
7
+ const escapeRegExp = (value: string): string =>
8
+ value.replace(/[-[\]/{}()*+?.\\^$|]/g, '\\$&')
9
+
10
+ /**
11
+ * Finds all case-insensitive, literal (auto-escaped) matches for the given
12
+ * search terms, merges overlapping/adjacent ranges, and returns a full set of
13
+ * highlight and non-highlight chunks covering the text.
14
+ *
15
+ * Mirrors highlight-words-core behavior used by react-highlight-words with
16
+ * autoEscape enabled and caseSensitive disabled.
17
+ */
18
+ export const findHighlightChunks = (
19
+ textToHighlight: string,
20
+ searchWords: string[],
21
+ ): HighlightChunk[] => {
22
+ const text = textToHighlight || ''
23
+
24
+ const matchRanges = searchWords
25
+ .filter((searchWord) => searchWord)
26
+ .reduce<{ start: number, end: number }[]>((chunks, searchWord) => {
27
+ const regex = new RegExp(escapeRegExp(searchWord), 'gi')
28
+ let match: RegExpExecArray | null
29
+
30
+ while ((match = regex.exec(text))) {
31
+ const start = match.index
32
+ const end = regex.lastIndex
33
+
34
+ if (end > start) {
35
+ chunks.push({ start, end })
36
+ }
37
+
38
+ // Prevent zero-length match infinite loops (e.g. Firefox)
39
+ if (match.index === regex.lastIndex) {
40
+ regex.lastIndex++
41
+ }
42
+ }
43
+
44
+ return chunks
45
+ }, [])
46
+
47
+ const combined = matchRanges
48
+ .sort((first, second) => first.start - second.start)
49
+ .reduce<{ start: number, end: number }[]>((processedChunks, nextChunk) => {
50
+ if (processedChunks.length === 0) {
51
+ return [nextChunk]
52
+ }
53
+
54
+ const prevChunk = processedChunks.pop() as { start: number, end: number }
55
+
56
+ // Merge overlapping or adjacent ranges (start <= prev end)
57
+ if (nextChunk.start <= prevChunk.end) {
58
+ processedChunks.push({
59
+ start: prevChunk.start,
60
+ end: Math.max(prevChunk.end, nextChunk.end),
61
+ })
62
+ } else {
63
+ processedChunks.push(prevChunk, nextChunk)
64
+ }
65
+
66
+ return processedChunks
67
+ }, [])
68
+
69
+ const allChunks: HighlightChunk[] = []
70
+ const append = (start: number, end: number, highlight: boolean) => {
71
+ if (end - start > 0) {
72
+ allChunks.push({ start, end, highlight })
73
+ }
74
+ }
75
+
76
+ if (combined.length === 0) {
77
+ append(0, text.length, false)
78
+ } else {
79
+ let lastIndex = 0
80
+ combined.forEach((chunk) => {
81
+ append(lastIndex, chunk.start, false)
82
+ append(chunk.start, chunk.end, true)
83
+ lastIndex = chunk.end
84
+ })
85
+ append(lastIndex, text.length, false)
86
+ }
87
+
88
+ return allChunks
89
+ }