@canonical/react-components 1.7.3 → 1.8.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.
- package/dist/components/CustomSelect/CustomSelect.d.ts +36 -0
- package/dist/components/CustomSelect/CustomSelect.js +145 -0
- package/dist/components/CustomSelect/CustomSelect.scss +82 -0
- package/dist/components/CustomSelect/CustomSelect.stories.d.ts +28 -0
- package/dist/components/CustomSelect/CustomSelect.stories.js +132 -0
- package/dist/components/CustomSelect/CustomSelect.test.d.ts +1 -0
- package/dist/components/CustomSelect/CustomSelectDropdown/CustomSelectDropdown.d.ts +25 -0
- package/dist/components/CustomSelect/CustomSelectDropdown/CustomSelectDropdown.js +300 -0
- package/dist/components/CustomSelect/CustomSelectDropdown/CustomSelectDropdown.test.d.ts +1 -0
- package/dist/components/CustomSelect/CustomSelectDropdown/index.d.ts +2 -0
- package/dist/components/CustomSelect/CustomSelectDropdown/index.js +20 -0
- package/dist/components/CustomSelect/index.d.ts +3 -0
- package/dist/components/CustomSelect/index.js +13 -0
- package/dist/esm/components/CustomSelect/CustomSelect.d.ts +36 -0
- package/dist/esm/components/CustomSelect/CustomSelect.js +139 -0
- package/dist/esm/components/CustomSelect/CustomSelect.scss +82 -0
- package/dist/esm/components/CustomSelect/CustomSelect.stories.d.ts +28 -0
- package/dist/esm/components/CustomSelect/CustomSelect.stories.js +126 -0
- package/dist/esm/components/CustomSelect/CustomSelect.test.d.ts +1 -0
- package/dist/esm/components/CustomSelect/CustomSelectDropdown/CustomSelectDropdown.d.ts +25 -0
- package/dist/esm/components/CustomSelect/CustomSelectDropdown/CustomSelectDropdown.js +285 -0
- package/dist/esm/components/CustomSelect/CustomSelectDropdown/CustomSelectDropdown.test.d.ts +1 -0
- package/dist/esm/components/CustomSelect/CustomSelectDropdown/index.d.ts +2 -0
- package/dist/esm/components/CustomSelect/CustomSelectDropdown/index.js +1 -0
- package/dist/esm/components/CustomSelect/index.d.ts +3 -0
- package/dist/esm/components/CustomSelect/index.js +1 -0
- package/dist/esm/index.d.ts +2 -0
- package/dist/esm/index.js +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +8 -0
- package/package.json +1 -1
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.getOptionText = exports.getNearestParentsZIndex = exports.dropdownIsAbove = exports.default = exports.adjustDropdownHeightBelow = exports.adjustDropdownHeightAbove = exports.adjustDropdownHeight = void 0;
|
|
7
|
+
var _propTypes = _interopRequireDefault(require("prop-types"));
|
|
8
|
+
var _react = _interopRequireWildcard(require("react"));
|
|
9
|
+
var _classnames = _interopRequireDefault(require("classnames"));
|
|
10
|
+
var _hooks = require("../../../hooks");
|
|
11
|
+
var _SearchBox = _interopRequireDefault(require("../../SearchBox"));
|
|
12
|
+
function _getRequireWildcardCache(e) { if ("function" != typeof WeakMap) return null; var r = new WeakMap(), t = new WeakMap(); return (_getRequireWildcardCache = function (e) { return e ? t : r; })(e); }
|
|
13
|
+
function _interopRequireWildcard(e, r) { if (!r && e && e.__esModule) return e; if (null === e || "object" != typeof e && "function" != typeof e) return { default: e }; var t = _getRequireWildcardCache(r); if (t && t.has(e)) return t.get(e); var n = { __proto__: null }, a = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var u in e) if ("default" !== u && {}.hasOwnProperty.call(e, u)) { var i = a ? Object.getOwnPropertyDescriptor(e, u) : null; i && (i.get || i.set) ? Object.defineProperty(n, u, i) : n[u] = e[u]; } return n.default = e, t && t.set(e, n), n; }
|
|
14
|
+
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
|
|
15
|
+
const DROPDOWN_MAX_HEIGHT = 16 * 30; // 30rem with base 16px
|
|
16
|
+
const DROPDOWN_MARGIN = 20;
|
|
17
|
+
const adjustDropdownHeightBelow = dropdown => {
|
|
18
|
+
var _window$visualViewpor;
|
|
19
|
+
const dropdownRect = dropdown.getBoundingClientRect();
|
|
20
|
+
const dropdownHeight = dropdown.offsetHeight;
|
|
21
|
+
const viewportHeight = ((_window$visualViewpor = window.visualViewport) === null || _window$visualViewpor === void 0 ? void 0 : _window$visualViewpor.height) || window.innerHeight;
|
|
22
|
+
|
|
23
|
+
// If the dropdown is cut off at the bottom of the viewport
|
|
24
|
+
// adjust the height to fit within the viewport minus fixed margin.
|
|
25
|
+
// This usually becomes an issue when the dropdown is at the bottom of the viewport or screen getting smaller.
|
|
26
|
+
if (dropdownRect.bottom >= viewportHeight) {
|
|
27
|
+
const adjustedHeight = dropdownHeight - dropdownRect.bottom + viewportHeight - DROPDOWN_MARGIN;
|
|
28
|
+
dropdown.style.height = "".concat(adjustedHeight, "px");
|
|
29
|
+
dropdown.style.maxHeight = "".concat(adjustedHeight, "px");
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// If the dropdown does not have overflow, the dropdown should fit its content.
|
|
34
|
+
const hasOverflow = dropdown.scrollHeight > dropdown.clientHeight;
|
|
35
|
+
if (!hasOverflow) {
|
|
36
|
+
dropdown.style.height = "auto";
|
|
37
|
+
dropdown.style.maxHeight = "";
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// If the dropdown is not cut off at the bottom of the viewport
|
|
42
|
+
// adjust the height of the dropdown so that its bottom edge is 20px from the bottom of the viewport
|
|
43
|
+
// until the dropdown max height is reached.
|
|
44
|
+
const adjustedHeight = Math.min(viewportHeight - dropdownRect.top - DROPDOWN_MARGIN, DROPDOWN_MAX_HEIGHT);
|
|
45
|
+
dropdown.style.height = "".concat(adjustedHeight, "px");
|
|
46
|
+
dropdown.style.maxHeight = "".concat(adjustedHeight, "px");
|
|
47
|
+
};
|
|
48
|
+
exports.adjustDropdownHeightBelow = adjustDropdownHeightBelow;
|
|
49
|
+
const adjustDropdownHeightAbove = (dropdown, search) => {
|
|
50
|
+
// The search height is subtracted (if necessary) so that no options will be hidden behind the search input.
|
|
51
|
+
const searchRect = search === null || search === void 0 ? void 0 : search.getBoundingClientRect();
|
|
52
|
+
const searchHeight = (searchRect === null || searchRect === void 0 ? void 0 : searchRect.height) || 0;
|
|
53
|
+
const dropdownRect = dropdown.getBoundingClientRect();
|
|
54
|
+
|
|
55
|
+
// If the dropdown does not have overflow, do not adjust.
|
|
56
|
+
const hasOverflow = dropdown.scrollHeight > dropdown.clientHeight;
|
|
57
|
+
if (!hasOverflow) {
|
|
58
|
+
dropdown.style.height = "auto";
|
|
59
|
+
dropdown.style.maxHeight = "";
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// adjust the height of the dropdown so that its top edge is 20px from the top of the viewport.
|
|
64
|
+
// until the dropdown max height is reached.
|
|
65
|
+
// unlike the case where the dropdown is bellow the toggle, dropdown.bottom represents the available space above the toggle always.
|
|
66
|
+
// this makes the calculation simpler since we only need to work with dropdown.bottom regardless if the element is cut off or not.
|
|
67
|
+
const adjustedHeight = Math.min(dropdownRect.bottom - searchHeight - DROPDOWN_MARGIN, DROPDOWN_MAX_HEIGHT);
|
|
68
|
+
dropdown.style.height = "".concat(adjustedHeight, "px");
|
|
69
|
+
dropdown.style.maxHeight = "".concat(adjustedHeight, "px");
|
|
70
|
+
};
|
|
71
|
+
exports.adjustDropdownHeightAbove = adjustDropdownHeightAbove;
|
|
72
|
+
const dropdownIsAbove = dropdown => {
|
|
73
|
+
const toggle = document.querySelector(".p-custom-select__toggle");
|
|
74
|
+
const dropdownRect = dropdown.getBoundingClientRect();
|
|
75
|
+
const toggleRect = toggle.getBoundingClientRect();
|
|
76
|
+
return toggleRect.top >= dropdownRect.bottom;
|
|
77
|
+
};
|
|
78
|
+
exports.dropdownIsAbove = dropdownIsAbove;
|
|
79
|
+
const adjustDropdownHeight = (dropdown, search) => {
|
|
80
|
+
if (!dropdown) {
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
if (dropdownIsAbove(dropdown)) {
|
|
84
|
+
adjustDropdownHeightAbove(dropdown, search);
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
adjustDropdownHeightBelow(dropdown);
|
|
88
|
+
};
|
|
89
|
+
exports.adjustDropdownHeight = adjustDropdownHeight;
|
|
90
|
+
const getNearestParentsZIndex = element => {
|
|
91
|
+
if (!document.defaultView || !element) {
|
|
92
|
+
return "0";
|
|
93
|
+
}
|
|
94
|
+
const zIndex = document.defaultView.getComputedStyle(element, null).getPropertyValue("z-index");
|
|
95
|
+
if (!element.parentElement) {
|
|
96
|
+
return zIndex;
|
|
97
|
+
}
|
|
98
|
+
if (zIndex === "auto" || zIndex === "0" || zIndex === "") {
|
|
99
|
+
return getNearestParentsZIndex(element.parentElement);
|
|
100
|
+
}
|
|
101
|
+
return zIndex;
|
|
102
|
+
};
|
|
103
|
+
exports.getNearestParentsZIndex = getNearestParentsZIndex;
|
|
104
|
+
const getOptionText = option => {
|
|
105
|
+
if (option.text) {
|
|
106
|
+
return option.text;
|
|
107
|
+
}
|
|
108
|
+
if (typeof option.label === "string") {
|
|
109
|
+
return option.label;
|
|
110
|
+
}
|
|
111
|
+
throw new Error("CustomSelect: options must have a string label or a text property");
|
|
112
|
+
};
|
|
113
|
+
exports.getOptionText = getOptionText;
|
|
114
|
+
const CustomSelectDropdown = _ref => {
|
|
115
|
+
let {
|
|
116
|
+
searchable,
|
|
117
|
+
name,
|
|
118
|
+
options,
|
|
119
|
+
onSelect,
|
|
120
|
+
onSearch,
|
|
121
|
+
onClose,
|
|
122
|
+
header,
|
|
123
|
+
toggleId
|
|
124
|
+
} = _ref;
|
|
125
|
+
const [search, setSearch] = (0, _react.useState)("");
|
|
126
|
+
// track highlighted option index for keyboard actions
|
|
127
|
+
const [highlightedOptionIndex, setHighlightedOptionIndex] = (0, _react.useState)(0);
|
|
128
|
+
// use ref to keep a reference to all option HTML elements so we do not need to make DOM calls later for scrolling
|
|
129
|
+
const optionsRef = (0, _react.useRef)([]);
|
|
130
|
+
const dropdownRef = (0, _react.useRef)(null);
|
|
131
|
+
const searchRef = (0, _react.useRef)(null);
|
|
132
|
+
const dropdownListRef = (0, _react.useRef)(null);
|
|
133
|
+
const isSearchable = searchable !== "never" && options.length > 1 && (searchable === "always" || searchable === "auto" && options.length >= 5);
|
|
134
|
+
(0, _react.useEffect)(() => {
|
|
135
|
+
if (dropdownRef.current) {
|
|
136
|
+
var _toggle$getBoundingCl, _toggle$getBoundingCl2;
|
|
137
|
+
const toggle = document.getElementById(toggleId);
|
|
138
|
+
|
|
139
|
+
// align width with wrapper toggle width
|
|
140
|
+
const toggleWidth = (_toggle$getBoundingCl = toggle === null || toggle === void 0 || (_toggle$getBoundingCl2 = toggle.getBoundingClientRect()) === null || _toggle$getBoundingCl2 === void 0 ? void 0 : _toggle$getBoundingCl2.width) !== null && _toggle$getBoundingCl !== void 0 ? _toggle$getBoundingCl : 0;
|
|
141
|
+
dropdownRef.current.style.setProperty("min-width", "".concat(toggleWidth, "px"));
|
|
142
|
+
|
|
143
|
+
// align z-index: when we are in a modal context, we want the dropdown to be above the modal
|
|
144
|
+
// apply the nearest parents z-index + 1
|
|
145
|
+
const zIndex = getNearestParentsZIndex(toggle);
|
|
146
|
+
if (parseInt(zIndex) > 0) {
|
|
147
|
+
var _dropdownRef$current$;
|
|
148
|
+
(_dropdownRef$current$ = dropdownRef.current.parentElement) === null || _dropdownRef$current$ === void 0 || _dropdownRef$current$.style.setProperty("z-index", zIndex + 1);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
setTimeout(() => {
|
|
152
|
+
var _dropdownRef$current;
|
|
153
|
+
if (isSearchable) {
|
|
154
|
+
var _searchRef$current;
|
|
155
|
+
(_searchRef$current = searchRef.current) === null || _searchRef$current === void 0 || _searchRef$current.focus();
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
(_dropdownRef$current = dropdownRef.current) === null || _dropdownRef$current === void 0 || _dropdownRef$current.focus();
|
|
159
|
+
}, 100);
|
|
160
|
+
}, [isSearchable, toggleId]);
|
|
161
|
+
const handleResize = () => {
|
|
162
|
+
adjustDropdownHeight(dropdownListRef.current, searchRef.current);
|
|
163
|
+
};
|
|
164
|
+
(0, _react.useLayoutEffect)(handleResize, []);
|
|
165
|
+
(0, _hooks.useListener)(window, handleResize, "resize");
|
|
166
|
+
|
|
167
|
+
// track selected index from key board action and scroll into view if needed
|
|
168
|
+
(0, _react.useEffect)(() => {
|
|
169
|
+
var _optionsRef$current$h;
|
|
170
|
+
(_optionsRef$current$h = optionsRef.current[highlightedOptionIndex]) === null || _optionsRef$current$h === void 0 || _optionsRef$current$h.scrollIntoView({
|
|
171
|
+
block: "nearest",
|
|
172
|
+
inline: "nearest"
|
|
173
|
+
});
|
|
174
|
+
}, [highlightedOptionIndex]);
|
|
175
|
+
const filteredOptions = onSearch ? options : options === null || options === void 0 ? void 0 : options.filter(option => {
|
|
176
|
+
if (!search || option.disabled) return true;
|
|
177
|
+
const searchText = getOptionText(option) || option.value;
|
|
178
|
+
return searchText.toLowerCase().includes(search);
|
|
179
|
+
});
|
|
180
|
+
const getNextOptionIndex = (goingUp, prevIndex) => {
|
|
181
|
+
const increment = goingUp ? -1 : 1;
|
|
182
|
+
let currIndex = prevIndex + increment;
|
|
183
|
+
// skip disabled options for key board action
|
|
184
|
+
while (filteredOptions[currIndex] && (_filteredOptions$curr = filteredOptions[currIndex]) !== null && _filteredOptions$curr !== void 0 && _filteredOptions$curr.disabled) {
|
|
185
|
+
var _filteredOptions$curr;
|
|
186
|
+
currIndex += increment;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// consider upper bound for navigating down the list
|
|
190
|
+
if (increment > 0) {
|
|
191
|
+
return currIndex < filteredOptions.length ? currIndex : prevIndex;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// consider lower bound for navigating up the list
|
|
195
|
+
return currIndex >= 0 ? currIndex : prevIndex;
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
// handle keyboard actions for navigating the select dropdown
|
|
199
|
+
const handleKeyDown = event => {
|
|
200
|
+
const upDownKeys = ["ArrowUp", "ArrowDown"];
|
|
201
|
+
|
|
202
|
+
// prevent default browser actions for up, down, enter and escape keys
|
|
203
|
+
// also prevent any other event listeners from being called up the DOM tree
|
|
204
|
+
if ([...upDownKeys, "Enter", "Escape", "Tab"].includes(event.key)) {
|
|
205
|
+
event.preventDefault();
|
|
206
|
+
event.nativeEvent.stopImmediatePropagation();
|
|
207
|
+
}
|
|
208
|
+
if (upDownKeys.includes(event.key)) {
|
|
209
|
+
setHighlightedOptionIndex(prevIndex => {
|
|
210
|
+
const goingUp = event.key === "ArrowUp";
|
|
211
|
+
return getNextOptionIndex(goingUp, prevIndex);
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
if (event.key === "Enter" && filteredOptions[highlightedOptionIndex]) {
|
|
215
|
+
onSelect(filteredOptions[highlightedOptionIndex].value);
|
|
216
|
+
}
|
|
217
|
+
if (event.key === "Escape" || event.key === "Tab") {
|
|
218
|
+
onClose();
|
|
219
|
+
}
|
|
220
|
+
};
|
|
221
|
+
const handleSearch = value => {
|
|
222
|
+
setSearch(value.toLowerCase());
|
|
223
|
+
// reset selected index when search text changes
|
|
224
|
+
setHighlightedOptionIndex(0);
|
|
225
|
+
optionsRef.current = [];
|
|
226
|
+
if (onSearch) {
|
|
227
|
+
onSearch(value);
|
|
228
|
+
}
|
|
229
|
+
};
|
|
230
|
+
const handleSelect = option => {
|
|
231
|
+
if (option.disabled) {
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
onSelect(option.value);
|
|
235
|
+
};
|
|
236
|
+
const optionItems = filteredOptions.map((option, idx) => {
|
|
237
|
+
return /*#__PURE__*/_react.default.createElement("li", {
|
|
238
|
+
key: "".concat(option.value, "-").concat(idx),
|
|
239
|
+
onClick: () => handleSelect(option),
|
|
240
|
+
className: (0, _classnames.default)("p-list__item", "p-custom-select__option", "u-truncate", {
|
|
241
|
+
disabled: option.disabled,
|
|
242
|
+
highlight: idx === highlightedOptionIndex && !option.disabled
|
|
243
|
+
})
|
|
244
|
+
// adding option elements to a ref array makes it easier to scroll the element later
|
|
245
|
+
// else we'd have to make a DOM call to find the element based on some identifier
|
|
246
|
+
,
|
|
247
|
+
ref: el => {
|
|
248
|
+
if (!el) return;
|
|
249
|
+
optionsRef.current[idx] = el;
|
|
250
|
+
},
|
|
251
|
+
role: "option",
|
|
252
|
+
onMouseMove: () => setHighlightedOptionIndex(idx)
|
|
253
|
+
}, /*#__PURE__*/_react.default.createElement("span", {
|
|
254
|
+
className: (0, _classnames.default)({
|
|
255
|
+
"u-text--muted": option.disabled
|
|
256
|
+
})
|
|
257
|
+
}, option.label));
|
|
258
|
+
});
|
|
259
|
+
return /*#__PURE__*/_react.default.createElement("div", {
|
|
260
|
+
className: "p-custom-select__dropdown u-no-padding",
|
|
261
|
+
role: "combobox",
|
|
262
|
+
onKeyDownCapture: handleKeyDown
|
|
263
|
+
// allow focus on the dropdown so that keyboard actions can be captured
|
|
264
|
+
,
|
|
265
|
+
tabIndex: -1,
|
|
266
|
+
ref: dropdownRef,
|
|
267
|
+
onMouseDown: e => {
|
|
268
|
+
// when custom select is used in a modal, which is a portal, a dropdown click
|
|
269
|
+
// should not close the modal itself, so we stop the event right here.
|
|
270
|
+
e.stopPropagation();
|
|
271
|
+
}
|
|
272
|
+
}, isSearchable && /*#__PURE__*/_react.default.createElement("div", {
|
|
273
|
+
className: "p-custom-select__search u-no-padding--bottom"
|
|
274
|
+
}, /*#__PURE__*/_react.default.createElement(_SearchBox.default, {
|
|
275
|
+
ref: searchRef,
|
|
276
|
+
id: "select-search-".concat(name),
|
|
277
|
+
name: "select-search-".concat(name),
|
|
278
|
+
type: "text",
|
|
279
|
+
"aria-label": "Search for ".concat(name),
|
|
280
|
+
className: "u-no-margin--bottom",
|
|
281
|
+
onChange: handleSearch,
|
|
282
|
+
value: search,
|
|
283
|
+
autocomplete: "off"
|
|
284
|
+
})), header, /*#__PURE__*/_react.default.createElement("ul", {
|
|
285
|
+
className: "p-list u-no-margin--bottom",
|
|
286
|
+
role: "listbox",
|
|
287
|
+
ref: dropdownListRef
|
|
288
|
+
}, optionItems));
|
|
289
|
+
};
|
|
290
|
+
CustomSelectDropdown.propTypes = {
|
|
291
|
+
searchable: _propTypes.default.oneOf(["auto", "always", "never"]),
|
|
292
|
+
name: _propTypes.default.string.isRequired,
|
|
293
|
+
options: _propTypes.default.array.isRequired,
|
|
294
|
+
onSelect: _propTypes.default.func.isRequired,
|
|
295
|
+
onSearch: _propTypes.default.func,
|
|
296
|
+
onClose: _propTypes.default.func.isRequired,
|
|
297
|
+
header: _propTypes.default.node,
|
|
298
|
+
toggleId: _propTypes.default.string.isRequired
|
|
299
|
+
};
|
|
300
|
+
var _default = exports.default = CustomSelectDropdown;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
Object.defineProperty(exports, "default", {
|
|
7
|
+
enumerable: true,
|
|
8
|
+
get: function () {
|
|
9
|
+
return _CustomSelectDropdown.default;
|
|
10
|
+
}
|
|
11
|
+
});
|
|
12
|
+
Object.defineProperty(exports, "getOptionText", {
|
|
13
|
+
enumerable: true,
|
|
14
|
+
get: function () {
|
|
15
|
+
return _CustomSelectDropdown.getOptionText;
|
|
16
|
+
}
|
|
17
|
+
});
|
|
18
|
+
var _CustomSelectDropdown = _interopRequireWildcard(require("./CustomSelectDropdown"));
|
|
19
|
+
function _getRequireWildcardCache(e) { if ("function" != typeof WeakMap) return null; var r = new WeakMap(), t = new WeakMap(); return (_getRequireWildcardCache = function (e) { return e ? t : r; })(e); }
|
|
20
|
+
function _interopRequireWildcard(e, r) { if (!r && e && e.__esModule) return e; if (null === e || "object" != typeof e && "function" != typeof e) return { default: e }; var t = _getRequireWildcardCache(r); if (t && t.has(e)) return t.get(e); var n = { __proto__: null }, a = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var u in e) if ("default" !== u && {}.hasOwnProperty.call(e, u)) { var i = a ? Object.getOwnPropertyDescriptor(e, u) : null; i && (i.get || i.set) ? Object.defineProperty(n, u, i) : n[u] = e[u]; } return n.default = e, t && t.set(e, n), n; }
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
Object.defineProperty(exports, "default", {
|
|
7
|
+
enumerable: true,
|
|
8
|
+
get: function () {
|
|
9
|
+
return _CustomSelect.default;
|
|
10
|
+
}
|
|
11
|
+
});
|
|
12
|
+
var _CustomSelect = _interopRequireDefault(require("./CustomSelect"));
|
|
13
|
+
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { MutableRefObject, ReactNode } from "react";
|
|
2
|
+
import { ClassName, PropsWithSpread } from "../../types";
|
|
3
|
+
import { FieldProps } from "../Field";
|
|
4
|
+
import { Position } from "../ContextualMenu";
|
|
5
|
+
import { CustomSelectOption } from "./CustomSelectDropdown";
|
|
6
|
+
import "./CustomSelect.scss";
|
|
7
|
+
export type SelectRef = MutableRefObject<{
|
|
8
|
+
open: () => void;
|
|
9
|
+
close: () => void;
|
|
10
|
+
isOpen: boolean;
|
|
11
|
+
focus: () => void;
|
|
12
|
+
} | undefined>;
|
|
13
|
+
export type Props = PropsWithSpread<FieldProps, {
|
|
14
|
+
value: string;
|
|
15
|
+
options: CustomSelectOption[];
|
|
16
|
+
onChange: (value: string) => void;
|
|
17
|
+
onSearch?: (value: string) => void;
|
|
18
|
+
id?: string | null;
|
|
19
|
+
name?: string;
|
|
20
|
+
disabled?: boolean;
|
|
21
|
+
wrapperClassName?: ClassName;
|
|
22
|
+
toggleClassName?: ClassName;
|
|
23
|
+
dropdownClassName?: string;
|
|
24
|
+
searchable?: "auto" | "always" | "never";
|
|
25
|
+
takeFocus?: boolean;
|
|
26
|
+
header?: ReactNode;
|
|
27
|
+
selectRef?: SelectRef;
|
|
28
|
+
initialPosition?: Position;
|
|
29
|
+
}>;
|
|
30
|
+
/**
|
|
31
|
+
* This is a [React](https://reactjs.org/) component that extends from the Vanilla [Select](https://vanillaframework.io/docs/base/forms#select) element.
|
|
32
|
+
*
|
|
33
|
+
* The aim of this component is to provide a select component with customisable options and a dropdown menu, whilst maintaining accessibility and usability.
|
|
34
|
+
*/
|
|
35
|
+
declare const CustomSelect: ({ value, options, onChange, onSearch, id, name, disabled, success, error, help, wrapperClassName, toggleClassName, dropdownClassName, searchable, takeFocus, header, selectRef, initialPosition, ...fieldProps }: Props) => JSX.Element;
|
|
36
|
+
export default CustomSelect;
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
var _excluded = ["value", "options", "onChange", "onSearch", "id", "name", "disabled", "success", "error", "help", "wrapperClassName", "toggleClassName", "dropdownClassName", "searchable", "takeFocus", "header", "selectRef", "initialPosition"];
|
|
2
|
+
function _extends() { return _extends = Object.assign ? Object.assign.bind() : function (n) { for (var e = 1; e < arguments.length; e++) { var t = arguments[e]; for (var r in t) ({}).hasOwnProperty.call(t, r) && (n[r] = t[r]); } return n; }, _extends.apply(null, arguments); }
|
|
3
|
+
function _objectWithoutProperties(e, t) { if (null == e) return {}; var o, r, i = _objectWithoutPropertiesLoose(e, t); if (Object.getOwnPropertySymbols) { var n = Object.getOwnPropertySymbols(e); for (r = 0; r < n.length; r++) o = n[r], t.indexOf(o) >= 0 || {}.propertyIsEnumerable.call(e, o) && (i[o] = e[o]); } return i; }
|
|
4
|
+
function _objectWithoutPropertiesLoose(r, e) { if (null == r) return {}; var t = {}; for (var n in r) if ({}.hasOwnProperty.call(r, n)) { if (e.indexOf(n) >= 0) continue; t[n] = r[n]; } return t; }
|
|
5
|
+
import classNames from "classnames";
|
|
6
|
+
import React, { useEffect, useId, useImperativeHandle, useState } from "react";
|
|
7
|
+
import Field from "../Field";
|
|
8
|
+
import ContextualMenu from "../ContextualMenu";
|
|
9
|
+
import { useListener } from "../../hooks";
|
|
10
|
+
import CustomSelectDropdown, { getOptionText } from "./CustomSelectDropdown";
|
|
11
|
+
import "./CustomSelect.scss";
|
|
12
|
+
/**
|
|
13
|
+
* This is a [React](https://reactjs.org/) component that extends from the Vanilla [Select](https://vanillaframework.io/docs/base/forms#select) element.
|
|
14
|
+
*
|
|
15
|
+
* The aim of this component is to provide a select component with customisable options and a dropdown menu, whilst maintaining accessibility and usability.
|
|
16
|
+
*/
|
|
17
|
+
var CustomSelect = _ref => {
|
|
18
|
+
var {
|
|
19
|
+
value,
|
|
20
|
+
options,
|
|
21
|
+
onChange,
|
|
22
|
+
onSearch,
|
|
23
|
+
id,
|
|
24
|
+
name,
|
|
25
|
+
disabled,
|
|
26
|
+
success,
|
|
27
|
+
error,
|
|
28
|
+
help,
|
|
29
|
+
wrapperClassName,
|
|
30
|
+
toggleClassName,
|
|
31
|
+
dropdownClassName,
|
|
32
|
+
searchable = "auto",
|
|
33
|
+
takeFocus,
|
|
34
|
+
header,
|
|
35
|
+
selectRef,
|
|
36
|
+
initialPosition = "left"
|
|
37
|
+
} = _ref,
|
|
38
|
+
fieldProps = _objectWithoutProperties(_ref, _excluded);
|
|
39
|
+
var [isOpen, setIsOpen] = useState(false);
|
|
40
|
+
var validationId = useId();
|
|
41
|
+
var defaultSelectId = useId();
|
|
42
|
+
var selectId = id || defaultSelectId;
|
|
43
|
+
var helpId = useId();
|
|
44
|
+
var hasError = !!error;
|
|
45
|
+
|
|
46
|
+
// Close the dropdown when the browser tab is hidden
|
|
47
|
+
var onBrowserTabHidden = () => {
|
|
48
|
+
if (document.visibilityState === "hidden") {
|
|
49
|
+
setIsOpen(false);
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
useListener(window, onBrowserTabHidden, "visibilitychange");
|
|
53
|
+
|
|
54
|
+
// Close the dropdown when the browser window loses focus
|
|
55
|
+
useListener(window, () => setIsOpen(false), "blur");
|
|
56
|
+
useImperativeHandle(selectRef, () => ({
|
|
57
|
+
open: () => {
|
|
58
|
+
var _document$getElementB;
|
|
59
|
+
setIsOpen(true);
|
|
60
|
+
(_document$getElementB = document.getElementById(selectId)) === null || _document$getElementB === void 0 || _document$getElementB.focus();
|
|
61
|
+
},
|
|
62
|
+
focus: () => {
|
|
63
|
+
var _document$getElementB2;
|
|
64
|
+
return (_document$getElementB2 = document.getElementById(selectId)) === null || _document$getElementB2 === void 0 ? void 0 : _document$getElementB2.focus();
|
|
65
|
+
},
|
|
66
|
+
close: setIsOpen.bind(null, false),
|
|
67
|
+
isOpen: isOpen
|
|
68
|
+
}), [isOpen, selectId]);
|
|
69
|
+
useEffect(() => {
|
|
70
|
+
if (takeFocus) {
|
|
71
|
+
var toggleButton = document.getElementById(selectId);
|
|
72
|
+
toggleButton === null || toggleButton === void 0 || toggleButton.focus();
|
|
73
|
+
}
|
|
74
|
+
}, [takeFocus, selectId]);
|
|
75
|
+
var selectedOption = options.find(option => option.value === value);
|
|
76
|
+
var toggleLabel = /*#__PURE__*/React.createElement("span", {
|
|
77
|
+
className: "toggle-label u-truncate"
|
|
78
|
+
}, selectedOption ? getOptionText(selectedOption) : "Select an option");
|
|
79
|
+
var handleSelect = value => {
|
|
80
|
+
var _document$getElementB3;
|
|
81
|
+
(_document$getElementB3 = document.getElementById(selectId)) === null || _document$getElementB3 === void 0 || _document$getElementB3.focus();
|
|
82
|
+
setIsOpen(false);
|
|
83
|
+
onChange(value);
|
|
84
|
+
};
|
|
85
|
+
return /*#__PURE__*/React.createElement(Field, _extends({}, fieldProps, {
|
|
86
|
+
className: classNames("p-custom-select", wrapperClassName),
|
|
87
|
+
error: error,
|
|
88
|
+
forId: selectId,
|
|
89
|
+
help: help,
|
|
90
|
+
helpId: helpId,
|
|
91
|
+
isSelect: true,
|
|
92
|
+
success: success,
|
|
93
|
+
validationId: validationId
|
|
94
|
+
}), /*#__PURE__*/React.createElement(ContextualMenu, {
|
|
95
|
+
"aria-describedby": [help ? helpId : null, success ? validationId : null].filter(Boolean).join(" "),
|
|
96
|
+
"aria-errormessage": hasError ? validationId : undefined,
|
|
97
|
+
"aria-invalid": hasError,
|
|
98
|
+
toggleClassName: classNames("p-custom-select__toggle", "p-form-validation__input", toggleClassName, {
|
|
99
|
+
active: isOpen
|
|
100
|
+
}),
|
|
101
|
+
toggleLabel: toggleLabel,
|
|
102
|
+
visible: isOpen,
|
|
103
|
+
onToggleMenu: open => {
|
|
104
|
+
// Handle syncing the state when toggling the menu from within the
|
|
105
|
+
// contextual menu component e.g. when clicking outside.
|
|
106
|
+
if (open !== isOpen) {
|
|
107
|
+
setIsOpen(open);
|
|
108
|
+
}
|
|
109
|
+
},
|
|
110
|
+
toggleProps: {
|
|
111
|
+
id: selectId,
|
|
112
|
+
disabled: disabled,
|
|
113
|
+
// tabIndex is set to -1 when disabled to prevent keyboard navigation to the select toggle
|
|
114
|
+
tabIndex: disabled ? -1 : 0
|
|
115
|
+
},
|
|
116
|
+
className: "p-custom-select__wrapper",
|
|
117
|
+
dropdownClassName: dropdownClassName,
|
|
118
|
+
style: {
|
|
119
|
+
width: "100%"
|
|
120
|
+
},
|
|
121
|
+
autoAdjust: true,
|
|
122
|
+
position: initialPosition
|
|
123
|
+
}, close => /*#__PURE__*/React.createElement(CustomSelectDropdown, {
|
|
124
|
+
searchable: searchable,
|
|
125
|
+
onSearch: onSearch,
|
|
126
|
+
name: name || "",
|
|
127
|
+
options: options || [],
|
|
128
|
+
onSelect: handleSelect,
|
|
129
|
+
onClose: () => {
|
|
130
|
+
var _document$getElementB4;
|
|
131
|
+
// When pressing ESC to close the dropdown, we keep focus on the toggle button
|
|
132
|
+
close();
|
|
133
|
+
(_document$getElementB4 = document.getElementById(selectId)) === null || _document$getElementB4 === void 0 || _document$getElementB4.focus();
|
|
134
|
+
},
|
|
135
|
+
header: header,
|
|
136
|
+
toggleId: selectId
|
|
137
|
+
})));
|
|
138
|
+
};
|
|
139
|
+
export default CustomSelect;
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
@use "sass:map";
|
|
2
|
+
@import "vanilla-framework";
|
|
3
|
+
@include vf-b-placeholders; // Vanilla base placeholders to extend from
|
|
4
|
+
|
|
5
|
+
.p-custom-select {
|
|
6
|
+
@include vf-b-forms;
|
|
7
|
+
|
|
8
|
+
// style copied directly from vanilla-framework for the select element
|
|
9
|
+
.p-custom-select__toggle {
|
|
10
|
+
@include vf-icon-chevron-themed;
|
|
11
|
+
@extend %vf-input-elements;
|
|
12
|
+
|
|
13
|
+
// stylelint-disable property-no-vendor-prefix
|
|
14
|
+
-moz-appearance: none;
|
|
15
|
+
-webkit-appearance: none;
|
|
16
|
+
appearance: none;
|
|
17
|
+
// stylelint-enable property-no-vendor-prefix
|
|
18
|
+
background-position: right calc(map-get($grid-margin-widths, default) / 2)
|
|
19
|
+
center;
|
|
20
|
+
background-repeat: no-repeat;
|
|
21
|
+
background-size: map-get($icon-sizes, default);
|
|
22
|
+
border-top: none;
|
|
23
|
+
box-shadow: none;
|
|
24
|
+
min-height: map-get($line-heights, default-text);
|
|
25
|
+
padding-right: calc($default-icon-size + 2 * $sph--small);
|
|
26
|
+
text-indent: 0.01px;
|
|
27
|
+
|
|
28
|
+
&:hover {
|
|
29
|
+
cursor: pointer;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// this emulates the highlight effect when the select is focused
|
|
33
|
+
// without crowding the content with a border
|
|
34
|
+
&.active,
|
|
35
|
+
&:focus {
|
|
36
|
+
box-shadow: inset 0 0 0 3px $color-focus;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
.toggle-label {
|
|
40
|
+
display: flow-root;
|
|
41
|
+
text-align: left;
|
|
42
|
+
width: 100%;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
.p-custom-select__dropdown {
|
|
48
|
+
background-color: $colors--theme--background-alt;
|
|
49
|
+
box-shadow: $box-shadow--deep;
|
|
50
|
+
outline: none;
|
|
51
|
+
position: relative;
|
|
52
|
+
|
|
53
|
+
.p-custom-select__option {
|
|
54
|
+
background-color: $colors--theme--background-alt;
|
|
55
|
+
font-weight: $font-weight-regular-text;
|
|
56
|
+
padding: $sph--x-small $sph--small;
|
|
57
|
+
|
|
58
|
+
&.highlight {
|
|
59
|
+
// browser default styling for options when hovered
|
|
60
|
+
background-color: #06c;
|
|
61
|
+
cursor: pointer;
|
|
62
|
+
|
|
63
|
+
// make sure that if an option is highlighted, its text is white for good contrast
|
|
64
|
+
* {
|
|
65
|
+
color: white;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
.p-custom-select__search {
|
|
71
|
+
background-color: $colors--theme--background-alt;
|
|
72
|
+
padding: $sph--x-small;
|
|
73
|
+
padding-bottom: $sph--small;
|
|
74
|
+
position: sticky;
|
|
75
|
+
top: 0;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
.p-list {
|
|
79
|
+
max-height: 30rem;
|
|
80
|
+
overflow: auto;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { Meta, StoryObj } from "@storybook/react/*";
|
|
2
|
+
import CustomSelect from "./CustomSelect";
|
|
3
|
+
import { ComponentProps } from "react";
|
|
4
|
+
type StoryProps = ComponentProps<typeof CustomSelect>;
|
|
5
|
+
declare const meta: Meta<StoryProps>;
|
|
6
|
+
export default meta;
|
|
7
|
+
type Story = StoryObj<StoryProps>;
|
|
8
|
+
/**
|
|
9
|
+
* If `label` is of `string` type. You do not have to do anything extra to render it.
|
|
10
|
+
*/
|
|
11
|
+
export declare const StandardOptions: Story;
|
|
12
|
+
/**
|
|
13
|
+
* If `label` is of `ReactNode` type. You can render custom content.
|
|
14
|
+
* In this case, the `text` property for each option is required and is used for display in the toggle, search and sort functionalities.
|
|
15
|
+
*/
|
|
16
|
+
export declare const CustomOptions: Story;
|
|
17
|
+
/**
|
|
18
|
+
* For each option, if `disabled` is set to `true`, the option will be disabled.
|
|
19
|
+
*/
|
|
20
|
+
export declare const DisabledOptions: Story;
|
|
21
|
+
/**
|
|
22
|
+
* Search is enabled by default when there are 5 or more options.
|
|
23
|
+
*/
|
|
24
|
+
export declare const AutoSearchable: Story;
|
|
25
|
+
/**
|
|
26
|
+
* Search can be enabled manually by setting `searchable` to `always`.
|
|
27
|
+
*/
|
|
28
|
+
export declare const ManualSearchable: Story;
|