draft-components 1.6.0 → 1.7.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 (174) hide show
  1. package/cjs/components/dialog/dialog.cjs +1 -1
  2. package/cjs/components/filtered-search/filter-item.cjs +23 -0
  3. package/cjs/components/filtered-search/filter-operator-select.cjs +17 -0
  4. package/cjs/components/filtered-search/filter-token.cjs +17 -0
  5. package/cjs/components/filtered-search/filter-value-list.cjs +20 -0
  6. package/cjs/components/filtered-search/filtered-search.cjs +202 -0
  7. package/cjs/components/filtered-search/icons.cjs +21 -0
  8. package/cjs/components/filtered-search/model/abstract-filter.cjs +6 -0
  9. package/cjs/components/filtered-search/model/string-filter.cjs +46 -0
  10. package/cjs/components/filtered-search/model/string-set-filter.cjs +44 -0
  11. package/cjs/components/filtered-search/string-filter-input.cjs +17 -0
  12. package/cjs/components/filtered-search/string-filter-item.cjs +68 -0
  13. package/cjs/components/filtered-search/string-set-filter-item.cjs +77 -0
  14. package/cjs/components/filtered-search/use-combobox-ids.cjs +17 -0
  15. package/cjs/components/filtered-search/use-translations.cjs +24 -0
  16. package/cjs/components/index.cjs +12 -0
  17. package/cjs/components/popover/popover.cjs +3 -4
  18. package/cjs/components/slide-over/slide-over-body.cjs +10 -0
  19. package/cjs/components/slide-over/slide-over-context.cjs +24 -0
  20. package/cjs/components/slide-over/slide-over-header.cjs +21 -0
  21. package/cjs/components/slide-over/slide-over.cjs +106 -0
  22. package/cjs/hooks/index.cjs +2 -2
  23. package/cjs/hooks/use-disable-body-scroll.cjs +3 -1
  24. package/cjs/hooks/use-esc-key-down.cjs +3 -1
  25. package/cjs/hooks/use-focus-trap.cjs +3 -1
  26. package/cjs/index.cjs +16 -2
  27. package/cjs/lib/helpers.cjs +5 -1
  28. package/cjs/lib/index.cjs +2 -0
  29. package/cjs/lib/keyboard-keys.cjs +1 -0
  30. package/cjs/lib/react-helpers.cjs +8 -0
  31. package/css/draft-components.css +345 -2
  32. package/css/draft-components.dark.css +65 -40
  33. package/esm/components/dialog/dialog.js +1 -1
  34. package/esm/components/filtered-search/filter-item.js +21 -0
  35. package/esm/components/filtered-search/filter-operator-select.js +15 -0
  36. package/esm/components/filtered-search/filter-token.js +15 -0
  37. package/esm/components/filtered-search/filter-value-list.js +18 -0
  38. package/esm/components/filtered-search/filtered-search.js +200 -0
  39. package/esm/components/filtered-search/icons.js +16 -0
  40. package/esm/components/filtered-search/model/abstract-filter.js +4 -0
  41. package/esm/components/filtered-search/model/string-filter.js +44 -0
  42. package/esm/components/filtered-search/model/string-set-filter.js +42 -0
  43. package/esm/components/filtered-search/string-filter-input.js +15 -0
  44. package/esm/components/filtered-search/string-filter-item.js +66 -0
  45. package/esm/components/filtered-search/string-set-filter-item.js +75 -0
  46. package/esm/components/filtered-search/use-combobox-ids.js +15 -0
  47. package/esm/components/filtered-search/use-translations.js +21 -0
  48. package/esm/components/index.js +6 -0
  49. package/esm/components/popover/popover.js +3 -4
  50. package/esm/components/slide-over/slide-over-body.js +8 -0
  51. package/esm/components/slide-over/slide-over-context.js +21 -0
  52. package/esm/components/slide-over/slide-over-header.js +19 -0
  53. package/esm/components/slide-over/slide-over.js +104 -0
  54. package/esm/hooks/index.js +1 -1
  55. package/esm/hooks/use-disable-body-scroll.js +3 -1
  56. package/esm/hooks/use-esc-key-down.js +3 -1
  57. package/esm/hooks/use-focus-trap.js +3 -1
  58. package/esm/index.js +9 -3
  59. package/esm/lib/helpers.js +5 -2
  60. package/esm/lib/index.js +2 -2
  61. package/esm/lib/keyboard-keys.js +1 -0
  62. package/esm/lib/react-helpers.js +8 -1
  63. package/package.json +30 -30
  64. package/types/components/alert/alert.d.ts +1 -1
  65. package/types/components/avatar/avatar.d.ts +1 -1
  66. package/types/components/avatar-group/avatar-group.d.ts +1 -1
  67. package/types/components/badge/badge.d.ts +1 -1
  68. package/types/components/breadcrumbs/breadcrumbs-context.d.ts +1 -1
  69. package/types/components/breadcrumbs/breadcrumbs-item.d.ts +1 -1
  70. package/types/components/breadcrumbs/breadcrumbs.d.ts +1 -1
  71. package/types/components/button/button.d.ts +1 -1
  72. package/types/components/button/icon-button.d.ts +2 -2
  73. package/types/components/button-group/button-group.d.ts +1 -1
  74. package/types/components/caption/caption.d.ts +1 -1
  75. package/types/components/caption/icons.d.ts +1 -1
  76. package/types/components/checkbox/checkbox.d.ts +1 -1
  77. package/types/components/color-picker/color-picker-button.d.ts +1 -1
  78. package/types/components/color-picker/color-picker.d.ts +1 -1
  79. package/types/components/date-picker/calendar-day.d.ts +1 -1
  80. package/types/components/date-picker/calendar-grid-head.d.ts +1 -1
  81. package/types/components/date-picker/calendar-grid.d.ts +1 -1
  82. package/types/components/date-picker/calendar.d.ts +3 -3
  83. package/types/components/date-picker/date-picker.d.ts +3 -3
  84. package/types/components/date-picker/date-range-picker.d.ts +4 -4
  85. package/types/components/date-picker/date-range.d.ts +1 -1
  86. package/types/components/date-picker/icons.d.ts +1 -1
  87. package/types/components/date-picker-popover/date-picker-popover.d.ts +3 -3
  88. package/types/components/date-range-picker-popover/date-range-picker-popover-footer.d.ts +1 -1
  89. package/types/components/date-range-picker-popover/date-range-picker-popover-presets.d.ts +1 -1
  90. package/types/components/date-range-picker-popover/date-range-picker-popover.d.ts +4 -4
  91. package/types/components/date-range-picker-popover/types.d.ts +1 -1
  92. package/types/components/dialog/dialog-body.d.ts +1 -1
  93. package/types/components/dialog/dialog-context.d.ts +1 -1
  94. package/types/components/dialog/dialog-footer.d.ts +1 -1
  95. package/types/components/dialog/dialog-header.d.ts +1 -1
  96. package/types/components/dialog/dialog.d.ts +1 -1
  97. package/types/components/dialog/x-mark-icon.d.ts +1 -1
  98. package/types/components/empty-state/empty-state.d.ts +1 -1
  99. package/types/components/filter-buttons/filter-button.d.ts +1 -1
  100. package/types/components/filter-buttons/filter-buttons.d.ts +1 -1
  101. package/types/components/filtered-search/filter-item.d.ts +10 -0
  102. package/types/components/filtered-search/filter-operator-select.d.ts +10 -0
  103. package/types/components/filtered-search/filter-token.d.ts +9 -0
  104. package/types/components/filtered-search/filter-value-list.d.ts +8 -0
  105. package/types/components/filtered-search/filtered-search.d.ts +15 -0
  106. package/types/components/filtered-search/icons.d.ts +5 -0
  107. package/types/components/filtered-search/index.d.ts +4 -0
  108. package/types/components/filtered-search/model/abstract-filter.d.ts +6 -0
  109. package/types/components/filtered-search/model/string-filter.d.ts +47 -0
  110. package/types/components/filtered-search/model/string-set-filter.d.ts +41 -0
  111. package/types/components/filtered-search/model/validation-result.d.ts +6 -0
  112. package/types/components/filtered-search/string-filter-input.d.ts +9 -0
  113. package/types/components/filtered-search/string-filter-item.d.ts +13 -0
  114. package/types/components/filtered-search/string-set-filter-item.d.ts +15 -0
  115. package/types/components/filtered-search/types.d.ts +4 -0
  116. package/types/components/filtered-search/use-combobox-ids.d.ts +5 -0
  117. package/types/components/filtered-search/use-translations.d.ts +13 -0
  118. package/types/components/form-field/form-field.d.ts +1 -1
  119. package/types/components/index.d.ts +2 -0
  120. package/types/components/label/label.d.ts +1 -1
  121. package/types/components/menu/menu-item.d.ts +1 -1
  122. package/types/components/menu/menu-separator.d.ts +1 -1
  123. package/types/components/menu/menu.d.ts +3 -3
  124. package/types/components/nav-list/nav-list-item.d.ts +1 -1
  125. package/types/components/nav-list/nav-list-title.d.ts +1 -1
  126. package/types/components/nav-list/nav-list.d.ts +1 -1
  127. package/types/components/password-input/icons.d.ts +1 -1
  128. package/types/components/password-input/password-input.d.ts +1 -1
  129. package/types/components/popover/popover.d.ts +3 -1
  130. package/types/components/portal/portal-context.d.ts +1 -1
  131. package/types/components/portal/portal.d.ts +1 -1
  132. package/types/components/positioner/calc-position.d.ts +1 -1
  133. package/types/components/positioner/positioner.d.ts +2 -2
  134. package/types/components/radio/radio.d.ts +1 -1
  135. package/types/components/segmented-control/segmented-control-button.d.ts +1 -1
  136. package/types/components/segmented-control/segmented-control.d.ts +1 -1
  137. package/types/components/select/select.d.ts +1 -1
  138. package/types/components/selection-control/selection-control.d.ts +1 -1
  139. package/types/components/slide-over/index.d.ts +4 -0
  140. package/types/components/slide-over/slide-over-body.d.ts +3 -0
  141. package/types/components/slide-over/slide-over-context.d.ts +14 -0
  142. package/types/components/slide-over/slide-over-header.d.ts +11 -0
  143. package/types/components/slide-over/slide-over.d.ts +15 -0
  144. package/types/components/slide-over/types.d.ts +1 -0
  145. package/types/components/slider/slider-tick-marks.d.ts +1 -1
  146. package/types/components/slider/slider.d.ts +2 -2
  147. package/types/components/spinner/spinner.d.ts +1 -1
  148. package/types/components/switch/switch.d.ts +1 -1
  149. package/types/components/table/table-body.d.ts +1 -1
  150. package/types/components/table/table-cell.d.ts +1 -1
  151. package/types/components/table/table-container.d.ts +1 -1
  152. package/types/components/table/table-head-cell.d.ts +1 -1
  153. package/types/components/table/table-head.d.ts +1 -1
  154. package/types/components/table/table-row.d.ts +1 -1
  155. package/types/components/table/table.d.ts +1 -1
  156. package/types/components/tabs/tab-list.d.ts +1 -1
  157. package/types/components/tabs/tab-panel.d.ts +2 -2
  158. package/types/components/tabs/tab.d.ts +1 -1
  159. package/types/components/tabs/tabs-context.d.ts +2 -2
  160. package/types/components/tabs/tabs.d.ts +2 -2
  161. package/types/components/tag/tag.d.ts +1 -1
  162. package/types/components/text-input/text-input.d.ts +1 -1
  163. package/types/components/textarea/textarea.d.ts +1 -1
  164. package/types/components/toast/toast-button.d.ts +1 -1
  165. package/types/components/toast/toast.d.ts +1 -1
  166. package/types/components/toaster/toaster.d.ts +1 -1
  167. package/types/components/tooltip/tooltip.d.ts +2 -2
  168. package/types/hooks/index.d.ts +1 -1
  169. package/types/hooks/use-disable-body-scroll.d.ts +1 -1
  170. package/types/hooks/use-esc-key-down.d.ts +1 -1
  171. package/types/hooks/use-focus-trap.d.ts +2 -2
  172. package/types/lib/helpers.d.ts +1 -0
  173. package/types/lib/keyboard-keys.d.ts +1 -0
  174. package/types/lib/react-helpers.d.ts +2 -1
