@wordpress/dataviews 0.4.0 → 0.5.0

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 (77) hide show
  1. package/CHANGELOG.md +2 -0
  2. package/README.md +1 -0
  3. package/build/add-filter.js +25 -108
  4. package/build/add-filter.js.map +1 -1
  5. package/build/constants.js +9 -18
  6. package/build/constants.js.map +1 -1
  7. package/build/dataviews.js +22 -16
  8. package/build/dataviews.js.map +1 -1
  9. package/build/dropdown-menu-helper.js +1 -2
  10. package/build/dropdown-menu-helper.js.map +1 -1
  11. package/build/filter-summary.js +180 -77
  12. package/build/filter-summary.js.map +1 -1
  13. package/build/filters.js +32 -18
  14. package/build/filters.js.map +1 -1
  15. package/build/pagination.js +1 -2
  16. package/build/pagination.js.map +1 -1
  17. package/build/reset-filters.js +4 -1
  18. package/build/reset-filters.js.map +1 -1
  19. package/build/search-widget.js +111 -0
  20. package/build/search-widget.js.map +1 -0
  21. package/build/search.js +2 -3
  22. package/build/search.js.map +1 -1
  23. package/build/single-selection-checkbox.js +54 -0
  24. package/build/single-selection-checkbox.js.map +1 -0
  25. package/build/utils.js +14 -1
  26. package/build/utils.js.map +1 -1
  27. package/build/view-actions.js +2 -3
  28. package/build/view-actions.js.map +1 -1
  29. package/build/view-grid.js +92 -22
  30. package/build/view-grid.js.map +1 -1
  31. package/build/view-list.js +2 -1
  32. package/build/view-list.js.map +1 -1
  33. package/build/view-table.js +45 -134
  34. package/build/view-table.js.map +1 -1
  35. package/build-module/add-filter.js +28 -111
  36. package/build-module/add-filter.js.map +1 -1
  37. package/build-module/dataviews.js +23 -17
  38. package/build-module/dataviews.js.map +1 -1
  39. package/build-module/filter-summary.js +181 -79
  40. package/build-module/filter-summary.js.map +1 -1
  41. package/build-module/filters.js +32 -17
  42. package/build-module/filters.js.map +1 -1
  43. package/build-module/reset-filters.js +4 -1
  44. package/build-module/reset-filters.js.map +1 -1
  45. package/build-module/search-widget.js +101 -0
  46. package/build-module/search-widget.js.map +1 -0
  47. package/build-module/search.js +1 -1
  48. package/build-module/search.js.map +1 -1
  49. package/build-module/single-selection-checkbox.js +47 -0
  50. package/build-module/single-selection-checkbox.js.map +1 -0
  51. package/build-module/utils.js +12 -0
  52. package/build-module/utils.js.map +1 -1
  53. package/build-module/view-actions.js +1 -1
  54. package/build-module/view-actions.js.map +1 -1
  55. package/build-module/view-grid.js +92 -22
  56. package/build-module/view-grid.js.map +1 -1
  57. package/build-module/view-list.js +2 -1
  58. package/build-module/view-list.js.map +1 -1
  59. package/build-module/view-table.js +45 -133
  60. package/build-module/view-table.js.map +1 -1
  61. package/build-style/style-rtl.css +257 -44
  62. package/build-style/style.css +257 -44
  63. package/package.json +12 -11
  64. package/src/add-filter.js +39 -230
  65. package/src/dataviews.js +31 -20
  66. package/src/filter-summary.js +228 -135
  67. package/src/filters.js +42 -29
  68. package/src/reset-filters.js +12 -2
  69. package/src/search-widget.js +128 -0
  70. package/src/search.js +1 -1
  71. package/src/single-selection-checkbox.js +59 -0
  72. package/src/style.scss +264 -44
  73. package/src/utils.js +15 -0
  74. package/src/view-actions.js +1 -2
  75. package/src/view-grid.js +127 -53
  76. package/src/view-list.js +5 -1
  77. package/src/view-table.js +61 -234
@@ -1,45 +1,54 @@
1
+ /**
2
+ * External dependencies
3
+ */
4
+ import classnames from 'classnames';
5
+
1
6
  /**
2
7
  * WordPress dependencies
3
8
  */
