playbook_ui 16.10.0.pre.rc.0 → 16.10.0.pre.rc.1
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_filter/Filter/CurrentFilters.tsx +55 -28
- data/app/pb_kits/playbook/pb_filter/Filter/FilterDouble.tsx +4 -1
- data/app/pb_kits/playbook/pb_filter/Filter/FilterSingle.tsx +4 -1
- data/app/pb_kits/playbook/pb_filter/Filter/InteractiveFilter.tsx +277 -0
- data/app/pb_kits/playbook/pb_filter/_filter.scss +88 -1
- data/app/pb_kits/playbook/pb_filter/docs/_filter_interactive.html.erb +116 -0
- data/app/pb_kits/playbook/pb_filter/docs/_filter_interactive.md +12 -0
- data/app/pb_kits/playbook/pb_filter/docs/_filter_interactive_react.jsx +153 -0
- data/app/pb_kits/playbook/pb_filter/docs/_filter_interactive_react.md +10 -0
- data/app/pb_kits/playbook/pb_filter/docs/example.yml +2 -0
- data/app/pb_kits/playbook/pb_filter/docs/index.js +2 -1
- data/app/pb_kits/playbook/pb_filter/filter.html.erb +80 -3
- data/app/pb_kits/playbook/pb_filter/filter.rb +71 -0
- data/app/pb_kits/playbook/pb_filter/filter.test.js +78 -0
- data/app/pb_kits/playbook/pb_filter/index.ts +349 -0
- data/app/pb_kits/playbook/pb_filter/kit.schema.json +6 -0
- data/app/pb_kits/playbook/pb_icon/docs/_icon_default.jsx +1 -1
- data/app/pb_kits/playbook/pb_loading_inline/_loading_inline.tsx +3 -1
- data/app/pb_kits/playbook/pb_loading_inline/docs/_loading_inline_color.html.erb +21 -0
- data/app/pb_kits/playbook/pb_loading_inline/docs/_loading_inline_color.jsx +55 -0
- data/app/pb_kits/playbook/pb_loading_inline/docs/_loading_inline_color.md +1 -0
- data/app/pb_kits/playbook/pb_loading_inline/docs/example.yml +2 -0
- data/app/pb_kits/playbook/pb_loading_inline/docs/index.js +1 -0
- data/app/pb_kits/playbook/pb_loading_inline/kit.schema.json +18 -2
- data/app/pb_kits/playbook/pb_loading_inline/loading_inline.html.erb +1 -1
- data/app/pb_kits/playbook/pb_loading_inline/loading_inline.rb +3 -0
- data/dist/chunks/{_pb_line_graph-DxHutusS.js → _pb_line_graph-D8PSzzEY.js} +1 -1
- data/dist/chunks/{_typeahead-DoLAfwVt.js → _typeahead-CuXG_NFx.js} +2 -2
- data/dist/chunks/{globalProps-D2_gFcf5.js → globalProps-B8stOeTI.js} +1 -1
- data/dist/chunks/lib-BAri19Ko.js +29 -0
- data/dist/chunks/vendor.js +5 -5
- 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 +15 -6
- data/dist/chunks/lib-C6NzIorw.js +0 -29
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 0c56cffd7ae20d303e7ebfb111960cd77e6b3ef3020867b335b91ad0743ec88c
|
|
4
|
+
data.tar.gz: 12ed387ffcb0607f46a4ef6f231bb7823b5b3c81f7a988783b8b652491ef29cf
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: dc097cb2fe1d11fefe23ea335188808e0a15abcf214e0de13c7f8b6f1320974af4f1d33c813bf46c3650007ab0e903e838e91738ae8cb48c93c9875f28ccfc66
|
|
7
|
+
data.tar.gz: 2ef66b6d238eb564c593ceb9eaad541f898a5bbad90afd2c834b53dfbcf4624ce56e350eae499b674f7cccece8ff40d1c50201838529782fbf330a92c920b86a
|
|
@@ -1,22 +1,32 @@
|
|
|
1
|
-
import React from 'react'
|
|
1
|
+
import React, { useEffect } from 'react'
|
|
2
2
|
import { isEmpty, omitBy, map } from '../../utilities/object'
|
|
3
3
|
|
|
4
4
|
import Body from '../../pb_body/_body'
|
|
5
5
|
import Caption from '../../pb_caption/_caption'
|
|
6
6
|
import Title from '../../pb_title/_title'
|
|
7
|
+
import InteractiveFilter, { InteractiveFilterConfig } from './InteractiveFilter'
|
|
7
8
|
|
|
8
9
|
export type FilterDescription = {
|
|
9
10
|
[key: string]: string | null | boolean,
|
|
10
11
|
}
|
|
11
12
|
|
|
13
|
+
export type InteractiveFilters = {
|
|
14
|
+
[key: string]: InteractiveFilterConfig,
|
|
15
|
+
}
|
|
16
|
+
|
|
12
17
|
export type CurrentFiltersProps = {
|
|
13
18
|
dark: boolean,
|
|
14
19
|
filters: FilterDescription,
|
|
20
|
+
interactiveFilters?: InteractiveFilters,
|
|
15
21
|
}
|
|
16
22
|
|
|
17
23
|
const hiddenFilters = (value: any) => isEmpty(value) && value !== true
|
|
18
24
|
|
|
19
|
-
const CurrentFilters = ({
|
|
25
|
+
const CurrentFilters = ({
|
|
26
|
+
dark,
|
|
27
|
+
filters,
|
|
28
|
+
interactiveFilters = {},
|
|
29
|
+
}: CurrentFiltersProps): React.ReactElement => {
|
|
20
30
|
const displayableFilters = omitBy(filters, hiddenFilters)
|
|
21
31
|
|
|
22
32
|
return (
|
|
@@ -35,33 +45,50 @@ const CurrentFilters = ({ dark, filters }: CurrentFiltersProps): React.ReactElem
|
|
|
35
45
|
{ !isEmpty(filters) &&
|
|
36
46
|
<div className="filters">
|
|
37
47
|
<div className="left_gradient" />
|
|
38
|
-
{map(displayableFilters, (value, name) =>
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
48
|
+
{map(displayableFilters, (value, name) => {
|
|
49
|
+
const interactiveConfig = interactiveFilters[name]
|
|
50
|
+
return (
|
|
51
|
+
<div
|
|
52
|
+
className={`filter${interactiveConfig ? ' interactive' : ''}`}
|
|
53
|
+
key={`filter-${name}`}
|
|
54
|
+
>
|
|
55
|
+
{ interactiveConfig ?
|
|
56
|
+
<InteractiveFilter
|
|
57
|
+
config={interactiveConfig}
|
|
58
|
+
dark={dark}
|
|
59
|
+
editorValue={interactiveConfig.editorValue}
|
|
60
|
+
name={String(name)}
|
|
61
|
+
value={
|
|
62
|
+
interactiveConfig.value !== undefined
|
|
63
|
+
? interactiveConfig.value
|
|
64
|
+
: value === true
|
|
65
|
+
? ''
|
|
66
|
+
: (value as string)
|
|
67
|
+
}
|
|
68
|
+
/> :
|
|
69
|
+
value === true ?
|
|
70
|
+
<Title
|
|
71
|
+
dark={dark}
|
|
72
|
+
size={4}
|
|
73
|
+
tag="h4"
|
|
74
|
+
text={`${name}`}
|
|
75
|
+
/> :
|
|
76
|
+
<div>
|
|
77
|
+
<Caption
|
|
78
|
+
dark={dark}
|
|
79
|
+
text={`${name}`}
|
|
80
|
+
/>
|
|
81
|
+
<Title
|
|
82
|
+
dark={dark}
|
|
83
|
+
size={4}
|
|
84
|
+
tag="h4"
|
|
85
|
+
text={value}
|
|
86
|
+
/>
|
|
87
|
+
</div>
|
|
88
|
+
}
|
|
61
89
|
</div>
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
))}
|
|
90
|
+
)
|
|
91
|
+
})}
|
|
65
92
|
<div className="right_gradient" />
|
|
66
93
|
</div>
|
|
67
94
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import React from 'react'
|
|
2
2
|
|
|
3
|
-
import CurrentFilters, { FilterDescription } from './CurrentFilters'
|
|
3
|
+
import CurrentFilters, { FilterDescription, InteractiveFilters } from './CurrentFilters'
|
|
4
4
|
import FilterBackground, { FilterBackgroundProps } from './FilterBackground'
|
|
5
5
|
import FiltersPopover from './FiltersPopover'
|
|
6
6
|
import ResultsCount from './ResultsCount'
|
|
@@ -17,6 +17,7 @@ import SectionSeparator from '../../pb_section_separator/_section_separator'
|
|
|
17
17
|
export type FilterDoubleProps = {
|
|
18
18
|
children?: React.ReactChild[] | React.ReactChild,
|
|
19
19
|
filters?: FilterDescription,
|
|
20
|
+
interactiveFilters?: InteractiveFilters,
|
|
20
21
|
onSortChange?: SortingChangeCallback,
|
|
21
22
|
results?: number,
|
|
22
23
|
sortOptions?: SortOptions,
|
|
@@ -28,6 +29,7 @@ const FilterDouble = ({
|
|
|
28
29
|
sortOptions,
|
|
29
30
|
sortValue,
|
|
30
31
|
filters,
|
|
32
|
+
interactiveFilters,
|
|
31
33
|
results,
|
|
32
34
|
children,
|
|
33
35
|
dark,
|
|
@@ -57,6 +59,7 @@ const FilterDouble = ({
|
|
|
57
59
|
<CurrentFilters
|
|
58
60
|
dark={dark}
|
|
59
61
|
filters={filters}
|
|
62
|
+
interactiveFilters={interactiveFilters}
|
|
60
63
|
/>
|
|
61
64
|
</Flex>
|
|
62
65
|
<SectionSeparator dark={dark} />
|
|
@@ -3,7 +3,7 @@ import { isEmpty } from '../../utilities/object'
|
|
|
3
3
|
|
|
4
4
|
import Flex from '../../pb_flex/_flex'
|
|
5
5
|
|
|
6
|
-
import CurrentFilters, { FilterDescription } from './CurrentFilters'
|
|
6
|
+
import CurrentFilters, { FilterDescription, InteractiveFilters } from './CurrentFilters'
|
|
7
7
|
import FilterBackground, { FilterBackgroundProps } from './FilterBackground'
|
|
8
8
|
import FiltersPopover from './FiltersPopover'
|
|
9
9
|
import ResultsCount from './ResultsCount'
|
|
@@ -16,6 +16,7 @@ import SortMenu, {
|
|
|
16
16
|
export type FilterSingleProps = {
|
|
17
17
|
children?: React.ReactChild[] | React.ReactChild,
|
|
18
18
|
filters?: FilterDescription,
|
|
19
|
+
interactiveFilters?: InteractiveFilters,
|
|
19
20
|
onSortChange?: SortingChangeCallback,
|
|
20
21
|
results?: number,
|
|
21
22
|
sortOptions?: SortOptions,
|
|
@@ -27,6 +28,7 @@ const FilterSingle = ({
|
|
|
27
28
|
sortOptions,
|
|
28
29
|
sortValue,
|
|
29
30
|
filters,
|
|
31
|
+
interactiveFilters,
|
|
30
32
|
results,
|
|
31
33
|
children,
|
|
32
34
|
dark,
|
|
@@ -60,6 +62,7 @@ const FilterSingle = ({
|
|
|
60
62
|
<CurrentFilters
|
|
61
63
|
dark={dark}
|
|
62
64
|
filters={filters}
|
|
65
|
+
interactiveFilters={interactiveFilters}
|
|
63
66
|
/>
|
|
64
67
|
</>
|
|
65
68
|
}
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
import React, { useEffect, useMemo, useRef, useState } from 'react'
|
|
2
|
+
import flatpickr from 'flatpickr'
|
|
3
|
+
import { Instance } from 'flatpickr/dist/types/instance'
|
|
4
|
+
|
|
5
|
+
import Flex from '../../pb_flex/_flex'
|
|
6
|
+
import Caption from '../../pb_caption/_caption'
|
|
7
|
+
import Icon from '../../pb_icon/_icon'
|
|
8
|
+
import PbReactPopover from '../../pb_popover/_popover'
|
|
9
|
+
import getQuickPickOptions from '../../pb_dropdown/quickpick'
|
|
10
|
+
import { uniqueId } from '../../utilities/object'
|
|
11
|
+
import Title from '../../pb_title/_title'
|
|
12
|
+
|
|
13
|
+
export type SelectInteractiveConfig = {
|
|
14
|
+
type: 'select',
|
|
15
|
+
options: { value: string, text?: string }[],
|
|
16
|
+
value?: string,
|
|
17
|
+
/** Draft value for the inline editor active state (chip label uses `value`). */
|
|
18
|
+
editorValue?: string,
|
|
19
|
+
onChange: (value: string) => void,
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export type DropdownInteractiveConfig = {
|
|
23
|
+
type: 'dropdown',
|
|
24
|
+
options?: { value?: string, label: string, id?: string }[],
|
|
25
|
+
variant?: 'default' | 'quickpick',
|
|
26
|
+
rangeEndsToday?: boolean,
|
|
27
|
+
customQuickPickDates?: {
|
|
28
|
+
override?: boolean,
|
|
29
|
+
dates: {
|
|
30
|
+
label: string,
|
|
31
|
+
value: string[] | { timePeriod: string, amount: number },
|
|
32
|
+
}[],
|
|
33
|
+
},
|
|
34
|
+
value?: string,
|
|
35
|
+
editorValue?: string,
|
|
36
|
+
onChange: (value: string) => void,
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export type DatePickerInteractiveConfig = {
|
|
40
|
+
type: 'date-picker',
|
|
41
|
+
value?: string,
|
|
42
|
+
editorValue?: string,
|
|
43
|
+
onChange: (value: string) => void,
|
|
44
|
+
format?: string,
|
|
45
|
+
minDate?: string,
|
|
46
|
+
maxDate?: string,
|
|
47
|
+
mode?: 'single' | 'range' | 'multiple',
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export type InteractiveFilterConfig =
|
|
51
|
+
| SelectInteractiveConfig
|
|
52
|
+
| DropdownInteractiveConfig
|
|
53
|
+
| DatePickerInteractiveConfig
|
|
54
|
+
|
|
55
|
+
const dropdownOptionsFor = (
|
|
56
|
+
config: DropdownInteractiveConfig
|
|
57
|
+
): { value: string, label: string }[] => {
|
|
58
|
+
if (config.variant === 'quickpick' && !config.options?.length) {
|
|
59
|
+
return getQuickPickOptions(
|
|
60
|
+
config.rangeEndsToday,
|
|
61
|
+
config.customQuickPickDates
|
|
62
|
+
).map((opt) => ({
|
|
63
|
+
value: opt.id || opt.label,
|
|
64
|
+
label: opt.label,
|
|
65
|
+
}))
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return (config.options || []).map((opt) => ({
|
|
69
|
+
value: opt.id || opt.value || opt.label,
|
|
70
|
+
label: opt.label,
|
|
71
|
+
}))
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
type InteractiveFilterProps = {
|
|
75
|
+
dark?: boolean,
|
|
76
|
+
name: string,
|
|
77
|
+
value: string,
|
|
78
|
+
editorValue?: string,
|
|
79
|
+
config: InteractiveFilterConfig,
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const labelFor = (
|
|
83
|
+
config: InteractiveFilterConfig,
|
|
84
|
+
value: string
|
|
85
|
+
): string => {
|
|
86
|
+
if (config.type === 'select') {
|
|
87
|
+
const match = config.options.find((opt) => opt.value === value)
|
|
88
|
+
if (match) return match.text || match.value
|
|
89
|
+
}
|
|
90
|
+
if (config.type === 'dropdown') {
|
|
91
|
+
const match = dropdownOptionsFor(config).find((opt) => opt.value === value)
|
|
92
|
+
if (match) return match.label || match.value
|
|
93
|
+
}
|
|
94
|
+
return value
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
type ChipVisualProps = {
|
|
98
|
+
dark: boolean,
|
|
99
|
+
name: string,
|
|
100
|
+
displayValue: string,
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const ChipVisual = ({
|
|
104
|
+
dark,
|
|
105
|
+
name,
|
|
106
|
+
displayValue,
|
|
107
|
+
}: ChipVisualProps): React.ReactElement => (
|
|
108
|
+
<div className="pb_interactive_filter_container">
|
|
109
|
+
<Caption
|
|
110
|
+
dark={dark}
|
|
111
|
+
text={name}
|
|
112
|
+
/>
|
|
113
|
+
<Flex
|
|
114
|
+
alignItems="center"
|
|
115
|
+
gap="xxs"
|
|
116
|
+
>
|
|
117
|
+
<Title
|
|
118
|
+
dark={dark}
|
|
119
|
+
size={4}
|
|
120
|
+
text={displayValue || '—'}
|
|
121
|
+
/>
|
|
122
|
+
<Icon
|
|
123
|
+
color="primary"
|
|
124
|
+
fixedWidth
|
|
125
|
+
icon="angle-down"
|
|
126
|
+
size="xs"
|
|
127
|
+
/>
|
|
128
|
+
</Flex>
|
|
129
|
+
</div>
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
type InlineCalendarProps = {
|
|
133
|
+
calendarValue: string,
|
|
134
|
+
config: DatePickerInteractiveConfig,
|
|
135
|
+
onSinglePick: () => void,
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const InlineCalendar = ({
|
|
139
|
+
calendarValue,
|
|
140
|
+
config,
|
|
141
|
+
onSinglePick,
|
|
142
|
+
}: InlineCalendarProps): React.ReactElement => {
|
|
143
|
+
const containerRef = useRef<HTMLDivElement | null>(null)
|
|
144
|
+
|
|
145
|
+
useEffect(() => {
|
|
146
|
+
if (!containerRef.current) return
|
|
147
|
+
const fp = flatpickr(containerRef.current, {
|
|
148
|
+
inline: true,
|
|
149
|
+
defaultDate: calendarValue || undefined,
|
|
150
|
+
mode: config.mode || 'single',
|
|
151
|
+
dateFormat: config.format || 'm/d/Y',
|
|
152
|
+
minDate: config.minDate,
|
|
153
|
+
maxDate: config.maxDate,
|
|
154
|
+
nextArrow: '<i class="far fa-angle-right"></i>',
|
|
155
|
+
prevArrow: '<i class="far fa-angle-left"></i>',
|
|
156
|
+
onChange: (_selectedDates, dateStr) => {
|
|
157
|
+
config.onChange(dateStr)
|
|
158
|
+
if ((config.mode || 'single') === 'single') onSinglePick()
|
|
159
|
+
},
|
|
160
|
+
}) as Instance
|
|
161
|
+
return () => fp.destroy()
|
|
162
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
163
|
+
}, [calendarValue])
|
|
164
|
+
|
|
165
|
+
return <div ref={containerRef} />
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const InteractiveFilter = ({
|
|
169
|
+
dark = false,
|
|
170
|
+
name,
|
|
171
|
+
value,
|
|
172
|
+
editorValue,
|
|
173
|
+
config,
|
|
174
|
+
}: InteractiveFilterProps): React.ReactElement => {
|
|
175
|
+
const [open, setOpen] = useState(false)
|
|
176
|
+
const pickerId = useMemo(() => uniqueId('interactive-filter-date-'), [])
|
|
177
|
+
const displayValue = labelFor(config, value)
|
|
178
|
+
const activeValue =
|
|
179
|
+
editorValue ??
|
|
180
|
+
config.editorValue ??
|
|
181
|
+
value
|
|
182
|
+
|
|
183
|
+
const renderEditor = () => {
|
|
184
|
+
switch (config.type) {
|
|
185
|
+
case 'select':
|
|
186
|
+
case 'dropdown': {
|
|
187
|
+
const options =
|
|
188
|
+
config.type === 'dropdown'
|
|
189
|
+
? dropdownOptionsFor(config)
|
|
190
|
+
: config.options.map((opt) => ({
|
|
191
|
+
value: opt.value,
|
|
192
|
+
label: opt.text || opt.value,
|
|
193
|
+
}))
|
|
194
|
+
return (
|
|
195
|
+
<ul
|
|
196
|
+
className="pb_interactive_filter_options"
|
|
197
|
+
role="listbox"
|
|
198
|
+
>
|
|
199
|
+
{options.map((option) => {
|
|
200
|
+
const isActive = option.value === activeValue
|
|
201
|
+
return (
|
|
202
|
+
<li
|
|
203
|
+
aria-selected={isActive}
|
|
204
|
+
className={`pb_interactive_filter_option${
|
|
205
|
+
isActive ? ' active' : ''
|
|
206
|
+
}`}
|
|
207
|
+
key={option.value}
|
|
208
|
+
onClick={() => {
|
|
209
|
+
config.onChange(option.value)
|
|
210
|
+
setOpen(false)
|
|
211
|
+
}}
|
|
212
|
+
onKeyDown={(event) => {
|
|
213
|
+
if (event.key === 'Enter' || event.key === ' ') {
|
|
214
|
+
event.preventDefault()
|
|
215
|
+
config.onChange(option.value)
|
|
216
|
+
setOpen(false)
|
|
217
|
+
}
|
|
218
|
+
}}
|
|
219
|
+
role="option"
|
|
220
|
+
tabIndex={0}
|
|
221
|
+
>
|
|
222
|
+
<span>{option.label}</span>
|
|
223
|
+
</li>
|
|
224
|
+
)
|
|
225
|
+
})}
|
|
226
|
+
</ul>
|
|
227
|
+
)
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
case 'date-picker':
|
|
231
|
+
return (
|
|
232
|
+
<InlineCalendar
|
|
233
|
+
calendarValue={activeValue}
|
|
234
|
+
config={config}
|
|
235
|
+
onSinglePick={() => setOpen(false)}
|
|
236
|
+
/>
|
|
237
|
+
)
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const trigger = (
|
|
242
|
+
<button
|
|
243
|
+
aria-expanded={open}
|
|
244
|
+
aria-haspopup="dialog"
|
|
245
|
+
className={`pb_interactive_filter_trigger${dark ? ' dark' : ''}`}
|
|
246
|
+
data-picker-id={pickerId}
|
|
247
|
+
onClick={() => setOpen((prev) => !prev)}
|
|
248
|
+
type="button"
|
|
249
|
+
>
|
|
250
|
+
<ChipVisual
|
|
251
|
+
dark={dark}
|
|
252
|
+
displayValue={displayValue}
|
|
253
|
+
name={name}
|
|
254
|
+
/>
|
|
255
|
+
</button>
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
return (
|
|
259
|
+
<PbReactPopover
|
|
260
|
+
closeOnClick="outside"
|
|
261
|
+
offset
|
|
262
|
+
placement="bottom-start"
|
|
263
|
+
reference={trigger}
|
|
264
|
+
shouldClosePopover={() => setOpen(false)}
|
|
265
|
+
show={open}
|
|
266
|
+
zIndex={10}
|
|
267
|
+
>
|
|
268
|
+
<div
|
|
269
|
+
className={`pb_interactive_filter_editor pb_interactive_filter_editor--${config.type}`}
|
|
270
|
+
>
|
|
271
|
+
{renderEditor()}
|
|
272
|
+
</div>
|
|
273
|
+
</PbReactPopover>
|
|
274
|
+
)
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
export default InteractiveFilter
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
@import "../tokens/spacing";
|
|
2
2
|
@import "../tokens/colors";
|
|
3
|
+
@import "../tokens/border_radius";
|
|
3
4
|
|
|
4
5
|
.pb_filter_sort_menu {
|
|
5
6
|
li {
|
|
@@ -70,6 +71,26 @@
|
|
|
70
71
|
padding-right: $space_xs;
|
|
71
72
|
border-right: 1px solid $border_light !important;
|
|
72
73
|
}
|
|
74
|
+
|
|
75
|
+
.filter.interactive {
|
|
76
|
+
padding-top: 2px;
|
|
77
|
+
padding-bottom: 2px;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Interactive filter chip: clickable button that opens a popover whose
|
|
82
|
+
// contents are the editor for that filter type.
|
|
83
|
+
.pb_interactive_filter_trigger {
|
|
84
|
+
background: transparent;
|
|
85
|
+
border: none;
|
|
86
|
+
cursor: pointer;
|
|
87
|
+
border-radius: 4px;
|
|
88
|
+
text-align: left;
|
|
89
|
+
|
|
90
|
+
&:focus-visible {
|
|
91
|
+
outline: $primary solid 2px;
|
|
92
|
+
outline-offset: -1px;
|
|
93
|
+
}
|
|
73
94
|
}
|
|
74
95
|
|
|
75
96
|
.maskContainer::after {
|
|
@@ -103,4 +124,70 @@
|
|
|
103
124
|
opacity:0;
|
|
104
125
|
}
|
|
105
126
|
}
|
|
106
|
-
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
.pb_popover_tooltip.pb_interactive_filter_editor {
|
|
130
|
+
position: fixed;
|
|
131
|
+
top: 0;
|
|
132
|
+
left: 0;
|
|
133
|
+
margin: 0;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Popover contents for an interactive filter.
|
|
137
|
+
.pb_interactive_filter_editor {
|
|
138
|
+
min-width: 220px;
|
|
139
|
+
|
|
140
|
+
.pb_date_picker_kit {
|
|
141
|
+
margin-bottom: 0 !important;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Select editor: option list. (Native HTML <select> can't be auto-opened
|
|
145
|
+
// without a user gesture in modern browsers, so we render options
|
|
146
|
+
// directly — one click on the chip = options visible.)
|
|
147
|
+
.pb_interactive_filter_options {
|
|
148
|
+
list-style: none;
|
|
149
|
+
margin: 0;
|
|
150
|
+
padding: 0;
|
|
151
|
+
border-radius: $border_rad_heavier;
|
|
152
|
+
border: 1px solid $border_light;
|
|
153
|
+
max-height: 280px;
|
|
154
|
+
overflow-y: auto;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
.pb_interactive_filter_option {
|
|
158
|
+
display: flex;
|
|
159
|
+
align-items: center;
|
|
160
|
+
justify-content: space-between;
|
|
161
|
+
gap: $space_xs;
|
|
162
|
+
padding: $space_xs $space_sm;
|
|
163
|
+
cursor: pointer;
|
|
164
|
+
font-size: $font_base;
|
|
165
|
+
color: $text_lt_default;
|
|
166
|
+
transition: background-color 0.15s ease;
|
|
167
|
+
border-bottom: 1px solid $border_light;
|
|
168
|
+
&:last-child {
|
|
169
|
+
border-bottom: none;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
&:hover {
|
|
173
|
+
background-color: rgba($primary, 0.08);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
&.active {
|
|
177
|
+
background-color: $primary;
|
|
178
|
+
color: $white;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
&.pb_interactive_filter_editor--date-picker {
|
|
183
|
+
// flatpickr inline mode renders directly into our wrapper; suppress its
|
|
184
|
+
// own shadow since the popover already provides the floating surface.
|
|
185
|
+
.flatpickr-calendar {
|
|
186
|
+
box-shadow: none;
|
|
187
|
+
}
|
|
188
|
+
.flatpickr-calendar.inline {
|
|
189
|
+
position: static;
|
|
190
|
+
top: 0;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
<%
|
|
2
|
+
territory_options = [
|
|
3
|
+
{ value: "USA", label: "USA" },
|
|
4
|
+
{ value: "Canada", label: "Canada" },
|
|
5
|
+
{ value: "Brazil", label: "Brazil" },
|
|
6
|
+
{ value: "Philippines", label: "Philippines" },
|
|
7
|
+
]
|
|
8
|
+
|
|
9
|
+
status_options = [
|
|
10
|
+
{ value: "open", label: "Open", id: "open" },
|
|
11
|
+
{ value: "in_progress", label: "In progress", id: "in_progress" },
|
|
12
|
+
{ value: "resolved", label: "Resolved", id: "resolved" },
|
|
13
|
+
{ value: "closed", label: "Closed", id: "closed" },
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
raw_example = params[:example]
|
|
17
|
+
example_params =
|
|
18
|
+
if raw_example.respond_to?(:permit)
|
|
19
|
+
raw_example.permit(:territory, :status, :date_range, :start_date)
|
|
20
|
+
else
|
|
21
|
+
raw_example || {}
|
|
22
|
+
end
|
|
23
|
+
current_territory = example_params[:territory].presence || "USA"
|
|
24
|
+
current_status = example_params[:status].presence || "open"
|
|
25
|
+
current_date_range = example_params[:date_range].presence || "quickpick-this-week"
|
|
26
|
+
current_start = example_params[:start_date].presence || "05/01/2026"
|
|
27
|
+
status_default = status_options.find { |o| o[:value] == current_status }
|
|
28
|
+
%>
|
|
29
|
+
|
|
30
|
+
<%=
|
|
31
|
+
pb_rails("filter", props: {
|
|
32
|
+
id: "filter-interactive-demo",
|
|
33
|
+
min_width: "360px",
|
|
34
|
+
margin_bottom: "xl",
|
|
35
|
+
template: "default",
|
|
36
|
+
results: 546,
|
|
37
|
+
filters: [
|
|
38
|
+
{ name: "Territory", value: current_territory },
|
|
39
|
+
{ name: "Status", value: current_status },
|
|
40
|
+
{ name: "Date range", value: current_date_range },
|
|
41
|
+
{ name: "Start date", value: current_start },
|
|
42
|
+
],
|
|
43
|
+
interactive_filters: [
|
|
44
|
+
{
|
|
45
|
+
name: "Territory",
|
|
46
|
+
type: "select",
|
|
47
|
+
options: territory_options,
|
|
48
|
+
target_input: "filter-interactive-territory",
|
|
49
|
+
auto_submit: true,
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
name: "Status",
|
|
53
|
+
type: "dropdown",
|
|
54
|
+
options: status_options,
|
|
55
|
+
target_input: "filter-interactive-status",
|
|
56
|
+
auto_submit: true,
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
name: "Date range",
|
|
60
|
+
type: "dropdown",
|
|
61
|
+
variant: "quickpick",
|
|
62
|
+
target_input: "filter-interactive-date-range",
|
|
63
|
+
auto_submit: true,
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
name: "Start date",
|
|
67
|
+
type: "date-picker",
|
|
68
|
+
target_input: "filter-interactive-start",
|
|
69
|
+
format: "m/d/Y",
|
|
70
|
+
auto_submit: true,
|
|
71
|
+
},
|
|
72
|
+
],
|
|
73
|
+
sort_menu: [
|
|
74
|
+
{ item: "Popularity", link: "?q[sorts]=managers_popularity+asc", active: true, direction: "desc" },
|
|
75
|
+
],
|
|
76
|
+
}) do
|
|
77
|
+
%>
|
|
78
|
+
<%= pb_rails("form", props: { form_system_options: { scope: :example, method: :get } }) do |form| %>
|
|
79
|
+
<%= pb_rails("select", props: {
|
|
80
|
+
label: "Territory",
|
|
81
|
+
name: "territory",
|
|
82
|
+
options: territory_options.map { |o| { value: o[:value], text: o[:label] } },
|
|
83
|
+
value: current_territory,
|
|
84
|
+
input_options: { id: "filter-interactive-territory" }
|
|
85
|
+
}) %>
|
|
86
|
+
|
|
87
|
+
<%= pb_rails("dropdown", props: {
|
|
88
|
+
id: "filter-interactive-status",
|
|
89
|
+
label: "Status",
|
|
90
|
+
name: "status",
|
|
91
|
+
options: status_options,
|
|
92
|
+
default_value: status_default,
|
|
93
|
+
margin_bottom: "sm",
|
|
94
|
+
}) %>
|
|
95
|
+
|
|
96
|
+
<%= pb_rails("dropdown", props: {
|
|
97
|
+
id: "filter-interactive-date-range",
|
|
98
|
+
label: "Date range",
|
|
99
|
+
name: "date_range",
|
|
100
|
+
variant: "quickpick",
|
|
101
|
+
margin_bottom: "sm",
|
|
102
|
+
}) %>
|
|
103
|
+
|
|
104
|
+
<%= pb_rails("date_picker", props: {
|
|
105
|
+
label: "Start date",
|
|
106
|
+
name: "start_date",
|
|
107
|
+
picker_id: "filter-interactive-start",
|
|
108
|
+
default_date: current_start,
|
|
109
|
+
}) %>
|
|
110
|
+
|
|
111
|
+
<%= form.actions do |action| %>
|
|
112
|
+
<%= action.submit props: { text: "Apply" } %>
|
|
113
|
+
<%= action.button props: { type: "reset", text: "Clear", variant: "secondary" } %>
|
|
114
|
+
<% end %>
|
|
115
|
+
<% end %>
|
|
116
|
+
<% end %>
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
Click an applied filter chip to edit it inline. `interactive_filters` supports:
|
|
2
|
+
|
|
3
|
+
- `type: 'select'` / `'dropdown'`: pick from a list of options.
|
|
4
|
+
- `type: 'date-picker'`: inline calendar. Supports `format`, `min_date`, `max_date`, and `mode`.
|
|
5
|
+
|
|
6
|
+
Each entry needs a `target_input` that points to the form control it updates. For `select` and `date-picker`, use the input's `id`. For `dropdown`, use the Dropdown kit's `id`.
|
|
7
|
+
|
|
8
|
+
For date ranges, use `type: 'dropdown'` with `variant: 'quickpick'`. The Filter kit generates the same quickpick options as Dropdown, so no `options` are needed. Values are quickpick ids, such as `quickpick-this-week`.
|
|
9
|
+
|
|
10
|
+
Chip edits update the linked control inside the filter popover. Click **Apply** to submit, then pass the submitted values back into `filters` so chips re-render with the latest labels.
|
|
11
|
+
|
|
12
|
+
Optional `auto_submit: true` on a chip entry submits the form immediately when a chip value is picked.
|