playbook_ui 13.30.0 → 13.31.0.pre.alpha.PBNTR342navtabbing3230

Sign up to get free protection for your applications and to get access to all the features.
Files changed (88) hide show
  1. checksums.yaml +4 -4
  2. data/app/pb_kits/playbook/pb_advanced_table/docs/_advanced_table_beta_sort.html.erb +59 -0
  3. data/app/pb_kits/playbook/pb_advanced_table/docs/_advanced_table_beta_sort.md +6 -0
  4. data/app/pb_kits/playbook/pb_advanced_table/docs/example.yml +3 -2
  5. data/app/pb_kits/playbook/pb_advanced_table/table_header.html.erb +4 -4
  6. data/app/pb_kits/playbook/pb_button/_button.scss +3 -3
  7. data/app/pb_kits/playbook/pb_button/_button_mixins.scss +3 -2
  8. data/app/pb_kits/playbook/pb_caption/_caption_mixin.scss +1 -1
  9. data/app/pb_kits/playbook/pb_card/_card.tsx +4 -3
  10. data/app/pb_kits/playbook/pb_collapsible/index.js +6 -1
  11. data/app/pb_kits/playbook/pb_dashboard/commonSettings.js +1 -1
  12. data/app/pb_kits/playbook/pb_dashboard/pbChartsDarkTheme.ts +1 -1
  13. data/app/pb_kits/playbook/pb_dashboard/pbChartsLightTheme.ts +1 -1
  14. data/app/pb_kits/playbook/pb_date_time_stacked/docs/_date_time_stacked_default_swift.md +33 -0
  15. data/app/pb_kits/playbook/pb_date_time_stacked/docs/_date_time_stacked_props_swift.md +18 -0
  16. data/app/pb_kits/playbook/pb_date_time_stacked/docs/example.yml +6 -1
  17. data/app/pb_kits/playbook/pb_date_year_stacked/docs/_date_year_stacked_default.jsx +4 -1
  18. data/app/pb_kits/playbook/pb_draggable/_draggable.scss +2 -4
  19. data/app/pb_kits/playbook/pb_draggable/context/index.tsx +84 -50
  20. data/app/pb_kits/playbook/pb_draggable/context/types.ts +31 -0
  21. data/app/pb_kits/playbook/pb_draggable/docs/_draggable_default.jsx +15 -19
  22. data/app/pb_kits/playbook/pb_draggable/docs/_draggable_default.md +5 -3
  23. data/app/pb_kits/playbook/pb_draggable/docs/_draggable_multiple_containers.jsx +5 -1
  24. data/app/pb_kits/playbook/pb_draggable/docs/_draggable_with_cards.jsx +11 -4
  25. data/app/pb_kits/playbook/pb_draggable/docs/_draggable_with_cards.md +8 -2
  26. data/app/pb_kits/playbook/pb_draggable/docs/_draggable_with_list.jsx +2 -2
  27. data/app/pb_kits/playbook/pb_draggable/docs/_draggable_with_list.md +5 -1
  28. data/app/pb_kits/playbook/pb_draggable/docs/_draggable_with_selectable_list.jsx +6 -4
  29. data/app/pb_kits/playbook/pb_draggable/docs/_draggable_with_selectable_list.md +5 -0
  30. data/app/pb_kits/playbook/pb_draggable/draggable.test.jsx +142 -15
  31. data/app/pb_kits/playbook/pb_draggable/subcomponents/DraggableItem.tsx +6 -5
  32. data/app/pb_kits/playbook/pb_dropdown/_dropdown.scss +18 -0
  33. data/app/pb_kits/playbook/pb_dropdown/_dropdown.tsx +228 -218
  34. data/app/pb_kits/playbook/pb_dropdown/docs/_dropdown_error.html.erb +9 -0
  35. data/app/pb_kits/playbook/pb_dropdown/docs/_dropdown_error.jsx +34 -0
  36. data/app/pb_kits/playbook/pb_dropdown/docs/example.yml +2 -0
  37. data/app/pb_kits/playbook/pb_dropdown/docs/index.js +2 -1
  38. data/app/pb_kits/playbook/pb_dropdown/dropdown.html.erb +16 -12
  39. data/app/pb_kits/playbook/pb_dropdown/dropdown.rb +9 -0
  40. data/app/pb_kits/playbook/pb_dropdown/dropdown.test.jsx +17 -2
  41. data/app/pb_kits/playbook/pb_dropdown/index.js +33 -4
  42. data/app/pb_kits/playbook/pb_fixed_confirmation_toast/_fixed_confirmation_toast.tsx +9 -3
  43. data/app/pb_kits/playbook/pb_fixed_confirmation_toast/docs/_fixed_confirmation_toast_custom_icon.html.erb +32 -0
  44. data/app/pb_kits/playbook/pb_fixed_confirmation_toast/docs/_fixed_confirmation_toast_custom_icon.jsx +48 -0
  45. data/app/pb_kits/playbook/pb_fixed_confirmation_toast/docs/example.yml +2 -0
  46. data/app/pb_kits/playbook/pb_fixed_confirmation_toast/docs/index.js +1 -0
  47. data/app/pb_kits/playbook/pb_fixed_confirmation_toast/fixed_confirmation_toast.rb +16 -11
  48. data/app/pb_kits/playbook/pb_fixed_confirmation_toast/fixed_confirmation_toast.test.js +72 -0
  49. data/app/pb_kits/playbook/pb_form/docs/_form_form_with_validate.html.erb +9 -0
  50. data/app/pb_kits/playbook/pb_form_pill/_form_pill.scss +1 -1
  51. data/app/pb_kits/playbook/pb_icon/_icon.tsx +37 -6
  52. data/app/pb_kits/playbook/pb_icon/docs/_icon_default.html.erb +1 -1
  53. data/app/pb_kits/playbook/pb_icon/docs/_icon_default.jsx +2 -3
  54. data/app/pb_kits/playbook/pb_icon/icon.rb +21 -1
  55. data/app/pb_kits/playbook/pb_icon/icon_aliases.json +39 -0
  56. data/app/pb_kits/playbook/pb_list/_list.tsx +4 -4
  57. data/app/pb_kits/playbook/pb_list/_list_item.tsx +7 -3
  58. data/app/pb_kits/playbook/pb_nav/docs/_collapsible_nav.html.erb +1 -1
  59. data/app/pb_kits/playbook/pb_nav/docs/_collapsible_nav.jsx +1 -0
  60. data/app/pb_kits/playbook/pb_nav/docs/_collapsible_nav.md +3 -1
  61. data/app/pb_kits/playbook/pb_nav/docs/_tab_nav.html.erb +12 -0
  62. data/app/pb_kits/playbook/pb_nav/docs/_tab_nav.html.md +0 -0
  63. data/app/pb_kits/playbook/pb_nav/docs/example.yml +1 -0
  64. data/app/pb_kits/playbook/pb_nav/index.js +43 -0
  65. data/app/pb_kits/playbook/pb_nav/item.html.erb +1 -1
  66. data/app/pb_kits/playbook/pb_nav/item.rb +1 -0
  67. data/app/pb_kits/playbook/pb_nav/nav.rb +9 -0
  68. data/app/pb_kits/playbook/pb_overlay/docs/_overlay_default.html.erb +24 -0
  69. data/app/pb_kits/playbook/pb_overlay/docs/_overlay_default.md +4 -4
  70. data/app/pb_kits/playbook/pb_overlay/docs/_overlay_multi_directional.html.erb +11 -0
  71. data/app/pb_kits/playbook/pb_overlay/docs/example.yml +4 -0
  72. data/app/pb_kits/playbook/pb_overlay/overlay.html.erb +27 -0
  73. data/app/pb_kits/playbook/pb_overlay/overlay.rb +110 -0
  74. data/app/pb_kits/playbook/pb_rich_text_editor/_rich_text_editor.tsx +8 -23
  75. data/app/pb_kits/playbook/pb_selectable_list/_item.tsx +7 -3
  76. data/app/pb_kits/playbook/pb_selectable_list/_selectable_list.tsx +3 -3
  77. data/app/pb_kits/playbook/pb_table/table_header.html.erb +16 -2
  78. data/app/pb_kits/playbook/pb_timeline/_item.tsx +11 -10
  79. data/app/pb_kits/playbook/pb_timeline/_timeline.tsx +8 -6
  80. data/app/pb_kits/playbook/playbook-rails.js +3 -0
  81. data/app/pb_kits/playbook/tokens/_titles.scss +4 -4
  82. data/app/pb_kits/playbook/tokens/_typography.scss +10 -10
  83. data/app/pb_kits/playbook/utilities/globalProps.ts +1 -0
  84. data/dist/menu.yml +566 -472
  85. data/dist/playbook-rails.js +7 -7
  86. data/dist/reset.css +1 -1
  87. data/lib/playbook/version.rb +2 -2
  88. metadata +24 -5
