playbook_ui 15.5.0.pre.alpha.play250612607 → 15.5.0.pre.alpha.play265012819
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.
- checksums.yaml +4 -4
- data/app/pb_kits/playbook/pb_background/_background.tsx +6 -6
- data/app/pb_kits/playbook/pb_background/background.test.js +5 -1
- data/app/pb_kits/playbook/pb_background/docs/_background_light.html.erb +1 -1
- data/app/pb_kits/playbook/pb_background/docs/_background_light.jsx +0 -1
- data/app/pb_kits/playbook/pb_background/docs/_background_light.md +1 -0
- data/app/pb_kits/playbook/pb_background/docs/example.yml +2 -2
- data/app/pb_kits/playbook/pb_bar_graph/_bar_graph.tsx +6 -0
- data/app/pb_kits/playbook/pb_dialog/docs/_dialog_compound_components.html.erb +31 -0
- data/app/pb_kits/playbook/pb_home_address_street/_home_address_street.tsx +34 -22
- data/app/pb_kits/playbook/pb_home_address_street/city_emphasis.html.erb +16 -12
- data/app/pb_kits/playbook/pb_home_address_street/docs/_home_address_street_default.html.erb +1 -1
- data/app/pb_kits/playbook/pb_home_address_street/none_emphasis.html.erb +16 -12
- data/app/pb_kits/playbook/pb_home_address_street/street_emphasis.html.erb +16 -12
- data/app/pb_kits/playbook/pb_legend/_legend.tsx +6 -1
- data/app/pb_kits/playbook/pb_multiple_users/_multiple_users.scss +10 -0
- data/app/pb_kits/playbook/pb_multiple_users/_multiple_users.tsx +66 -15
- data/app/pb_kits/playbook/pb_multiple_users/docs/_multiple_users_with_tooltip.jsx +42 -0
- data/app/pb_kits/playbook/pb_multiple_users/docs/_multiple_users_with_tooltip.md +1 -0
- data/app/pb_kits/playbook/pb_multiple_users/docs/example.yml +1 -0
- data/app/pb_kits/playbook/pb_multiple_users/docs/index.js +1 -0
- data/app/pb_kits/playbook/pb_multiple_users/multiple_users.test.js +25 -0
- data/app/pb_kits/playbook/pb_phone_number_input/_phone_number_input.tsx +44 -10
- data/app/pb_kits/playbook/pb_phone_number_input/docs/_phone_number_input_validation.html.erb +34 -4
- data/app/pb_kits/playbook/pb_phone_number_input/docs/_phone_number_input_validation.jsx +16 -7
- data/app/pb_kits/playbook/pb_typeahead/_typeahead.test.jsx +15 -0
- data/app/pb_kits/playbook/pb_typeahead/_typeahead.tsx +3 -0
- data/app/pb_kits/playbook/pb_typeahead/components/ClearIndicator.tsx +13 -2
- data/app/pb_kits/playbook/pb_typeahead/components/MultiValue.tsx +6 -1
- data/app/pb_kits/playbook/pb_typeahead/components/ValueContainer.tsx +34 -7
- data/app/pb_kits/playbook/pb_typeahead/docs/_typeahead_input_display.html.erb +30 -0
- data/app/pb_kits/playbook/pb_typeahead/docs/_typeahead_input_display.jsx +37 -0
- data/app/pb_kits/playbook/pb_typeahead/docs/_typeahead_input_display.md +3 -0
- data/app/pb_kits/playbook/pb_typeahead/docs/example.yml +2 -0
- data/app/pb_kits/playbook/pb_typeahead/docs/index.js +2 -1
- data/app/pb_kits/playbook/pb_typeahead/typeahead.rb +6 -1
- data/app/pb_kits/playbook/utilities/DEPRECATION_WARNINGS.md +82 -0
- data/app/pb_kits/playbook/utilities/deprecated.ts +50 -0
- data/dist/chunks/_typeahead-CLnLoImA.js +6 -0
- data/dist/chunks/vendor.js +2 -2
- data/dist/menu.yml +3 -2
- data/dist/playbook-rails-react-bindings.js +1 -1
- data/dist/playbook-rails.js +1 -1
- data/dist/playbook.css +1 -1
- data/lib/playbook/version.rb +1 -1
- metadata +11 -3
- data/dist/chunks/_typeahead-DUmTKJUc.js +0 -6
|
@@ -110,13 +110,25 @@ const PhoneNumberInput = (props: PhoneNumberInputProps, ref?: React.Ref<unknown>
|
|
|
110
110
|
const inputRef = useRef<HTMLInputElement | null>(null)
|
|
111
111
|
const itiRef = useRef<any>(null);
|
|
112
112
|
const wrapperRef = useRef<HTMLDivElement | null>(null);
|
|
113
|
+
const hasBlurredRef = useRef<boolean>(false);
|
|
114
|
+
const formSubmittedRef = useRef<boolean>(false);
|
|
113
115
|
const [inputValue, setInputValue] = useState(value)
|
|
114
116
|
const [error, setError] = useState(props.error || "")
|
|
115
117
|
const [dropDownIsOpen, setDropDownIsOpen] = useState(false)
|
|
116
118
|
const [selectedData, setSelectedData] = useState()
|
|
117
119
|
const [hasTyped, setHasTyped] = useState(false)
|
|
120
|
+
const [hasBlurred, setHasBlurred] = useState(false)
|
|
118
121
|
const [formSubmitted, setFormSubmitted] = useState(false)
|
|
119
122
|
const [hasStartedValidating, setHasStartedValidating] = useState(false)
|
|
123
|
+
|
|
124
|
+
// Keep refs in sync with state for use in event listeners
|
|
125
|
+
useEffect(() => {
|
|
126
|
+
hasBlurredRef.current = hasBlurred
|
|
127
|
+
}, [hasBlurred])
|
|
128
|
+
|
|
129
|
+
useEffect(() => {
|
|
130
|
+
formSubmittedRef.current = formSubmitted
|
|
131
|
+
}, [formSubmitted])
|
|
120
132
|
|
|
121
133
|
// Only sync initial error from props, not continuous updates
|
|
122
134
|
// Once validation starts, internal validation takes over
|
|
@@ -143,8 +155,8 @@ const PhoneNumberInput = (props: PhoneNumberInputProps, ref?: React.Ref<unknown>
|
|
|
143
155
|
}
|
|
144
156
|
|
|
145
157
|
// Determine which error to display
|
|
146
|
-
// Show internal errors
|
|
147
|
-
const shouldShowInternalError = (
|
|
158
|
+
// Show internal errors only after blur (hasBlurred) or on form submission (formSubmitted)
|
|
159
|
+
const shouldShowInternalError = (hasBlurred || formSubmitted) && error
|
|
148
160
|
const displayError = shouldShowInternalError ? error : ""
|
|
149
161
|
|
|
150
162
|
useEffect(() => {
|
|
@@ -259,7 +271,9 @@ const PhoneNumberInput = (props: PhoneNumberInputProps, ref?: React.Ref<unknown>
|
|
|
259
271
|
return
|
|
260
272
|
}
|
|
261
273
|
|
|
262
|
-
if
|
|
274
|
+
// Only validate if field has been blurred or form has been submitted
|
|
275
|
+
// Use refs here since state updates are async and we need current values
|
|
276
|
+
if (!hasBlurredRef.current && !formSubmittedRef.current) return
|
|
263
277
|
|
|
264
278
|
// Run validation checks
|
|
265
279
|
if (itiRef.current) isValid(itiRef.current.isValidNumber())
|
|
@@ -280,6 +294,7 @@ const PhoneNumberInput = (props: PhoneNumberInputProps, ref?: React.Ref<unknown>
|
|
|
280
294
|
if (phoneNumberContainer && phoneNumberContainer === wrapperRef.current) {
|
|
281
295
|
const invalidInputName = target.name || target.getAttribute('name')
|
|
282
296
|
if (invalidInputName === name) {
|
|
297
|
+
formSubmittedRef.current = true
|
|
283
298
|
setFormSubmitted(true)
|
|
284
299
|
// Trigger validation when form is submitted
|
|
285
300
|
validateErrors()
|
|
@@ -305,6 +320,9 @@ const PhoneNumberInput = (props: PhoneNumberInputProps, ref?: React.Ref<unknown>
|
|
|
305
320
|
setInputValue("")
|
|
306
321
|
setError("")
|
|
307
322
|
setHasTyped(false)
|
|
323
|
+
hasBlurredRef.current = false
|
|
324
|
+
setHasBlurred(false)
|
|
325
|
+
formSubmittedRef.current = false
|
|
308
326
|
setFormSubmitted(false)
|
|
309
327
|
setHasStartedValidating(false)
|
|
310
328
|
// Only clear validation state if field was required
|
|
@@ -322,6 +340,7 @@ const PhoneNumberInput = (props: PhoneNumberInputProps, ref?: React.Ref<unknown>
|
|
|
322
340
|
|
|
323
341
|
if (required && isEmpty) {
|
|
324
342
|
setError('Missing phone number')
|
|
343
|
+
formSubmittedRef.current = true
|
|
325
344
|
setFormSubmitted(true)
|
|
326
345
|
return 'Missing phone number'
|
|
327
346
|
}
|
|
@@ -378,6 +397,7 @@ const PhoneNumberInput = (props: PhoneNumberInputProps, ref?: React.Ref<unknown>
|
|
|
378
397
|
|
|
379
398
|
// Set the error state so the validation attribute gets added
|
|
380
399
|
setError(errorMessage)
|
|
400
|
+
formSubmittedRef.current = true
|
|
381
401
|
setFormSubmitted(true)
|
|
382
402
|
setHasTyped(true)
|
|
383
403
|
|
|
@@ -401,6 +421,7 @@ const PhoneNumberInput = (props: PhoneNumberInputProps, ref?: React.Ref<unknown>
|
|
|
401
421
|
|
|
402
422
|
// Reset form submitted state when user types
|
|
403
423
|
if (formSubmitted) {
|
|
424
|
+
formSubmittedRef.current = false
|
|
404
425
|
setFormSubmitted(false)
|
|
405
426
|
}
|
|
406
427
|
|
|
@@ -416,11 +437,15 @@ const PhoneNumberInput = (props: PhoneNumberInputProps, ref?: React.Ref<unknown>
|
|
|
416
437
|
|
|
417
438
|
setSelectedData(phoneNumberData)
|
|
418
439
|
onChange(phoneNumberData)
|
|
419
|
-
|
|
440
|
+
|
|
441
|
+
// Don't call isValid callback on change - only on blur or form submission
|
|
442
|
+
// This prevents triggering validation while typing
|
|
443
|
+
// Use refs to get current values in case this is called from event listener
|
|
444
|
+
if (hasBlurredRef.current || formSubmittedRef.current) {
|
|
445
|
+
isValid(itiRef.current.isValidNumber())
|
|
446
|
+
}
|
|
420
447
|
|
|
421
|
-
//
|
|
422
|
-
// This ensures validation state is up-to-date
|
|
423
|
-
setTimeout(() => validateErrors(), 0)
|
|
448
|
+
// Don't validate on change - only validate on blur or form submission
|
|
424
449
|
}
|
|
425
450
|
|
|
426
451
|
// Separating Concerns as React Docs Recommend
|
|
@@ -482,7 +507,12 @@ const PhoneNumberInput = (props: PhoneNumberInputProps, ref?: React.Ref<unknown>
|
|
|
482
507
|
|
|
483
508
|
setSelectedData(phoneNumberData)
|
|
484
509
|
onChange(phoneNumberData)
|
|
485
|
-
|
|
510
|
+
|
|
511
|
+
// Don't call isValid callback on change - only on blur or form submission
|
|
512
|
+
// Use refs to check current blur state in the event listener (closure issue)
|
|
513
|
+
if (hasBlurredRef.current || formSubmittedRef.current) {
|
|
514
|
+
isValid(telInputInit.isValidNumber())
|
|
515
|
+
}
|
|
486
516
|
})
|
|
487
517
|
}
|
|
488
518
|
}
|
|
@@ -492,12 +522,16 @@ const PhoneNumberInput = (props: PhoneNumberInputProps, ref?: React.Ref<unknown>
|
|
|
492
522
|
dark,
|
|
493
523
|
"data-phone-number": JSON.stringify(selectedData),
|
|
494
524
|
disabled,
|
|
495
|
-
error:
|
|
525
|
+
error: displayError || props.error || "",
|
|
496
526
|
type: 'tel',
|
|
497
527
|
id,
|
|
498
528
|
label,
|
|
499
529
|
name,
|
|
500
|
-
onBlur:
|
|
530
|
+
onBlur: () => {
|
|
531
|
+
hasBlurredRef.current = true
|
|
532
|
+
setHasBlurred(true)
|
|
533
|
+
validateErrors()
|
|
534
|
+
},
|
|
501
535
|
onChange: formatAsYouType ? undefined : handleOnChange,
|
|
502
536
|
value: inputValue
|
|
503
537
|
}
|
data/app/pb_kits/playbook/pb_phone_number_input/docs/_phone_number_input_validation.html.erb
CHANGED
|
@@ -1,12 +1,42 @@
|
|
|
1
1
|
<form id="example-form-validation" action="" method="get">
|
|
2
|
-
<%= pb_rails("phone_number_input", props: {
|
|
2
|
+
<%= pb_rails("phone_number_input", props: {
|
|
3
|
+
id: "validation",
|
|
4
|
+
initial_country: "af",
|
|
5
|
+
value: "",
|
|
6
|
+
required: true
|
|
7
|
+
}) %>
|
|
3
8
|
<%= pb_rails("button", props: {html_type: "submit", text: "Save Phone Number"}) %>
|
|
4
9
|
</form>
|
|
5
10
|
|
|
6
11
|
<%= javascript_tag do %>
|
|
7
12
|
document.addEventListener('DOMContentLoaded', function () {
|
|
8
|
-
document.querySelector('#example-form-validation')
|
|
9
|
-
|
|
10
|
-
|
|
13
|
+
const form = document.querySelector('#example-form-validation');
|
|
14
|
+
|
|
15
|
+
// Wait for React component to mount
|
|
16
|
+
function waitForComponent() {
|
|
17
|
+
const phoneInput = form.querySelector('#validation');
|
|
18
|
+
|
|
19
|
+
if (!phoneInput) {
|
|
20
|
+
setTimeout(waitForComponent, 100);
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Wait for intl-tel-input to initialize, then focus and blur to trigger validation
|
|
25
|
+
setTimeout(function() {
|
|
26
|
+
phoneInput.focus({ preventScroll: true });
|
|
27
|
+
setTimeout(function() {
|
|
28
|
+
phoneInput.blur();
|
|
29
|
+
}, 100);
|
|
30
|
+
}, 500);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
waitForComponent();
|
|
34
|
+
|
|
35
|
+
// Prevent form submission if there are validation errors
|
|
36
|
+
form.addEventListener('submit', function (e) {
|
|
37
|
+
if (e.target.querySelectorAll('[error]:not([error=""])').length > 0) {
|
|
38
|
+
e.preventDefault();
|
|
39
|
+
}
|
|
40
|
+
});
|
|
11
41
|
})
|
|
12
42
|
<% end %>
|
|
@@ -10,8 +10,19 @@ const PhoneNumberInputValidation = (props) => {
|
|
|
10
10
|
const [showFormErrors, setShowFormErrors] = useState(false);
|
|
11
11
|
const [phoneNumber, setPhoneNumber] = useState("");
|
|
12
12
|
const [countryCode, setCountryCode] = useState("af");
|
|
13
|
+
const [isValid, setIsValid] = useState(false);
|
|
14
|
+
const [hasInteracted, setHasInteracted] = useState(false);
|
|
15
|
+
|
|
16
|
+
// Start with initial error - will be cleared on blur if valid
|
|
17
|
+
const initialError = (
|
|
18
|
+
<>
|
|
19
|
+
<Icon icon="warning" /> Missing phone number.
|
|
20
|
+
</>
|
|
21
|
+
);
|
|
13
22
|
|
|
14
23
|
const handleOnValidate = (valid) => {
|
|
24
|
+
setIsValid(valid);
|
|
25
|
+
setHasInteracted(true);
|
|
15
26
|
setFormErrors(
|
|
16
27
|
valid ? "" : "Please correct the fields below and try again."
|
|
17
28
|
);
|
|
@@ -23,18 +34,16 @@ const PhoneNumberInputValidation = (props) => {
|
|
|
23
34
|
};
|
|
24
35
|
|
|
25
36
|
const handleOnSubmit = (e) => {
|
|
26
|
-
if (
|
|
37
|
+
if (!isValid) e.preventDefault()
|
|
27
38
|
}
|
|
28
39
|
|
|
29
40
|
useEffect(() => {
|
|
30
41
|
setShowFormErrors(formErrors.length > 0);
|
|
31
42
|
}, [formErrors]);
|
|
32
43
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
</>
|
|
37
|
-
)
|
|
44
|
+
// Only show error prop initially, or if invalid after interaction
|
|
45
|
+
// Clear error prop once valid (component handles validation on blur)
|
|
46
|
+
const shouldShowError = !hasInteracted || (hasInteracted && !isValid);
|
|
38
47
|
|
|
39
48
|
return (
|
|
40
49
|
<form
|
|
@@ -50,7 +59,7 @@ const PhoneNumberInputValidation = (props) => {
|
|
|
50
59
|
/>
|
|
51
60
|
)}
|
|
52
61
|
<PhoneNumberInput
|
|
53
|
-
error={
|
|
62
|
+
error={shouldShowError ? initialError : undefined}
|
|
54
63
|
id="validation"
|
|
55
64
|
initialCountry={countryCode}
|
|
56
65
|
onChange={handleOnChange}
|
|
@@ -276,3 +276,18 @@ test('multi-value badges have tabIndex and focus class when focused', () => {
|
|
|
276
276
|
})
|
|
277
277
|
})
|
|
278
278
|
|
|
279
|
+
test('input display none shows number of selected items', () => {
|
|
280
|
+
render(
|
|
281
|
+
<Typeahead
|
|
282
|
+
data={{ testid: 'input-display-none-test' }}
|
|
283
|
+
defaultValue={[options[0], options[1]]}
|
|
284
|
+
inputDisplay="none"
|
|
285
|
+
isMulti
|
|
286
|
+
options={options}
|
|
287
|
+
/>
|
|
288
|
+
)
|
|
289
|
+
|
|
290
|
+
const kit = screen.getByTestId('input-display-none-test')
|
|
291
|
+
const inputDisplayDiv = kit.querySelector(".pb_typeahead_selection_count")
|
|
292
|
+
expect(inputDisplayDiv).toHaveTextContent("2 items selected")
|
|
293
|
+
})
|
|
@@ -43,6 +43,7 @@ type TypeaheadProps = {
|
|
|
43
43
|
error?: string,
|
|
44
44
|
htmlOptions?: {[key: string]: string | number | boolean | (() => void)},
|
|
45
45
|
id?: string,
|
|
46
|
+
inputDisplay?: "pills" | "none",
|
|
46
47
|
label?: string,
|
|
47
48
|
loadOptions?: string | Noop,
|
|
48
49
|
getOptionLabel?: string | (() => string),
|
|
@@ -89,6 +90,7 @@ const Typeahead = forwardRef<HTMLInputElement, TypeaheadProps>(({
|
|
|
89
90
|
getOptionValue,
|
|
90
91
|
htmlOptions = {},
|
|
91
92
|
id,
|
|
93
|
+
inputDisplay = "pills",
|
|
92
94
|
name,
|
|
93
95
|
loadOptions = noop,
|
|
94
96
|
marginBottom = "sm",
|
|
@@ -238,6 +240,7 @@ const Typeahead = forwardRef<HTMLInputElement, TypeaheadProps>(({
|
|
|
238
240
|
getOptionValue: isString(getOptionValue) ? get(window, getOptionValue) : getOptionValue,
|
|
239
241
|
defaultOptions: true,
|
|
240
242
|
id: id || uniqueId(),
|
|
243
|
+
inputDisplay: inputDisplay,
|
|
241
244
|
inline: false,
|
|
242
245
|
isClearable: true,
|
|
243
246
|
isSearchable: true,
|
|
@@ -7,18 +7,29 @@ type ClearContainerProps = {
|
|
|
7
7
|
id: string,
|
|
8
8
|
},
|
|
9
9
|
clearValue: () => void,
|
|
10
|
+
innerProps?: any,
|
|
10
11
|
}
|
|
11
12
|
|
|
12
|
-
const ClearContainer = (props: ClearContainerProps): React.ReactElement => {
|
|
13
|
-
const { selectProps, clearValue } = props
|
|
13
|
+
const ClearContainer = (props: ClearContainerProps | any): React.ReactElement => {
|
|
14
|
+
const { selectProps, clearValue, innerProps } = props
|
|
14
15
|
useEffect(() => {
|
|
15
16
|
document.addEventListener(`pb-typeahead-kit-${selectProps.id}:clear`, clearValue)
|
|
16
17
|
}, [clearValue, selectProps.id])
|
|
17
18
|
|
|
19
|
+
// To stop this from bubbling up when inside a dialog or other modal
|
|
20
|
+
const handleMouseDown = (event: React.MouseEvent) => {
|
|
21
|
+
event.stopPropagation()
|
|
22
|
+
innerProps?.onMouseDown?.(event)
|
|
23
|
+
}
|
|
24
|
+
|
|
18
25
|
return (
|
|
19
26
|
<components.ClearIndicator
|
|
20
27
|
className="clear_indicator"
|
|
21
28
|
{...props}
|
|
29
|
+
innerProps={{
|
|
30
|
+
...innerProps,
|
|
31
|
+
onMouseDown: handleMouseDown,
|
|
32
|
+
}}
|
|
22
33
|
/>
|
|
23
34
|
)
|
|
24
35
|
}
|
|
@@ -19,7 +19,12 @@ type Props = {
|
|
|
19
19
|
const MultiValue = (props: Props) => {
|
|
20
20
|
const { removeProps, isFocused } = props
|
|
21
21
|
const { imageUrl, label } = props.data
|
|
22
|
-
const { dark, multiKit, pillColor, truncate, wrapped } = props.selectProps
|
|
22
|
+
const { dark, multiKit, pillColor, truncate, wrapped, inputDisplay } = props.selectProps
|
|
23
|
+
|
|
24
|
+
// If inputDisplay is "none", don't render the pill/badge, just return null (the count handled in ValueContainer file)
|
|
25
|
+
if (inputDisplay === 'none') {
|
|
26
|
+
return null
|
|
27
|
+
}
|
|
23
28
|
|
|
24
29
|
const formPillProps = {
|
|
25
30
|
marginRight: 'xs',
|
|
@@ -1,15 +1,42 @@
|
|
|
1
1
|
import React from 'react'
|
|
2
2
|
import { components } from 'react-select'
|
|
3
|
+
import Body from '../../pb_body/_body'
|
|
3
4
|
|
|
4
5
|
type ValueContainerProps = {
|
|
5
|
-
children: React.ReactNode,
|
|
6
|
+
children: React.ReactNode | React.ReactNode[],
|
|
7
|
+
selectProps?: Record<string, unknown>,
|
|
8
|
+
hasValue?: boolean,
|
|
6
9
|
}
|
|
7
10
|
|
|
8
|
-
const ValueContainer = (props: ValueContainerProps): React.ReactElement =>
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
)
|
|
11
|
+
const ValueContainer = (props: ValueContainerProps | any): React.ReactElement => {
|
|
12
|
+
const { children, selectProps, hasValue } = props
|
|
13
|
+
const inputDisplay = (selectProps as any)?.inputDisplay
|
|
14
|
+
const value = (selectProps as any)?.value
|
|
15
|
+
|
|
16
|
+
// When inputDisplay is "none" and there are selected values, show count text (this is for multi-select only)
|
|
17
|
+
if (inputDisplay === 'none' && hasValue && value) {
|
|
18
|
+
const selectedCount = Array.isArray(value) ? value.length : 0
|
|
19
|
+
|
|
20
|
+
return (
|
|
21
|
+
<components.ValueContainer
|
|
22
|
+
className="text_input_value_container"
|
|
23
|
+
{...props}
|
|
24
|
+
>
|
|
25
|
+
<Body
|
|
26
|
+
className="pb_typeahead_selection_count"
|
|
27
|
+
text={`${selectedCount} item${selectedCount !== 1 ? 's' : ''} selected`}
|
|
28
|
+
/>
|
|
29
|
+
{children}
|
|
30
|
+
</components.ValueContainer>
|
|
31
|
+
)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return (
|
|
35
|
+
<components.ValueContainer
|
|
36
|
+
className="text_input_value_container"
|
|
37
|
+
{...props}
|
|
38
|
+
/>
|
|
39
|
+
)
|
|
40
|
+
}
|
|
14
41
|
|
|
15
42
|
export default ValueContainer
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
<%
|
|
2
|
+
options = [
|
|
3
|
+
{ label: 'Orange', value: '#FFA500' },
|
|
4
|
+
{ label: 'Red', value: '#FF0000' },
|
|
5
|
+
{ label: 'Green', value: '#00FF00' },
|
|
6
|
+
{ label: 'Blue', value: '#0000FF' },
|
|
7
|
+
{ label: 'Yellow', value: '#FFFF00' },
|
|
8
|
+
{ label: 'Purple', value: '#800080' },
|
|
9
|
+
{ label: 'Cyan', value: '#00FFFF' },
|
|
10
|
+
{ label: 'Magenta', value: '#FF00FF' }
|
|
11
|
+
]
|
|
12
|
+
%>
|
|
13
|
+
|
|
14
|
+
<%= pb_rails("typeahead", props: {
|
|
15
|
+
id: "typeahead-input-display-none",
|
|
16
|
+
label: "With Input Display None",
|
|
17
|
+
options: options,
|
|
18
|
+
name: :foo,
|
|
19
|
+
input_display: "none",
|
|
20
|
+
})
|
|
21
|
+
%>
|
|
22
|
+
<br/>
|
|
23
|
+
<%= pb_rails("typeahead", props: {
|
|
24
|
+
id: "typeahead-input-display-pills",
|
|
25
|
+
label: "With Input Display Pills (Default)",
|
|
26
|
+
options: options,
|
|
27
|
+
name: :foo,
|
|
28
|
+
pills: true,
|
|
29
|
+
})
|
|
30
|
+
%>
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
|
|
3
|
+
import Typeahead from '../_typeahead'
|
|
4
|
+
|
|
5
|
+
const options = [
|
|
6
|
+
{ label: 'Orange', value: '#FFA500' },
|
|
7
|
+
{ label: 'Red', value: '#FF0000' },
|
|
8
|
+
{ label: 'Green', value: '#00FF00' },
|
|
9
|
+
{ label: 'Blue', value: '#0000FF' },
|
|
10
|
+
{ label: 'Yellow', value: '#FFFF00' },
|
|
11
|
+
{ label: 'Purple', value: '#800080' },
|
|
12
|
+
{ label: 'Cyan', value: '#00FFFF' },
|
|
13
|
+
{ label: 'Magenta', value: '#FF00FF' }
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
const TypeaheadInputDisplay = (props) => {
|
|
17
|
+
return (
|
|
18
|
+
<>
|
|
19
|
+
<Typeahead
|
|
20
|
+
inputDisplay="none"
|
|
21
|
+
isMulti
|
|
22
|
+
label="With Input Display None"
|
|
23
|
+
options={options}
|
|
24
|
+
{...props}
|
|
25
|
+
/>
|
|
26
|
+
<br/>
|
|
27
|
+
<Typeahead
|
|
28
|
+
isMulti
|
|
29
|
+
label="With Input Display Pills (Default)"
|
|
30
|
+
options={options}
|
|
31
|
+
{...props}
|
|
32
|
+
/>
|
|
33
|
+
</>
|
|
34
|
+
)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export default TypeaheadInputDisplay
|
|
@@ -5,6 +5,7 @@ examples:
|
|
|
5
5
|
- typeahead_default_options: With Default Options
|
|
6
6
|
- typeahead_with_context: With Context
|
|
7
7
|
- typeahead_with_pills: With Pills
|
|
8
|
+
- typeahead_input_display: Input Display
|
|
8
9
|
- typeahead_without_pills: Without Pills (Single Select)
|
|
9
10
|
- typeahead_with_pills_async: With Pills (Async Data)
|
|
10
11
|
- typeahead_with_pills_async_users: With Pills (Async Data w/ Users)
|
|
@@ -26,6 +27,7 @@ examples:
|
|
|
26
27
|
- typeahead_react_hook: React Hook
|
|
27
28
|
- typeahead_with_highlight: With Highlight
|
|
28
29
|
- typeahead_with_pills: With Pills
|
|
30
|
+
- typeahead_input_display: Input Display
|
|
29
31
|
- typeahead_with_pills_async: With Pills (Async Data)
|
|
30
32
|
- typeahead_with_pills_async_users: With Pills (Async Data w/ Users)
|
|
31
33
|
- typeahead_with_pills_async_custom_options: With Pills (Async Data w/ Custom Options)
|
|
@@ -17,4 +17,5 @@ export { default as TypeaheadReactHook } from './_typeahead_react_hook.jsx'
|
|
|
17
17
|
export { default as TypeaheadDisabled } from './_typeahead_disabled.jsx'
|
|
18
18
|
export { default as TypeaheadPreserveInput } from './_typeahead_preserve_input.jsx'
|
|
19
19
|
export { default as TypeaheadDefaultValue } from './_typeahead_default_value.jsx'
|
|
20
|
-
export { default as TypeaheadCustomOptions } from './_typeahead_custom_options.jsx'
|
|
20
|
+
export { default as TypeaheadCustomOptions } from './_typeahead_custom_options.jsx'
|
|
21
|
+
export { default as TypeaheadInputDisplay } from './_typeahead_input_display.jsx'
|
|
@@ -25,6 +25,10 @@ module Playbook
|
|
|
25
25
|
prop :is_multi, type: Playbook::Props::Boolean,
|
|
26
26
|
default: true
|
|
27
27
|
|
|
28
|
+
prop :input_display, type: Playbook::Props::Enum,
|
|
29
|
+
values: %w[none pills],
|
|
30
|
+
default: "pills"
|
|
31
|
+
|
|
28
32
|
prop :pills, type: Playbook::Props::Boolean,
|
|
29
33
|
default: false
|
|
30
34
|
|
|
@@ -78,7 +82,7 @@ module Playbook
|
|
|
78
82
|
end
|
|
79
83
|
|
|
80
84
|
def is_react?
|
|
81
|
-
pills || !is_multi || wrapped
|
|
85
|
+
pills || !is_multi || wrapped || input_display == "none"
|
|
82
86
|
end
|
|
83
87
|
|
|
84
88
|
def typeahead_react_options
|
|
@@ -91,6 +95,7 @@ module Playbook
|
|
|
91
95
|
id: id,
|
|
92
96
|
inline: inline,
|
|
93
97
|
isMulti: is_multi,
|
|
98
|
+
inputDisplay: input_display,
|
|
94
99
|
label: label,
|
|
95
100
|
marginBottom: margin_bottom,
|
|
96
101
|
multiKit: multi_kit,
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# Deprecation Warning System for React Kits
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
This system provides runtime console warnings when deprecated Playbook kits are used in development mode. It helps developers identify deprecated kit usage in Nitro, Tempo, Runway, and other apps without impacting production builds.
|
|
6
|
+
|
|
7
|
+
## How It Works
|
|
8
|
+
|
|
9
|
+
### Key Features
|
|
10
|
+
|
|
11
|
+
✅ **Once per page load**: Each deprecated kit logs exactly one warning per page load, preventing spam on re-renders
|
|
12
|
+
✅ **Dev mode only**: No warnings in production builds (`process.env.NODE_ENV === 'production'`)
|
|
13
|
+
✅ **Platform-specific**: Can warn only for React or Rails, or both
|
|
14
|
+
✅ **Customizable messages**: Default or custom deprecation messages
|
|
15
|
+
|
|
16
|
+
## Usage
|
|
17
|
+
|
|
18
|
+
### Step 1: Import the utility
|
|
19
|
+
|
|
20
|
+
In your kit's main React component file (e.g., `_bar_graph.tsx`):
|
|
21
|
+
|
|
22
|
+
```tsx
|
|
23
|
+
import { deprecatedKitWarning } from "../utilities/deprecated";
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
### Step 2: Add the warning in a useEffect hook
|
|
27
|
+
|
|
28
|
+
Add a `useEffect` hook that calls the warning once when the component mounts:
|
|
29
|
+
|
|
30
|
+
```tsx
|
|
31
|
+
const YourKit = (props: YourKitProps): React.ReactElement => {
|
|
32
|
+
|
|
33
|
+
useEffect(() => {
|
|
34
|
+
deprecatedKitWarning('YourKitName', 'Optional custom message');
|
|
35
|
+
}, []); // Empty dependency array ensures it runs once
|
|
36
|
+
|
|
37
|
+
// rest of component
|
|
38
|
+
};
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
## API Reference
|
|
43
|
+
|
|
44
|
+
### `deprecatedKitWarning(kitName, message?)`
|
|
45
|
+
|
|
46
|
+
**Parameters:**
|
|
47
|
+
- `kitName` (string, required): Name of the deprecated kit (e.g., 'BarGraph')
|
|
48
|
+
- `message` (string, optional): Custom deprecation message. If omitted, uses default.
|
|
49
|
+
|
|
50
|
+
**Default message format:**
|
|
51
|
+
```
|
|
52
|
+
[Playbook] The "{kitName}" kit is deprecated and will be removed in a future version. Please migrate to the recommended alternative.
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
**Behavior:**
|
|
56
|
+
- Only logs in development mode (`process.env.NODE_ENV !== 'production'`)
|
|
57
|
+
- Tracks warned kits in a Set to prevent duplicate warnings
|
|
58
|
+
- Silent in production builds
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
## Best Practices
|
|
62
|
+
|
|
63
|
+
### 1. Place the warning early in the component lifecycle
|
|
64
|
+
```tsx
|
|
65
|
+
useEffect(() => {
|
|
66
|
+
deprecatedKitWarning('YourKit');
|
|
67
|
+
}, []); // Run once on mount
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### 2. Provide helpful migration messages
|
|
71
|
+
```tsx
|
|
72
|
+
deprecatedKitWarning(
|
|
73
|
+
'BarGraph',
|
|
74
|
+
'[Playbook] The "BarGraph" kit is deprecated. Please use "PbBarGraph" instead.'
|
|
75
|
+
);
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
### 3. Check menu.yml before applying
|
|
79
|
+
Always verify the kit's status in `playbook-website/config/menu.yml` to ensure it's actually deprecated for React.
|
|
80
|
+
|
|
81
|
+
### 4. Don't over-warn
|
|
82
|
+
The utility handles deduplication, but keep it to one `deprecatedKitWarning` call per kit component.
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tracks which deprecated kits have already logged warnings in this session
|
|
3
|
+
* to ensure we only log once per page load per kit
|
|
4
|
+
*/
|
|
5
|
+
const warnedKits = new Set<string>();
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Logs a deprecation warning for a Playbook kit
|
|
9
|
+
* - Only logs once per kit per page load (prevents spam on re-renders)
|
|
10
|
+
* - Only logs in development mode (not in production or test environments)
|
|
11
|
+
*
|
|
12
|
+
* @param kitName - The name of the deprecated kit (e.g., 'BarGraph', 'RichTextEditor')
|
|
13
|
+
* @param message - Optional custom deprecation message. If not provided, uses a default message.
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* // In your kit component:
|
|
17
|
+
* useEffect(() => {
|
|
18
|
+
* deprecatedKitWarning('BarGraph');
|
|
19
|
+
* }, []);
|
|
20
|
+
*/
|
|
21
|
+
export const deprecatedKitWarning = (
|
|
22
|
+
kitName: string,
|
|
23
|
+
message?: string
|
|
24
|
+
): void => {
|
|
25
|
+
// Only run in development mode, not in production or test
|
|
26
|
+
if (process.env.NODE_ENV === 'production' || process.env.NODE_ENV === 'test') {
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Only warn once per kit per page load
|
|
31
|
+
if (warnedKits.has(kitName)) {
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Mark this kit as warned
|
|
36
|
+
warnedKits.add(kitName);
|
|
37
|
+
|
|
38
|
+
// Log the warning
|
|
39
|
+
const defaultMessage = `[PLAYBOOK] The "${kitName}" kit is deprecated and will be removed in a future version. Please migrate to the recommended alternative.`;
|
|
40
|
+
|
|
41
|
+
console.warn(message || defaultMessage);
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Resets the warned kits tracker (useful for testing)
|
|
46
|
+
* @internal
|
|
47
|
+
*/
|
|
48
|
+
export const resetDeprecationWarnings = (): void => {
|
|
49
|
+
warnedKits.clear();
|
|
50
|
+
};
|