playbook_ui 18.0.0.pre.alpha.play3194filtericons19569 → 18.0.0.pre.alpha.play316819703
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 +4 -1
- data/app/pb_kits/playbook/pb_background/background.test.js +23 -1
- data/app/pb_kits/playbook/pb_copy_button/_copy_button.tsx +5 -1
- data/app/pb_kits/playbook/pb_copy_button/copy_button.test.jsx +12 -0
- data/app/pb_kits/playbook/pb_date_time_stacked/_date_time_stacked.tsx +10 -4
- data/app/pb_kits/playbook/pb_date_time_stacked/date_time_stacked.test.js +12 -0
- data/app/pb_kits/playbook/pb_dialog/child_kits/_dialog_body.tsx +9 -4
- data/app/pb_kits/playbook/pb_dialog/dialog.test.jsx +10 -0
- data/app/pb_kits/playbook/pb_dropdown/docs/_dropdown_async_custom_content.html.erb +57 -0
- data/app/pb_kits/playbook/pb_dropdown/docs/_dropdown_async_custom_content.md +10 -0
- data/app/pb_kits/playbook/pb_dropdown/docs/_dropdown_async_search.html.erb +42 -0
- data/app/pb_kits/playbook/pb_dropdown/docs/_dropdown_async_search.md +55 -0
- data/app/pb_kits/playbook/pb_dropdown/docs/example.yml +2 -0
- data/app/pb_kits/playbook/pb_dropdown/dropdown.rb +7 -0
- data/app/pb_kits/playbook/pb_dropdown/dropdown_index.test.js +773 -0
- data/app/pb_kits/playbook/pb_dropdown/index.js +238 -55
- data/app/pb_kits/playbook/pb_dropdown/keyboard_accessibility.js +5 -3
- data/app/pb_kits/playbook/pb_dropdown/kit.schema.json +19 -0
- data/app/pb_kits/playbook/pb_empty_state/_empty_state.tsx +5 -1
- data/app/pb_kits/playbook/pb_empty_state/empty_state.test.jsx +11 -0
- data/app/pb_kits/playbook/pb_filter/Filter/FilterBackground.tsx +8 -2
- data/app/pb_kits/playbook/pb_filter/Filter/FilterResponsive.tsx +116 -0
- data/app/pb_kits/playbook/pb_filter/Filter/SortMenu.tsx +1 -1
- data/app/pb_kits/playbook/pb_filter/Filter/index.tsx +48 -6
- data/app/pb_kits/playbook/pb_filter/_filter.scss +127 -0
- data/app/pb_kits/playbook/pb_filter/_filter.tsx +1 -0
- data/app/pb_kits/playbook/pb_filter/docs/_filter_responsive.html.erb +42 -0
- data/app/pb_kits/playbook/pb_filter/docs/_filter_responsive.jsx +76 -0
- data/app/pb_kits/playbook/pb_filter/docs/_filter_responsive.md +3 -0
- data/app/pb_kits/playbook/pb_filter/docs/_playground.json +2 -0
- data/app/pb_kits/playbook/pb_filter/docs/_playground.overrides.json +2 -0
- data/app/pb_kits/playbook/pb_filter/docs/example.yml +2 -0
- data/app/pb_kits/playbook/pb_filter/docs/index.js +1 -0
- data/app/pb_kits/playbook/pb_filter/filter.html.erb +139 -97
- data/app/pb_kits/playbook/pb_filter/filter.rb +23 -3
- data/app/pb_kits/playbook/pb_filter/filter.test.js +78 -0
- data/app/pb_kits/playbook/pb_filter/kit.schema.json +13 -2
- data/app/pb_kits/playbook/pb_form/docs/_form_model_values.html.erb +43 -0
- data/app/pb_kits/playbook/pb_form/docs/_form_model_values.md +22 -0
- data/app/pb_kits/playbook/pb_form/docs/example.yml +1 -0
- data/app/pb_kits/playbook/pb_multi_level_select/_helper_functions.tsx +12 -1
- data/app/pb_kits/playbook/pb_multi_level_select/_multi_level_select.tsx +2 -3
- data/app/pb_kits/playbook/pb_multi_level_select/multi_level_select.test.jsx +26 -0
- data/app/pb_kits/playbook/pb_multi_level_select/tree_helpers.js +10 -4
- data/app/pb_kits/playbook/pb_pill/_pill.test.jsx +43 -0
- data/app/pb_kits/playbook/pb_pill/_pill.tsx +12 -6
- data/app/pb_kits/playbook/pb_pill/docs/_pill_children.html.erb +9 -0
- data/app/pb_kits/playbook/pb_pill/docs/_pill_children.jsx +45 -0
- data/app/pb_kits/playbook/pb_pill/docs/_pill_children.md +1 -0
- data/app/pb_kits/playbook/pb_pill/docs/_playground.json +14 -1
- data/app/pb_kits/playbook/pb_pill/docs/_playground.overrides.json +17 -0
- data/app/pb_kits/playbook/pb_pill/docs/example.yml +2 -0
- data/app/pb_kits/playbook/pb_pill/docs/index.js +1 -0
- data/app/pb_kits/playbook/pb_pill/pill.html.erb +5 -1
- data/app/pb_kits/playbook/pb_progress_step/_progress_step_item.tsx +2 -1
- data/app/pb_kits/playbook/pb_progress_step/progress_step.test.js +16 -0
- data/app/pb_kits/playbook/pb_source/_source.tsx +14 -11
- data/app/pb_kits/playbook/pb_source/source.test.js +6 -0
- data/app/pb_kits/playbook/pb_table/_table.tsx +1 -1
- data/app/pb_kits/playbook/pb_table/docs/_table_header.jsx +193 -0
- data/app/pb_kits/playbook/pb_table/docs/{_table_header.md → _table_header_rails.md} +5 -3
- data/app/pb_kits/playbook/pb_table/docs/_table_header_react.md +15 -0
- data/app/pb_kits/playbook/pb_table/docs/_table_responsive_table.html.erb +75 -0
- data/app/pb_kits/playbook/pb_table/docs/_table_responsive_table.jsx +80 -0
- data/app/pb_kits/playbook/pb_table/docs/_table_responsive_table_rails.md +5 -0
- data/app/pb_kits/playbook/pb_table/docs/_table_responsive_table_react.md +5 -0
- data/app/pb_kits/playbook/pb_table/docs/example.yml +1 -0
- data/app/pb_kits/playbook/pb_table/docs/index.js +1 -0
- data/app/pb_kits/playbook/pb_table/subcomponents/_table_header.tsx +143 -12
- data/app/pb_kits/playbook/pb_table/table.rb +1 -1
- data/app/pb_kits/playbook/pb_table/table.test.js +134 -1
- data/app/pb_kits/playbook/pb_table/utilities/sortMenuHelpers.test.ts +76 -0
- data/app/pb_kits/playbook/pb_table/utilities/sortMenuHelpers.ts +84 -0
- data/dist/ai/all-schemas.json +244 -153
- data/dist/ai/forms.json +101 -99
- data/dist/ai/index.json +2 -2
- data/dist/ai/kits/button.schema.json +4 -3
- data/dist/ai/kits/checkbox.schema.json +7 -6
- data/dist/ai/kits/date_picker.schema.json +12 -10
- data/dist/ai/kits/dropdown.schema.json +81 -13
- data/dist/ai/kits/filter.schema.json +11 -0
- data/dist/ai/kits/multi_level_select.schema.json +13 -12
- data/dist/ai/kits/phone_number_input.schema.json +18 -17
- data/dist/ai/kits/select.schema.json +18 -17
- data/dist/ai/kits/star_rating.schema.json +10 -9
- data/dist/ai/kits/text_input.schema.json +35 -34
- data/dist/ai/kits/textarea.schema.json +10 -9
- data/dist/ai/kits/time_picker.schema.json +12 -11
- data/dist/ai/kits/typeahead.schema.json +13 -12
- data/dist/ai/playgrounds/index.json +1 -1
- data/dist/ai/playgrounds/pill.json +1 -1
- data/dist/ai/visual-index.json +1 -1
- data/dist/chunks/{_detail-Cy2bFz4f.js → _detail-BqqzPTNr.js} +2 -2
- data/dist/chunks/_table-D7wuBFbb.js +1 -0
- data/dist/chunks/_typeahead-KNcX62pc.js +1 -0
- data/dist/chunks/vendor.js +3 -3
- 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/changelog_generator.rb +33 -3
- data/lib/playbook/forms/builder/attribute_defaults.rb +118 -0
- data/lib/playbook/forms/builder/collection_select_field.rb +3 -0
- data/lib/playbook/forms/builder/date_picker_field.rb +22 -7
- data/lib/playbook/forms/builder/dropdown_field.rb +12 -0
- data/lib/playbook/forms/builder/form_field_builder.rb +2 -0
- data/lib/playbook/forms/builder/intl_telephone_field.rb +8 -0
- data/lib/playbook/forms/builder/multi_level_select_field.rb +10 -0
- data/lib/playbook/forms/builder/phone_number_field.rb +8 -0
- data/lib/playbook/forms/builder/select_field.rb +3 -0
- data/lib/playbook/forms/builder/star_rating_field.rb +7 -0
- data/lib/playbook/forms/builder/time_picker_field.rb +8 -0
- data/lib/playbook/forms/builder/typeahead_field.rb +15 -0
- data/lib/playbook/forms/builder.rb +3 -0
- data/lib/playbook/version.rb +1 -1
- metadata +26 -6
- data/dist/chunks/_table-BiUZZExj.js +0 -1
- data/dist/chunks/_typeahead-BiNcC1AJ.js +0 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d786f7aa30fa9c245ffabc812b1f4cc4eec3c066fa9e7e927ca02cde72bdbbf0
|
|
4
|
+
data.tar.gz: 5266e40cbf6df3c53f75013c6d8377bfdc66198703316aced89f93d7d2534321
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: db68dc179c9f281f22230e7bbb95c2ecf72c74376c0a39b6d94cc5ddf6f5b86383302cb83c96e385cfa764edf0a11e752e8e19860ba4eacb70e2b320b3424ca9
|
|
7
|
+
data.tar.gz: b765470abaf7b8e2d16a74e01faad0e4092ab40773984e0127a2d62444b0040636c2e1547225700e9fd2616af079775a82c1457b3f7138cb47ec55e588a7df01
|
|
@@ -98,7 +98,7 @@ const Background = (props: BackgroundProps): React.ReactElement => {
|
|
|
98
98
|
imageUrl: getResponsiveValue(imageUrl),
|
|
99
99
|
});
|
|
100
100
|
|
|
101
|
-
//
|
|
101
|
+
// Keep responsive values in sync when props change, and on window resize.
|
|
102
102
|
useEffect(() => {
|
|
103
103
|
const updateResponsiveProps = () => {
|
|
104
104
|
setResponsiveProps({
|
|
@@ -109,6 +109,7 @@ const Background = (props: BackgroundProps): React.ReactElement => {
|
|
|
109
109
|
imageUrl: getResponsiveValue(imageUrl),
|
|
110
110
|
});
|
|
111
111
|
};
|
|
112
|
+
updateResponsiveProps();
|
|
112
113
|
window.addEventListener('resize', updateResponsiveProps);
|
|
113
114
|
return () => window.removeEventListener('resize', updateResponsiveProps);
|
|
114
115
|
}, [backgroundSize, backgroundPosition, backgroundRepeat, backgroundColor, imageUrl]);
|
|
@@ -153,6 +154,8 @@ const Background = (props: BackgroundProps): React.ReactElement => {
|
|
|
153
154
|
...dynamicInlineProps
|
|
154
155
|
};
|
|
155
156
|
|
|
157
|
+
// Dynamic HTML tag from props; matches other kits (Title, Collapsible, etc.).
|
|
158
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
156
159
|
const Tag: React.ReactElement | any = `${tag}`;
|
|
157
160
|
const ariaProps = buildAriaProps(aria);
|
|
158
161
|
const dataProps = buildDataProps(data);
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import { ensureAccessible, render, renderKit } from '../utilities/test-utils'
|
|
2
3
|
|
|
3
4
|
import Background from './_background'
|
|
4
5
|
|
|
@@ -20,6 +21,27 @@ test('backgroundColor = category_1', () => {
|
|
|
20
21
|
expect(kit).toHaveClass('pb_background_kit pb_background_color_category_1')
|
|
21
22
|
})
|
|
22
23
|
|
|
24
|
+
test('updates backgroundColor class when the prop changes', () => {
|
|
25
|
+
const { getByTestId, rerender } = render(
|
|
26
|
+
<Background
|
|
27
|
+
backgroundColor="info_subtle"
|
|
28
|
+
data={{ testid: 'background' }}
|
|
29
|
+
/>
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
expect(getByTestId('background')).toHaveClass('pb_background_color_info_subtle')
|
|
33
|
+
|
|
34
|
+
rerender(
|
|
35
|
+
<Background
|
|
36
|
+
backgroundColor="card_light"
|
|
37
|
+
data={{ testid: 'background' }}
|
|
38
|
+
/>
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
expect(getByTestId('background')).toHaveClass('pb_background_color_card_light')
|
|
42
|
+
expect(getByTestId('background')).not.toHaveClass('pb_background_color_info_subtle')
|
|
43
|
+
})
|
|
44
|
+
|
|
23
45
|
test('customColor prop styles background color with a hex value', () => {
|
|
24
46
|
const kit = renderKit(Background, props, { customColor: '#1d99a8' })
|
|
25
47
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import React from 'react'
|
|
2
2
|
import classnames from 'classnames'
|
|
3
|
-
import { buildAriaProps, buildCss, buildDataProps } from '../utilities/props'
|
|
3
|
+
import { buildAriaProps, buildCss, buildDataProps, buildHtmlProps } from '../utilities/props'
|
|
4
4
|
import { globalProps } from '../utilities/globalProps'
|
|
5
5
|
|
|
6
6
|
import Button from '../pb_button/_button'
|
|
@@ -13,6 +13,7 @@ type CopyButtonProps = {
|
|
|
13
13
|
aria?: { [key: string]: string }
|
|
14
14
|
className?: string
|
|
15
15
|
data?: { [key: string]: string }
|
|
16
|
+
htmlOptions?: {[key: string]: string | number | boolean | (() => void)}
|
|
16
17
|
id?: string
|
|
17
18
|
from?: string
|
|
18
19
|
text?: string
|
|
@@ -29,6 +30,7 @@ const CopyButton = (props: CopyButtonProps) => {
|
|
|
29
30
|
className,
|
|
30
31
|
data = {},
|
|
31
32
|
from = '',
|
|
33
|
+
htmlOptions = {},
|
|
32
34
|
id,
|
|
33
35
|
text = 'Copy',
|
|
34
36
|
timeout = 1000,
|
|
@@ -42,12 +44,14 @@ const CopyButton = (props: CopyButtonProps) => {
|
|
|
42
44
|
|
|
43
45
|
const ariaProps = buildAriaProps(aria)
|
|
44
46
|
const dataProps = buildDataProps(data)
|
|
47
|
+
const htmlProps = buildHtmlProps(htmlOptions)
|
|
45
48
|
const classes = classnames(buildCss('pb_copy_button_kit'), globalProps(props), className)
|
|
46
49
|
|
|
47
50
|
return (
|
|
48
51
|
<div
|
|
49
52
|
{...ariaProps}
|
|
50
53
|
{...dataProps}
|
|
54
|
+
{...htmlProps}
|
|
51
55
|
className={classes}
|
|
52
56
|
id={id}
|
|
53
57
|
>
|
|
@@ -86,3 +86,15 @@ test('passes text and tooltip props to button', () => {
|
|
|
86
86
|
const tooltip = kit.querySelector('.pb_tooltip_kit')
|
|
87
87
|
expect(tooltip).toBeInTheDocument()
|
|
88
88
|
})
|
|
89
|
+
|
|
90
|
+
test('applies htmlOptions to the root element', () => {
|
|
91
|
+
render(
|
|
92
|
+
<CopyButton
|
|
93
|
+
data={{ testid: 'html-options-test' }}
|
|
94
|
+
htmlOptions={{ title: 'copy title' }}
|
|
95
|
+
value="copy"
|
|
96
|
+
/>
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
expect(screen.getByTestId('html-options-test')).toHaveAttribute('title', 'copy title')
|
|
100
|
+
})
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
|
|
2
2
|
import React from 'react'
|
|
3
|
+
import classnames from 'classnames'
|
|
3
4
|
|
|
4
5
|
import { buildCss, buildHtmlProps } from '../utilities/props'
|
|
5
6
|
import { deprecatedProps, globalProps } from '../utilities/globalProps'
|
|
@@ -11,6 +12,7 @@ import TimeStacked from '../pb_time_stacked/_time_stacked'
|
|
|
11
12
|
import DateStacked from '../pb_date_stacked/_date_stacked'
|
|
12
13
|
|
|
13
14
|
type DateTimeStackedProps = {
|
|
15
|
+
className?: string,
|
|
14
16
|
htmlOptions?: {[key: string]: string | number | boolean | (() => void)},
|
|
15
17
|
id?: string,
|
|
16
18
|
date: Date,
|
|
@@ -27,20 +29,25 @@ const DateTimeStacked = (props: DateTimeStackedProps): React.ReactElement => {
|
|
|
27
29
|
date,
|
|
28
30
|
datetime,
|
|
29
31
|
dark,
|
|
32
|
+
className,
|
|
30
33
|
htmlOptions = {},
|
|
31
34
|
timeZone = 'America/New_York',
|
|
32
35
|
showCurrentYear = false,
|
|
33
36
|
} = props
|
|
34
37
|
|
|
35
|
-
const classes =
|
|
36
|
-
|
|
38
|
+
const classes = classnames(
|
|
39
|
+
buildCss('pb_date_time_stacked_kit'),
|
|
40
|
+
globalProps(props),
|
|
41
|
+
className
|
|
42
|
+
)
|
|
37
43
|
|
|
38
44
|
return (
|
|
39
45
|
<Flex
|
|
46
|
+
htmlOptions={htmlOptions}
|
|
40
47
|
inline={false}
|
|
41
48
|
vertical="stretch"
|
|
42
|
-
{...htmlProps}
|
|
43
49
|
{...props}
|
|
50
|
+
className={classes}
|
|
44
51
|
>
|
|
45
52
|
<FlexItem>
|
|
46
53
|
<DateStacked
|
|
@@ -58,7 +65,6 @@ const DateTimeStacked = (props: DateTimeStackedProps): React.ReactElement => {
|
|
|
58
65
|
/>
|
|
59
66
|
<FlexItem>
|
|
60
67
|
<TimeStacked
|
|
61
|
-
className={classes}
|
|
62
68
|
dark={dark}
|
|
63
69
|
date={date || datetime}
|
|
64
70
|
timeZone={timeZone}
|
|
@@ -74,3 +74,15 @@ test('hides current year by default', () => {
|
|
|
74
74
|
expect(yearElement).toBeNull()
|
|
75
75
|
}
|
|
76
76
|
})
|
|
77
|
+
|
|
78
|
+
test('applies kit class and global props to the root', () => {
|
|
79
|
+
const kit = renderKit(DateTimeStacked, {
|
|
80
|
+
data: { testid: 'datetimestacked-root' },
|
|
81
|
+
datetime,
|
|
82
|
+
dark: false,
|
|
83
|
+
margin: 'md',
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
expect(kit).toHaveClass('pb_date_time_stacked_kit')
|
|
87
|
+
expect(kit).toHaveClass('m_md')
|
|
88
|
+
})
|
|
@@ -1,22 +1,27 @@
|
|
|
1
1
|
import React from 'react'
|
|
2
2
|
import classnames from 'classnames'
|
|
3
|
-
import { buildCss } from '../../utilities/props'
|
|
3
|
+
import { buildCss, buildHtmlProps } from '../../utilities/props'
|
|
4
4
|
import { globalProps } from '../../utilities/globalProps'
|
|
5
5
|
|
|
6
6
|
type DialogBodyProps = {
|
|
7
7
|
children: React.ReactNode | React.ReactNode[] | string,
|
|
8
8
|
padding?: "xxs" | "xs" | "sm" | "md" | "lg" | "xl",
|
|
9
|
-
className?: string
|
|
9
|
+
className?: string,
|
|
10
|
+
htmlOptions?: {[key: string]: string | number | boolean | (() => void)},
|
|
10
11
|
}
|
|
11
12
|
|
|
12
13
|
// Body component
|
|
13
14
|
const DialogBody = (props: DialogBodyProps): React.ReactElement => {
|
|
14
|
-
const { children, className } = props
|
|
15
|
+
const { children, className, htmlOptions = {} } = props
|
|
15
16
|
const bodyCSS = buildCss("dialog_body")
|
|
16
17
|
const bodySpacing = globalProps(props)
|
|
18
|
+
const htmlProps = buildHtmlProps(htmlOptions)
|
|
17
19
|
|
|
18
20
|
return (
|
|
19
|
-
<div
|
|
21
|
+
<div
|
|
22
|
+
{...htmlProps}
|
|
23
|
+
className={classnames(bodyCSS, bodySpacing, className)}
|
|
24
|
+
>
|
|
20
25
|
{children}
|
|
21
26
|
</div>
|
|
22
27
|
)
|
|
@@ -268,3 +268,13 @@ describe('isNativeSelectMenuInteraction', () => {
|
|
|
268
268
|
expect(isNativeSelectMenuInteraction(dialog, select)).toBe(false)
|
|
269
269
|
})
|
|
270
270
|
})
|
|
271
|
+
|
|
272
|
+
test("applies htmlOptions to Dialog.Body", () => {
|
|
273
|
+
render(
|
|
274
|
+
<Dialog.Body htmlOptions={{ title: "body title" }}>
|
|
275
|
+
{"Body content"}
|
|
276
|
+
</Dialog.Body>
|
|
277
|
+
)
|
|
278
|
+
|
|
279
|
+
expect(document.querySelector(".dialog_body")).toHaveAttribute("title", "body title")
|
|
280
|
+
})
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
<%= pb_rails("dropdown", props: {
|
|
2
|
+
id: "async-rich-user-dropdown",
|
|
3
|
+
label: "User",
|
|
4
|
+
name: "async_rich_user",
|
|
5
|
+
placeholder: "Try Emily or Michael",
|
|
6
|
+
autocomplete: true,
|
|
7
|
+
async: true,
|
|
8
|
+
options: [],
|
|
9
|
+
}) %>
|
|
10
|
+
|
|
11
|
+
<template id="async-user-option-template">
|
|
12
|
+
<%= pb_rails("flex", props: { orientation: "column", align: "start" }) do %>
|
|
13
|
+
<%= pb_rails("body", props: { text: "Name", data: { async_user_name: true } }) %>
|
|
14
|
+
<%= pb_rails("detail", props: { text: "Title and Department", data: { async_user_detail: true } }) %>
|
|
15
|
+
<% end %>
|
|
16
|
+
</template>
|
|
17
|
+
|
|
18
|
+
<script>
|
|
19
|
+
(() => {
|
|
20
|
+
const dropdown = document.getElementById("async-rich-user-dropdown");
|
|
21
|
+
const template = document.getElementById("async-user-option-template");
|
|
22
|
+
if (!dropdown || !template || dropdown.dataset.asyncExampleBound) return;
|
|
23
|
+
dropdown.dataset.asyncExampleBound = "true";
|
|
24
|
+
|
|
25
|
+
dropdown.addEventListener("pb:dropdown:search", (event) => {
|
|
26
|
+
const { searchingFor, setResults, setError } = event.detail;
|
|
27
|
+
const params = new URLSearchParams({
|
|
28
|
+
q: searchingFor,
|
|
29
|
+
limit: "10",
|
|
30
|
+
select: "firstName,lastName,company",
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
fetch(`https://dummyjson.com/users/search?${params}`, { credentials: "omit" })
|
|
34
|
+
.then((response) => {
|
|
35
|
+
if (!response.ok) throw new Error("User search failed");
|
|
36
|
+
return response.json();
|
|
37
|
+
})
|
|
38
|
+
.then(({ users }) => {
|
|
39
|
+
setResults(users.map((user) => {
|
|
40
|
+
const option = {
|
|
41
|
+
id: user.id,
|
|
42
|
+
label: `${user.firstName} ${user.lastName}`,
|
|
43
|
+
value: user.id,
|
|
44
|
+
title: user.company?.title || "",
|
|
45
|
+
department: user.company?.department || "",
|
|
46
|
+
};
|
|
47
|
+
const content = template.content.cloneNode(true);
|
|
48
|
+
content.querySelector("[data-async-user-name]").textContent = option.label;
|
|
49
|
+
content.querySelector("[data-async-user-detail]").textContent =
|
|
50
|
+
[option.title, option.department].filter(Boolean).join(" · ");
|
|
51
|
+
return { option, content };
|
|
52
|
+
}));
|
|
53
|
+
})
|
|
54
|
+
.catch(() => setError());
|
|
55
|
+
});
|
|
56
|
+
})();
|
|
57
|
+
</script>
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
For custom Rails result rows, pass `{ option, content }` entries to the async search event's `setResults` callback. Plain option objects and rich entries can appear in the same result array.
|
|
2
|
+
|
|
3
|
+
- `option` is the ordinary serializable Dropdown option object: `id`, `label`, `value`, and any custom data. `disabled: true` prevents selection.
|
|
4
|
+
- `content` is an `Element` or `DocumentFragment`, usually cloned from a Rails-rendered `<template>`. Dropdown clones it into its standard option wrapper; it does not consume or move your original nodes. Populate API text with `textContent`. HTML strings are not a rich-content input.
|
|
5
|
+
|
|
6
|
+
The explicit `option.label` is used in the selected input, even when the row contains additional text. Content is not serialized into option data or the existing `pb:dropdown:selected` payload. Nested content supports click selection and keyboard navigation, including when the menu is portaled.
|
|
7
|
+
|
|
8
|
+
Provide display content only: do not include another Dropdown option wrapper, interactive controls, or named form inputs in suggestions. Keep submitted fields outside the result content and update them from selection data. Cloning preserves markup and data attributes but does not copy JavaScript event listeners attached to the original nodes.
|
|
9
|
+
|
|
10
|
+
The example searches [DummyJSON’s public sample users API](https://dummyjson.com/docs/users) without credentials or an API key. Try `Emily` or `Michael`. It renders Rails Body and Detail kits for the name, job title, and department, retaining title and department in the selected option data. The same pattern works with a User kit or other custom display content.
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
<%= pb_rails("dropdown", props: {
|
|
2
|
+
id: "async-user-dropdown",
|
|
3
|
+
label: "User",
|
|
4
|
+
name: "async_user",
|
|
5
|
+
placeholder: "Try Emily or Michael",
|
|
6
|
+
autocomplete: true,
|
|
7
|
+
async: true,
|
|
8
|
+
search_term_minimum_length: 3,
|
|
9
|
+
search_debounce_timeout: 250,
|
|
10
|
+
options: [],
|
|
11
|
+
}) %>
|
|
12
|
+
|
|
13
|
+
<script>
|
|
14
|
+
(() => {
|
|
15
|
+
const dropdown = document.getElementById("async-user-dropdown");
|
|
16
|
+
if (!dropdown || dropdown.dataset.asyncExampleBound) return;
|
|
17
|
+
dropdown.dataset.asyncExampleBound = "true";
|
|
18
|
+
|
|
19
|
+
dropdown.addEventListener("pb:dropdown:search", (event) => {
|
|
20
|
+
const { searchingFor, setResults, setError } = event.detail;
|
|
21
|
+
const params = new URLSearchParams({
|
|
22
|
+
q: searchingFor,
|
|
23
|
+
limit: "10",
|
|
24
|
+
select: "firstName,lastName",
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
fetch(`https://dummyjson.com/users/search?${params}`, { credentials: "omit" })
|
|
28
|
+
.then((response) => {
|
|
29
|
+
if (!response.ok) throw new Error("User search failed");
|
|
30
|
+
return response.json();
|
|
31
|
+
})
|
|
32
|
+
.then(({ users }) => {
|
|
33
|
+
setResults(users.map((user) => ({
|
|
34
|
+
id: user.id,
|
|
35
|
+
label: `${user.firstName} ${user.lastName}`,
|
|
36
|
+
value: user.id,
|
|
37
|
+
})));
|
|
38
|
+
})
|
|
39
|
+
.catch(() => setError());
|
|
40
|
+
});
|
|
41
|
+
})();
|
|
42
|
+
</script>
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
Rails Dropdown supports event-driven remote search with `async: true` and either `autocomplete: true` or `searchbar: true`. Local Dropdown filtering remains unchanged when async is disabled.
|
|
2
|
+
|
|
3
|
+
Type at least `search_term_minimum_length` characters (default: `3`). After `search_debounce_timeout` milliseconds without another edit (default: `250`), the kit opens the menu, displays a loading message, and emits a bubbling `pb:dropdown:search` event from the Dropdown root. Scope listeners using the root's `id` or `data` attributes.
|
|
4
|
+
|
|
5
|
+
The event detail contains:
|
|
6
|
+
|
|
7
|
+
- `searchingFor`: the current query.
|
|
8
|
+
- `setResults(options)`: complete the search with an array of standard Dropdown option objects (`id`, `label`, `value`) or rich `{ option, content }` entries. Use an empty array for no matches.
|
|
9
|
+
- `setError()`: end loading and display a failure message.
|
|
10
|
+
|
|
11
|
+
The application owns fetching and mapping results. For example, call `fetch(yourUrl).then(...)`, check `response.ok`, map the response to options, pass them to `setResults`, and call `setError` on failure. This example searches [DummyJSON’s public sample users API](https://dummyjson.com/docs/users) over HTTPS without credentials or an API key. Try `Emily` or `Michael`. The API receives the search text and returns up to 10 results; only name fields are requested. HTTP or network failures display the kit’s error state.
|
|
12
|
+
|
|
13
|
+
Results are displayed without additional local label filtering and without clearing the typed query. Callbacks are request-scoped: subsequent edits, selection, clear/reset, dismissal with Escape/Tab or an outside click, and disconnect invalidate pending requests. Dismissing during the debounce delay cancels the search before it starts; completed results and status messages remain available when reopening. Only the first completion is accepted. Requests that do not complete within 15 seconds display a failure message. The kit ignores stale responses; it does not abort application-owned network requests.
|
|
14
|
+
|
|
15
|
+
For rich results, `content` is an Element or DocumentFragment cloned into the option wrapper; `option` supplies the label and selection data. See Async Search with Custom Content for a Rails template example.
|
|
16
|
+
|
|
17
|
+
Async Quick Pick is not supported; Quick Pick uses static date presets.
|
|
18
|
+
|
|
19
|
+
#### Selection data and defaults
|
|
20
|
+
|
|
21
|
+
`pb:dropdown:selected` bubbles from the Dropdown root after selection updates. Its existing detail shape is unchanged: the complete option object for single-select, an array for multi-select, and `null` / `[]` when cleared. Custom JSON-serializable fields, nested objects, booleans, and arrays are preserved; rendered rich content is not included. For example, read `event.detail.department_id` to update a dependent field. Numeric IDs retain their type in the event payload; form inputs submit strings.
|
|
22
|
+
|
|
23
|
+
Selected data is retained independently of remote result rows. A later search returning different options does not discard the selection or its metadata. In multi-select, previously selected IDs remain hidden when they reappear, even if the server returns updated labels or other fields. The selected payload stays as it was when selected until the selection is explicitly changed.
|
|
24
|
+
|
|
25
|
+
To initialize an async Dropdown before any results load, supply a complete `default_value` option object (an array for multi-select), including `id` and `label`. For example: `async: true, autocomplete: true, options: [], default_value: { id: 42, label: "Ada", department_id: 7 }`. When using `dropdown_field`, pass that explicit default: the builder cannot resolve an initial model ID from an empty options array.
|
|
26
|
+
|
|
27
|
+
#### Input and reset events
|
|
28
|
+
|
|
29
|
+
With `async: true`, `pb:dropdown:input` bubbles from the Dropdown root immediately on every edit, including text below the search minimum. Its detail is `{ value, reason }`:
|
|
30
|
+
|
|
31
|
+
- `reason: "input"`: the autocomplete or search-bar text was edited. `value` is the current text, without debouncing.
|
|
32
|
+
- `reason: "clear"`: the clear control or public `pb:dropdown:clear` command cleared the Dropdown. `value` is `""`.
|
|
33
|
+
- `reason: "reset"`: a native form reset cleared the Dropdown. `value` is `""`.
|
|
34
|
+
|
|
35
|
+
Selection changes continue to use `pb:dropdown:selected`; choosing an option does not emit an input event. For free-text forms such as a name search, maintain a separate field from both events:
|
|
36
|
+
|
|
37
|
+
```javascript
|
|
38
|
+
dropdown.addEventListener("pb:dropdown:input", ({ detail }) => {
|
|
39
|
+
nameField.value = detail.value;
|
|
40
|
+
// Application-owned IDs can be invalidated on any edit.
|
|
41
|
+
userIdField.value = "";
|
|
42
|
+
});
|
|
43
|
+
dropdown.addEventListener("pb:dropdown:selected", ({ detail }) => {
|
|
44
|
+
nameField.value = detail?.label || "";
|
|
45
|
+
userIdField.value = detail?.id ?? "";
|
|
46
|
+
});
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Editing nonempty text retains the Dropdown's selected value until it is changed or cleared. Emptying a single-select autocomplete clears its selected ID and emits `pb:dropdown:selected` with `null`. Emptying a search-bar query does not discard the selected option; emptying a multi-select query does not discard selected pills.
|
|
50
|
+
|
|
51
|
+
Explicit clear and native form reset clear query text, selection, remote results, loading/error messages, and pending callbacks, and close the menu. They emit the normal cleared selection payload (`null` or `[]`) and the input notification. Native reset completes after the browser resets form controls; a canceled reset leaves kit state intact. Reset clears the selection rather than restoring `default_value`, matching the kit's existing reset convention.
|
|
52
|
+
|
|
53
|
+
After clear, clicking an async autocomplete does not open an empty menu. The menu opens when a search starts or results/status content is available. An async search-bar menu can still open so its input remains accessible.
|
|
54
|
+
|
|
55
|
+
The clear control is also available for an unselected async query unless `clearable: false`. No async input notifications, remote-result cleanup, or changed reset timing are applied to synchronous Dropdowns.
|
|
@@ -2,6 +2,8 @@ examples:
|
|
|
2
2
|
rails:
|
|
3
3
|
- dropdown_default_rails: Default
|
|
4
4
|
- dropdown_with_autocomplete: Autocomplete
|
|
5
|
+
- dropdown_async_search: Async Search
|
|
6
|
+
- dropdown_async_custom_content: Async Search with Custom Content
|
|
5
7
|
- dropdown_multi_select_rails: Multi Select
|
|
6
8
|
- dropdown_multi_select_with_autocomplete: Multi Select with Autocomplete
|
|
7
9
|
- dropdown_multi_select_display_rails: Multi Select with Form Pill Props
|
|
@@ -27,6 +27,9 @@ module Playbook
|
|
|
27
27
|
default: true
|
|
28
28
|
prop :autocomplete, type: Playbook::Props::Boolean,
|
|
29
29
|
default: false
|
|
30
|
+
prop :async, type: Playbook::Props::Boolean, default: false
|
|
31
|
+
prop :search_term_minimum_length, default: 3
|
|
32
|
+
prop :search_debounce_timeout, default: 250
|
|
30
33
|
prop :searchbar, type: Playbook::Props::Boolean,
|
|
31
34
|
default: false
|
|
32
35
|
prop :multi_select, type: Playbook::Props::Boolean,
|
|
@@ -71,6 +74,10 @@ module Playbook
|
|
|
71
74
|
def data
|
|
72
75
|
Hash(prop(:data)).merge(
|
|
73
76
|
pb_dropdown: true,
|
|
77
|
+
pb_dropdown_async: async ? true : nil,
|
|
78
|
+
pb_dropdown_default_value: async && default_value.present? ? default_value.to_json : nil,
|
|
79
|
+
pb_dropdown_search_term_minimum_length: async ? search_term_minimum_length : nil,
|
|
80
|
+
pb_dropdown_search_debounce_timeout: async ? search_debounce_timeout : nil,
|
|
74
81
|
pb_dropdown_multi_select: multi_select,
|
|
75
82
|
pb_dropdown_disabled: disabled,
|
|
76
83
|
pb_dropdown_variant: variant,
|