@@ -4,6 +4,7 @@ import { buildAriaProps, buildCss, buildDataProps, buildHtmlProps } from "../uti
4
4
  import { globalProps } from "../utilities/globalProps";
5
5
  import { GenericObject } from "../types";
6
6
 
7
+ import Body from '../pb_body/_body';
7
8
  import Caption from "../pb_caption/_caption";
8
9
 
9
10
  import DropdownContainer from "./subcomponents/DropdownContainer";
@@ -13,236 +14,245 @@ import DropdownTrigger from "./subcomponents/DropdownTrigger";
13
14
  import useDropdown from "./hooks/useDropdown";
14
15
 
15
16
  import {
16
- separateChildComponents,
17
- prepareSubcomponents,
18
- handleClickOutside,
17
+ separateChildComponents,
18
+ prepareSubcomponents,
19
+ handleClickOutside,
19
20
  } from "./utilities";
20
21
 
21
22
  type DropdownProps = {
22
- aria?: { [key: string]: string };
23
- autocomplete?: boolean;
24
- children?: React.ReactChild[] | React.ReactChild | React.ReactElement[];
25
- className?: string;
26
- dark?: boolean;
27
- data?: { [key: string]: string };
28
- htmlOptions?: {[key: string]: string | number | boolean | (() => void)},
29
- id?: string;
30
- isClosed?: boolean;
31
- label?: string;
32
- onSelect?: (arg: GenericObject) => null;
33
- options: GenericObject;
34
- triggerRef?: any;
23
+ aria?: { [key: string]: string };
24
+ autocomplete?: boolean;
25
+ children?: React.ReactChild[] | React.ReactChild | React.ReactElement[];
26
+ className?: string;
27
+ dark?: boolean;
28
+ data?: { [key: string]: string };
29
+ error?: string;
30
+ htmlOptions?: { [key: string]: string | number | boolean | (() => void) },
31
+ id?: string;
32
+ isClosed?: boolean;
33
+ label?: string;
34
+ onSelect?: (arg: GenericObject) => null;
35
+ options: GenericObject;
36
+ triggerRef?: any;
35
37
  };
