@canonical/react-components 3.5.1 → 3.7.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 +23 -2
- package/dist/components/CustomSelect/CustomSelect.js +4 -1
- package/dist/components/CustomSelect/CustomSelect.stories.d.ts +5 -0
- package/dist/components/CustomSelect/CustomSelect.stories.js +15 -2
- package/dist/components/CustomSelect/CustomSelectDropdown/CustomSelectDropdown.d.ts +2 -1
- package/dist/components/CustomSelect/CustomSelectDropdown/CustomSelectDropdown.js +23 -2
- package/dist/components/Modal/Modal.d.ts +6 -2
- package/dist/components/Modal/Modal.js +14 -17
- package/dist/components/Modal/Modal.stories.d.ts +1 -0
- package/dist/components/Modal/Modal.stories.js +33 -1
- package/dist/esm/components/CustomSelect/CustomSelect.d.ts +23 -2
- package/dist/esm/components/CustomSelect/CustomSelect.js +5 -2
- package/dist/esm/components/CustomSelect/CustomSelect.stories.d.ts +5 -0
- package/dist/esm/components/CustomSelect/CustomSelect.stories.js +14 -1
- package/dist/esm/components/CustomSelect/CustomSelectDropdown/CustomSelectDropdown.d.ts +2 -1
- package/dist/esm/components/CustomSelect/CustomSelectDropdown/CustomSelectDropdown.js +23 -2
- package/dist/esm/components/Modal/Modal.d.ts +6 -2
- package/dist/esm/components/Modal/Modal.js +15 -18
- package/dist/esm/components/Modal/Modal.stories.d.ts +1 -0
- package/dist/esm/components/Modal/Modal.stories.js +33 -1
- package/package.json +2 -2
|
@@ -19,10 +19,31 @@ export type Props = PropsWithSpread<FieldProps, {
|
|
|
19
19
|
id?: string | null;
|
|
20
20
|
name?: string;
|
|
21
21
|
disabled?: boolean;
|
|
22
|
+
/**
|
|
23
|
+
* Toggle label when no option is selected
|
|
24
|
+
*
|
|
25
|
+
* @default - "Select an option"
|
|
26
|
+
*/
|
|
27
|
+
defaultToggleLabel?: string;
|
|
22
28
|
wrapperClassName?: ClassName;
|
|
23
29
|
toggleClassName?: ClassName;
|
|
24
30
|
dropdownClassName?: string;
|
|
25
|
-
|
|
31
|
+
/**
|
|
32
|
+
* Whether the select is searchable.
|
|
33
|
+
* - `auto`: the select will be searchable if it has 5 or more options.
|
|
34
|
+
* - `always`: the select will always be searchable if there is at least 1 option.
|
|
35
|
+
* - `async`: the select will always be searchable.
|
|
36
|
+
* - `never`: the select will never be searchable.
|
|
37
|
+
*
|
|
38
|
+
* @default - "auto"
|
|
39
|
+
*/
|
|
40
|
+
searchable?: "auto" | "always" | "async" | "never";
|
|
41
|
+
/**
|
|
42
|
+
* Placeholder text for the search input when searchable is enabled.
|
|
43
|
+
*
|
|
44
|
+
* @default - "Search"
|
|
45
|
+
*/
|
|
46
|
+
searchPlaceholder?: string;
|
|
26
47
|
takeFocus?: boolean;
|
|
27
48
|
header?: ReactNode;
|
|
28
49
|
selectRef?: SelectRef;
|
|
@@ -33,5 +54,5 @@ export type Props = PropsWithSpread<FieldProps, {
|
|
|
33
54
|
*
|
|
34
55
|
* The aim of this component is to provide a select component with customisable options and a dropdown menu, whilst maintaining accessibility and usability.
|
|
35
56
|
*/
|
|
36
|
-
declare const CustomSelect: ({ value, options, onChange, onSearch, id, name, disabled, success, error, help, wrapperClassName, toggleClassName, dropdownClassName, searchable, takeFocus, header, selectRef, initialPosition, ...fieldProps }: Props) => React.JSX.Element;
|
|
57
|
+
declare const CustomSelect: ({ value, options, onChange, onSearch, id, name, disabled, success, error, help, wrapperClassName, toggleClassName, dropdownClassName, defaultToggleLabel, searchable, searchPlaceholder, takeFocus, header, selectRef, initialPosition, ...fieldProps }: Props) => React.JSX.Element;
|
|
37
58
|
export default CustomSelect;
|
|
@@ -35,7 +35,9 @@ const CustomSelect = _ref => {
|
|
|
35
35
|
wrapperClassName,
|
|
36
36
|
toggleClassName,
|
|
37
37
|
dropdownClassName,
|
|
38
|
+
defaultToggleLabel = "Select an option",
|
|
38
39
|
searchable = "auto",
|
|
40
|
+
searchPlaceholder = "Search",
|
|
39
41
|
takeFocus,
|
|
40
42
|
header,
|
|
41
43
|
selectRef,
|
|
@@ -81,7 +83,7 @@ const CustomSelect = _ref => {
|
|
|
81
83
|
const selectedOption = options.find(option => option.value === value);
|
|
82
84
|
const toggleLabel = /*#__PURE__*/_react.default.createElement("span", {
|
|
83
85
|
className: "toggle-label u-truncate"
|
|
84
|
-
}, selectedOption ? selectedOption.selectedLabel || (0, _CustomSelectDropdown.getOptionText)(selectedOption) :
|
|
86
|
+
}, selectedOption ? selectedOption.selectedLabel || (0, _CustomSelectDropdown.getOptionText)(selectedOption) : defaultToggleLabel);
|
|
85
87
|
const handleSelect = value => {
|
|
86
88
|
var _document$getElementB3;
|
|
87
89
|
(_document$getElementB3 = document.getElementById(selectId)) === null || _document$getElementB3 === void 0 || _document$getElementB3.focus();
|
|
@@ -128,6 +130,7 @@ const CustomSelect = _ref => {
|
|
|
128
130
|
position: initialPosition
|
|
129
131
|
}, close => /*#__PURE__*/_react.default.createElement(_CustomSelectDropdown.default, {
|
|
130
132
|
searchable: searchable,
|
|
133
|
+
searchPlaceholder: searchPlaceholder,
|
|
131
134
|
onSearch: onSearch,
|
|
132
135
|
name: name || "",
|
|
133
136
|
options: options || [],
|
|
@@ -32,3 +32,8 @@ export declare const AutoSearchable: Story;
|
|
|
32
32
|
* Search can be enabled manually by setting `searchable` to `always`.
|
|
33
33
|
*/
|
|
34
34
|
export declare const ManualSearchable: Story;
|
|
35
|
+
/**
|
|
36
|
+
* Search can be enabled manually by setting `searchable` to `async`.
|
|
37
|
+
* This will always show the search input regardless of the number of options.
|
|
38
|
+
*/
|
|
39
|
+
export declare const AsyncSearchable: Story;
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
Object.defineProperty(exports, "__esModule", {
|
|
4
4
|
value: true
|
|
5
5
|
});
|
|
6
|
-
exports.default = exports.StandardOptions = exports.ManualSearchable = exports.DisabledOptions = exports.CustomOptionsAndSelectedLabel = exports.CustomOptions = exports.AutoSearchable = void 0;
|
|
6
|
+
exports.default = exports.StandardOptions = exports.ManualSearchable = exports.DisabledOptions = exports.CustomOptionsAndSelectedLabel = exports.CustomOptions = exports.AutoSearchable = exports.AsyncSearchable = void 0;
|
|
7
7
|
var _CustomSelect = _interopRequireDefault(require("./CustomSelect"));
|
|
8
8
|
var _react = _interopRequireWildcard(require("react"));
|
|
9
9
|
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); }
|
|
@@ -150,12 +150,14 @@ const meta = {
|
|
|
150
150
|
args: {
|
|
151
151
|
name: "customSelect",
|
|
152
152
|
label: "Custom Select",
|
|
153
|
+
defaultToggleLabel: "Select an option",
|
|
153
154
|
searchable: "auto",
|
|
155
|
+
searchPlaceholder: "Search",
|
|
154
156
|
initialPosition: "left"
|
|
155
157
|
},
|
|
156
158
|
argTypes: {
|
|
157
159
|
searchable: {
|
|
158
|
-
options: ["auto", "always", "never"],
|
|
160
|
+
options: ["auto", "always", "async", "never"],
|
|
159
161
|
control: {
|
|
160
162
|
type: "select"
|
|
161
163
|
}
|
|
@@ -231,4 +233,15 @@ const ManualSearchable = exports.ManualSearchable = {
|
|
|
231
233
|
options: generateStandardOptions(4),
|
|
232
234
|
searchable: "always"
|
|
233
235
|
}
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Search can be enabled manually by setting `searchable` to `async`.
|
|
240
|
+
* This will always show the search input regardless of the number of options.
|
|
241
|
+
*/
|
|
242
|
+
const AsyncSearchable = exports.AsyncSearchable = {
|
|
243
|
+
args: {
|
|
244
|
+
options: generateStandardOptions(0),
|
|
245
|
+
searchable: "async"
|
|
246
|
+
}
|
|
234
247
|
};
|
|
@@ -7,7 +7,8 @@ export type CustomSelectOption = LiHTMLAttributes<HTMLLIElement> & {
|
|
|
7
7
|
selectedLabel?: ReactNode;
|
|
8
8
|
};
|
|
9
9
|
export type Props = {
|
|
10
|
-
searchable?: "auto" | "always" | "never";
|
|
10
|
+
searchable?: "auto" | "always" | "async" | "never";
|
|
11
|
+
searchPlaceholder?: string;
|
|
11
12
|
name: string;
|
|
12
13
|
options: CustomSelectOption[];
|
|
13
14
|
onSelect: (value: string) => void;
|
|
@@ -97,9 +97,28 @@ const getOptionText = option => {
|
|
|
97
97
|
throw new Error("CustomSelect: options must have a string label or a text property");
|
|
98
98
|
};
|
|
99
99
|
exports.getOptionText = getOptionText;
|
|
100
|
+
const getIsSearchable = (searchable, numberOfOptions) => {
|
|
101
|
+
if (searchable === "async") {
|
|
102
|
+
return true;
|
|
103
|
+
}
|
|
104
|
+
if (searchable === "never") {
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
107
|
+
if (numberOfOptions <= 1) {
|
|
108
|
+
return false;
|
|
109
|
+
}
|
|
110
|
+
if (searchable === "always") {
|
|
111
|
+
return true;
|
|
112
|
+
}
|
|
113
|
+
if (searchable === "auto" && numberOfOptions >= 5) {
|
|
114
|
+
return true;
|
|
115
|
+
}
|
|
116
|
+
return false;
|
|
117
|
+
};
|
|
100
118
|
const CustomSelectDropdown = _ref => {
|
|
101
119
|
let {
|
|
102
120
|
searchable,
|
|
121
|
+
searchPlaceholder,
|
|
103
122
|
name,
|
|
104
123
|
options,
|
|
105
124
|
onSelect,
|
|
@@ -116,7 +135,7 @@ const CustomSelectDropdown = _ref => {
|
|
|
116
135
|
const dropdownRef = (0, _react.useRef)(null);
|
|
117
136
|
const searchRef = (0, _react.useRef)(null);
|
|
118
137
|
const dropdownListRef = (0, _react.useRef)(null);
|
|
119
|
-
const isSearchable =
|
|
138
|
+
const isSearchable = getIsSearchable(searchable, options.length);
|
|
120
139
|
(0, _react.useEffect)(() => {
|
|
121
140
|
if (dropdownRef.current) {
|
|
122
141
|
var _toggle$getBoundingCl, _toggle$getBoundingCl2;
|
|
@@ -255,6 +274,7 @@ const CustomSelectDropdown = _ref => {
|
|
|
255
274
|
name: "select-search-".concat(name),
|
|
256
275
|
type: "text",
|
|
257
276
|
"aria-label": "Search for ".concat(name),
|
|
277
|
+
placeholder: searchPlaceholder,
|
|
258
278
|
className: "u-no-margin--bottom",
|
|
259
279
|
onChange: handleSearch,
|
|
260
280
|
value: search,
|
|
@@ -266,7 +286,8 @@ const CustomSelectDropdown = _ref => {
|
|
|
266
286
|
}, optionItems));
|
|
267
287
|
};
|
|
268
288
|
CustomSelectDropdown.propTypes = {
|
|
269
|
-
searchable: _propTypes.default.oneOf(["auto", "always", "never"]),
|
|
289
|
+
searchable: _propTypes.default.oneOf(["auto", "always", "async", "never"]),
|
|
290
|
+
searchPlaceholder: _propTypes.default.string,
|
|
270
291
|
name: _propTypes.default.string.isRequired,
|
|
271
292
|
options: _propTypes.default.array.isRequired,
|
|
272
293
|
onSelect: _propTypes.default.func.isRequired,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import React from "react";
|
|
2
|
-
import type { HTMLProps, ReactNode } from "react";
|
|
2
|
+
import type { HTMLProps, ReactNode, RefObject } from "react";
|
|
3
3
|
import { ClassName, PropsWithSpread } from "../../types";
|
|
4
4
|
export type Props = PropsWithSpread<{
|
|
5
5
|
/**
|
|
@@ -18,6 +18,10 @@ export type Props = PropsWithSpread<{
|
|
|
18
18
|
* Function to handle closing the modal.
|
|
19
19
|
*/
|
|
20
20
|
close?: () => void | null;
|
|
21
|
+
/**
|
|
22
|
+
* The element that will be focused upon opening the modal.
|
|
23
|
+
*/
|
|
24
|
+
focusRef?: RefObject<HTMLElement | null>;
|
|
21
25
|
/**
|
|
22
26
|
* The title of the modal.
|
|
23
27
|
*/
|
|
@@ -36,5 +40,5 @@ export type Props = PropsWithSpread<{
|
|
|
36
40
|
*
|
|
37
41
|
* The modal component can be used to overlay an area of the screen which can contain a prompt, dialog or interaction.
|
|
38
42
|
*/
|
|
39
|
-
export declare const Modal: ({ buttonRow, children, className, close, title, shouldPropagateClickEvent, closeOnOutsideClick, ...wrapperProps }: Props) => React.JSX.Element;
|
|
43
|
+
export declare const Modal: ({ buttonRow, children, className, close, focusRef, title, shouldPropagateClickEvent, closeOnOutsideClick, ...wrapperProps }: Props) => React.JSX.Element;
|
|
40
44
|
export default Modal;
|
|
@@ -21,6 +21,7 @@ const Modal = _ref => {
|
|
|
21
21
|
children,
|
|
22
22
|
className,
|
|
23
23
|
close,
|
|
24
|
+
focusRef,
|
|
24
25
|
title,
|
|
25
26
|
shouldPropagateClickEvent = false,
|
|
26
27
|
closeOnOutsideClick = true,
|
|
@@ -33,6 +34,7 @@ const Modal = _ref => {
|
|
|
33
34
|
const titleId = (0, _react.useId)();
|
|
34
35
|
const shouldClose = (0, _react.useRef)(false);
|
|
35
36
|
const modalRef = (0, _react.useRef)(null);
|
|
37
|
+
const closeButtonRef = (0, _react.useRef)(null);
|
|
36
38
|
const focusableModalElements = (0, _react.useRef)(null);
|
|
37
39
|
const handleTabKey = event => {
|
|
38
40
|
if (focusableModalElements.current.length > 0) {
|
|
@@ -60,24 +62,18 @@ const Modal = _ref => {
|
|
|
60
62
|
close();
|
|
61
63
|
}
|
|
62
64
|
};
|
|
63
|
-
const keyListenersMap = new Map([["Escape", handleEscKey], ["Tab", handleTabKey]]);
|
|
64
65
|
(0, _react.useEffect)(() => {
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
let focusIndex = 0;
|
|
72
|
-
// when the close button is rendered, focus on the 2nd content element and not the close btn.
|
|
73
|
-
if (hasCloseButton && focusableModalElements.current.length > 1) {
|
|
74
|
-
focusIndex = 1;
|
|
66
|
+
if (focusRef !== null && focusRef !== void 0 && focusRef.current) {
|
|
67
|
+
focusRef.current.focus();
|
|
68
|
+
} else if (closeButtonRef.current) {
|
|
69
|
+
closeButtonRef.current.focus();
|
|
70
|
+
} else {
|
|
71
|
+
modalRef.current.focus();
|
|
75
72
|
}
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
});
|
|
79
|
-
}, [hasCloseButton]);
|
|
73
|
+
focusableModalElements.current = modalRef.current.querySelectorAll(focusableElementSelectors);
|
|
74
|
+
}, [focusRef]);
|
|
80
75
|
(0, _react.useEffect)(() => {
|
|
76
|
+
const keyListenersMap = new Map([["Escape", handleEscKey], ["Tab", handleTabKey]]);
|
|
81
77
|
const keyDown = event => {
|
|
82
78
|
const listener = keyListenersMap.get(event.code);
|
|
83
79
|
return listener && listener(event);
|
|
@@ -130,11 +126,12 @@ const Modal = _ref => {
|
|
|
130
126
|
}, /*#__PURE__*/_react.default.createElement("h2", {
|
|
131
127
|
className: "p-modal__title",
|
|
132
128
|
id: titleId
|
|
133
|
-
}, title),
|
|
129
|
+
}, title), close && /*#__PURE__*/_react.default.createElement("button", {
|
|
134
130
|
type: "button",
|
|
135
131
|
className: "p-modal__close",
|
|
136
132
|
"aria-label": "Close active modal",
|
|
137
|
-
onClick: handleClose
|
|
133
|
+
onClick: handleClose,
|
|
134
|
+
ref: closeButtonRef
|
|
138
135
|
}, "Close")), /*#__PURE__*/_react.default.createElement("div", {
|
|
139
136
|
id: descriptionId
|
|
140
137
|
}, children), !!buttonRow && /*#__PURE__*/_react.default.createElement("footer", {
|
|
@@ -3,8 +3,9 @@
|
|
|
3
3
|
Object.defineProperty(exports, "__esModule", {
|
|
4
4
|
value: true
|
|
5
5
|
});
|
|
6
|
-
exports.default = exports.Default = void 0;
|
|
6
|
+
exports.default = exports.Focus = exports.Default = void 0;
|
|
7
7
|
var _react = _interopRequireWildcard(require("react"));
|
|
8
|
+
var _Button = _interopRequireDefault(require("../Button"));
|
|
8
9
|
var _Modal = _interopRequireDefault(require("./Modal"));
|
|
9
10
|
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
|
|
10
11
|
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); }
|
|
@@ -61,4 +62,35 @@ const Default = exports.Default = {
|
|
|
61
62
|
}, /*#__PURE__*/_react.default.createElement("p", null, "This will permanently delete the user \"Simon\".", /*#__PURE__*/_react.default.createElement("br", null), "You cannot undo this action.")) : null);
|
|
62
63
|
},
|
|
63
64
|
name: "Default"
|
|
65
|
+
};
|
|
66
|
+
const Focus = exports.Focus = {
|
|
67
|
+
render: _ref2 => {
|
|
68
|
+
let {
|
|
69
|
+
closeOnOutsideClick
|
|
70
|
+
} = _ref2;
|
|
71
|
+
/* eslint-disable react-hooks/rules-of-hooks */
|
|
72
|
+
const [modalOpen, setModalOpen] = (0, _react.useState)(true);
|
|
73
|
+
const buttonRef = (0, _react.useRef)(null);
|
|
74
|
+
/* eslint-enable react-hooks/rules-of-hooks */
|
|
75
|
+
|
|
76
|
+
const closeHandler = () => setModalOpen(false);
|
|
77
|
+
return /*#__PURE__*/_react.default.createElement(_react.default.Fragment, null, /*#__PURE__*/_react.default.createElement("button", {
|
|
78
|
+
onClick: () => setModalOpen(true)
|
|
79
|
+
}, "Open modal"), modalOpen ? /*#__PURE__*/_react.default.createElement(_Modal.default, {
|
|
80
|
+
close: closeHandler,
|
|
81
|
+
title: "Confirm delete",
|
|
82
|
+
closeOnOutsideClick: closeOnOutsideClick,
|
|
83
|
+
buttonRow: /*#__PURE__*/_react.default.createElement(_react.default.Fragment, null, /*#__PURE__*/_react.default.createElement("button", {
|
|
84
|
+
className: "u-no-margin--bottom",
|
|
85
|
+
onClick: closeHandler
|
|
86
|
+
}, "Cancel"), /*#__PURE__*/_react.default.createElement("button", {
|
|
87
|
+
className: "p-button--negative u-no-margin--bottom"
|
|
88
|
+
}, "Delete")),
|
|
89
|
+
focusRef: buttonRef
|
|
90
|
+
}, /*#__PURE__*/_react.default.createElement("p", null, "This will permanently delete the user \"Simon\".", /*#__PURE__*/_react.default.createElement("br", null), "You cannot undo this action."), /*#__PURE__*/_react.default.createElement("p", null, /*#__PURE__*/_react.default.createElement(_Button.default, {
|
|
91
|
+
appearance: "link",
|
|
92
|
+
ref: buttonRef
|
|
93
|
+
}, "More information"))) : null);
|
|
94
|
+
},
|
|
95
|
+
name: "Focus"
|
|
64
96
|
};
|
|
@@ -19,10 +19,31 @@ export type Props = PropsWithSpread<FieldProps, {
|
|
|
19
19
|
id?: string | null;
|
|
20
20
|
name?: string;
|
|
21
21
|
disabled?: boolean;
|
|
22
|
+
/**
|
|
23
|
+
* Toggle label when no option is selected
|
|
24
|
+
*
|
|
25
|
+
* @default - "Select an option"
|
|
26
|
+
*/
|
|
27
|
+
defaultToggleLabel?: string;
|
|
22
28
|
wrapperClassName?: ClassName;
|
|
23
29
|
toggleClassName?: ClassName;
|
|
24
30
|
dropdownClassName?: string;
|
|
25
|
-
|
|
31
|
+
/**
|
|
32
|
+
* Whether the select is searchable.
|
|
33
|
+
* - `auto`: the select will be searchable if it has 5 or more options.
|
|
34
|
+
* - `always`: the select will always be searchable if there is at least 1 option.
|
|
35
|
+
* - `async`: the select will always be searchable.
|
|
36
|
+
* - `never`: the select will never be searchable.
|
|
37
|
+
*
|
|
38
|
+
* @default - "auto"
|
|
39
|
+
*/
|
|
40
|
+
searchable?: "auto" | "always" | "async" | "never";
|
|
41
|
+
/**
|
|
42
|
+
* Placeholder text for the search input when searchable is enabled.
|
|
43
|
+
*
|
|
44
|
+
* @default - "Search"
|
|
45
|
+
*/
|
|
46
|
+
searchPlaceholder?: string;
|
|
26
47
|
takeFocus?: boolean;
|
|
27
48
|
header?: ReactNode;
|
|
28
49
|
selectRef?: SelectRef;
|
|
@@ -33,5 +54,5 @@ export type Props = PropsWithSpread<FieldProps, {
|
|
|
33
54
|
*
|
|
34
55
|
* The aim of this component is to provide a select component with customisable options and a dropdown menu, whilst maintaining accessibility and usability.
|
|
35
56
|
*/
|
|
36
|
-
declare const CustomSelect: ({ value, options, onChange, onSearch, id, name, disabled, success, error, help, wrapperClassName, toggleClassName, dropdownClassName, searchable, takeFocus, header, selectRef, initialPosition, ...fieldProps }: Props) => React.JSX.Element;
|
|
57
|
+
declare const CustomSelect: ({ value, options, onChange, onSearch, id, name, disabled, success, error, help, wrapperClassName, toggleClassName, dropdownClassName, defaultToggleLabel, searchable, searchPlaceholder, takeFocus, header, selectRef, initialPosition, ...fieldProps }: Props) => React.JSX.Element;
|
|
37
58
|
export default CustomSelect;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
var _excluded = ["value", "options", "onChange", "onSearch", "id", "name", "disabled", "success", "error", "help", "wrapperClassName", "toggleClassName", "dropdownClassName", "searchable", "takeFocus", "header", "selectRef", "initialPosition"];
|
|
1
|
+
var _excluded = ["value", "options", "onChange", "onSearch", "id", "name", "disabled", "success", "error", "help", "wrapperClassName", "toggleClassName", "dropdownClassName", "defaultToggleLabel", "searchable", "searchPlaceholder", "takeFocus", "header", "selectRef", "initialPosition"];
|
|
2
2
|
function _extends() { _extends = Object.assign ? Object.assign.bind() : function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; return _extends.apply(this, arguments); }
|
|
3
3
|
function _objectWithoutProperties(source, excluded) { if (source == null) return {}; var target = _objectWithoutPropertiesLoose(source, excluded); var key, i; if (Object.getOwnPropertySymbols) { var sourceSymbolKeys = Object.getOwnPropertySymbols(source); for (i = 0; i < sourceSymbolKeys.length; i++) { key = sourceSymbolKeys[i]; if (excluded.indexOf(key) >= 0) continue; if (!Object.prototype.propertyIsEnumerable.call(source, key)) continue; target[key] = source[key]; } } return target; }
|
|
4
4
|
function _objectWithoutPropertiesLoose(source, excluded) { if (source == null) return {}; var target = {}; var sourceKeys = Object.keys(source); var key, i; for (i = 0; i < sourceKeys.length; i++) { key = sourceKeys[i]; if (excluded.indexOf(key) >= 0) continue; target[key] = source[key]; } return target; }
|
|
@@ -29,7 +29,9 @@ var CustomSelect = _ref => {
|
|
|
29
29
|
wrapperClassName,
|
|
30
30
|
toggleClassName,
|
|
31
31
|
dropdownClassName,
|
|
32
|
+
defaultToggleLabel = "Select an option",
|
|
32
33
|
searchable = "auto",
|
|
34
|
+
searchPlaceholder = "Search",
|
|
33
35
|
takeFocus,
|
|
34
36
|
header,
|
|
35
37
|
selectRef,
|
|
@@ -75,7 +77,7 @@ var CustomSelect = _ref => {
|
|
|
75
77
|
var selectedOption = options.find(option => option.value === value);
|
|
76
78
|
var toggleLabel = /*#__PURE__*/React.createElement("span", {
|
|
77
79
|
className: "toggle-label u-truncate"
|
|
78
|
-
}, selectedOption ? selectedOption.selectedLabel || getOptionText(selectedOption) :
|
|
80
|
+
}, selectedOption ? selectedOption.selectedLabel || getOptionText(selectedOption) : defaultToggleLabel);
|
|
79
81
|
var handleSelect = value => {
|
|
80
82
|
var _document$getElementB3;
|
|
81
83
|
(_document$getElementB3 = document.getElementById(selectId)) === null || _document$getElementB3 === void 0 || _document$getElementB3.focus();
|
|
@@ -122,6 +124,7 @@ var CustomSelect = _ref => {
|
|
|
122
124
|
position: initialPosition
|
|
123
125
|
}, close => /*#__PURE__*/React.createElement(CustomSelectDropdown, {
|
|
124
126
|
searchable: searchable,
|
|
127
|
+
searchPlaceholder: searchPlaceholder,
|
|
125
128
|
onSearch: onSearch,
|
|
126
129
|
name: name || "",
|
|
127
130
|
options: options || [],
|
|
@@ -32,3 +32,8 @@ export declare const AutoSearchable: Story;
|
|
|
32
32
|
* Search can be enabled manually by setting `searchable` to `always`.
|
|
33
33
|
*/
|
|
34
34
|
export declare const ManualSearchable: Story;
|
|
35
|
+
/**
|
|
36
|
+
* Search can be enabled manually by setting `searchable` to `async`.
|
|
37
|
+
* This will always show the search input regardless of the number of options.
|
|
38
|
+
*/
|
|
39
|
+
export declare const AsyncSearchable: Story;
|
|
@@ -145,12 +145,14 @@ var meta = {
|
|
|
145
145
|
args: {
|
|
146
146
|
name: "customSelect",
|
|
147
147
|
label: "Custom Select",
|
|
148
|
+
defaultToggleLabel: "Select an option",
|
|
148
149
|
searchable: "auto",
|
|
150
|
+
searchPlaceholder: "Search",
|
|
149
151
|
initialPosition: "left"
|
|
150
152
|
},
|
|
151
153
|
argTypes: {
|
|
152
154
|
searchable: {
|
|
153
|
-
options: ["auto", "always", "never"],
|
|
155
|
+
options: ["auto", "always", "async", "never"],
|
|
154
156
|
control: {
|
|
155
157
|
type: "select"
|
|
156
158
|
}
|
|
@@ -225,4 +227,15 @@ export var ManualSearchable = {
|
|
|
225
227
|
options: generateStandardOptions(4),
|
|
226
228
|
searchable: "always"
|
|
227
229
|
}
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Search can be enabled manually by setting `searchable` to `async`.
|
|
234
|
+
* This will always show the search input regardless of the number of options.
|
|
235
|
+
*/
|
|
236
|
+
export var AsyncSearchable = {
|
|
237
|
+
args: {
|
|
238
|
+
options: generateStandardOptions(0),
|
|
239
|
+
searchable: "async"
|
|
240
|
+
}
|
|
228
241
|
};
|
|
@@ -7,7 +7,8 @@ export type CustomSelectOption = LiHTMLAttributes<HTMLLIElement> & {
|
|
|
7
7
|
selectedLabel?: ReactNode;
|
|
8
8
|
};
|
|
9
9
|
export type Props = {
|
|
10
|
-
searchable?: "auto" | "always" | "never";
|
|
10
|
+
searchable?: "auto" | "always" | "async" | "never";
|
|
11
|
+
searchPlaceholder?: string;
|
|
11
12
|
name: string;
|
|
12
13
|
options: CustomSelectOption[];
|
|
13
14
|
onSelect: (value: string) => void;
|
|
@@ -83,9 +83,28 @@ export var getOptionText = option => {
|
|
|
83
83
|
}
|
|
84
84
|
throw new Error("CustomSelect: options must have a string label or a text property");
|
|
85
85
|
};
|
|
86
|
+
var getIsSearchable = (searchable, numberOfOptions) => {
|
|
87
|
+
if (searchable === "async") {
|
|
88
|
+
return true;
|
|
89
|
+
}
|
|
90
|
+
if (searchable === "never") {
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
if (numberOfOptions <= 1) {
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
if (searchable === "always") {
|
|
97
|
+
return true;
|
|
98
|
+
}
|
|
99
|
+
if (searchable === "auto" && numberOfOptions >= 5) {
|
|
100
|
+
return true;
|
|
101
|
+
}
|
|
102
|
+
return false;
|
|
103
|
+
};
|
|
86
104
|
var CustomSelectDropdown = _ref => {
|
|
87
105
|
var {
|
|
88
106
|
searchable,
|
|
107
|
+
searchPlaceholder,
|
|
89
108
|
name,
|
|
90
109
|
options,
|
|
91
110
|
onSelect,
|
|
@@ -102,7 +121,7 @@ var CustomSelectDropdown = _ref => {
|
|
|
102
121
|
var dropdownRef = useRef(null);
|
|
103
122
|
var searchRef = useRef(null);
|
|
104
123
|
var dropdownListRef = useRef(null);
|
|
105
|
-
var isSearchable =
|
|
124
|
+
var isSearchable = getIsSearchable(searchable, options.length);
|
|
106
125
|
useEffect(() => {
|
|
107
126
|
if (dropdownRef.current) {
|
|
108
127
|
var _toggle$getBoundingCl, _toggle$getBoundingCl2;
|
|
@@ -241,6 +260,7 @@ var CustomSelectDropdown = _ref => {
|
|
|
241
260
|
name: "select-search-".concat(name),
|
|
242
261
|
type: "text",
|
|
243
262
|
"aria-label": "Search for ".concat(name),
|
|
263
|
+
placeholder: searchPlaceholder,
|
|
244
264
|
className: "u-no-margin--bottom",
|
|
245
265
|
onChange: handleSearch,
|
|
246
266
|
value: search,
|
|
@@ -252,7 +272,8 @@ var CustomSelectDropdown = _ref => {
|
|
|
252
272
|
}, optionItems));
|
|
253
273
|
};
|
|
254
274
|
CustomSelectDropdown.propTypes = {
|
|
255
|
-
searchable: _pt.oneOf(["auto", "always", "never"]),
|
|
275
|
+
searchable: _pt.oneOf(["auto", "always", "async", "never"]),
|
|
276
|
+
searchPlaceholder: _pt.string,
|
|
256
277
|
name: _pt.string.isRequired,
|
|
257
278
|
options: _pt.array.isRequired,
|
|
258
279
|
onSelect: _pt.func.isRequired,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import React from "react";
|
|
2
|
-
import type { HTMLProps, ReactNode } from "react";
|
|
2
|
+
import type { HTMLProps, ReactNode, RefObject } from "react";
|
|
3
3
|
import { ClassName, PropsWithSpread } from "../../types";
|
|
4
4
|
export type Props = PropsWithSpread<{
|
|
5
5
|
/**
|
|
@@ -18,6 +18,10 @@ export type Props = PropsWithSpread<{
|
|
|
18
18
|
* Function to handle closing the modal.
|
|
19
19
|
*/
|
|
20
20
|
close?: () => void | null;
|
|
21
|
+
/**
|
|
22
|
+
* The element that will be focused upon opening the modal.
|
|
23
|
+
*/
|
|
24
|
+
focusRef?: RefObject<HTMLElement | null>;
|
|
21
25
|
/**
|
|
22
26
|
* The title of the modal.
|
|
23
27
|
*/
|
|
@@ -36,5 +40,5 @@ export type Props = PropsWithSpread<{
|
|
|
36
40
|
*
|
|
37
41
|
* The modal component can be used to overlay an area of the screen which can contain a prompt, dialog or interaction.
|
|
38
42
|
*/
|
|
39
|
-
export declare const Modal: ({ buttonRow, children, className, close, title, shouldPropagateClickEvent, closeOnOutsideClick, ...wrapperProps }: Props) => React.JSX.Element;
|
|
43
|
+
export declare const Modal: ({ buttonRow, children, className, close, focusRef, title, shouldPropagateClickEvent, closeOnOutsideClick, ...wrapperProps }: Props) => React.JSX.Element;
|
|
40
44
|
export default Modal;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
var _excluded = ["buttonRow", "children", "className", "close", "title", "shouldPropagateClickEvent", "closeOnOutsideClick"];
|
|
1
|
+
var _excluded = ["buttonRow", "children", "className", "close", "focusRef", "title", "shouldPropagateClickEvent", "closeOnOutsideClick"];
|
|
2
2
|
function _extends() { _extends = Object.assign ? Object.assign.bind() : function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; return _extends.apply(this, arguments); }
|
|
3
3
|
function _objectWithoutProperties(source, excluded) { if (source == null) return {}; var target = _objectWithoutPropertiesLoose(source, excluded); var key, i; if (Object.getOwnPropertySymbols) { var sourceSymbolKeys = Object.getOwnPropertySymbols(source); for (i = 0; i < sourceSymbolKeys.length; i++) { key = sourceSymbolKeys[i]; if (excluded.indexOf(key) >= 0) continue; if (!Object.prototype.propertyIsEnumerable.call(source, key)) continue; target[key] = source[key]; } } return target; }
|
|
4
4
|
function _objectWithoutPropertiesLoose(source, excluded) { if (source == null) return {}; var target = {}; var sourceKeys = Object.keys(source); var key, i; for (i = 0; i < sourceKeys.length; i++) { key = sourceKeys[i]; if (excluded.indexOf(key) >= 0) continue; target[key] = source[key]; } return target; }
|
|
@@ -15,6 +15,7 @@ export var Modal = _ref => {
|
|
|
15
15
|
children,
|
|
16
16
|
className,
|
|
17
17
|
close,
|
|
18
|
+
focusRef,
|
|
18
19
|
title,
|
|
19
20
|
shouldPropagateClickEvent = false,
|
|
20
21
|
closeOnOutsideClick = true
|
|
@@ -27,6 +28,7 @@ export var Modal = _ref => {
|
|
|
27
28
|
var titleId = useId();
|
|
28
29
|
var shouldClose = useRef(false);
|
|
29
30
|
var modalRef = useRef(null);
|
|
31
|
+
var closeButtonRef = useRef(null);
|
|
30
32
|
var focusableModalElements = useRef(null);
|
|
31
33
|
var handleTabKey = event => {
|
|
32
34
|
if (focusableModalElements.current.length > 0) {
|
|
@@ -54,24 +56,18 @@ export var Modal = _ref => {
|
|
|
54
56
|
close();
|
|
55
57
|
}
|
|
56
58
|
};
|
|
57
|
-
var keyListenersMap = new Map([["Escape", handleEscKey], ["Tab", handleTabKey]]);
|
|
58
59
|
useEffect(() => {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
var focusIndex = 0;
|
|
66
|
-
// when the close button is rendered, focus on the 2nd content element and not the close btn.
|
|
67
|
-
if (hasCloseButton && focusableModalElements.current.length > 1) {
|
|
68
|
-
focusIndex = 1;
|
|
60
|
+
if (focusRef !== null && focusRef !== void 0 && focusRef.current) {
|
|
61
|
+
focusRef.current.focus();
|
|
62
|
+
} else if (closeButtonRef.current) {
|
|
63
|
+
closeButtonRef.current.focus();
|
|
64
|
+
} else {
|
|
65
|
+
modalRef.current.focus();
|
|
69
66
|
}
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
});
|
|
73
|
-
}, [hasCloseButton]);
|
|
67
|
+
focusableModalElements.current = modalRef.current.querySelectorAll(focusableElementSelectors);
|
|
68
|
+
}, [focusRef]);
|
|
74
69
|
useEffect(() => {
|
|
70
|
+
var keyListenersMap = new Map([["Escape", handleEscKey], ["Tab", handleTabKey]]);
|
|
75
71
|
var keyDown = event => {
|
|
76
72
|
var listener = keyListenersMap.get(event.code);
|
|
77
73
|
return listener && listener(event);
|
|
@@ -124,11 +120,12 @@ export var Modal = _ref => {
|
|
|
124
120
|
}, /*#__PURE__*/React.createElement("h2", {
|
|
125
121
|
className: "p-modal__title",
|
|
126
122
|
id: titleId
|
|
127
|
-
}, title),
|
|
123
|
+
}, title), close && /*#__PURE__*/React.createElement("button", {
|
|
128
124
|
type: "button",
|
|
129
125
|
className: "p-modal__close",
|
|
130
126
|
"aria-label": "Close active modal",
|
|
131
|
-
onClick: handleClose
|
|
127
|
+
onClick: handleClose,
|
|
128
|
+
ref: closeButtonRef
|
|
132
129
|
}, "Close")), /*#__PURE__*/React.createElement("div", {
|
|
133
130
|
id: descriptionId
|
|
134
131
|
}, children), !!buttonRow && /*#__PURE__*/React.createElement("footer", {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { useState } from "react";
|
|
1
|
+
import { useRef, useState } from "react";
|
|
2
2
|
import React from "react";
|
|
3
|
+
import Button from "../Button";
|
|
3
4
|
import Modal from "./Modal";
|
|
4
5
|
var Template = args => {
|
|
5
6
|
return /*#__PURE__*/React.createElement("div", {
|
|
@@ -53,4 +54,35 @@ export var Default = {
|
|
|
53
54
|
}, /*#__PURE__*/React.createElement("p", null, "This will permanently delete the user \"Simon\".", /*#__PURE__*/React.createElement("br", null), "You cannot undo this action.")) : null);
|
|
54
55
|
},
|
|
55
56
|
name: "Default"
|
|
57
|
+
};
|
|
58
|
+
export var Focus = {
|
|
59
|
+
render: _ref2 => {
|
|
60
|
+
var {
|
|
61
|
+
closeOnOutsideClick
|
|
62
|
+
} = _ref2;
|
|
63
|
+
/* eslint-disable react-hooks/rules-of-hooks */
|
|
64
|
+
var [modalOpen, setModalOpen] = useState(true);
|
|
65
|
+
var buttonRef = useRef(null);
|
|
66
|
+
/* eslint-enable react-hooks/rules-of-hooks */
|
|
67
|
+
|
|
68
|
+
var closeHandler = () => setModalOpen(false);
|
|
69
|
+
return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement("button", {
|
|
70
|
+
onClick: () => setModalOpen(true)
|
|
71
|
+
}, "Open modal"), modalOpen ? /*#__PURE__*/React.createElement(Modal, {
|
|
72
|
+
close: closeHandler,
|
|
73
|
+
title: "Confirm delete",
|
|
74
|
+
closeOnOutsideClick: closeOnOutsideClick,
|
|
75
|
+
buttonRow: /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement("button", {
|
|
76
|
+
className: "u-no-margin--bottom",
|
|
77
|
+
onClick: closeHandler
|
|
78
|
+
}, "Cancel"), /*#__PURE__*/React.createElement("button", {
|
|
79
|
+
className: "p-button--negative u-no-margin--bottom"
|
|
80
|
+
}, "Delete")),
|
|
81
|
+
focusRef: buttonRef
|
|
82
|
+
}, /*#__PURE__*/React.createElement("p", null, "This will permanently delete the user \"Simon\".", /*#__PURE__*/React.createElement("br", null), "You cannot undo this action."), /*#__PURE__*/React.createElement("p", null, /*#__PURE__*/React.createElement(Button, {
|
|
83
|
+
appearance: "link",
|
|
84
|
+
ref: buttonRef
|
|
85
|
+
}, "More information"))) : null);
|
|
86
|
+
},
|
|
87
|
+
name: "Focus"
|
|
56
88
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@canonical/react-components",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.7.0",
|
|
4
4
|
"main": "dist/index.js",
|
|
5
5
|
"module": "dist/index.js",
|
|
6
6
|
"author": {
|
|
@@ -93,7 +93,7 @@
|
|
|
93
93
|
"tsc-alias": "1.8.10",
|
|
94
94
|
"typescript": "5.7.3",
|
|
95
95
|
"typescript-eslint": "8.24.1",
|
|
96
|
-
"vanilla-framework": "4.
|
|
96
|
+
"vanilla-framework": "4.37.1",
|
|
97
97
|
"wait-on": "8.0.2",
|
|
98
98
|
"webpack": "5.98.0"
|
|
99
99
|
},
|