playbook_ui 16.1.0 → 16.2.0.pre.rc.0

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 (85) hide show
  1. checksums.yaml +4 -4
  2. data/app/pb_kits/playbook/pb_dialog/_dialog.scss +8 -6
  3. data/app/pb_kits/playbook/pb_dropdown/_dropdown.scss +6 -0
  4. data/app/pb_kits/playbook/pb_dropdown/_dropdown.tsx +37 -2
  5. data/app/pb_kits/playbook/pb_dropdown/docs/_dropdown_blank_selection_rails.md +3 -0
  6. data/app/pb_kits/playbook/pb_dropdown/docs/_dropdown_blank_selection_react.md +3 -0
  7. data/app/pb_kits/playbook/pb_dropdown/docs/_dropdown_with_clearable.html.erb +52 -0
  8. data/app/pb_kits/playbook/pb_dropdown/docs/_dropdown_with_clearable.jsx +72 -0
  9. data/app/pb_kits/playbook/pb_dropdown/docs/_dropdown_with_clearable.md +5 -0
  10. data/app/pb_kits/playbook/pb_dropdown/docs/_dropdown_with_constrain_height.jsx +33 -0
  11. data/app/pb_kits/playbook/pb_dropdown/docs/_dropdown_with_constrain_height_rails.html.erb +20 -0
  12. data/app/pb_kits/playbook/pb_dropdown/docs/_dropdown_with_constrain_height_rails.md +8 -0
  13. data/app/pb_kits/playbook/pb_dropdown/docs/_dropdown_with_constrain_height_react.md +8 -0
  14. data/app/pb_kits/playbook/pb_dropdown/docs/_dropdown_with_placeholder.html.erb +9 -0
  15. data/app/pb_kits/playbook/pb_dropdown/docs/_dropdown_with_placeholder.jsx +33 -0
  16. data/app/pb_kits/playbook/pb_dropdown/docs/_dropdown_with_placeholder.md +3 -0
  17. data/app/pb_kits/playbook/pb_dropdown/docs/example.yml +6 -0
  18. data/app/pb_kits/playbook/pb_dropdown/docs/index.js +4 -1
  19. data/app/pb_kits/playbook/pb_dropdown/dropdown.html.erb +2 -2
  20. data/app/pb_kits/playbook/pb_dropdown/dropdown.rb +6 -0
  21. data/app/pb_kits/playbook/pb_dropdown/dropdown.test.jsx +94 -0
  22. data/app/pb_kits/playbook/pb_dropdown/dropdown_container.rb +5 -1
  23. data/app/pb_kits/playbook/pb_dropdown/index.js +59 -4
  24. data/app/pb_kits/playbook/pb_dropdown/subcomponents/DropdownContainer.tsx +3 -0
  25. data/app/pb_kits/playbook/pb_dropdown/subcomponents/DropdownTrigger.tsx +2 -1
  26. data/app/pb_kits/playbook/pb_filter/Filter/SortMenu.tsx +1 -1
  27. data/app/pb_kits/playbook/pb_filter/docs/_filter_default.html.erb +2 -2
  28. data/app/pb_kits/playbook/pb_filter/docs/_filter_default.jsx +16 -9
  29. data/app/pb_kits/playbook/pb_filter/filter.rb +2 -2
  30. data/app/pb_kits/playbook/pb_form/docs/_form_with_required_indicator.html.erb +1 -0
  31. data/app/pb_kits/playbook/pb_form_pill/docs/_form_pill_truncated_text.html.erb +5 -5
  32. data/app/pb_kits/playbook/pb_form_pill/docs/_form_pill_truncated_text.jsx +4 -4
  33. data/app/pb_kits/playbook/pb_form_pill/form_pill.rb +4 -0
  34. data/app/pb_kits/playbook/pb_passphrase/_passphrase.tsx +20 -5
  35. data/app/pb_kits/playbook/pb_passphrase/docs/_passphrase_meter_settings.jsx +1 -0
  36. data/app/pb_kits/playbook/pb_passphrase/docs/_passphrase_required_indicator.html.erb +7 -0
  37. data/app/pb_kits/playbook/pb_passphrase/docs/_passphrase_required_indicator.jsx +24 -0
  38. data/app/pb_kits/playbook/pb_passphrase/docs/_passphrase_required_indicator.md +3 -0
  39. data/app/pb_kits/playbook/pb_passphrase/docs/example.yml +2 -0
  40. data/app/pb_kits/playbook/pb_passphrase/docs/index.js +1 -0
  41. data/app/pb_kits/playbook/pb_passphrase/passphrase.rb +2 -0
  42. data/app/pb_kits/playbook/pb_passphrase/passphrase.test.jsx +30 -1
  43. data/app/pb_kits/playbook/pb_phone_number_input/_phone_number_input.tsx +3 -0
  44. data/app/pb_kits/playbook/pb_phone_number_input/docs/_phone_number_input_required_indicator.html.erb +5 -0
  45. data/app/pb_kits/playbook/pb_phone_number_input/docs/_phone_number_input_required_indicator.jsx +14 -0
  46. data/app/pb_kits/playbook/pb_phone_number_input/docs/_phone_number_input_required_indicator.md +3 -0
  47. data/app/pb_kits/playbook/pb_phone_number_input/docs/example.yml +2 -0
  48. data/app/pb_kits/playbook/pb_phone_number_input/docs/index.js +1 -0
  49. data/app/pb_kits/playbook/pb_phone_number_input/phone_number_input.rb +3 -0
  50. data/app/pb_kits/playbook/pb_phone_number_input/phone_number_input.test.js +34 -3
  51. data/app/pb_kits/playbook/pb_typeahead/_typeahead.test.jsx +24 -1
  52. data/app/pb_kits/playbook/pb_typeahead/_typeahead.tsx +2 -1
  53. data/app/pb_kits/playbook/pb_typeahead/components/MultiValue.tsx +4 -1
  54. data/app/pb_kits/playbook/pb_typeahead/docs/_typeahead_truncated_text.html.erb +1 -1
  55. data/app/pb_kits/playbook/pb_typeahead/docs/_typeahead_truncated_text.jsx +1 -1
  56. data/app/pb_kits/playbook/pb_typeahead/typeahead.rb +4 -0
  57. data/dist/chunks/{_pb_line_graph-ERhTGrxH.js → _pb_line_graph-BgKF_zz1.js} +1 -1
  58. data/dist/chunks/_typeahead-Bfy-4mll.js +1 -0
  59. data/dist/chunks/{globalProps-C5qTX7aJ.js → globalProps-BhVYCqRf.js} +1 -1
  60. data/dist/chunks/{lib-B7ivt23s.js → lib-DD34ZrWL.js} +2 -2
  61. data/dist/chunks/vendor.js +3 -3
  62. data/dist/playbook-rails-react-bindings.js +1 -1
  63. data/dist/playbook-rails.js +1 -1
  64. data/dist/playbook.css +1 -1
  65. data/lib/playbook/align_content.rb +13 -3
  66. data/lib/playbook/align_items.rb +13 -3
  67. data/lib/playbook/align_self.rb +13 -3
  68. data/lib/playbook/display.rb +5 -0
  69. data/lib/playbook/flex.rb +13 -3
  70. data/lib/playbook/flex_direction.rb +13 -3
  71. data/lib/playbook/flex_grow.rb +13 -3
  72. data/lib/playbook/flex_shrink.rb +13 -3
  73. data/lib/playbook/flex_wrap.rb +13 -3
  74. data/lib/playbook/forms/builder/phone_number_field.rb +9 -0
  75. data/lib/playbook/justify_content.rb +13 -3
  76. data/lib/playbook/justify_self.rb +13 -3
  77. data/lib/playbook/order.rb +13 -3
  78. data/lib/playbook/spacing.rb +39 -9
  79. data/lib/playbook/text_align.rb +13 -3
  80. data/lib/playbook/truncate.rb +1 -1
  81. data/lib/playbook/version.rb +2 -2
  82. data/lib/playbook/vertical_align.rb +13 -3
  83. data/lib/playbook/z_index.rb +5 -0
  84. metadata +24 -6
  85. data/dist/chunks/_typeahead-DmWq2Utd.js +0 -1
