@canonical/react-components 1.7.2 → 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.
Files changed (33) hide show
  1. package/dist/components/CustomSelect/CustomSelect.d.ts +36 -0
  2. package/dist/components/CustomSelect/CustomSelect.js +145 -0
  3. package/dist/components/CustomSelect/CustomSelect.scss +82 -0
  4. package/dist/components/CustomSelect/CustomSelect.stories.d.ts +28 -0
  5. package/dist/components/CustomSelect/CustomSelect.stories.js +132 -0
  6. package/dist/components/CustomSelect/CustomSelect.test.d.ts +1 -0
  7. package/dist/components/CustomSelect/CustomSelectDropdown/CustomSelectDropdown.d.ts +25 -0
  8. package/dist/components/CustomSelect/CustomSelectDropdown/CustomSelectDropdown.js +300 -0
  9. package/dist/components/CustomSelect/CustomSelectDropdown/CustomSelectDropdown.test.d.ts +1 -0
  10. package/dist/components/CustomSelect/CustomSelectDropdown/index.d.ts +2 -0
  11. package/dist/components/CustomSelect/CustomSelectDropdown/index.js +20 -0
  12. package/dist/components/CustomSelect/index.d.ts +3 -0
  13. package/dist/components/CustomSelect/index.js +13 -0
  14. package/dist/components/MultiSelect/MultiSelect.scss +1 -0
  15. package/dist/esm/components/CustomSelect/CustomSelect.d.ts +36 -0
  16. package/dist/esm/components/CustomSelect/CustomSelect.js +139 -0
  17. package/dist/esm/components/CustomSelect/CustomSelect.scss +82 -0
  18. package/dist/esm/components/CustomSelect/CustomSelect.stories.d.ts +28 -0
  19. package/dist/esm/components/CustomSelect/CustomSelect.stories.js +126 -0
  20. package/dist/esm/components/CustomSelect/CustomSelect.test.d.ts +1 -0
  21. package/dist/esm/components/CustomSelect/CustomSelectDropdown/CustomSelectDropdown.d.ts +25 -0
  22. package/dist/esm/components/CustomSelect/CustomSelectDropdown/CustomSelectDropdown.js +285 -0
  23. package/dist/esm/components/CustomSelect/CustomSelectDropdown/CustomSelectDropdown.test.d.ts +1 -0
  24. package/dist/esm/components/CustomSelect/CustomSelectDropdown/index.d.ts +2 -0
  25. package/dist/esm/components/CustomSelect/CustomSelectDropdown/index.js +1 -0
  26. package/dist/esm/components/CustomSelect/index.d.ts +3 -0
  27. package/dist/esm/components/CustomSelect/index.js +1 -0
  28. package/dist/esm/components/MultiSelect/MultiSelect.scss +1 -0
  29. package/dist/esm/index.d.ts +2 -0
  30. package/dist/esm/index.js +1 -0
  31. package/dist/index.d.ts +2 -0
  32. package/dist/index.js +8 -0
  33. 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,2 @@
1
+ export { default, getOptionText } from "./CustomSelectDropdown";
2
+ export type { CustomSelectOption, Props as CustomSelectDropdownProps, } from "./CustomSelectDropdown";
@@ -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,3 @@
1
+ export { default } from "./CustomSelect";
2
+ export type { Props as CustomSelectProps } from "./CustomSelect";
3
+ export type { CustomSelectDropdownProps, CustomSelectOption, } from "./CustomSelectDropdown";
@@ -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 }; }
@@ -56,6 +56,7 @@ $dropdown-max-height: 20rem;
56
56
  list-style: none;
57
57
  margin-bottom: $sph--x-small;
58
58
  margin-left: 0;
59
+ margin-top: $sph--x-small;
59
60
  padding-left: 0;
60
61
  }
61
62
 
@@ -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;