playbook_ui 16.13.0.pre.rc.0 → 16.13.0.pre.rc.1

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 (42) 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/pb_form_validation.js +12 -7
  14. data/app/pb_kits/playbook/pb_text_input/text_input.html.erb +7 -9
  15. data/app/pb_kits/playbook/pb_text_input/text_input.rb +4 -0
  16. data/app/pb_kits/playbook/pb_timestamp/_timestamp.tsx +2 -2
  17. data/app/pb_kits/playbook/pb_timestamp/docs/_timestamp_updated.html.erb +19 -0
  18. data/app/pb_kits/playbook/pb_timestamp/docs/_timestamp_updated.jsx +21 -0
  19. data/app/pb_kits/playbook/pb_timestamp/docs/_timestamp_updated.md +3 -1
  20. data/app/pb_kits/playbook/pb_timestamp/timestamp.test.js +42 -0
  21. data/dist/chunks/vendor.js +1 -1
  22. data/dist/menu.yml +0 -6
  23. data/dist/playbook-rails.js +1 -1
  24. data/dist/playbook.css +1 -1
  25. data/lib/playbook/forms/builder/form_field_builder.rb +2 -0
  26. data/lib/playbook/version.rb +1 -1
  27. metadata +4 -17
  28. data/app/pb_kits/playbook/pb_icon_button/_icon_button.scss +0 -78
  29. data/app/pb_kits/playbook/pb_icon_button/_icon_button.tsx +0 -75
  30. data/app/pb_kits/playbook/pb_icon_button/docs/_icon_button_click.jsx +0 -13
  31. data/app/pb_kits/playbook/pb_icon_button/docs/_icon_button_default.html.erb +0 -2
  32. data/app/pb_kits/playbook/pb_icon_button/docs/_icon_button_default.jsx +0 -17
  33. data/app/pb_kits/playbook/pb_icon_button/docs/_icon_button_sizes.html.erb +0 -27
  34. data/app/pb_kits/playbook/pb_icon_button/docs/_icon_button_sizes.jsx +0 -63
  35. data/app/pb_kits/playbook/pb_icon_button/docs/_playground.json +0 -135
  36. data/app/pb_kits/playbook/pb_icon_button/docs/_playground.overrides.json +0 -131
  37. data/app/pb_kits/playbook/pb_icon_button/docs/example.yml +0 -10
  38. data/app/pb_kits/playbook/pb_icon_button/docs/index.js +0 -3
  39. data/app/pb_kits/playbook/pb_icon_button/icon_button.html.erb +0 -17
  40. data/app/pb_kits/playbook/pb_icon_button/icon_button.rb +0 -25
  41. data/app/pb_kits/playbook/pb_icon_button/icon_button.test.jsx +0 -39
  42. 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: f88b832ff7611e88af8ad226ab93b3a482b433e054224ebc50e160b0408b4507
4
- data.tar.gz: 3823106c98aee4b2f75df5a55b323e4f0053f3601bbe89c26de6a9b8805abee3
3
+ metadata.gz: 888568d7c9aa8f1d5944438d4bb6f2916650c9f7bd80d32c0d912a2b209118f5
4
+ data.tar.gz: c97e4908f8ae4da80bf23994a4994e4f5647fb05f426f913cd74710274826026
5
5
  SHA512:
6
- metadata.gz: 61f636f888ecc07287dbbd756660c9ee48c699ff058359360ff943fca29b84ece948acb08fccbd460f8592958358eb191480ce1539a1f979a37094f52ddd7252
7
- data.tar.gz: 2ecfc8e99e535d7eba067ad09bcb30cdac8c3be243e4ee8f3a94be3a00bec5d042f3d256c31894da51e70c3a3359a70c317b48dd77e90f5ca3ce2ac40c69381f
6
+ metadata.gz: bed14c811f50da03eaaef7847b5ad6310e41f7cf121ce61d22c45715222edcc28c8b6da11c02a598cdf1cfd175a5c3f19c4d2f8a327d866cad41375cd37c5c84
7
+ data.tar.gz: 1d58072414ac82162cec930707d1182ac3c4d0f8ca64d4febf5e521091e43c78b5d298678683112354a60ee8c40f3d36f43cfcebea60640a91fdd20c7dfb6465
@@ -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"}
@@ -4,6 +4,7 @@ import { debounce } from '../utilities/object'
4
4
  // Kit selectors
5
5
  const KIT_SELECTOR = '[class^="pb_"][class*="_kit"]'