@@ -48,6 +48,7 @@ export default class PbDropdown extends PbEnhancedElement {
48
48
  this.updatePills();
49
49
 
50
50
  this.clearBtn = this.element.querySelector(CLEAR_ICON_SELECTOR);
51
+ this.isClearable = this.element.dataset.pbDropdownClearable !== "false";
51
52
  if (this.clearBtn) {
52
53
  this.clearBtn.style.display = "none";
53
54
  this.clearBtn.addEventListener("click", (e) => {
@@ -60,6 +61,10 @@ export default class PbDropdown extends PbEnhancedElement {
60
61
 
61
62
  updateClearButton() {
62
63
  if (!this.clearBtn) return;
64
+ if (!this.isClearable) {
65
+ this.clearBtn.style.display = "none";
66
+ return;
67
+ }
63
68
  const hasSelection = this.isMultiSelect
64
69
  ? this.selectedOptions.size > 0
65
70
  : Boolean(this.element.querySelector(DROPDOWN_INPUT).value);
@@ -109,15 +114,51 @@ export default class PbDropdown extends PbEnhancedElement {
109
114
  adjustDropdownHeight() {
110
115
  if (this.target.classList.contains("open")) {
111
116
  const el = this.target;
117
+ const shouldConstrain = el.classList.contains("constrain_height");
112
118
  el.style.height = "auto";
113
119
  requestAnimationFrame(() => {
114
- const newHeight = el.scrollHeight + "px";
115
- el.offsetHeight; // force reflow
116
- el.style.height = newHeight;
120
+ if (shouldConstrain) {
121
+ // Calculate 18em in pixels (matches SCSS max-height: 18em)
122
+ const fontSize = parseFloat(getComputedStyle(el).fontSize) || 16;
123
+ const maxHeight = fontSize * 18;
124
+ const scrollHeight = el.scrollHeight;
125
+ const newHeight = Math.min(scrollHeight, maxHeight);
126
+ el.offsetHeight; // force reflow
127
+ el.style.height = newHeight + "px";
128
+ } else {
129
+ el.offsetHeight; // force reflow
130
+ el.style.height = el.scrollHeight + "px";
131
+ }
117
132
  });
118
133
  }
119
134
  }
120
135
 
136
+ adjustDropdownPosition(container) {
137
+ if (!container) return;
138
+
139
+ const wrapper = this.element.querySelector(".dropdown_wrapper");
140
+ if (!wrapper) return;
141
+
142
+ const wrapperRect = wrapper.getBoundingClientRect();
143
+ const h = container.getBoundingClientRect().height || container.scrollHeight;
144
+ const spaceBelow = window.innerHeight - wrapperRect.bottom;
145
+ const spaceAbove = wrapperRect.top;
146
+
147
+ // If not enough space below but enough space above, position above
148
+ if (spaceBelow < h + 10 && spaceAbove >= h + 10) {
149
+ container.style.top = "auto";
150
+ container.style.bottom = "calc(100% + 5px)";
151
+ container.style.marginTop = "0";
152
+ container.style.marginBottom = "0";
153
+ } else {
154
+ // Default: position below
155
+ container.style.top = "";
156
+ container.style.bottom = "";
157
+ container.style.marginTop = "";
158
+ container.style.marginBottom = "";
159
+ }
160
+ }
161
+
121
162
  handleSearch(term = "") {
122
163
  const lcTerm = term.toLowerCase();
123
164
  let hasMatch = false
@@ -365,7 +406,21 @@ export default class PbDropdown extends PbEnhancedElement {
365
406
  showElement(elem) {
366
407
  elem.classList.remove("close");
367
408
  elem.classList.add("open");
368
- elem.style.height = elem.scrollHeight + "px";
409
+
410
+ const shouldConstrain = elem.classList.contains("constrain_height");
411
+ if (shouldConstrain) {
412
+ // Calculate height respecting max-height constraint (18em)
413
+ const fontSize = parseFloat(getComputedStyle(elem).fontSize) || 16;
414
+ const maxHeight = fontSize * 18; // matches SCSS max-height: 18em
415
+ const scrollHeight = elem.scrollHeight;
416
+ const height = Math.min(scrollHeight, maxHeight);
417
+ elem.style.height = height + "px";
418
+ } else {
419
+ elem.style.height = elem.scrollHeight + "px";
420
+ }
421
+
422
+ // Auto-position dropdown above if not enough space below
423
+ this.adjustDropdownPosition(elem);
369
424
  }
370
425
 
371
426
  hideElement(elem) {
@@ -19,6 +19,7 @@ type DropdownContainerProps = {
19
19
  aria?: { [key: string]: string };
20
20
  children?: React.ReactChild[] | React.ReactChild;
21
21
  className?: string;
22
+ constrainHeight?: boolean;
22
23
  dark?: boolean;
23
24
  data?: { [key: string]: string };
24
25
  htmlOptions?: {[key: string]: string | number | boolean | (() => void)},
@@ -31,6 +32,7 @@ const DropdownContainer = (props: DropdownContainerProps) => {
31
32
  aria = {},
32
33
  children,
33
34
  className,
35
+ constrainHeight = false,
34
36
  dark = false,
35
37
  data = {},
36
38
  htmlOptions = {},
@@ -54,6 +56,7 @@ const DropdownContainer = (props: DropdownContainerProps) => {
54
56
  const classes = classnames(
55
57
  buildCss("pb_dropdown_container"),
56
58
  `${isDropDownClosed ? "close" : "open"}`,
59
+ constrainHeight && "constrain_height",
57
60
  globalProps(props),
58
61
  className
59
62
  );
@@ -44,6 +44,7 @@ const DropdownTrigger = (props: DropdownTriggerProps) => {
44
44
 
45
45
  const {
46
46
  autocomplete,
47
+ clearable,
47
48
  filterItem,
48
49
  handleBackspace,
49
50
  handleChange,
@@ -225,7 +226,7 @@ const DropdownTrigger = (props: DropdownTriggerProps) => {
225
226
  key={`${isDropDownClosed ? "chevron-down" : "chevron-up"}`}
226
227
  >
227
228
  {
228
- selectedArray.length > 0 && (
229
+ clearable !== false && selectedArray.length > 0 && (
229
230
  <div onClick={(e)=>{e.stopPropagation();handleBackspace()}}>
230
231
  <Icon
231
232
  cursor="pointer"
@@ -21,7 +21,7 @@ const nextValue = (value: SortValue[], name: string): SortValue => {
21
21
  }
22
22
 
23
23
  const directionIcon = (dir: Direction) => (
24
- dir == 'asc' ? 'sort-amount-up' : 'sort-amount-down'
24
+ dir == 'asc' ? 'arrow-up-short-wide' : 'arrow-down-wide-short'
25
25
  )
26
26
 
27
27
  const renderOptions = (options: SortOptions, value: SortValue[], handleChange: (arg0: SortValue) => void) => (
@@ -1,7 +1,7 @@
1
1
  <%=
2
2
  pb_rails("filter", props: {
3
3
  min_width: "360px",
4
- id: "1",
4
+ id: "filter-demo-1",
5
5
  margin_bottom: "xl",
6
6
  filters: [
7
7
  { name: "name", value: "John Wick" },
@@ -44,7 +44,7 @@
44
44
  <%=
45
45
  pb_rails("filter", props: {
46
46
  min_width: "360px",
47
- id: "def2",
47
+ id: "filter-demo-2",
48
48
  sort_menu: [
49
49
  { item: "Popularity", link: "?q[sorts]=managers_popularity+asc", active: true, direction: "desc" },
50
50
  { item: "Mananger's Title", link: "?q[sorts]=managers_title+asc", active: false },
@@ -1,4 +1,4 @@
1
- import React from 'react'
1
+ import React, { useState } from 'react'
2
2
 
3
3
  import Button from '../../pb_button/_button'
4
4
  import Filter from '../../pb_filter/_filter'
@@ -6,11 +6,18 @@ import Flex from '../../pb_flex/_flex'
6
6
  import Select from '../../pb_select/_select'
7
7
  import TextInput from '../../pb_text_input/_text_input'
8
8
 
9
- const SortingChangeCallback = (sortOptions) => {
10
- alert(JSON.stringify(sortOptions[0]))
11
- }
12
-
13
9
  const FilterDefault = (props) => {
10
+ const [sortValue, setSortValue] = useState([{ name: 'popularity', dir: 'desc' }])
11
+ const [sortValue2, setSortValue2] = useState([{ name: 'popularity', dir: 'desc' }])
12
+
13
+ const handleSortChange = (sortOptions) => {
14
+ setSortValue(sortOptions)
15
+ alert(JSON.stringify(sortOptions[0]))
16
+ }
17
+
18
+ const handleSortChange2 = (sortOptions) => {
19
+ setSortValue2(sortOptions)
20
+ }
14
21
  const options = [
15
22
  { value: 'USA' },
16
23
  { value: 'Canada' },
@@ -29,7 +36,7 @@ const FilterDefault = (props) => {
29
36
  }}
30
37
  marginBottom="xl"
31
38
  minWidth="375px"
32
- onSortChange={SortingChangeCallback}
39
+ onSortChange={handleSortChange}
33
40
  results={1}
34
41
  sortOptions={{
35
42
  popularity: 'Popularity',
@@ -38,7 +45,7 @@ const FilterDefault = (props) => {
38
45
  // eslint-disable-next-line
39
46
  manager_name: 'Manager\'s Name',
40
47
  }}
41
- sortValue={[{ name: 'popularity', dir: 'desc' }]}
48
+ sortValue={sortValue}
42
49
  {...props}
43
50
  >
44
51
  {({ closePopover }) => (
@@ -82,7 +89,7 @@ const FilterDefault = (props) => {
82
89
  <Filter
83
90
  double
84
91
  minWidth="375px"
85
- onSortChange={SortingChangeCallback}
92
+ onSortChange={handleSortChange2}
86
93
  results={0}
87
94
  sortOptions={{
88
95
  popularity: 'Popularity',
@@ -91,7 +98,7 @@ const FilterDefault = (props) => {
91
98
  // eslint-disable-next-line
92
99
  manager_name: 'Manager\'s Name',
93
100
  }}
94
- sortValue={[{ name: 'popularity', dir: 'desc' }]}
101
+ sortValue={sortValue2}
95
102
  {...props}
96
103
  >
97
104
  {({ closePopover }) => (
@@ -44,9 +44,9 @@ module Playbook
44
44
  def sort_icon(direction)
45
45
  case direction
46
46
  when "asc"
47
- "sort-amount-up"
47
+ "arrow-up-short-wide"
48
48
  when "desc"
49
- "sort-amount-down"
49
+ "arrow-down-wide-short"
50
50
  else
51
51
  ""
52
52
  end
@@ -8,6 +8,7 @@
8
8
  <%= form.search_field :example_search_field, props: { label: true, required: true, required_indicator: true } %>
9
9
  <%= form.password_field :example_password_field, props: { label: true, required: true, required_indicator: true } %>
10
10
  <%= form.url_field :example_url_field, props: { label: true, required: true, required_indicator: true } %>
11
+ <%= form.phone_number_field :example_phone_number_field, props: { label: true, required: true, required_indicator: true } %>
11
12
 
12
13
  <%= form.actions do |action| %>
13
14
  <%= action.submit %>
@@ -15,7 +15,7 @@
15
15
  options: names,
16
16
  label: "Truncation Within Typeahead",
17
17
  pills: true,
18
- truncate: 1,
18
+ truncate: "1",
19
19
  }) %>
20
20
 
21
21
  <%= pb_rails("caption", props: { text: "Form Pill Truncation" }) %>
@@ -24,19 +24,19 @@
24
24
  name: "Princess Amelia Mignonette Grimaldi Thermopolis Renaldo",
25
25
  avatar_url: "https://randomuser.me/api/portraits/women/44.jpg",
26
26
  tabindex: 0,
27
- truncate: 1,
27
+ truncate: "1",
28
28
  id: "truncation-1"
29
29
  }) %>
30
30
  <%= pb_rails("form_pill", props: {
31
31
  icon: "badge-check",
32
32
  text: "icon and a very long tag to show truncation",
33
33
  tabindex: 0,
34
- truncate: 1,
34
+ truncate: "1",
35
35
  id: "truncation-2"
36
36
  }) %>
37
37
  <%= pb_rails("form_pill", props: {
38
38
  text: "form pill long tag no tooltip show truncation",
39
39
  tabindex: 0,
40
- truncate: 1,
40
+ truncate: "1",
41
41
  }) %>
42
- <% end %>
42
+ <% end %>
@@ -21,7 +21,7 @@ const FormPillTruncatedText = (props) => {
21
21
  isMulti
22
22
  label="Truncation Within Typeahead"
23
23
  options={names}
24
- truncate={1}
24
+ truncate={"1"}
25
25
  {...props}
26
26
  />
27
27
  <Caption text="Form Pill Truncation"/>
@@ -31,20 +31,20 @@ const FormPillTruncatedText = (props) => {
31
31
  name="Princess Amelia Mignonette Grimaldi Thermopolis Renaldo"
32
32
  onClick={() => alert('Click!')}
33
33
  tabIndex={0}
34
- truncate={1}
34
+ truncate={"1"}
35
35
  />
36
36
  <FormPill
37
37
  icon="badge-check"
38
38
  onClick={() => {alert('Click!')}}
39
39
  tabIndex={0}
40
40
  text="icon and a very long tag to show truncation"
41
- truncate={1}
41
+ truncate={"1"}
42
42
  />
43
43
  <FormPill
44
44
  onClick={() => {alert('Click!')}}
45
45
  tabIndex={0}
46
46
  text="form pill with a very long tag to show truncation"
47
- truncate={1}
47
+ truncate={"1"}
48
48
  />
49
49
  </Card>
50
50
  </>
@@ -49,6 +49,10 @@ module Playbook
49
49
  def wrapped_class
50
50
  wrapped ? "wrapped" : nil
51
51
  end
52
+
53
+ def truncate_props
54
+ nil
55
+ end
52
56
  end
53
57
  end
54
58
  end
@@ -7,6 +7,7 @@ import { globalProps } from "../utilities/globalProps"
7
7
  import Body from '../pb_body/_body'
8
8
  import Caption from '../pb_caption/_caption'
9
9
  import CircleIconButton from '../pb_circle_icon_button/_circle_icon_button'
10
+ import colors from '../tokens/exports/_colors.module.scss'
10
11
  import Flex from '../pb_flex/_flex'
11
12
  import Icon from '../pb_icon/_icon'
12
13
  import PbReactPopover from '../pb_popover/_popover'
@@ -25,6 +26,7 @@ type PassphraseProps = {
25
26
  inputProps?: GenericObject,
26
27
  label?: string,
27
28
  onChange: (inputValue: string) => void,
29
+ requiredIndicator?: boolean,
28
30
  showTipsBelow?: "always" | "xs" | "sm" | "md" | "lg" | "xl",
29
31
  tips?: Array<string>,
30
32
  uncontrolled?: boolean,
@@ -43,6 +45,7 @@ const Passphrase = (props: PassphraseProps): React.ReactElement => {
43
45
  inputProps = {},
44
46
  label = confirmation ? "Confirm Passphrase" : "Passphrase",
45
47
  onChange = () => undefined,
48
+ requiredIndicator = false,
46
49
  showTipsBelow = "always",
47
50
  tips = [],
48
51
  uncontrolled = false,
@@ -99,6 +102,7 @@ const Passphrase = (props: PassphraseProps): React.ReactElement => {
99
102
 
100
103
  const shieldIcon = getAllIcons()["shieldCheck"]
101
104
  const eyeIcon = getAllIcons()["eye"]
105
+ const hasLabel = label && label !== ""
102
106
 
103
107
  return (
104
108
  <div
@@ -109,11 +113,22 @@ const Passphrase = (props: PassphraseProps): React.ReactElement => {
109
113
  id={id}
110
114
  >
111
115
  <label>
112
- <Flex align="baseline">
113
- <Caption
114
- className="passphrase-label"
115
- text={label}
116
- />
116
+ <Flex
117
+ align="baseline"
118
+ {...(hasLabel ? { marginBottom: "xs" } : {})}
119
+ >
120
+ {hasLabel && (requiredIndicator ? (
121
+ <Caption
122
+ className="passphrase-label"
123
+ >
124
+ {label} <span style={{ color: `${colors.error}` }}>*</span>
125
+ </Caption>
126
+ ) : (
127
+ <Caption
128
+ className="passphrase-label"
129
+ text={label}
130
+ />
131
+ ))}
117
132
  {tips.length > 0 && !confirmation &&
118
133
  <PbReactPopover
119
134
  className="passphrase-tips"
@@ -120,6 +120,7 @@ const PassphraseMeterSettings = (props) => {
120
120
  "These examples will all share the same input value. Type in any of the inputs to see how the strength meter changes in response to different settings."
121
121
  }
122
122
  </Body>
123
+ <br/>
123
124
  <Passphrase
124
125
  label={"Type your passphrase"}
125
126
  onChange={handleChange}
@@ -0,0 +1,7 @@
1
+ <%= pb_rails("passphrase", props: {
2
+ id: "passphrase_required_indicator",
3
+ label: "Passphrase",
4
+ placeholder: "Enter passphrase",
5
+ required_indicator: true,
6
+ value: "passphrase",
7
+ }) %>
@@ -0,0 +1,24 @@
1
+ import React, { useState } from 'react'
2
+
3
+ import Passphrase from '../_passphrase'
4
+
5
+ const PassphraseRequiredIndicator = (props) => {
6
+ const [passphrase, setPassphrase] = useState('')
7
+ const handleOnChangePassphrase = (e) => {
8
+ setPassphrase(e.target ? e.target.value : e)
9
+ }
10
+
11
+ return (
12
+ <Passphrase
13
+ id="passphrase_required_indicator"
14
+ label="Passphrase"
15
+ name="passphrase"
16
+ onChange={handleOnChangePassphrase}
17
+ requiredIndicator
18
+ value={passphrase}
19
+ {...props}
20
+ />
21
+ )
22
+ }
23
+
24
+ export default PassphraseRequiredIndicator
@@ -0,0 +1,3 @@
1
+ The `requiredIndicator`/`required_indicator` prop displays a red asterisk (*) next to the label, visually indicating that the field is required. This is purely visual and does not enforce validation.
2
+
3
+ You can use `requiredIndicator`/`required_indicator` with any validation approach: HTML5 validation via the `required` prop, client-side validation, or backend validation. For this reason, it works independently and doesn't need to be paired with the `required` prop.
@@ -9,6 +9,7 @@ examples:
9
9
  - passphrase_strength_change: Strength Change
10
10
  - passphrase_common: Common Passphrases
11
11
  - passphrase_breached: Breached Passphrases
12
+ - passphrase_required_indicator: Required Indicator
12
13
 
13
14
  react:
14
15
  - passphrase_default: Default
@@ -19,3 +20,4 @@ examples:
19
20
  - passphrase_strength_change: Strength Change
20
21
  - passphrase_common: Common Passphrases
21
22
  - passphrase_breached: Breached Passphrases
23
+ - passphrase_required_indicator: Required Indicator
@@ -6,3 +6,4 @@ export { default as PassphraseTips } from './_passphrase_tips'
6
6
  export { default as PassphraseStrengthChange } from './_passphrase_strength_change'
7
7
  export { default as PassphraseCommon } from './_passphrase_common'
8
8
  export { default as PassphraseBreached } from './_passphrase_breached'
9
+ export { default as PassphraseRequiredIndicator } from './_passphrase_required_indicator.jsx'
@@ -10,6 +10,7 @@ module Playbook
10
10
  values: %w[always xs sm md lg xl],
11
11
  default: "always"
12
12
  prop :tips, type: Playbook::Props::Array, default: []
13
+ prop :required_indicator, type: Playbook::Props::Boolean, default: false
13
14
  prop :value, type: Playbook::Props::String
14
15
 
15
16
  def classname
@@ -23,6 +24,7 @@ module Playbook
23
24
  confirmation: confirmation,
24
25
  inputProps: input_props,
25
26
  label: label,
27
+ requiredIndicator: required_indicator,
26
28
  showTipsBelow: show_tips_below,
27
29
  tips: tips,
28
30
  uncontrolled: true,
@@ -1,5 +1,5 @@
1
1
  import React from 'react'
2
- import { render, screen } from '../utilities/test-utils'
2
+ import { render, screen, within } from '../utilities/test-utils'
3
3
  import { Passphrase } from 'playbook-ui'
4
4
 
5
5
  const testId = 'text-input1',
@@ -86,3 +86,32 @@ test('popover target does not show when tips are not given', () => {
86
86
  const kit = screen.getByTestId(testId)
87
87
  expect(kit.querySelector('[class^=pb_popover_reference_wrapper]')).toBeNull()
88
88
  })
89
+
90
+ test('renders required indicator asterisk when requiredIndicator is true', () => {
91
+ render(
92
+ <Passphrase
93
+ data={{ testid: testId }}
94
+ label="Passphrase"
95
+ requiredIndicator
96
+ />
97
+ )
98
+
99
+ const kit = screen.getByTestId(testId)
100
+ const label = within(kit).getByText(/Passphrase/)
101
+ expect(label).toBeInTheDocument()
102
+ expect(kit).toHaveTextContent('*')
103
+ })
104
+
105
+ test('does not render required indicator asterisk when requiredIndicator is false', () => {
106
+ render(
107
+ <Passphrase
108
+ data={{ testid: testId }}
109
+ label="Passphrase"
110
+ />
111
+ )
112
+
113
+ const kit = screen.getByTestId(testId)
114
+ const label = within(kit).getByText(/Passphrase/)
115
+ expect(label).toBeInTheDocument()
116
+ expect(kit).not.toHaveTextContent('*')
117
+ })
@@ -36,6 +36,7 @@ type PhoneNumberInputProps = {
36
36
  excludeCountries: string[],
37
37
  preferredCountries?: string[],
38
38
  required?: boolean,
39
+ requiredIndicator?: boolean,
39
40
  value?: string,
40
41
  formatAsYouType?: boolean,
41
42
  strictMode?: boolean,
@@ -91,6 +92,7 @@ const PhoneNumberInput = (props: PhoneNumberInputProps, ref?: React.Ref<unknown>
91
92
  onlyCountries = [],
92
93
  excludeCountries = [],
93
94
  required = false,
95
+ requiredIndicator = false,
94
96
  preferredCountries = [],
95
97
  value = "",
96
98
  formatAsYouType = false,
@@ -533,6 +535,7 @@ const PhoneNumberInput = (props: PhoneNumberInputProps, ref?: React.Ref<unknown>
533
535
  validateErrors()
534
536
  },
535
537
  onChange: formatAsYouType ? undefined : handleOnChange,
538
+ requiredIndicator,
536
539
  value: inputValue
537
540
  }
538
541
 
@@ -0,0 +1,5 @@
1
+ <%= pb_rails("phone_number_input", props: {
2
+ id: "phone_number_input_required_indicator",
3
+ label: "Required Phone Number",
4
+ required_indicator: true,
5
+ }) %>
@@ -0,0 +1,14 @@
1
+ import React from 'react'
2
+ import PhoneNumberInput from '../../pb_phone_number_input/_phone_number_input'
3
+
4
+ const PhoneNumberInputRequiredIndicator = (props) => (
5
+ <>
6
+ <PhoneNumberInput
7
+ id='phone_number_input_required_indicator'
8
+ label='Phone Number'
9
+ requiredIndicator
10
+ {...props} />
11
+ </>
12
+ )
13
+
14
+ export default PhoneNumberInputRequiredIndicator
@@ -0,0 +1,3 @@
1
+ The `requiredIndicator`/`required_indicator` prop displays a red asterisk (*) next to the label, visually indicating that the field is required. This is purely visual and does not enforce validation.
2
+
3
+ You can use `requiredIndicator`/`required_indicator` with any validation approach: HTML5 validation via the `required` prop, client-side validation, or backend validation. For this reason, it works independently and doesn't need to be paired with the `required` prop.
@@ -12,6 +12,7 @@ examples:
12
12
  - phone_number_input_format: Format as You Type
13
13
  - phone_number_input_strict_mode: Strict Mode
14
14
  - phone_number_input_country_search: Country Search
15
+ - phone_number_input_required_indicator: Required Indicator
15
16
 
16
17
  rails:
17
18
  - phone_number_input_default: Default
@@ -24,3 +25,4 @@ examples:
24
25
  - phone_number_input_strict_mode: Strict Mode
25
26
  - phone_number_input_hidden_inputs: Hidden Inputs
26
27
  - phone_number_input_country_search: Country Search
28
+ - phone_number_input_required_indicator: Required Indicator
@@ -9,3 +9,4 @@ export { default as PhoneNumberInputAccessInputElement } from './_phone_number_i
9
9
  export { default as PhoneNumberInputFormat } from './_phone_number_input_format'
10
10
  export { default as PhoneNumberInputStrictMode } from './_phone_number_input_strict_mode'
11
11
  export { default as PhoneNumberInputCountrySearch } from './_phone_number_input_country_search'
12
+ export { default as PhoneNumberInputRequiredIndicator } from './_phone_number_input_required_indicator.jsx'
@@ -7,6 +7,8 @@ module Playbook
7
7
  default: false
8
8
  prop :required, type: Playbook::Props::Boolean,
9
9
  default: false
10
+ prop :required_indicator, type: Playbook::Props::Boolean,
11
+ default: false
10
12
  prop :initial_country, type: Playbook::Props::String,
11
13
  default: ""
12
14
  prop :label, type: Playbook::Props::String,
@@ -52,6 +54,7 @@ module Playbook
52
54
  excludeCountries: exclude_countries,
53
55
  preferredCountries: preferred_countries,
54
56
  required: required,
57
+ requiredIndicator: required_indicator,
55
58
  value: value,
56
59
  countrySearch: country_search,
57
60
  }
@@ -1,5 +1,5 @@
1
1
  import React from "react";
2
- import { render, screen, act } from "../utilities/test-utils";
2
+ import { render, screen, act, within } from "../utilities/test-utils";
3
3
  import PhoneNumberInput from "./_phone_number_input";
4
4
 
5
5
  const testId = "phoneNumberInput";
@@ -129,7 +129,7 @@ test("should format phone number as '555-555-5555' with formatAsYouType and 'us'
129
129
  };
130
130
 
131
131
  render(<PhoneNumberInput {...props} />);
132
-
132
+
133
133
  const input = screen.getByRole("textbox");
134
134
 
135
135
  act(() => {
@@ -154,7 +154,38 @@ test("should pass countrySearch prop to component", () => {
154
154
  };
155
155
 
156
156
  render(<PhoneNumberInput {...props} />);
157
-
157
+
158
158
  const wrapper = screen.getByTestId('phone-input-with-search');
159
159
  expect(wrapper).toBeInTheDocument();
160
160
  });
161
+
162
+ test("renders required indicator asterisk when requiredIndicator is true", () => {
163
+ const props = {
164
+ data: { testid: testId },
165
+ id: testId,
166
+ label: "Required Phone Number",
167
+ requiredIndicator: true,
168
+ };
169
+
170
+ render(<PhoneNumberInput {...props} />);
171
+
172
+ const kit = screen.getByTestId(testId);
173
+ const label = within(kit).getByText(/Required Phone Number/);
174
+ expect(label).toBeInTheDocument();
175
+ expect(kit).toHaveTextContent("*");
176
+ });
177
+
178
+ test("does not render required indicator asterisk when requiredIndicator is false", () => {
179
+ const props = {
180
+ data: { testid: testId },
181
+ id: testId,
182
+ label: "Phone Number",
183
+ };
184
+
185
+ render(<PhoneNumberInput {...props} />);
186
+
187
+ const kit = screen.getByTestId(testId);
188
+ const label = within(kit).getByText(/Phone Number/);
189
+ expect(label).toBeInTheDocument();
190
+ expect(kit).not.toHaveTextContent("*");
191
+ });