36
38
 
37
39
  const Dropdown = (props: DropdownProps) => {
38
- const {
39
- aria = {},
40
- autocomplete = false,
41
- children,
42
- className,
43
- dark = false,
44
- data = {},
45
- htmlOptions = {},
46
- id,
47
- isClosed = true,
48
- label,
49
- onSelect,
50
- options,
51
- triggerRef
52
- } = props;
53
-
54
- const ariaProps = buildAriaProps(aria);
55
- const dataProps = buildDataProps(data);
56
- const htmlProps = buildHtmlProps(htmlOptions);
57
- const classes = classnames(
58
- buildCss("pb_dropdown"),
59
- globalProps(props),
60
- className
61
- );
62
-
63
- const [isDropDownClosed, setIsDropDownClosed, toggleDropdown] = useDropdown(isClosed);
64
-
65
- const [filterItem, setFilterItem] = useState("");
66
- const [selected, setSelected] = useState<GenericObject>({});
67
- const [isInputFocused, setIsInputFocused] = useState(false);
68
- const [hasTriggerSubcomponent, setHasTriggerSubcomponent] = useState(true);
69
- const [hasContainerSubcomponent, setHasContainerSubcomponent] =
70
- useState(true);
71
- //state for keyboard events
72
- const [focusedOptionIndex, setFocusedOptionIndex] = useState(-1);
73
-
74
- const dropdownRef = useRef(null);
75
- const inputRef = useRef(null);
76
- const inputWrapperRef = useRef(null);
77
- const dropdownContainerRef = useRef(null);
78
-
79
- const { trigger, container, otherChildren } =
80
- separateChildComponents(children);
81
-
82
- useEffect(() => {
83
- // Set the parent element of the trigger to relative to allow for absolute positioning of the dropdown
84
- //Only needed for when useDropdown hook used with external trigger
85
- if (triggerRef?.current) {
86
- const parentElement = triggerRef.current.parentNode;
87
- if (parentElement) {
88
- parentElement.style.position = 'relative';
89
- }
90
- }
91
- // Handle clicks outside the dropdown
92
- const handleClick = handleClickOutside({
93
- inputWrapperRef,
94
- dropdownContainerRef,
95
- setIsDropDownClosed,
96
- setFocusedOptionIndex,
97
- setIsInputFocused,
98
- });
99
-
100
- window.addEventListener("click", handleClick);
101
- return () => {
102
- window.removeEventListener("click", handleClick);
103
- };
104
- }, []);
105
-
106
- useEffect(() => {
107
- setHasTriggerSubcomponent(!!trigger);
108
- setHasContainerSubcomponent(!!container);
109
- }, []);
110
-
111
- // dropdown to toggle with external control
112
- useEffect(()=> {
113
- setIsDropDownClosed(isClosed)
114
- },[isClosed])
115
-
116
- const filteredOptions = options?.filter((option: GenericObject) => {
117
- const label = typeof option.label === 'string' ? option.label.toLowerCase() : option.label;
118
- return String(label).toLowerCase().includes(filterItem.toLowerCase());
119
- }
120
- );
121
-
122
- // For keyboard accessibility: Set focus within dropdown to selected item if it exists
123
- useEffect(() => {
124
- if (!isDropDownClosed) {
125
- let newIndex = 0;
126
- if (selected && selected?.label) {
127
- const selectedIndex = filteredOptions.findIndex((option: GenericObject) => option.label === selected.label);
128
- if (selectedIndex >= 0) {
129
- newIndex = selectedIndex;
40
+ const {
41
+ aria = {},
42
+ autocomplete = false,
43
+ children,
44
+ className,
45
+ dark = false,
46
+ data = {},
47
+ error,
48
+ htmlOptions = {},
49
+ id,
50
+ isClosed = true,
51
+ label,
52
+ onSelect,
53
+ options,
54
+ triggerRef
55
+ } = props;
56
+
57
+ const ariaProps = buildAriaProps(aria);
58
+ const dataProps = buildDataProps(data);
59
+ const htmlProps = buildHtmlProps(htmlOptions);
60
+ const classes = classnames(
61
+ buildCss("pb_dropdown"),
62
+ globalProps(props),
63
+ className
64
+ );
65
+
66
+ const [isDropDownClosed, setIsDropDownClosed, toggleDropdown] = useDropdown(isClosed);
67
+
68
+ const [filterItem, setFilterItem] = useState("");
69
+ const [selected, setSelected] = useState<GenericObject>({});
70
+ const [isInputFocused, setIsInputFocused] = useState(false);
71
+ const [hasTriggerSubcomponent, setHasTriggerSubcomponent] = useState(true);
72
+ const [hasContainerSubcomponent, setHasContainerSubcomponent] =
73
+ useState(true);
74
+ //state for keyboard events
75
+ const [focusedOptionIndex, setFocusedOptionIndex] = useState(-1);
76
+
77
+ const dropdownRef = useRef(null);
78
+ const inputRef = useRef(null);
79
+ const inputWrapperRef = useRef(null);
80
+ const dropdownContainerRef = useRef(null);
81
+
82
+ const { trigger, container, otherChildren } =
83
+ separateChildComponents(children);
84
+
85
+ useEffect(() => {
86
+ // Set the parent element of the trigger to relative to allow for absolute positioning of the dropdown
87
+ //Only needed for when useDropdown hook used with external trigger
88
+ if (triggerRef?.current) {
89
+ const parentElement = triggerRef.current.parentNode;
90
+ if (parentElement) {
91
+ parentElement.style.position = 'relative';
130
92
  }
131
93
  }
132
- setFocusedOptionIndex(newIndex);
94
+ // Handle clicks outside the dropdown
95
+ const handleClick = handleClickOutside({
96
+ inputWrapperRef,
97
+ dropdownContainerRef,
98
+ setIsDropDownClosed,
99
+ setFocusedOptionIndex,
100
+ setIsInputFocused,
101
+ });
102
+
103
+ window.addEventListener("click", handleClick);
104
+ return () => {
105
+ window.removeEventListener("click", handleClick);
106
+ };
107
+ }, []);
108
+
109
+ useEffect(() => {
110
+ setHasTriggerSubcomponent(!!trigger);
111
+ setHasContainerSubcomponent(!!container);
112
+ }, []);
113
+
114
+ // dropdown to toggle with external control
115
+ useEffect(() => {
116
+ setIsDropDownClosed(isClosed)
117
+ }, [isClosed])
118
+
119
+ const filteredOptions = options?.filter((option: GenericObject) => {
120
+ const label = typeof option.label === 'string' ? option.label.toLowerCase() : option.label;
121
+ return String(label).toLowerCase().includes(filterItem.toLowerCase());
133
122
  }
134
- }, [isDropDownClosed]);
135
-
136
-
137
- const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
138
- setFilterItem(e.target.value);
139
- setIsDropDownClosed(false);
140
- };
141
-
142
- const handleOptionClick = (selectedItem: GenericObject) => {
143
- setSelected(selectedItem);
144
- setFilterItem("");
145
- setIsDropDownClosed(true);
146
- onSelect && onSelect(selectedItem);
147
- };
148
-
149
- const handleWrapperClick = () => {
150
- autocomplete && inputRef.current.focus();
151
- toggleDropdown();
152
- };
153
-
154
- const handleBackspace = () => {
155
- setSelected({});
156
- onSelect && onSelect(null);
157
- setFocusedOptionIndex(-1);
158
- };
159
-
160
- const componentsToRender = prepareSubcomponents({
161
- children,
162
- hasTriggerSubcomponent,
163
- hasContainerSubcomponent,
164
- trigger,
165
- container,
166
- otherChildren,
167
- dark
168
- });
169
-
170
-
171
- return (
172
- <div {...ariaProps}
173
- {...dataProps}
174
- {...htmlProps}
175
- className={classes}
176
- id={id}
177
- style={triggerRef ? { position: "absolute"} : { position: "relative"}}
178
- >
179
- <DropdownContext.Provider
180
- value={{
181
- autocomplete,
182
- dropdownContainerRef,
183
- filteredOptions,
184
- filterItem,
185
- focusedOptionIndex,
186
- handleBackspace,
187
- handleChange,
188
- handleOptionClick,
189
- handleWrapperClick,
190
- inputRef,
191
- inputWrapperRef,
192
- isDropDownClosed,
193
- isInputFocused,
194
- options,
195
- selected,
196
- setFocusedOptionIndex,
197
- setIsDropDownClosed,
198
- setIsInputFocused,
199
- setSelected,
200
- toggleDropdown,
201
- triggerRef
202
- }}
203
- >
204
- {label &&
205
- <Caption
206
- dark={dark}
207
- marginBottom="xs"
208
- text={label}
209
- />
123
+ );
124
+
125
+ // For keyboard accessibility: Set focus within dropdown to selected item if it exists
126
+ useEffect(() => {
127
+ if (!isDropDownClosed) {
128
+ let newIndex = 0;
129
+ if (selected && selected?.label) {
130
+ const selectedIndex = filteredOptions.findIndex((option: GenericObject) => option.label === selected.label);
131
+ if (selectedIndex >= 0) {
132
+ newIndex = selectedIndex;
133
+ }
134
+ }
135
+ setFocusedOptionIndex(newIndex);
210
136
  }
211
- <div className="dropdown_wrapper"
212
- onBlur={() => {
213
- // Debounce to delay the execution to prevent jumpiness in Focus state
214
- setTimeout(() => {
215
- if (!dropdownRef.current.contains(document.activeElement)) {
216
- setIsInputFocused(false);
217
- }
218
- }, 0);
219
- }}
220
- onFocus={() => setIsInputFocused(true)}
221
- ref={dropdownRef}
137
+ }, [isDropDownClosed]);
138
+
139
+
140
+ const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
141
+ setFilterItem(e.target.value);
142
+ setIsDropDownClosed(false);
143
+ };
144
+
145
+ const handleOptionClick = (selectedItem: GenericObject) => {
146
+ setSelected(selectedItem);
147
+ setFilterItem("");
148
+ setIsDropDownClosed(true);
149
+ onSelect && onSelect(selectedItem);
150
+ };
151
+
152
+ const handleWrapperClick = () => {
153
+ autocomplete && inputRef.current.focus();
154
+ toggleDropdown();
155
+ };
156
+
157
+ const handleBackspace = () => {
158
+ setSelected({});
159
+ onSelect && onSelect(null);
160
+ setFocusedOptionIndex(-1);
161
+ };
162
+
163
+ const componentsToRender = prepareSubcomponents({
164
+ children,
165
+ hasTriggerSubcomponent,
166
+ hasContainerSubcomponent,
167
+ trigger,
168
+ container,
169
+ otherChildren,
170
+ dark
171
+ });
172
+
173
+
174
+ return (
175
+ <div {...ariaProps}
176
+ {...dataProps}
177
+ {...htmlProps}
178
+ className={classes}
179
+ id={id}
180
+ style={triggerRef ? { position: "absolute" } : { position: "relative" }}
222
181
  >
223
- {children ? (
224
- <>
225
- {componentsToRender.map((component, index) => (
226
- <React.Fragment key={index}>{component}</React.Fragment>
227
- ))}
228
- </>
229
- ) : (
230
- <>
231
- <DropdownTrigger />
232
- <DropdownContainer>
233
- {options &&
234
- options?.map((option: GenericObject) => (
235
- <Dropdown.Option key={option.id}
236
- option={option}
237
- />
238
- ))}
239
- </DropdownContainer>
240
- </>
241
- )}
182
+ <DropdownContext.Provider
183
+ value={{
184
+ autocomplete,
185
+ dropdownContainerRef,
186
+ filteredOptions,
187
+ filterItem,
188
+ focusedOptionIndex,
189
+ handleBackspace,
190
+ handleChange,
191
+ handleOptionClick,
192
+ handleWrapperClick,
193
+ inputRef,
194
+ inputWrapperRef,
195
+ isDropDownClosed,
196
+ isInputFocused,
197
+ options,
198
+ selected,
199
+ setFocusedOptionIndex,
200
+ setIsDropDownClosed,
201
+ setIsInputFocused,
202
+ setSelected,
203
+ toggleDropdown,
204
+ triggerRef
205
+ }}
206
+ >
207
+ {label &&
208
+ <Caption
209
+ dark={dark}
210
+ marginBottom="xs"
211
+ text={label}
212
+ />
213
+ }
214
+ <div className={`dropdown_wrapper ${error ? 'error' : ''}`}
215
+ onBlur={() => {
216
+ // Debounce to delay the execution to prevent jumpiness in Focus state
217
+ setTimeout(() => {
218
+ if (!dropdownRef.current.contains(document.activeElement)) {
219
+ setIsInputFocused(false);
220
+ }
221
+ }, 0);
222
+ }}
223
+ onFocus={() => setIsInputFocused(true)}
224
+ ref={dropdownRef}
225
+ >
226
+ {children ? (
227
+ <>
228
+ {componentsToRender.map((component, index) => (
229
+ <React.Fragment key={index}>{component}</React.Fragment>
230
+ ))}
231
+ </>
232
+ ) : (
233
+ <>
234
+ <DropdownTrigger />
235
+ <DropdownContainer>
236
+ {options &&
237
+ options?.map((option: GenericObject) => (
238
+ <Dropdown.Option key={option.id}
239
+ option={option}
240
+ />
241
+ ))}
242
+ </DropdownContainer>
243
+ </>
244
+ )}
245
+
246
+ {error &&
247
+ <Body
248
+ status="negative"
249
+ text={error}
250
+ />
251
+ }
252
+ </div>
253
+ </DropdownContext.Provider>
242
254
  </div>
243
- </DropdownContext.Provider>
244
- </div>
245
- )
255
+ )
246
256
  };
247
257
 
248
258
  Dropdown.Option = DropdownOption;
@@ -0,0 +1,9 @@
1
+ <%
2
+ options = [
3
+ { label: 'United States', value: 'United States', id: 'us' },
4
+ { label: 'Canada', value: 'Canada', id: 'ca' },
5
+ { label: 'Pakistan', value: 'Pakistan', id: 'pk' },
6
+ ]
7
+ %>
8
+
9
+ <%= pb_rails("dropdown", props: { error: "Please make a valid selection", options: options }) %>
@@ -0,0 +1,34 @@
1
+ import React, { useState } from 'react'
2
+ import { Dropdown } from '../../'
3
+
4
+ const DropdownError = (props) => {
5
+ const [selectedOption, setSelectedOption] = useState()
6
+ const error = selectedOption?.value ? null : "Please make a valid selection"
7
+ const options = [
8
+ {
9
+ label: "United States",
10
+ value: "United States",
11
+ },
12
+ {
13
+ label: "Canada",
14
+ value: "Canada",
15
+ },
16
+ {
17
+ label: "Pakistan",
18
+ value: "Pakistan",
19
+ }
20
+ ]
21
+
22
+ return (
23
+ <>
24
+ <Dropdown
25
+ error={error}
26
+ onSelect={(selectedItem) => setSelectedOption(selectedItem)}
27
+ options={options}
28
+ {...props}
29
+ />
30
+ </>
31
+ )
32
+ }
33
+
34
+ export default DropdownError
@@ -7,6 +7,7 @@ examples:
7
7
  - dropdown_with_custom_display_rails: Custom Display
8
8
  - dropdown_with_custom_trigger_rails: Custom Trigger
9
9
  - dropdown_with_custom_padding: Custom Option Padding
10
+ - dropdown_error: Dropdown with Error
10
11
 
11
12
  react:
12
13
  - dropdown_default: Default
@@ -16,6 +17,7 @@ examples:
16
17
  - dropdown_with_custom_display: Custom Display
17
18
  - dropdown_with_custom_trigger: Custom Trigger
18
19
  - dropdown_with_custom_padding: Custom Option Padding
20
+ - dropdown_error: Dropdown with Error
19
21
  # - dropdown_with_autocomplete: Autocomplete
20
22
  # - dropdown_with_autocomplete_and_custom_display: Autocomplete with Custom Display
21
23
  # - dropdown_with_external_control: useDropdown Hook
@@ -8,4 +8,5 @@ export { default as DropdownWithCustomPadding } from './_dropdown_with_custom_pa
8
8
  export { default as DropdownWithLabel } from './_dropdown_with_label.jsx'
9
9
  export { default as DropdownWithExternalControl } from './_dropdown_with_external_control.jsx'
10
10
  export { default as DropdownWithHook } from './_dropdown_with_hook.jsx'
11
- export { default as DropdownSubcomponentStructure } from './_dropdown_subcomponent_structure.jsx'
11
+ export { default as DropdownSubcomponentStructure } from './_dropdown_subcomponent_structure.jsx'
12
+ export { default as DropdownError } from './_dropdown_error.jsx'
@@ -7,20 +7,24 @@
7
7
  <% if object.label.present? %>
8
8
  <%= pb_rails("caption", props: {text: object.label, margin_bottom:"xs"}) %>
9
9
  <% end %>
10
- <div class="dropdown_wrapper" style="position: relative">
11
- <input type="hidden" name="<%= object.name %>" id="dropdown-selected-option" value=""/>
12
- <% if content.present? %>
13
- <%= content.presence %>
14
- <% else %>
15
- <%= pb_rails("dropdown/dropdown_trigger") %>
16
- <%= pb_rails("dropdown/dropdown_container") do %>
17
- <% if object.options.present? %>
18
- <% object.options.each do |option| %>
19
- <%= pb_rails("dropdown/dropdown_option", props: {option: option}) %>
10
+ <div class="dropdown_wrapper<%= error_class %>" style="position: relative">
11
+ <input type="hidden" name="<%= object.name %>" id="dropdown-selected-option" value="" />
12
+ <input id="dropdown-form-validation" name="<%= object.name %>_form_validation" value="" style="display: none" <%= object.required ? "required" : ""%> />
13
+
14
+ <% if content.present? %>
15
+ <%= content.presence %>
16
+ <%= pb_rails("body", props: { status: "negative", text: object.error }) %>
17
+ <% else %>
18
+ <%= pb_rails("dropdown/dropdown_trigger") %>
19
+ <%= pb_rails("dropdown/dropdown_container") do %>
20
+ <% if object.options.present? %>
21
+ <% object.options.each do |option| %>
22
+ <%= pb_rails("dropdown/dropdown_option", props: {option: option}) %>
23
+ <% end %>
20
24
  <% end %>
21
25
  <% end %>
26
+
27
+ <%= pb_rails("body", props: { status: "negative", text: object.error }) %>
22
28
  <% end %>
23
- <% end %>
24
29
  </div>
25
30
  <% end %>
26
-
@@ -7,6 +7,9 @@ module Playbook
7
7
  default: []
8
8
  prop :label, type: Playbook::Props::String
9
9
  prop :name, type: Playbook::Props::String
10
+ prop :error, type: Playbook::Props::String
11
+ prop :required, type: Playbook::Props::Boolean,
12
+ default: false
10
13
 
11
14
  def data
12
15
  Hash(prop(:data)).merge(pb_dropdown: true)
@@ -15,6 +18,12 @@ module Playbook
15
18
  def classname
16
19
  generate_classname("pb_dropdown")
17
20
  end
21
+
22
+ private
23
+
24
+ def error_class
25
+ error.present? ? " error" : ""
26
+ end
18
27
  end
19
28
  end
20
29
  end
@@ -170,7 +170,7 @@ test('generated custom Trigger', () => {
170
170
  options={options}
171
171
  >
172
172
  <Dropdown.Trigger>
173
- <Icon icon="home" />
173
+ <Icon icon="elephant" />
174
174
  </Dropdown.Trigger>
175
175
  {options.map((option) => (
176
176
  <Dropdown.Option key={option.id}
@@ -182,7 +182,7 @@ test('generated custom Trigger', () => {
182
182
 
183
183
  const kit = screen.getByTestId(testId)
184
184
  const trigger = kit.querySelector('.pb_dropdown_trigger')
185
- const customTrigger = trigger.querySelector('.fa-home.pb_icon_kit.fa-fw')
185
+ const customTrigger = trigger.querySelector('.fa-elephant.pb_icon_kit.fa-fw')
186
186
  expect(customTrigger).toBeInTheDocument()
187
187
  })
188
188
 
@@ -204,4 +204,19 @@ test('selected option on click', () => {
204
204
  const option = kit.querySelector('.pb_dropdown_option_list')
205
205
  option.click()
206
206
  expect(option).toHaveClass('pb_dropdown_option_selected')
207
+ })
208
+
209
+ test('show error message', () => {
210
+ const errorMessage = 'Please make a valid selection'
211
+
212
+ render (
213
+ <Dropdown
214
+ data={{ testid: testId }}
215
+ error={errorMessage}
216
+ options={options}
217
+ />
218
+ )
219
+
220
+ const kit = screen.getByTestId(testId)
221
+ expect(kit).toHaveTextContent(errorMessage)
207
222
  })