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.
Files changed (38) hide show
  1. checksums.yaml +4 -4
  2. data/app/pb_kits/playbook/pb_filter/Filter/CurrentFilters.tsx +55 -28
  3. data/app/pb_kits/playbook/pb_filter/Filter/FilterDouble.tsx +4 -1
  4. data/app/pb_kits/playbook/pb_filter/Filter/FilterSingle.tsx +4 -1
  5. data/app/pb_kits/playbook/pb_filter/Filter/InteractiveFilter.tsx +277 -0
  6. data/app/pb_kits/playbook/pb_filter/_filter.scss +88 -1
  7. data/app/pb_kits/playbook/pb_filter/docs/_filter_interactive.html.erb +116 -0
  8. data/app/pb_kits/playbook/pb_filter/docs/_filter_interactive.md +12 -0
  9. data/app/pb_kits/playbook/pb_filter/docs/_filter_interactive_react.jsx +153 -0
  10. data/app/pb_kits/playbook/pb_filter/docs/_filter_interactive_react.md +10 -0
  11. data/app/pb_kits/playbook/pb_filter/docs/example.yml +2 -0
  12. data/app/pb_kits/playbook/pb_filter/docs/index.js +2 -1
  13. data/app/pb_kits/playbook/pb_filter/filter.html.erb +80 -3
  14. data/app/pb_kits/playbook/pb_filter/filter.rb +71 -0
  15. data/app/pb_kits/playbook/pb_filter/filter.test.js +78 -0
  16. data/app/pb_kits/playbook/pb_filter/index.ts +349 -0
  17. data/app/pb_kits/playbook/pb_filter/kit.schema.json +6 -0
  18. data/app/pb_kits/playbook/pb_icon/docs/_icon_default.jsx +1 -1
  19. data/app/pb_kits/playbook/pb_loading_inline/_loading_inline.tsx +3 -1
  20. data/app/pb_kits/playbook/pb_loading_inline/docs/_loading_inline_color.html.erb +21 -0
  21. data/app/pb_kits/playbook/pb_loading_inline/docs/_loading_inline_color.jsx +55 -0
  22. data/app/pb_kits/playbook/pb_loading_inline/docs/_loading_inline_color.md +1 -0
  23. data/app/pb_kits/playbook/pb_loading_inline/docs/example.yml +2 -0
  24. data/app/pb_kits/playbook/pb_loading_inline/docs/index.js +1 -0
  25. data/app/pb_kits/playbook/pb_loading_inline/kit.schema.json +18 -2
  26. data/app/pb_kits/playbook/pb_loading_inline/loading_inline.html.erb +1 -1
  27. data/app/pb_kits/playbook/pb_loading_inline/loading_inline.rb +3 -0
  28. data/dist/chunks/{_pb_line_graph-DxHutusS.js → _pb_line_graph-D8PSzzEY.js} +1 -1
  29. data/dist/chunks/{_typeahead-DoLAfwVt.js → _typeahead-CuXG_NFx.js} +2 -2
  30. data/dist/chunks/{globalProps-D2_gFcf5.js → globalProps-B8stOeTI.js} +1 -1
  31. data/dist/chunks/lib-BAri19Ko.js +29 -0
  32. data/dist/chunks/vendor.js +5 -5
  33. data/dist/playbook-rails-react-bindings.js +1 -1
  34. data/dist/playbook-rails.js +1 -1
  35. data/dist/playbook.css +1 -1
  36. data/lib/playbook/version.rb +1 -1
  37. metadata +15 -6
  38. data/dist/chunks/lib-C6NzIorw.js +0 -29
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: fbc8ab8a2cb4410a11719212ed4dd8808b3de275ae738263a157c700f2124bee
4
- data.tar.gz: 6c12f43c0557d53ddc4bea574d6312893883ea55886623a09f2d46e5180693ef
3
+ metadata.gz: 0c56cffd7ae20d303e7ebfb111960cd77e6b3ef3020867b335b91ad0743ec88c
4
+ data.tar.gz: 12ed387ffcb0607f46a4ef6f231bb7823b5b3c81f7a988783b8b652491ef29cf
5
5
  SHA512:
6
- metadata.gz: 10b8dff1e91a51f136767f109b0cf688a0b6356505316876adabebd3bac28324fc3c1c11be147f888d6532abd8818fba0f7e83844dc4dbd96a3e8aea88cab265
7
- data.tar.gz: ba68c30c290c2a3a77324e775703c272157c3cecfb1e321d78fb620acc5b1f3a42e36ccb9e5d6ec706eb7cef1c2584efc940ad5cd61d89ffff91c378b3bba7ae
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 = ({ dark, filters }: CurrentFiltersProps): React.ReactElement => {
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
- <div
40
- className="filter"
41
- key={`filter-${name}`}
42
- >
43
- { value === true ?
44
- <Title
45
- dark={dark}
46
- size={4}
47
- tag="h4"
48
- text={`${name}`}
49
- /> :
50
- <div>
51
- <Caption
52
- dark={dark}
53
- text={`${name}`}
54
- />
55
- <Title
56
- dark={dark}
57
- size={4}
58
- tag="h4"
59
- text={value}
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
- </div>
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.