playbook_ui 15.5.0.pre.alpha.play260612706 → 15.5.0.pre.alpha.play265012854

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 (40) hide show
  1. checksums.yaml +4 -4
  2. data/app/pb_kits/playbook/pb_bar_graph/_bar_graph.tsx +6 -0
  3. data/app/pb_kits/playbook/pb_dialog/index.js +10 -15
  4. data/app/pb_kits/playbook/pb_home_address_street/_home_address_street.tsx +34 -22
  5. data/app/pb_kits/playbook/pb_home_address_street/city_emphasis.html.erb +16 -12
  6. data/app/pb_kits/playbook/pb_home_address_street/docs/_home_address_street_default.html.erb +1 -1
  7. data/app/pb_kits/playbook/pb_home_address_street/none_emphasis.html.erb +16 -12
  8. data/app/pb_kits/playbook/pb_home_address_street/street_emphasis.html.erb +16 -12
  9. data/app/pb_kits/playbook/pb_legend/_legend.tsx +6 -1
  10. data/app/pb_kits/playbook/pb_multiple_users/_multiple_users.scss +10 -0
  11. data/app/pb_kits/playbook/pb_multiple_users/_multiple_users.tsx +66 -15
  12. data/app/pb_kits/playbook/pb_multiple_users/docs/_multiple_users_with_tooltip.jsx +42 -0
  13. data/app/pb_kits/playbook/pb_multiple_users/docs/_multiple_users_with_tooltip.md +1 -0
  14. data/app/pb_kits/playbook/pb_multiple_users/docs/example.yml +1 -0
  15. data/app/pb_kits/playbook/pb_multiple_users/docs/index.js +1 -0
  16. data/app/pb_kits/playbook/pb_multiple_users/multiple_users.test.js +25 -0
  17. data/app/pb_kits/playbook/pb_phone_number_input/_phone_number_input.tsx +44 -10
  18. data/app/pb_kits/playbook/pb_phone_number_input/docs/_phone_number_input_validation.html.erb +34 -4
  19. data/app/pb_kits/playbook/pb_phone_number_input/docs/_phone_number_input_validation.jsx +16 -7
  20. data/app/pb_kits/playbook/pb_typeahead/_typeahead.test.jsx +15 -0
  21. data/app/pb_kits/playbook/pb_typeahead/_typeahead.tsx +3 -0
  22. data/app/pb_kits/playbook/pb_typeahead/components/MultiValue.tsx +6 -1
  23. data/app/pb_kits/playbook/pb_typeahead/components/ValueContainer.tsx +34 -7
  24. data/app/pb_kits/playbook/pb_typeahead/docs/_typeahead_input_display.html.erb +30 -0
  25. data/app/pb_kits/playbook/pb_typeahead/docs/_typeahead_input_display.jsx +37 -0
  26. data/app/pb_kits/playbook/pb_typeahead/docs/_typeahead_input_display.md +3 -0
  27. data/app/pb_kits/playbook/pb_typeahead/docs/example.yml +2 -0
  28. data/app/pb_kits/playbook/pb_typeahead/docs/index.js +2 -1
  29. data/app/pb_kits/playbook/pb_typeahead/typeahead.rb +6 -1
  30. data/app/pb_kits/playbook/utilities/DEPRECATION_WARNINGS.md +82 -0
  31. data/app/pb_kits/playbook/utilities/deprecated.ts +71 -0
  32. data/dist/chunks/_typeahead-EauOtKPs.js +6 -0
  33. data/dist/chunks/vendor.js +2 -2
  34. data/dist/menu.yml +3 -2
  35. data/dist/playbook-rails-react-bindings.js +1 -1
  36. data/dist/playbook-rails.js +1 -1
  37. data/dist/playbook.css +1 -1
  38. data/lib/playbook/version.rb +1 -1
  39. metadata +10 -3
  40. data/dist/chunks/_typeahead-eXsdpMc6.js +0 -6
@@ -1,12 +1,42 @@
1
1
  <form id="example-form-validation" action="" method="get">
2
- <%= pb_rails("phone_number_input", props: { error: "Missing phone number", id: "validation", initial_country: "af", value: "", required: true }) %>
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').addEventListener('submit', function (e) {
9
- if (e.target.querySelectorAll('[error]:not([error=""])').length > 0) e.preventDefault();
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 (showFormErrors) e.preventDefault()
37
+ if (!isValid) e.preventDefault()
27
38
  }
28
39
 
29
40
  useEffect(() => {
30
41
  setShowFormErrors(formErrors.length > 0);
31
42
  }, [formErrors]);
32
43
 
33
- const error = (
34
- <>
35
- <Icon icon="warning" /> Missing phone number.
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={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,
@@ -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
- <components.ValueContainer
10
- className="text_input_value_container"
11
- {...props}
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
@@ -0,0 +1,3 @@
1
+ Use the `inputDisplay`/`input_display` prop to optionally display only the count in the display as opposed to multiple pills. This prop is set to 'pills' by default.
2
+
3
+ **NOTE**: `inputDisplay`/`input_display` should only be used with typeaheads that allow multi selection.
@@ -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,71 @@
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
+ // Skip in test environments (Jest sets NODE_ENV to 'test')
26
+ if (typeof process !== 'undefined' && process.env?.NODE_ENV === 'test') {
27
+ return;
28
+ }
29
+
30
+ // Skip if production mode was set at build time
31
+ if (typeof process !== 'undefined' && process.env?.NODE_ENV === 'production') {
32
+ return;
33
+ }
34
+
35
+ // Skip if this looks like a production build (minified, no sourcemaps in browser)
36
+ // This helps catch cases where the package was built for production but consumed in dev
37
+ if (typeof window !== 'undefined') {
38
+ // Check for common production indicators
39
+ const isMinified = !new Error().stack?.includes('.ts:') && !new Error().stack?.includes('.tsx:');
40
+ // Allow warnings even in built packages when consumed locally (localhost)
41
+ const isLocalhost = window.location?.hostname === 'localhost' ||
42
+ window.location?.hostname === '127.0.0.1' ||
43
+ window.location?.hostname === '';
44
+
45
+ // Only skip if it's minified AND not on localhost
46
+ if (isMinified && !isLocalhost) {
47
+ return;
48
+ }
49
+ }
50
+
51
+ // Only warn once per kit per page load
52
+ if (warnedKits.has(kitName)) {
53
+ return;
54
+ }
55
+
56
+ // Mark this kit as warned
57
+ warnedKits.add(kitName);
58
+
59
+ // Log the warning
60
+ const defaultMessage = `[PLAYBOOK] The "${kitName}" kit is deprecated and will be removed in a future version. Please migrate to the recommended alternative.`;
61
+
62
+ console.warn(message || defaultMessage);
63
+ };
64
+
65
+ /**
66
+ * Resets the warned kits tracker (useful for testing)
67
+ * @internal
68
+ */
69
+ export const resetDeprecationWarnings = (): void => {
70
+ warnedKits.clear();
71
+ };