6
6
  const ERROR_MESSAGE_SELECTOR = '.pb_body_kit_negative'
7
+ const MESSAGE_CONTAINER_SELECTOR = '[data-pb-validation-container="true"]'
7
8
 
8
9
  // Validation selectors
9
10
  const FORM_SELECTOR = 'form[data-pb-form-validation="true"]'
@@ -63,8 +64,8 @@ class PbFormValidation extends PbEnhancedElement {
63
64
  }
64
65
 
65
66
  showValidationMessage(target) {
66
- const { parentElement } = target
67
- const kitElement = parentElement.closest(KIT_SELECTOR)
67
+ const messageContainer = this.messageContainerFor(target)
68
+ const kitElement = messageContainer.closest(KIT_SELECTOR)
68
69
 
69
70
  // FIX: Add null check for kitElement
70
71
  if (!kitElement) return
@@ -89,20 +90,24 @@ class PbFormValidation extends PbEnhancedElement {
89
90
  errorMessageContainer.innerHTML = target.validationMessage
90
91
 
91
92
  // add the error message element to the dom tree
92
- parentElement.appendChild(errorMessageContainer)
93
+ messageContainer.appendChild(errorMessageContainer)
93
94
  }
94
95
  }
95
96
 
96
97
  clearError(target) {
97
- const { parentElement } = target
98
- const kitElement = parentElement.closest(KIT_SELECTOR)
98
+ const messageContainer = this.messageContainerFor(target)
99
+ const kitElement = messageContainer.closest(KIT_SELECTOR)
99
100
  // Remove error class from kit element
100
101
  if (kitElement) kitElement.classList.remove('error')
101
- // Remove error message from parent element
102
- const errorMessageContainer = parentElement.querySelector(ERROR_MESSAGE_SELECTOR)
102
+ // Remove error message from the message container (which is the parent element most of thetime)
103
+ const errorMessageContainer = messageContainer.querySelector(ERROR_MESSAGE_SELECTOR)
103
104
  if (errorMessageContainer) errorMessageContainer.remove()
104
105
  }
105
106
 
107
+ messageContainerFor(target) {
108
+ return target.closest(MESSAGE_CONTAINER_SELECTOR) || target.parentElement
109
+ }
110
+
106
111
  // Check if there are phone number input errors
107
112
  hasPhoneNumberValidationErrors() {
108
113
  const phoneNumberErrors = this.element.querySelectorAll(PHONE_NUMBER_VALIDATION_ERROR_SELECTOR)
@@ -11,19 +11,17 @@
11
11
  </label>
12
12
  <% end %>
13
13
 
14
- <%= content_tag(:div, class: "#{add_on_class} text_input_wrapper") do %>
15
- <% if content.present? %>
16
- <%= content %>
17
- <% elsif has_add_on? %>
14
+ <%= content_tag(:div, class: "#{add_on_class} text_input_wrapper", data: { pb_validation_container: true }) do %>
15
+ <% field = content.presence || input_tag %>
16
+ <% if has_add_on? %>
18
17
  <%= pb_rails("text_input/add_on", props: object.add_on_props) do %>
19
- <%= input_tag %>
18
+ <%= field %>
20
19
  <% end %>
21
- <% elsif mask.present? %>
22
- <%= input_tag %>
23
- <%= tag(:input, data: "sanitized-pb-input", id: sanitized_id, name: object.name, style: "display: none;") %>
24
20
  <% else %>
25
- <%= input_tag %>
21
+ <%= field %>
26
22
  <% end %>
23
+ <%# A masked input gives up its name, so the sanitized value is submitted through this hidden input. %>
24
+ <%= sanitized_input_tag if mask.present? && content.blank? %>
27
25
  <%= pb_rails("body", props: {dark: object.dark, status: "negative", text: object.error, id: object.error_id, aria: { atomic: "true", live: "polite" }, html_options: { role: "alert" }}) if object.error %>
28
26
  <% end %>
29
27
  <% end %>
@@ -52,6 +52,10 @@ module Playbook
52
52
  tag(:input, all_input_options)
53
53
  end
54
54
 
55
+ def sanitized_input_tag
56
+ tag(:input, data: "sanitized-pb-input", id: sanitized_id, name: name, style: "display: none;")
57
+ end
58
+
55
59
  def has_add_on?
56
60
  add_on.present?
57
61
  end
@@ -92,8 +92,8 @@ const Timestamp = (props: TimestampProps): React.ReactElement => {
92
92
  const finalUpdatedString = []
93
93
  if (shouldShowUser) finalUpdatedString.push(`by ${text}`)
94
94
  if (showDate && !showTime) finalUpdatedString.push(`on ${baseDateDisplay()}`)
95
- if (showDate && showTime) finalUpdatedString.push(`on ${baseDateDisplay()} at ${timeDisplay}`)
96
- if (showTime && !showDate) finalUpdatedString.push(`at ${timeDisplay}`)
95
+ if (showDate && showTime) finalUpdatedString.push(`on ${baseDateDisplay()} at ${fullTimeDisplay()}`)
96
+ if (showTime && !showDate) finalUpdatedString.push(`at ${fullTimeDisplay()}`)
97
97
  return `Last updated ${finalUpdatedString.join(' ')}`
98
98
  }
99
99
 
@@ -12,3 +12,22 @@
12
12
  variant: "updated",
13
13
  show_user: false
14
14
  }) %>