4
9
  import {
10
+ Dropdown,
5
11
  Button,
6
- privateApis as componentsPrivateApis,
12
+ __experimentalVStack as VStack,
13
+ __experimentalHStack as HStack,
14
+ FlexItem,
15
+ SelectControl,
16
+ Tooltip,
7
17
  Icon,
8
18
  } from '@wordpress/components';
9
- import { chevronDown } from '@wordpress/icons';
10
19
  import { __, sprintf } from '@wordpress/i18n';
11
- import { Children, Fragment } from '@wordpress/element';
20
+ import { useRef, createInterpolateElement } from '@wordpress/element';
21
+ import { closeSmall } from '@wordpress/icons';
22
+ import { ENTER, SPACE } from '@wordpress/keycodes';
12
23
 
13
24
  /**
14
25
  * Internal dependencies
15
26
  */
27
+ import SearchWidget from './search-widget';
16
28
  import { OPERATOR_IN, OPERATOR_NOT_IN, OPERATORS } from './constants';
17
- import { unlock } from './lock-unlock';
18
- import { DropdownMenuRadioItemCustom } from './dropdown-menu-helper';
19
-
20
- const {
21
- DropdownMenuV2: DropdownMenu,
22
- DropdownMenuGroupV2: DropdownMenuGroup,
23
- DropdownMenuItemV2: DropdownMenuItem,
24
- DropdownMenuSeparatorV2: DropdownMenuSeparator,
25
- DropdownMenuItemLabelV2: DropdownMenuItemLabel,
26
- DropdownMenuItemHelpTextV2: DropdownMenuItemHelpText,
27
- } = unlock( componentsPrivateApis );
28
29
 