@@ -0,0 +1,200 @@
1
+ import { jsx, jsxs } from 'react/jsx-runtime';
2
+ import { useRef, useState, useMemo } from 'react';
3
+ import { StringFilter } from './model/string-filter.js';
4
+ import { StringSetFilter } from './model/string-set-filter.js';
5
+ import { TranslationsProvider } from './use-translations.js';
6
+ import { useComboboxIds } from './use-combobox-ids.js';
7
+ import '../button/button.js';
8
+ import { IconButton } from '../button/icon-button.js';
9
+ import { FilterItem } from './filter-item.js';
10
+ import { MagnifyingGlassIcon, TrashIcon } from './icons.js';
11
+ import { KeyboardKeys } from '../../lib/keyboard-keys.js';
12
+ import { exhaustiveCheck } from '../../lib/helpers.js';
13
+ import { classNames } from '../../lib/react-helpers.js';
14
+
15
+ function FilteredSearch({ className, placeholder = 'Search and filter', applyButtonLabel = 'Apply', cancelButtonLabel = 'Cancel', clearButtonAccessibleName = 'Clear', removeFilterButtonAccessibleName = 'Remove filter', filtersConfig, filters, onChange, onMouseDown, ...props }) {
16
+ const containerRef = useRef(null);
17
+ const [query, setQuery] = useState('');
18
+ const [hasFocus, setHasFocus] = useState(false);
19
+ const [expanded, setExpanded] = useState(false);
20
+ const [selectedId, setSelectedId] = useState('');
21
+ const [activeField, setActiveField] = useState('');
22
+ const { textBoxId, listBoxId, getOptionId } = useComboboxIds();
23
+ const filtersConfigMap = useMemo(() => {
24
+ const map = new Map();
25
+ for (const config of filtersConfig) {
26
+ map.set(getOptionId(config.field), config);
27
+ }
28
+ return map;
29
+ }, [filtersConfig, getOptionId]);
30
+ const getTextBoxElement = () => {
31
+ const textBox = document.getElementById(textBoxId);
32
+ if (!(textBox instanceof HTMLInputElement)) {
33
+ throw new Error(`Unable to find input element with ID ${textBoxId}.`);
34
+ }
35
+ return textBox;
36
+ };
37
+ const addFilter = (config) => {
38
+ const filter = createFilter(config);
39
+ const textBoxElement = getTextBoxElement();
40
+ onChange([...filters, filter]);
41
+ textBoxElement.blur();
42
+ setQuery('');
43
+ setActiveField(filter.field);
44
+ };
45
+ const changeFilter = (changedFilter) => {
46
+ const newFilters = filters.map((filter) => (filter.field === changedFilter.field ? changedFilter : filter));
47
+ onChange(newFilters);
48
+ };
49
+ const removeFilter = (filterToRemove) => {
50
+ const newFilters = filters.filter((filter) => (filter.field !== filterToRemove.field));
51
+ onChange(newFilters);
52
+ };
53
+ const onFilterEditStarted = (filter) => {
54
+ setActiveField(filter.field);
55
+ };
56
+ const onFilterEditCanceled = (filter) => {
57
+ setActiveField('');
58
+ if (filter.isEmpty()) {
59
+ removeFilter(filter);
60
+ getTextBoxElement().focus();
61
+ }
62
+ };
63
+ const onFilterChanged = (filter) => {
64
+ changeFilter(filter);
65
+ setActiveField('');
66
+ };
67
+ const renderedFilters = [];
68
+ const fieldsWithAppliedFilters = new Set();
69
+ for (const filter of filters) {
70
+ const field = filter.field;
71
+ fieldsWithAppliedFilters.add(field);
72
+ renderedFilters.push(jsx(FilterItem, { filter: filter, isEditing: activeField === field, onEditStart: onFilterEditStarted, onEditCancel: onFilterEditCanceled, onChange: onFilterChanged, onRemove: removeFilter }, field));
73
+ }
74
+ const onOptionHovered = (event) => {
75
+ const listItemElement = event.currentTarget;
76
+ setSelectedId(listItemElement.id);
77
+ };
78
+ const onOptionPressed = (event) => {
79
+ event.preventDefault();
80
+ event.stopPropagation();
81
+ const listItemElement = event.currentTarget;
82
+ const config = filtersConfigMap.get(listItemElement.id);
83
+ if (config) {
84
+ addFilter(config);
85
+ }
86
+ };
87
+ const renderedOptions = [];
88
+ const search = query.toLowerCase();
89
+ for (const [id, config] of filtersConfigMap) {
90
+ if (fieldsWithAppliedFilters.has(config.field)) {
91
+ continue;
92
+ }
93
+ if (!config.label.toLowerCase().includes(search)) {
94
+ continue;
95
+ }
96
+ renderedOptions.push(jsx("li", { id: id, role: "option", "data-field": config.field, "aria-selected": selectedId === id, onMouseEnter: onOptionHovered, onMouseDown: onOptionPressed, children: config.label }, id));
97
+ }
98
+ const onInputFocused = () => {
99
+ setHasFocus(true);
100
+ setExpanded(true);
101
+ };
102
+ const onInputBlurred = () => {
103
+ setExpanded(false);
104
+ setHasFocus(false);
105
+ setQuery('');
106
+ setSelectedId('');
107
+ };
108
+ const onInputKeyPressed = (event) => {
109
+ const key = event.key;
110
+ const inputElement = event.currentTarget;
111
+ const optionIds = renderedOptions.map((opt) => opt.props.id);
112
+ const firstIndex = 0;
113
+ const lastIndex = optionIds.length - 1;
114
+ const selectedIdIndex = optionIds.findIndex((id) => id === selectedId);
115
+ let isHandled = false;
116
+ let nextIdIndex = selectedIdIndex;
117
+ if (key === KeyboardKeys.ArrowDown) {
118
+ nextIdIndex = selectedIdIndex + 1;
119
+ isHandled = true;
120
+ }
121
+ else if (key === KeyboardKeys.ArrowUp) {
122
+ nextIdIndex = selectedIdIndex - 1;
123
+ isHandled = true;
124
+ }
125
+ else if (key === KeyboardKeys.Enter) {
126
+ const config = filtersConfigMap.get(selectedId);
127
+ if (config) {
128
+ addFilter(config);
129
+ isHandled = true;
130
+ }
131
+ }
132
+ else if (key === KeyboardKeys.Backspace) {
133
+ if (query === '' && filters.length > 0) {
134
+ onChange(filters.slice(0, -1));
135
+ isHandled = true;
136
+ }
137
+ }
138
+ else if (key === KeyboardKeys.Escape) {
139
+ inputElement.blur();
140
+ isHandled = true;
141
+ }
142
+ if (nextIdIndex !== selectedIdIndex) {
143
+ if (nextIdIndex < firstIndex) {
144
+ nextIdIndex = lastIndex;
145
+ }
146
+ if (nextIdIndex > lastIndex) {
147
+ nextIdIndex = firstIndex;
148
+ }
149
+ setSelectedId(optionIds[nextIdIndex]);
150
+ }
151
+ else if (renderedOptions.length === 0) {
152
+ setSelectedId('');
153
+ }
154
+ if (isHandled) {
155
+ event.preventDefault();
156
+ event.stopPropagation();
157
+ }
158
+ };
159
+ const onInputChanged = (event) => {
160
+ setQuery(event.target.value);
161
+ };
162
+ const onClearButtonPressed = (event) => {
163
+ event.stopPropagation();
164
+ onChange([]);
165
+ };
166
+ const onContainerPressed = (event) => {
167
+ if (event.currentTarget === event.target) {
168
+ const textBoxElement = getTextBoxElement();
169
+ textBoxElement.focus();
170
+ event.stopPropagation();
171
+ event.preventDefault();
172
+ }
173
+ if (typeof onMouseDown === 'function') {
174
+ onMouseDown(event);
175
+ }
176
+ };
177
+ return (jsx(TranslationsProvider, { applyButton: applyButtonLabel, cancelButton: cancelButtonLabel, removeFilterButton: removeFilterButtonAccessibleName, children: jsxs("div", { ref: containerRef, "data-testid": "combobox-container", className: classNames(className, {
178
+ 'dc-filtered-search': true,
179
+ 'dc-filtered-search_has_focus': hasFocus,
180
+ }), onMouseDown: onContainerPressed, ...props, children: [jsx(MagnifyingGlassIcon, { className: "dc-filtered-search__icon" }), jsxs("div", { className: "dc-filtered-search__filters", children: [renderedFilters, jsx("input", { className: "dc-filtered-search__input", id: textBoxId, placeholder: placeholder, type: "text", role: "combobox", "aria-autocomplete": "list", "aria-expanded": expanded, "aria-controls": listBoxId, "aria-activedescendant": selectedId, value: query, onFocus: onInputFocused, onBlur: onInputBlurred, onKeyDown: onInputKeyPressed, onChange: onInputChanged })] }), jsx(IconButton, { className: "dc-filtered-search__clear-button", "aria-label": clearButtonAccessibleName, icon: jsx(TrashIcon, {}), size: "sm", variant: "plain", appearance: "primary", onClick: onClearButtonPressed }), expanded && renderedOptions.length > 0 && (jsx("ul", { className: "dc-filtered-search__list-box", id: listBoxId, role: "listbox", children: renderedOptions }))] }) }));
181
+ }
182
+ function createFilter(config) {
183
+ const filterType = config.type;
184
+ switch (filterType) {
185
+ case StringFilter.Type:
186
+ return new StringFilter(config, {
187
+ value: '',
188
+ operator: config.operators[0] || StringFilter.Operators.equal,
189
+ });
190
+ case StringSetFilter.Type:
191
+ return new StringSetFilter(config, {
192
+ value: [],
193
+ operator: config.operators[0] || StringSetFilter.Operators.in,
194
+ });
195
+ default:
196
+ exhaustiveCheck(filterType, `Unable to create a filter with ${filterType} type.`);
197
+ }
198
+ }
199
+
200
+ export { FilteredSearch };
@@ -0,0 +1,16 @@
1
+ import { jsx } from 'react/jsx-runtime';
2
+
3
+ function MagnifyingGlassIcon(props) {
4
+ return (jsx("svg", { xmlns: "http://www.w3.org/2000/svg", viewBox: "0 0 24 24", width: 18, height: 18, fill: "none", stroke: "currentColor", strokeWidth: 1.5, ...props, children: jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M21 21l-5.197-5.197m0 0A7.5 7.5 0 105.196 5.196a7.5 7.5 0 0010.607 10.607z" }) }));
5
+ }
6
+ function TrashIcon(props) {
7
+ return (jsx("svg", { xmlns: "http://www.w3.org/2000/svg", viewBox: "0 0 24 24", width: 18, height: 18, fill: "none", stroke: "currentColor", strokeWidth: 1.5, ...props, children: jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M14.74 9l-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 01-2.244 2.077H8.084a2.25 2.25 0 01-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 00-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 013.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 00-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 00-7.5 0" }) }));
8
+ }
9
+ function XMarkIcon(props) {
10
+ return (jsx("svg", { xmlns: "http://www.w3.org/2000/svg", viewBox: "0 0 24 24", width: 16, height: 16, fill: "none", stroke: "currentColor", strokeWidth: 1.5, ...props, children: jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M6 18L18 6M6 6l12 12" }) }));
11
+ }
12
+ function ArrowReturnRight(props) {
13
+ return (jsx("svg", { xmlns: "http://www.w3.org/2000/svg", viewBox: "0 0 16 16", width: 16, height: 16, fill: "currentColor", ...props, children: jsx("path", { fillRule: "evenodd", d: "M1.5 1.5A.5.5 0 0 0 1 2v4.8a2.5 2.5 0 0 0 2.5 2.5h9.793l-3.347 3.346a.5.5 0 0 0 .708.708l4.2-4.2a.5.5 0 0 0 0-.708l-4-4a.5.5 0 0 0-.708.708L13.293 8.3H3.5A1.5 1.5 0 0 1 2 6.8V2a.5.5 0 0 0-.5-.5z" }) }));
14
+ }
15
+
16
+ export { ArrowReturnRight, MagnifyingGlassIcon, TrashIcon, XMarkIcon };
@@ -0,0 +1,4 @@
1
+ class AbstractFilter {
2
+ }
3
+
4
+ export { AbstractFilter };
@@ -0,0 +1,44 @@
1
+ import { AbstractFilter } from './abstract-filter.js';
2
+
3
+ const TYPE = 'STRING';
4
+ const OPERATORS = {
5
+ equal: 'EQUAL',
6
+ notEqual: 'NOT_EQUAL',
7
+ contain: 'CONTAIN',
8
+ notContain: 'NOT_CONTAIN',
9
+ };
10
+ class StringFilter extends AbstractFilter {
11
+ constructor(config, state) {
12
+ super();
13
+ this.config = config;
14
+ this.state = state;
15
+ }
16
+ get type() {
17
+ return this.config.type;
18
+ }
19
+ get field() {
20
+ return this.config.field;
21
+ }
22
+ get label() {
23
+ return this.config.label;
24
+ }
25
+ get value() {
26
+ return this.state.value;
27
+ }
28
+ get operator() {
29
+ return this.state.operator;
30
+ }
31
+ setValue(value) {
32
+ return new StringFilter(this.config, { ...this.state, value });
33
+ }
34
+ setOperator(operator) {
35
+ return new StringFilter(this.config, { ...this.state, operator });
36
+ }
37
+ isEmpty() {
38
+ return this.value === '';
39
+ }
40
+ }
41
+ StringFilter.Type = TYPE;
42
+ StringFilter.Operators = OPERATORS;
43
+
44
+ export { StringFilter };
@@ -0,0 +1,42 @@
1
+ import { AbstractFilter } from './abstract-filter.js';
2
+
3
+ const TYPE = 'STRING_SET';
4
+ const OPERATORS = {
5
+ in: 'IN',
6
+ notIn: 'NOT_IN',
7
+ };
8
+ class StringSetFilter extends AbstractFilter {
9
+ constructor(config, state) {
10
+ super();
11
+ this.config = config;
12
+ this.state = state;
13
+ }
14
+ get type() {
15
+ return this.config.type;
16
+ }
17
+ get field() {
18
+ return this.config.field;
19
+ }
20
+ get label() {
21
+ return this.config.label;
22
+ }
23
+ get value() {
24
+ return this.state.value;
25
+ }
26
+ get operator() {
27
+ return this.state.operator;
28
+ }
29
+ setValue(value) {
30
+ return new StringSetFilter(this.config, { ...this.state, value });
31
+ }
32
+ setOperator(operator) {
33
+ return new StringSetFilter(this.config, { ...this.state, operator });
34
+ }
35
+ isEmpty() {
36
+ return this.value.length === 0;
37
+ }
38
+ }
39
+ StringSetFilter.Type = TYPE;
40
+ StringSetFilter.Operators = OPERATORS;
41
+
42
+ export { StringSetFilter };
@@ -0,0 +1,15 @@
1
+ import { jsxs, jsx } from 'react/jsx-runtime';
2
+ import { classNames } from '../../lib/react-helpers.js';
3
+ import { ArrowReturnRight } from './icons.js';
4
+ import { TextInput } from '../text-input/text-input.js';
5
+ import { Caption } from '../caption/caption.js';
6
+ import { useId } from 'react';
7
+
8
+ function StringFilterInput({ className, accessibleName, placeholder, error, value, onChangeValue, }) {
9
+ const id = useId();
10
+ const alertId = `${id}alert`;
11
+ const hasError = Boolean(error);
12
+ return (jsxs("div", { className: classNames('dc-string-filter-input', className), children: [jsx(ArrowReturnRight, {}), jsx(TextInput, { "data-testid": "string-filter-input", size: "sm", isBlock: true, "aria-label": accessibleName, "aria-describedby": hasError ? alertId : '', placeholder: placeholder, type: "text", required: true, autoFocus: true, value: value, hasError: hasError, onChangeValue: onChangeValue }), hasError && (jsx(Caption, { id: alertId, className: "dc-string-filter-input__error", appearance: "error", showIcon: true, role: "alert", children: error }))] }));
13
+ }
14
+
15
+ export { StringFilterInput };
@@ -0,0 +1,66 @@
1
+ import { jsxs, Fragment, jsx } from 'react/jsx-runtime';
2
+ import { StringFilter } from './model/string-filter.js';
3
+ import { useState } from 'react';
4
+ import { useTranslations } from './use-translations.js';
5
+ import { Popover } from '../popover/popover.js';
6
+ import { FilterToken } from './filter-token.js';
7
+ import { FilterOperatorSelect } from './filter-operator-select.js';
8
+ import { StringFilterInput } from './string-filter-input.js';
9
+ import { Button } from '../button/button.js';
10
+ import '../button/icon-button.js';
11
+
12
+ function StringFilterItem({ filter, isEditing, onEditStart, onEditCancel, onRemove, onChange, }) {
13
+ const translations = useTranslations();
14
+ const [filterOperator, setFilterOperator] = useState(filter.operator);
15
+ const [filterValue, setFilterValue] = useState(filter.value);
16
+ const [error, setError] = useState('');
17
+ const { label, operators, valueInputAccessibleName, valueInputPlaceholder, operatorSelectAccessibleName, valueValidator, operatorFormatter: formatOperator = defaultOperatorFormatter, } = filter.config;
18
+ const cancelEdit = () => {
19
+ onEditCancel(filter);
20
+ };
21
+ const startEdit = () => {
22
+ if (isEditing) {
23
+ cancelEdit();
24
+ }
25
+ else {
26
+ setFilterOperator(filter.operator);
27
+ setFilterValue(filter.value);
28
+ onEditStart(filter);
29
+ }
30
+ };
31
+ const onClickCloseButton = () => {
32
+ onRemove(filter);
33
+ };
34
+ const onSubmit = (event) => {
35
+ event.preventDefault();
36
+ event.stopPropagation();
37
+ if (typeof valueValidator === 'function') {
38
+ const result = valueValidator(filterValue);
39
+ if (!result.valid) {
40
+ return setError(result.error);
41
+ }
42
+ setError('');
43
+ }
44
+ if (filterOperator !== filter.operator) {
45
+ filter = filter.setOperator(filterOperator);
46
+ }
47
+ if (filterValue !== filter.value) {
48
+ filter = filter.setValue(filterValue);
49
+ }
50
+ onChange(filter);
51
+ };
52
+ const anchor = (jsxs(FilterToken, { isHighlighted: isEditing, onClickLabel: startEdit, onClickCloseButton: onClickCloseButton, children: [label, filter.value ? (jsxs(Fragment, { children: ["\u00A0", jsx("span", { children: formatOperator(filter.operator) }), "\u00A0", jsx("b", { children: filter.value })] })) : null] }));
53
+ return (jsx(Popover, { className: "dc-filter-popover", anchor: anchor, anchorGap: 2, transitionDurationMs: 0, isOpen: isEditing, onClose: cancelEdit, children: jsxs("form", { className: "dc-filter-form", onSubmit: onSubmit, children: [jsx(FilterOperatorSelect, { className: "dc-filter-form__operator", accessibleName: operatorSelectAccessibleName, label: label, values: operators, value: filterOperator, onChange: setFilterOperator, formatValue: formatOperator }), jsx(StringFilterInput, { placeholder: valueInputPlaceholder, accessibleName: valueInputAccessibleName, error: error, value: filterValue, onChangeValue: setFilterValue }), jsxs("div", { className: "dc-filter-form__buttons", children: [jsx(Button, { variant: "plain", type: "button", onClick: cancelEdit, children: translations.cancelButton }), jsx(Button, { appearance: "primary", type: "submit", disabled: !filterValue, children: translations.applyButton })] })] }) }));
54
+ }
55
+ StringFilterItem.defaultOperatorFormatter = defaultOperatorFormatter;
56
+ function defaultOperatorFormatter(operator) {
57
+ const messages = {
58
+ [StringFilter.Operators.equal]: 'is',
59
+ [StringFilter.Operators.notEqual]: 'is not',
60
+ [StringFilter.Operators.contain]: 'contains',
61
+ [StringFilter.Operators.notContain]: "doesn't contain",
62
+ };
63
+ return messages[operator];
64
+ }
65
+
66
+ export { StringFilterItem };
@@ -0,0 +1,75 @@
1
+ import { jsxs, Fragment, jsx } from 'react/jsx-runtime';
2
+ import { StringSetFilter } from './model/string-set-filter.js';
3
+ import { useState } from 'react';
4
+ import { useTranslations } from './use-translations.js';
5
+ import { Popover } from '../popover/popover.js';
6
+ import { FilterToken } from './filter-token.js';
7
+ import { FilterOperatorSelect } from './filter-operator-select.js';
8
+ import { FilterValueList } from './filter-value-list.js';
9
+ import { Button } from '../button/button.js';
10
+ import '../button/icon-button.js';
11
+
12
+ function StringSetFilterItem({ filter, isEditing, onEditStart, onEditCancel, onRemove, onChange, }) {
13
+ const translations = useTranslations();
14
+ const [filterOperator, setFilterOperator] = useState(filter.operator);
15
+ const [filterValue, setFilterValue] = useState(filter.value);
16
+ const { label, values, operators, operatorSelectAccessibleName, valueFormatter: formatValue = defaultValueFormatter, operatorFormatter: formatOperator = defaultOperatorFormatter, } = filter.config;
17
+ const cancelEdit = () => {
18
+ onEditCancel(filter);
19
+ };
20
+ const startEdit = () => {
21
+ if (isEditing) {
22
+ cancelEdit();
23
+ }
24
+ else {
25
+ setFilterOperator(filter.operator);
26
+ setFilterValue(filter.value);
27
+ onEditStart(filter);
28
+ }
29
+ };
30
+ const onClickCloseButton = () => {
31
+ onRemove(filter);
32
+ };
33
+ const onSubmit = (event) => {
34
+ event.preventDefault();
35
+ event.stopPropagation();
36
+ let changedFilter = filter;
37
+ if (filterOperator !== filter.operator) {
38
+ changedFilter = changedFilter.setOperator(filterOperator);
39
+ }
40
+ if (filterValue !== filter.value) {
41
+ changedFilter = changedFilter.setValue(filterValue);
42
+ }
43
+ onChange(changedFilter);
44
+ };
45
+ const anchor = (jsxs(FilterToken, { isHighlighted: isEditing, onClickLabel: startEdit, onClickCloseButton: onClickCloseButton, children: [label, filter.value.length > 0 ? (jsxs(Fragment, { children: ["\u00A0", jsx("span", { children: formatOperator(filter.operator) }), "\u00A0", jsx("b", { children: formatFilterValue(filter.value) })] })) : null] }));
46
+ return (jsx(Popover, { className: "dc-filter-popover", anchor: anchor, anchorGap: 2, transitionDurationMs: 0, isOpen: isEditing, onClose: cancelEdit, children: jsxs("form", { className: "dc-filter-form", onSubmit: onSubmit, children: [jsx(FilterOperatorSelect, { className: "dc-filter-form__operator", accessibleName: operatorSelectAccessibleName, label: label, values: operators, value: filterOperator, onChange: setFilterOperator, formatValue: formatOperator }), jsx(FilterValueList, { className: "dc-filter-form__value-list", values: values, checkedValues: filterValue, onChangeCheckedValues: setFilterValue, formatValue: formatValue }), jsxs("div", { className: "dc-filter-form__buttons", children: [jsx(Button, { variant: "plain", type: "button", onClick: cancelEdit, children: translations.cancelButton }), jsx(Button, { appearance: "primary", type: "submit", disabled: filterValue.length < 1, children: translations.applyButton })] })] }) }));
47
+ }
48
+ StringSetFilterItem.defaultValueFormatter = defaultValueFormatter;
49
+ StringSetFilterItem.defaultOperatorFormatter = defaultOperatorFormatter;
50
+ StringSetFilterItem.formatFilterValue = formatFilterValue;
51
+ function defaultValueFormatter(value) {
52
+ return value[0].toUpperCase() + value.slice(1);
53
+ }
54
+ function defaultOperatorFormatter(operator) {
55
+ const messages = {
56
+ [StringSetFilter.Operators.in]: 'is',
57
+ [StringSetFilter.Operators.notIn]: 'is not',
58
+ };
59
+ return messages[operator];
60
+ }
61
+ function formatFilterValue(values) {
62
+ const list = values.map(defaultValueFormatter);
63
+ if (list.length <= 1) {
64
+ return list.toString();
65
+ }
66
+ if (list.length <= 2) {
67
+ return list.join(' or ');
68
+ }
69
+ if (list.length <= 3) {
70
+ return list.slice(0, -1).join(', ') + ', or ' + list.slice(-1);
71
+ }
72
+ return list.slice(0, 2).join(', ') + `, and ${list.length - 2} more`;
73
+ }
74
+
75
+ export { StringSetFilterItem };
@@ -0,0 +1,15 @@
1
+ import { useId, useCallback } from 'react';
2
+
3
+ function useComboboxIds() {
4
+ const id = useId();
5
+ const textBoxId = `${id}textBox`;
6
+ const listBoxId = `${id}listBox`;
7
+ const getOptionId = useCallback((key) => `${id}option[${key}]`, [id]);
8
+ return {
9
+ textBoxId,
10
+ listBoxId,
11
+ getOptionId,
12
+ };
13
+ }
14
+
15
+ export { useComboboxIds };
@@ -0,0 +1,21 @@
1
+ import { jsx } from 'react/jsx-runtime';
2
+ import { createContext, useContext, useMemo } from 'react';
3
+
4
+ const Context = createContext(null);
5
+ function useTranslations() {
6
+ const ctx = useContext(Context);
7
+ if (ctx == null) {
8
+ throw new Error('useTranslations must be used within TranslationsProvider');
9
+ }
10
+ return ctx;
11
+ }
12
+ function TranslationsProvider({ applyButton, cancelButton, removeFilterButton, children, }) {
13
+ const translations = useMemo(() => ({
14
+ applyButton,
15
+ cancelButton,
16
+ removeFilterButton,
17
+ }), [applyButton, cancelButton, removeFilterButton]);
18
+ return (jsx(Context.Provider, { value: translations, children: children }));
19
+ }
20
+
21
+ export { TranslationsProvider, useTranslations };
@@ -22,6 +22,9 @@ export { EmptyState } from './empty-state/empty-state.js';
22
22
  export { FilePicker } from './file-picker/file-picker.js';
23
23
  export { FilterButtons } from './filter-buttons/filter-buttons.js';
24
24
  export { FilterButton } from './filter-buttons/filter-button.js';
25
+ export { StringFilter } from './filtered-search/model/string-filter.js';
26
+ export { StringSetFilter } from './filtered-search/model/string-set-filter.js';
27
+ export { FilteredSearch } from './filtered-search/filtered-search.js';
25
28
  export { FormField } from './form-field/form-field.js';
26
29
  export { Label } from './label/label.js';
27
30
  export { Menu } from './menu/menu.js';
@@ -38,6 +41,9 @@ export { Radio } from './radio/radio.js';
38
41
  export { SegmentedControl } from './segmented-control/segmented-control.js';
39
42
  export { Select } from './select/select.js';
40
43
  export { SelectionControl } from './selection-control/selection-control.js';
44
+ export { SlideOver } from './slide-over/slide-over.js';
45
+ export { SlideOverHeader } from './slide-over/slide-over-header.js';
46
+ export { SlideOverBody } from './slide-over/slide-over-body.js';
41
47
  export { Slider } from './slider/slider.js';
42
48
  export { SliderTickMarks } from './slider/slider-tick-marks.js';
43
49
  export { Spinner } from './spinner/spinner.js';
@@ -7,14 +7,13 @@ import { useFocusTrap } from '../../hooks/use-focus-trap.js';
7
7
  import { usePageClick } from './use-page-click.js';
8
8
  import { Positioner } from '../positioner/positioner.js';
9
9
 
10
- const Popover = forwardRef(function Popover({ shouldTrapFocus = true, shouldFocusAnchorAfterEscPress = true, placement = 'bottom', alignment = 'start', anchorGap, viewportGap, anchor, className, children, onOpen, onClose, ...props }, ref) {
10
+ const Popover = forwardRef(function Popover({ shouldTrapFocus = true, shouldFocusAnchorAfterEscPress = true, transitionDurationMs = 100, placement = 'bottom', alignment = 'start', anchorGap, viewportGap, anchor, className, children, onOpen, onClose, ...props }, ref) {
11
11
  const [defaultIsOpen, setDefaultIsOpen] = useState(props.defaultIsOpen ?? false);
12
12
  const anchorRef = useRef(null);
13
13
  const contentRef = useRef(null);
14
14
  const isOpen = props.isOpen ?? defaultIsOpen;
15
- const durationMs = 100;
16
15
  const { isMounted, className: transitionClass } = useMountTransition({
17
- durationMs,
16
+ durationMs: transitionDurationMs,
18
17
  isShown: isOpen,
19
18
  enterFrom: 'dc-popover_closed',
20
19
  enterTo: 'dc-popover_opened',
@@ -84,7 +83,7 @@ const Popover = forwardRef(function Popover({ shouldTrapFocus = true, shouldFocu
84
83
  delete props.defaultIsOpen;
85
84
  delete props.isOpen;
86
85
  return (jsx("div", { ref: portalRef, style: {
87
- '--dc-popover-transition-duration': `${durationMs}ms`,
86
+ '--dc-popover-transition-duration': `${transitionDurationMs}ms`,
88
87
  ...portalStyle,
89
88
  }, className: classNames('dc-popover', transitionClass), children: jsx("div", { ...props, ref: contentRef, className: classNames('dc-popover-modal', className), children: children }) }));
90
89
  }
@@ -0,0 +1,8 @@
1
+ import { jsx } from 'react/jsx-runtime';
2
+ import { classNames } from '../../lib/react-helpers.js';
3
+
4
+ function SlideOverBody({ className, ...props }) {
5
+ return (jsx("div", { className: classNames('dc-slide-over-body', className), ...props }));
6
+ }
7
+
8
+ export { SlideOverBody };
@@ -0,0 +1,21 @@
1
+ import { jsx } from 'react/jsx-runtime';
2
+ import { createContext, useContext, useMemo } from 'react';
3
+
4
+ const Context = createContext(null);
5
+ function useSlideOverContext() {
6
+ const ctx = useContext(Context);
7
+ if (!ctx) {
8
+ throw new Error('useSlideOverContext must be used within SlideOverContextProvider');
9
+ }
10
+ return ctx;
11
+ }
12
+ function SlideOverContextProvider({ titleId, descriptionId, closePanel, children, }) {
13
+ const ctx = useMemo(() => ({
14
+ titleId,
15
+ descriptionId,
16
+ closePanel,
17
+ }), [titleId, descriptionId, closePanel]);
18
+ return (jsx(Context.Provider, { value: ctx, children: children }));
19
+ }
20
+
21
+ export { SlideOverContextProvider, useSlideOverContext };
@@ -0,0 +1,19 @@
1
+ import { jsxs, jsx } from 'react/jsx-runtime';
2
+ import { classNames } from '../../lib/react-helpers.js';
3
+ import '../button/button.js';
4
+ import { IconButton } from '../button/icon-button.js';
5
+ import { useSlideOverContext } from './slide-over-context.js';
6
+
7
+ function SlideOverHeader({ className, htmlTitle, title, description, closeButtonAccessibleName, onClickCloseButton, ...props }) {
8
+ const { titleId, descriptionId, closePanel } = useSlideOverContext();
9
+ const onCloseButtonClicked = (ev) => {
10
+ closePanel('close-button');
11
+ onClickCloseButton?.(ev);
12
+ };
13
+ return (jsxs("div", { className: classNames('dc-slide-over-header', className), title: htmlTitle, ...props, children: [jsxs("div", { className: "dc-slide-over-header__title", children: [jsx("h2", { id: titleId, children: title }), jsx(IconButton, { icon: jsx(XMarkIcon, {}), "aria-label": closeButtonAccessibleName, variant: "tinted", size: "xs", onClick: onCloseButtonClicked })] }), description ? (jsx("div", { id: descriptionId, className: "dc-slide-over-header__description", children: description })) : null] }));
14
+ }
15
+ function XMarkIcon(props) {
16
+ return (jsx("svg", { xmlns: "http://www.w3.org/2000/svg", viewBox: "0 0 24 24", width: 20, height: 20, fill: "none", stroke: "currentColor", strokeWidth: 1.5, ...props, children: jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M6 18L18 6M6 6l12 12" }) }));
17
+ }
18
+
19
+ export { SlideOverHeader };