15
+
16
+ <br>
17
+
18
+ <%= pb_rails("timestamp", props: {
19
+ show_timezone: true,
20
+ timestamp: DateTime.now,
21
+ timezone: "America/New_York",
22
+ variant: "updated",
23
+ }) %>
24
+
25
+ <br />
26
+
27
+ <%= pb_rails("timestamp", props: {
28
+ show_date: false,
29
+ show_timezone: true,
30
+ timestamp: DateTime.now,
31
+ timezone: "Asia/Hong_Kong",
32
+ variant: "updated",
33
+ }) %>
@@ -21,6 +21,27 @@ const TimestampUpdated = (props) => {
21
21
  variant="updated"
22
22
  {...props}
23
23
  />
24
+
25
+ <br />
26
+
27
+ <Timestamp
28
+ showTimezone
29
+ timestamp={todaysDate}
30
+ timezone="America/New_York"
31
+ variant="updated"
32
+ {...props}
33
+ />
34
+
35
+ <br />
36
+
37
+ <Timestamp
38
+ showDate={false}
39
+ showTimezone
40
+ timestamp={todaysDate}
41
+ timezone="Asia/Hong_Kong"
42
+ variant="updated"
43
+ {...props}
44
+ />
24
45
  </div>
25
46
  )
26
47
  }
@@ -2,4 +2,6 @@ Use variant `updated` to show last updated at timestamp.
2
2
 
3
3
  This variant can be customized using the optional `showUser`/`show_user` prop to show user as part of the text. When showing the user, pass in the user name using the `text` prop as shown.
4
4
 
5
- `showUser`/`show_user` is set to false by default.
5
+ `showUser`/`show_user` is set to false by default.
6
+
7
+ This variant can also display timezone when the `showTimezone`/`show_timezone` and `timezone` props are used together as instructed in the Show Timezones example above.
@@ -271,3 +271,45 @@ describe("Timestamp Kit", () => {
271
271
  expect(text?.textContent).toEqual("Last updated at 12:00a")
272
272
  })
273
273
 
274
+ test("updated variant: shows timezone when showTimezone is enabled", () => {
275
+ render(
276
+ <Timestamp
277
+ data={{ testid: testId }}
278
+ showTimezone
279
+ timestamp={new Date()}
280
+ timezone="America/New_York"
281
+ variant="updated"
282
+ />
283
+ )
284
+ const text = screen.getByTestId(testId).querySelector(".pb_caption_kit_xs")
285
+ expect(text?.textContent).toEqual("Last updated on Jan 1 at 12:00a EST")
286
+ })
287
+
288
+ test("updated variant: shows timezone with time only when showDate=false", () => {
289
+ render(
290
+ <Timestamp
291
+ data={{ testid: testId }}
292
+ showDate={false}
293
+ showTimezone
294
+ timestamp={new Date()}
295
+ timezone="America/New_York"
296
+ variant="updated"
297
+ />
298
+ )
299
+ const text = screen.getByTestId(testId).querySelector(".pb_caption_kit_xs")
300
+ expect(text?.textContent).toEqual("Last updated at 12:00a EST")
301
+ })
302
+
303
+ test("updated variant: hides timezone when showTimezone is not provided", () => {
304
+ render(
305
+ <Timestamp
306
+ data={{ testid: testId }}
307
+ timestamp={new Date()}
308
+ timezone="America/New_York"
309
+ variant="updated"
310
+ />
311
+ )
312
+ const text = screen.getByTestId(testId).querySelector(".pb_caption_kit_xs")
313
+ expect(text?.textContent).toEqual("Last updated on Jan 1 at 12:00a")
314
+ })
315
+