29
30
  const FilterText = ( { activeElement, filterInView, filter } ) => {
30
31
  if ( activeElement === undefined ) {
31
32
  return filter.name;
32
33
  }
33
34
 
35
+ const filterTextWrappers = {
36
+ Span1: <span className="dataviews-filter-summary__filter-text-name" />,
37
+ Span2: <span className="dataviews-filter-summary__filter-text-value" />,
38
+ };
39
+
34
40
  if (
35
41
  activeElement !== undefined &&
36
42
  filterInView?.operator === OPERATOR_IN
37
43
  ) {
38
- return sprintf(
39
- /* translators: 1: Filter name. 2: Filter value. e.g.: "Author is Admin". */
40
- __( '%1$s is %2$s' ),
41
- filter.name,
42
- activeElement.label
44
+ return createInterpolateElement(
45
+ sprintf(
46
+ /* translators: 1: Filter name. 2: Filter value. e.g.: "Author is Admin". */
47
+ __( '<Span1>%1$s </Span1><Span2>is %2$s</Span2>' ),
48
+ filter.name,
49
+ activeElement.label
50
+ ),
51
+ filterTextWrappers
43
52
  );
44
53
  }
45
54
 
@@ -47,11 +56,14 @@ const FilterText = ( { activeElement, filterInView, filter } ) => {
47
56
  activeElement !== undefined &&
48
57
  filterInView?.operator === OPERATOR_NOT_IN
49
58
  ) {
50
- return sprintf(
51
- /* translators: 1: Filter name. 2: Filter value. e.g.: "Author is not Admin". */
52
- __( '%1$s is not %2$s' ),
53
- filter.name,
54
- activeElement.label
59
+ return createInterpolateElement(
60
+ sprintf(
61
+ /* translators: 1: Filter name. 2: Filter value. e.g.: "Author is not Admin". */
62
+ __( '<Span1>%1$s </Span1><Span2>is not %2$s</Span2>' ),
63
+ filter.name,
64
+ activeElement.label
65
+ ),
66
+ filterTextWrappers
55
67
  );
56
68
  }
57
69
 
@@ -62,127 +74,208 @@ const FilterText = ( { activeElement, filterInView, filter } ) => {
62
74
  );
63
75
  };
64
76
 
65
- function WithSeparators( { children } ) {
66
- return Children.toArray( children )
67
- .filter( Boolean )
68
- .map( ( child, i ) => (
69
- <Fragment key={ i }>
70
- { i > 0 && <DropdownMenuSeparator /> }
71
- { child }
72
- </Fragment>
73
- ) );
77
+ function OperatorSelector( { filter, view, onChangeView } ) {
78
+ const operatorOptions = filter.operators?.map( ( operator ) => ( {
79
+ value: operator,
80
+ label: OPERATORS[ operator ]?.label,
81
+ } ) );
82
+ const currentFilter = view.filters.find(
83
+ ( _filter ) => _filter.field === filter.field
84
+ );
85
+ const value = currentFilter?.operator || filter.operators[ 0 ];
86
+ return (
87
+ operatorOptions.length > 1 && (
88
+ <HStack
89
+ spacing={ 2 }
90
+ justify="flex-start"
91
+ className="dataviews-filter-summary__operators-container"
92
+ >
93
+ <FlexItem className="dataviews-filter-summary__operators-filter-name">
94
+ { filter.name }
95
+ </FlexItem>
96
+
97
+ <SelectControl
98
+ label={ __( 'Conditions' ) }
99
+ value={ value }
100
+ options={ operatorOptions }
101
+ onChange={ ( newValue ) => {
102
+ const newFilters = currentFilter
103
+ ? [
104
+ ...view.filters.map( ( _filter ) => {
105
+ if ( _filter.field === filter.field ) {
106
+ return {
107
+ ..._filter,
108
+ operator: newValue,
109
+ };
110
+ }
111
+ return _filter;
112
+ } ),
113
+ ]
114
+ : [
115
+ ...view.filters,
116
+ {
117
+ field: filter.field,
118
+ operator: newValue,
119
+ },
120
+ ];
121
+ onChangeView( {
122
+ ...view,
123
+ page: 1,
124
+ filters: newFilters,
125
+ } );
126
+ } }
127
+ size="small"
128
+ __nextHasNoMarginBottom
129
+ hideLabelFromVision
130
+ />
131
+ </HStack>
132
+ )
133
+ );
74
134
  }
75
135
 
76
- export default function FilterSummary( { filter, view, onChangeView } ) {
77
- const filterInView = view.filters.find( ( f ) => f.field === filter.field );
78
- const otherFilters = view.filters.filter(
79
- ( f ) => f.field !== filter.field
136
+ function ResetFilter( { filter, view, onChangeView, addFilterRef } ) {
137
+ const isDisabled =
138
+ filter.isPrimary &&
139
+ view.filters.find( ( _filter ) => _filter.field === filter.field )
140
+ ?.value === undefined;
141
+ return (
142
+ <div className="dataviews-filter-summary__reset">
143
+ <Button
144
+ disabled={ isDisabled }
145
+ __experimentalIsFocusable
146
+ size="compact"
147
+ variant="tertiary"
148
+ style={ { justifyContent: 'center', width: '100%' } }
149
+ onClick={ () => {
150
+ onChangeView( {
151
+ ...view,
152
+ page: 1,
153
+ filters: view.filters.filter(
154
+ ( _filter ) => _filter.field !== filter.field
155
+ ),
156
+ } );
157
+ // If the filter is not primary and can be removed, it will be added
158
+ // back to the available filters from `Add filter` component.
159
+ if ( ! filter.isPrimary ) {
160
+ addFilterRef.current?.focus();
161
+ }
162
+ } }
163
+ >
164
+ { filter.isPrimary ? __( 'Reset' ) : __( 'Remove' ) }
165
+ </Button>
166
+ </div>
80
167
  );
168
+ }
169
+
170
+ export default function FilterSummary( {
171
+ addFilterRef,
172
+ openedFilter,
173
+ ...commonProps
174
+ } ) {
175
+ const toggleRef = useRef();
176
+ const { filter, view, onChangeView } = commonProps;
177
+ const filterInView = view.filters.find( ( f ) => f.field === filter.field );
81
178
  const activeElement = filter.elements.find(
82
179
  ( element ) => element.value === filterInView?.value
83
180
  );
84
- const activeOperator = filterInView?.operator || filter.operators[ 0 ];
85
-
181
+ const isPrimary = filter.isPrimary;
182
+ const hasValues = filterInView?.value !== undefined;
183
+ const canResetOrRemove = ! isPrimary || hasValues;
86
184
  return (
87
- <DropdownMenu
88
- key={ filter.field }
89
- trigger={
90
- <Button variant="tertiary" size="compact" label={ filter.name }>
91
- <FilterText
92
- activeElement={ activeElement }
93
- filterInView={ filterInView }
94
- filter={ filter }
95
- />
96
- <Icon icon={ chevronDown } style={ { flexShrink: 0 } } />
97
- </Button>
98
- }
99
- >
100
- <WithSeparators>
101
- <DropdownMenuGroup>
102
- { filter.elements.map( ( element ) => {
103
- const isActive = activeElement?.value === element.value;
104
- return (
105
- <DropdownMenuRadioItemCustom
106
- key={ element.value }
107
- name={ `filter-summary-${ filter.field }` }
108
- value={ element.value }
109
- checked={ isActive }
110
- onClick={ () =>
185
+ <Dropdown
186
+ defaultOpen={ openedFilter === filter.field }
187
+ contentClassName="dataviews-filter-summary__popover"
188
+ popoverProps={ { placement: 'bottom-start', role: 'dialog' } }
189
+ onClose={ () => {
190
+ toggleRef.current?.focus();
191
+ } }
192
+ renderToggle={ ( { isOpen, onToggle } ) => (
193
+ <div className="dataviews-filter-summary__chip-container">
194
+ <Tooltip
195
+ text={ sprintf(
196
+ /* translators: 1: Filter name. */
197
+ __( 'Filter by: %1$s' ),
198
+ filter.name.toLowerCase()
199
+ ) }
200
+ placement="top"
201
+ >
202
+ <div
203
+ className={ classnames(
204
+ 'dataviews-filter-summary__chip',
205
+ {
206
+ 'has-reset': canResetOrRemove,
207
+ 'has-values': hasValues,
208
+ }
209
+ ) }
210
+ role="button"
211
+ tabIndex={ 0 }
212
+ onClick={ onToggle }
213
+ onKeyDown={ ( event ) => {
214
+ if (
215
+ [ ENTER, SPACE ].includes( event.keyCode )
216
+ ) {
217
+ onToggle();
218
+ event.preventDefault();
219
+ }
220
+ } }
221
+ aria-pressed={ isOpen }
222
+ aria-expanded={ isOpen }
223
+ ref={ toggleRef }
224
+ >
225
+ <FilterText
226
+ activeElement={ activeElement }
227
+ filterInView={ filterInView }
228
+ filter={ filter }
229
+ />
230
+ </div>
231
+ </Tooltip>
232
+ { canResetOrRemove && (
233
+ <Tooltip
234
+ text={ isPrimary ? __( 'Reset' ) : __( 'Remove' ) }
235
+ placement="top"
236
+ >
237
+ <button
238
+ className={ classnames(
239
+ 'dataviews-filter-summary__chip-remove',
240
+ { 'has-values': hasValues }
241
+ ) }
242
+ onClick={ () => {
111
243
  onChangeView( {
112
244
  ...view,
113
245
  page: 1,
114
- filters: [
115
- ...otherFilters,
116
- {
117
- field: filter.field,
118
- operator: activeOperator,
119
- value: isActive
120
- ? undefined
121
- : element.value,
122
- },
123
- ],
124
- } )
125
- }
126
- >
127
- <DropdownMenuItemLabel>
128
- { element.label }
129
- </DropdownMenuItemLabel>
130
- { !! element.description && (
131
- <DropdownMenuItemHelpText>
132
- { element.description }
133
- </DropdownMenuItemHelpText>
134
- ) }
135
- </DropdownMenuRadioItemCustom>
136
- );
137
- } ) }
138
- </DropdownMenuGroup>
139
- { filter.operators.length > 1 && (
140
- <DropdownMenu
141
- trigger={
142
- <DropdownMenuItem
143
- suffix={
144
- <span aria-hidden="true">
145
- { OPERATORS[ activeOperator ]?.label }
146
- </span>
147
- }
246
+ filters: view.filters.filter(
247
+ ( _filter ) =>
248
+ _filter.field !== filter.field
249
+ ),
250
+ } );
251
+ // If the filter is not primary and can be removed, it will be added
252
+ // back to the available filters from `Add filter` component.
253
+ if ( ! isPrimary ) {
254
+ addFilterRef.current?.focus();
255
+ } else {
256
+ // If is primary, focus the toggle button.
257
+ toggleRef.current?.focus();
258
+ }
259
+ } }
148
260
  >
149
- <DropdownMenuItemLabel>
150
- { __( 'Conditions' ) }
151
- </DropdownMenuItemLabel>
152
- </DropdownMenuItem>
153
- }
154
- >
155
- { Object.entries( OPERATORS ).map(
156
- ( [ operator, { label, key } ] ) => (
157
- <DropdownMenuRadioItemCustom
158
- key={ key }
159
- name={ `filter-summary-${ filter.field }-conditions` }
160
- value={ operator }
161
- checked={ activeOperator === operator }
162
- onChange={ ( e ) => {
163
- onChangeView( {
164
- ...view,
165
- page: 1,
166
- filters: [
167
- ...otherFilters,
168
- {
169
- field: filter.field,
170
- operator: e.target.value,
171
- value: filterInView?.value,
172
- },
173
- ],
174
- } );
175
- } }
176
- >
177
- <DropdownMenuItemLabel>
178
- { label }
179
- </DropdownMenuItemLabel>
180
- </DropdownMenuRadioItemCustom>
181
- )
182
- ) }
183
- </DropdownMenu>
184
- ) }
185
- </WithSeparators>
186
- </DropdownMenu>
261
+ <Icon icon={ closeSmall } />
262
+ </button>
263
+ </Tooltip>
264
+ ) }
265
+ </div>
266
+ ) }
267
+ renderContent={ () => {
268
+ return (
269
+ <VStack spacing={ 0 } justify="flex-start">
270
+ <OperatorSelector { ...commonProps } />
271
+ <SearchWidget { ...commonProps } />
272
+ <ResetFilter
273
+ { ...commonProps }
274
+ addFilterRef={ addFilterRef }
275
+ />
276
+ </VStack>
277
+ );
278
+ } }
279
+ />
187
280
  );
188
281
  }
package/src/filters.js CHANGED
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * WordPress dependencies
3
3
  */
4
- import { memo } from '@wordpress/element';
4
+ import { memo, useRef } from '@wordpress/element';
5
5
 
6
6
  /**
7
7
  * Internal dependencies
@@ -9,24 +9,17 @@ import { memo } from '@wordpress/element';
9
9
  import FilterSummary from './filter-summary';
10
10
  import AddFilter from './add-filter';
11
11
  import ResetFilters from './reset-filters';
12
- import {
13
- ENUMERATION_TYPE,
14
- OPERATOR_IN,
15
- OPERATOR_NOT_IN,
16
- LAYOUT_LIST,
17
- } from './constants';
12
+ import { sanitizeOperators } from './utils';
13
+ import { ENUMERATION_TYPE, OPERATOR_IN, OPERATOR_NOT_IN } from './constants';
18
14
 
19
- const sanitizeOperators = ( field ) => {
20
- let operators = field.filterBy?.operators;
21
- if ( ! operators || ! Array.isArray( operators ) ) {
22
- operators = [ OPERATOR_IN, OPERATOR_NOT_IN ];
23
- }
24
- return operators.filter( ( operator ) =>
25
- [ OPERATOR_IN, OPERATOR_NOT_IN ].includes( operator )
26
- );
27
- };
28
-
29
- const Filters = memo( function Filters( { fields, view, onChangeView } ) {
15
+ const Filters = memo( function Filters( {
16
+ fields,
17
+ view,
18
+ onChangeView,
19
+ openedFilter,
20
+ setOpenedFilter,
21
+ } ) {
22
+ const addFilterRef = useRef();
30
23
  const filters = [];
31
24
  fields.forEach( ( field ) => {
32
25
  if ( ! field.type ) {
@@ -43,34 +36,50 @@ const Filters = memo( function Filters( { fields, view, onChangeView } ) {
43
36
  if ( ! field.elements?.length ) {
44
37
  return;
45
38
  }
39
+
40
+ const isPrimary = !! field.filterBy?.isPrimary;
46
41
  filters.push( {
47
42
  field: field.id,
48
43
  name: field.header,
49
44
  elements: field.elements,
50
45
  operators,
51
- isVisible: view.filters.some(
52
- ( f ) =>
53
- f.field === field.id &&
54
- [ OPERATOR_IN, OPERATOR_NOT_IN ].includes(
55
- f.operator
56
- )
57
- ),
46
+ isVisible:
47
+ isPrimary ||
48
+ view.filters.some(
49
+ ( f ) =>
50
+ f.field === field.id &&
51
+ [ OPERATOR_IN, OPERATOR_NOT_IN ].includes(
52
+ f.operator
53
+ )
54
+ ),
55
+ isPrimary,
58
56
  } );
59
57
  }
60
58
  } );
61
-
59
+ // Sort filters by primary property. We need the primary filters to be first.
60
+ // Then we sort by name.
61
+ filters.sort( ( a, b ) => {
62
+ if ( a.isPrimary && ! b.isPrimary ) {
63
+ return -1;
64
+ }
65
+ if ( ! a.isPrimary && b.isPrimary ) {
66
+ return 1;
67
+ }
68
+ return a.name.localeCompare( b.name );
69
+ } );
62
70
  const addFilter = (
63
71
  <AddFilter
64
72
  key="add-filter"
65
73
  filters={ filters }
66
74
  view={ view }
67
75
  onChangeView={ onChangeView }
76
+ ref={ addFilterRef }
77
+ setOpenedFilter={ setOpenedFilter }
68
78
  />
69
79
  );
70
80
  const filterComponents = [
71
- addFilter,
72
81
  ...filters.map( ( filter ) => {
73
- if ( ! filter.isVisible || view.type === LAYOUT_LIST ) {
82
+ if ( ! filter.isVisible ) {
74
83
  return null;
75
84
  }
76
85
 
@@ -80,15 +89,19 @@ const Filters = memo( function Filters( { fields, view, onChangeView } ) {
80
89
  filter={ filter }
81
90
  view={ view }
82
91
  onChangeView={ onChangeView }
92
+ addFilterRef={ addFilterRef }
93
+ openedFilter={ openedFilter }
83
94
  />
84
95
  );
85
96
  } ),
97
+ addFilter,
86
98
  ];
87
99
 
88
- if ( filterComponents.length > 1 && view.type !== LAYOUT_LIST ) {
100
+ if ( filterComponents.length > 1 ) {
89
101
  filterComponents.push(
90
102
  <ResetFilters
91
103
  key="reset-filters"
104
+ filters={ filters }
92
105
  view={ view }
93
106
  onChangeView={ onChangeView }
94
107
  />
@@ -4,10 +4,20 @@
4
4
  import { Button } from '@wordpress/components';
5
5
  import { __ } from '@wordpress/i18n';
6
6
 
7
- export default function ResetFilter( { view, onChangeView } ) {
7
+ export default function ResetFilter( { filters, view, onChangeView } ) {
8
+ const isPrimary = ( field ) =>
9
+ filters.some(
10
+ ( _filter ) => _filter.field === field && _filter.isPrimary
11
+ );
12
+ const isDisabled =
13
+ ! view.search &&
14
+ ! view.filters?.some(
15
+ ( _filter ) =>
16
+ _filter.value !== undefined || ! isPrimary( _filter.field )
17
+ );
8
18
  return (
9
19
  <Button
10
- disabled={ view.search === '' && view.filters?.length === 0 }
20
+ disabled={ isDisabled }
11
21
  __experimentalIsFocusable
12
22
  size="compact"
13
23
  variant="tertiary"
@@ -0,0 +1,128 @@
1
+ /**
2
+ * External dependencies
3
+ */
4
+ // eslint-disable-next-line no-restricted-imports
5
+ import * as Ariakit from '@ariakit/react';
6
+ import removeAccents from 'remove-accents';
7
+
8
+ /**
9
+ * WordPress dependencies
10
+ */
11
+ import { __ } from '@wordpress/i18n';
12
+ import { useState, useMemo, useDeferredValue } from '@wordpress/element';
13
+ import { VisuallyHidden, Icon } from '@wordpress/components';
14
+ import { search } from '@wordpress/icons';
15
+ import { SVG, Circle } from '@wordpress/primitives';
16
+
17
+ const radioCheck = (
18
+ <SVG xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
19
+ <Circle cx={ 12 } cy={ 12 } r={ 3 }></Circle>
20
+ </SVG>
21
+ );
22
+
23
+ function normalizeSearchInput( input = '' ) {
24
+ return removeAccents( input.trim().toLowerCase() );
25
+ }
26
+
27
+ export default function SearchWidget( { filter, view, onChangeView } ) {
28
+ const [ searchValue, setSearchValue ] = useState( '' );
29
+ const deferredSearchValue = useDeferredValue( searchValue );
30
+ const selectedFilter = view.filters.find(
31
+ ( _filter ) => _filter.field === filter.field
32
+ );
33
+ const selectedValues = selectedFilter?.value;
34
+ const matches = useMemo( () => {
35
+ const normalizedSearch = normalizeSearchInput( deferredSearchValue );
36
+ return filter.elements.filter( ( item ) =>
37
+ normalizeSearchInput( item.label ).includes( normalizedSearch )
38
+ );
39
+ }, [ filter.elements, deferredSearchValue ] );
40
+ return (
41
+ <Ariakit.ComboboxProvider
42
+ value={ searchValue }
43
+ setSelectedValue={ ( value ) => {
44
+ const currentFilter = view.filters.find(
45
+ ( _filter ) => _filter.field === filter.field
46
+ );
47
+ const newFilters = currentFilter
48
+ ? [
49
+ ...view.filters.map( ( _filter ) => {
50
+ if ( _filter.field === filter.field ) {
51
+ return {
52
+ ..._filter,
53
+ operator:
54
+ currentFilter.operator ||
55
+ filter.operators[ 0 ],
56
+ value,
57
+ };
58
+ }
59
+ return _filter;
60
+ } ),
61
+ ]
62
+ : [
63
+ ...view.filters,
64
+ {
65
+ field: filter.field,
66
+ operator: filter.operators[ 0 ],
67
+ value,
68
+ },
69
+ ];
70
+ onChangeView( {
71
+ ...view,
72
+ page: 1,
73
+ filters: newFilters,
74
+ } );
75
+ } }
76
+ setValue={ setSearchValue }
77
+ >
78
+ <div className="dataviews-search-widget-filter-combobox__wrapper">
79
+ <Ariakit.ComboboxLabel render={ <VisuallyHidden /> }>
80
+ { __( 'Search items' ) }
81
+ </Ariakit.ComboboxLabel>
82
+ <Ariakit.Combobox
83
+ autoSelect="always"
84
+ placeholder={ __( 'Search' ) }
85
+ className="dataviews-search-widget-filter-combobox__input"
86
+ />
87
+ <div className="dataviews-search-widget-filter-combobox__icon">
88
+ <Icon icon={ search } />
89
+ </div>
90
+ </div>
91
+ <Ariakit.ComboboxList
92
+ className="dataviews-search-widget-filter-combobox-list"
93
+ alwaysVisible
94
+ >
95
+ { matches.map( ( element ) => {
96
+ return (
97
+ <Ariakit.ComboboxItem
98
+ key={ element.value }
99
+ value={ element.value }
100
+ className="dataviews-search-widget-filter-combobox-item"
101
+ hideOnClick={ false }
102
+ setValueOnClick={ false }
103
+ focusOnHover
104
+ >
105
+ <span className="dataviews-search-widget-filter-combobox-item-check">
106
+ { selectedValues === element.value && (
107
+ <Icon icon={ radioCheck } />
108
+ ) }
109
+ </span>
110
+ <span>
111
+ <Ariakit.ComboboxItemValue
112
+ className="dataviews-search-widget-filter-combobox-item-value"
113
+ value={ element.label }
114
+ />
115
+ { !! element.description && (
116
+ <span className="dataviews-search-widget-filter-combobox-item-description">
117
+ { element.description }
118
+ </span>
119
+ ) }
120
+ </span>
121
+ </Ariakit.ComboboxItem>
122
+ );
123
+ } ) }
124
+ { ! matches.length && <p>{ __( 'No results found' ) }</p> }
125
+ </Ariakit.ComboboxList>
126
+ </Ariakit.ComboboxProvider>
127
+ );
128
+ }