@canonical/react-components 1.7.3 → 1.9.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 (35) 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.d.ts +1 -0
  15. package/dist/components/MultiSelect/MultiSelect.js +6 -3
  16. package/dist/esm/components/CustomSelect/CustomSelect.d.ts +36 -0
  17. package/dist/esm/components/CustomSelect/CustomSelect.js +139 -0
  18. package/dist/esm/components/CustomSelect/CustomSelect.scss +82 -0
  19. package/dist/esm/components/CustomSelect/CustomSelect.stories.d.ts +28 -0
  20. package/dist/esm/components/CustomSelect/CustomSelect.stories.js +126 -0
  21. package/dist/esm/components/CustomSelect/CustomSelect.test.d.ts +1 -0
  22. package/dist/esm/components/CustomSelect/CustomSelectDropdown/CustomSelectDropdown.d.ts +25 -0
  23. package/dist/esm/components/CustomSelect/CustomSelectDropdown/CustomSelectDropdown.js +285 -0
  24. package/dist/esm/components/CustomSelect/CustomSelectDropdown/CustomSelectDropdown.test.d.ts +1 -0
  25. package/dist/esm/components/CustomSelect/CustomSelectDropdown/index.d.ts +2 -0
  26. package/dist/esm/components/CustomSelect/CustomSelectDropdown/index.js +1 -0
  27. package/dist/esm/components/CustomSelect/index.d.ts +3 -0
  28. package/dist/esm/components/CustomSelect/index.js +1 -0
  29. package/dist/esm/components/MultiSelect/MultiSelect.d.ts +1 -0
  30. package/dist/esm/components/MultiSelect/MultiSelect.js +6 -3
  31. package/dist/esm/index.d.ts +2 -0
  32. package/dist/esm/index.js +1 -0
  33. package/dist/index.d.ts +2 -0
  34. package/dist/index.js +8 -0
  35. package/package.json +1 -1
@@ -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,145 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.default = void 0;
7
+ var _classnames = _interopRequireDefault(require("classnames"));
8
+ var _react = _interopRequireWildcard(require("react"));
9
+ var _Field = _interopRequireDefault(require("../Field"));
10
+ var _ContextualMenu = _interopRequireDefault(require("../ContextualMenu"));
11
+ var _hooks = require("../../hooks");
12
+ var _CustomSelectDropdown = _interopRequireWildcard(require("./CustomSelectDropdown"));
13
+ require("./CustomSelect.scss");
14
+ 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); }
15
+ 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; }
16
+ function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
17
+ 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); }
18
+ /**
19
+ * This is a [React](https://reactjs.org/) component that extends from the Vanilla [Select](https://vanillaframework.io/docs/base/forms#select) element.
20
+ *
21
+ * The aim of this component is to provide a select component with customisable options and a dropdown menu, whilst maintaining accessibility and usability.
22
+ */
23
+ const CustomSelect = _ref => {
24
+ let {
25
+ value,
26
+ options,
27
+ onChange,
28
+ onSearch,
29
+ id,
30
+ name,
31
+ disabled,
32
+ success,
33
+ error,
34
+ help,
35
+ wrapperClassName,
36
+ toggleClassName,
37
+ dropdownClassName,
38
+ searchable = "auto",
39
+ takeFocus,
40
+ header,
41
+ selectRef,
42
+ initialPosition = "left",
43
+ ...fieldProps
44
+ } = _ref;
45
+ const [isOpen, setIsOpen] = (0, _react.useState)(false);
46
+ const validationId = (0, _react.useId)();
47
+ const defaultSelectId = (0, _react.useId)();
48
+ const selectId = id || defaultSelectId;
49
+ const helpId = (0, _react.useId)();
50
+ const hasError = !!error;
51
+
52
+ // Close the dropdown when the browser tab is hidden
53
+ const onBrowserTabHidden = () => {
54
+ if (document.visibilityState === "hidden") {
55
+ setIsOpen(false);
56
+ }
57
+ };
58
+ (0, _hooks.useListener)(window, onBrowserTabHidden, "visibilitychange");
59
+
60
+ // Close the dropdown when the browser window loses focus
61
+ (0, _hooks.useListener)(window, () => setIsOpen(false), "blur");
62
+ (0, _react.useImperativeHandle)(selectRef, () => ({
63
+ open: () => {
64
+ var _document$getElementB;
65
+ setIsOpen(true);
66
+ (_document$getElementB = document.getElementById(selectId)) === null || _document$getElementB === void 0 || _document$getElementB.focus();
67
+ },
68
+ focus: () => {
69
+ var _document$getElementB2;
70
+ return (_document$getElementB2 = document.getElementById(selectId)) === null || _document$getElementB2 === void 0 ? void 0 : _document$getElementB2.focus();
71
+ },
72
+ close: setIsOpen.bind(null, false),
73
+ isOpen: isOpen
74
+ }), [isOpen, selectId]);
75
+ (0, _react.useEffect)(() => {
76
+ if (takeFocus) {
77
+ const toggleButton = document.getElementById(selectId);
78
+ toggleButton === null || toggleButton === void 0 || toggleButton.focus();
79
+ }
80
+ }, [takeFocus, selectId]);
81
+ const selectedOption = options.find(option => option.value === value);
82
+ const toggleLabel = /*#__PURE__*/_react.default.createElement("span", {
83
+ className: "toggle-label u-truncate"
84
+ }, selectedOption ? (0, _CustomSelectDropdown.getOptionText)(selectedOption) : "Select an option");
85
+ const handleSelect = value => {
86
+ var _document$getElementB3;
87
+ (_document$getElementB3 = document.getElementById(selectId)) === null || _document$getElementB3 === void 0 || _document$getElementB3.focus();
88
+ setIsOpen(false);
89
+ onChange(value);
90
+ };
91
+ return /*#__PURE__*/_react.default.createElement(_Field.default, _extends({}, fieldProps, {
92
+ className: (0, _classnames.default)("p-custom-select", wrapperClassName),
93
+ error: error,
94
+ forId: selectId,
95
+ help: help,
96
+ helpId: helpId,
97
+ isSelect: true,
98
+ success: success,
99
+ validationId: validationId
100
+ }), /*#__PURE__*/_react.default.createElement(_ContextualMenu.default, {
101
+ "aria-describedby": [help ? helpId : null, success ? validationId : null].filter(Boolean).join(" "),
102
+ "aria-errormessage": hasError ? validationId : undefined,
103
+ "aria-invalid": hasError,
104
+ toggleClassName: (0, _classnames.default)("p-custom-select__toggle", "p-form-validation__input", toggleClassName, {
105
+ active: isOpen
106
+ }),
107
+ toggleLabel: toggleLabel,
108
+ visible: isOpen,
109
+ onToggleMenu: open => {
110
+ // Handle syncing the state when toggling the menu from within the
111
+ // contextual menu component e.g. when clicking outside.
112
+ if (open !== isOpen) {
113
+ setIsOpen(open);
114
+ }
115
+ },
116
+ toggleProps: {
117
+ id: selectId,
118
+ disabled: disabled,
119
+ // tabIndex is set to -1 when disabled to prevent keyboard navigation to the select toggle
120
+ tabIndex: disabled ? -1 : 0
121
+ },
122
+ className: "p-custom-select__wrapper",
123
+ dropdownClassName: dropdownClassName,
124
+ style: {
125
+ width: "100%"
126
+ },
127
+ autoAdjust: true,
128
+ position: initialPosition
129
+ }, close => /*#__PURE__*/_react.default.createElement(_CustomSelectDropdown.default, {
130
+ searchable: searchable,
131
+ onSearch: onSearch,
132
+ name: name || "",
133
+ options: options || [],
134
+ onSelect: handleSelect,
135
+ onClose: () => {
136
+ var _document$getElementB4;
137
+ // When pressing ESC to close the dropdown, we keep focus on the toggle button
138
+ close();
139
+ (_document$getElementB4 = document.getElementById(selectId)) === null || _document$getElementB4 === void 0 || _document$getElementB4.focus();
140
+ },
141
+ header: header,
142
+ toggleId: selectId
143
+ })));
144
+ };
145
+ var _default = exports.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;
@@ -0,0 +1,132 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.default = exports.StandardOptions = exports.ManualSearchable = exports.DisabledOptions = exports.CustomOptions = exports.AutoSearchable = void 0;
7
+ var _CustomSelect = _interopRequireDefault(require("./CustomSelect"));
8
+ var _react = _interopRequireWildcard(require("react"));
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); }
10
+ 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; }
11
+ function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
12
+ 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); }
13
+ const generateStandardOptions = num => Array(num).fill(null).map((_, i) => ({
14
+ value: "option-".concat(i + 1),
15
+ label: "Option ".concat(i + 1),
16
+ text: "Option ".concat(i + 1),
17
+ disabled: false
18
+ }));
19
+ const generateCustomOptions = () => {
20
+ return [{
21
+ value: "smile",
22
+ label: /*#__PURE__*/_react.default.createElement("div", null, "\uD83D\uDE00"),
23
+ text: "Smile",
24
+ disabled: false
25
+ }, {
26
+ value: "grin",
27
+ label: /*#__PURE__*/_react.default.createElement("div", null, "\uD83D\uDE01"),
28
+ text: "Grin",
29
+ disabled: false
30
+ }, {
31
+ value: "cry",
32
+ label: /*#__PURE__*/_react.default.createElement("div", null, "\uD83D\uDE2D"),
33
+ text: "Cry",
34
+ disabled: false
35
+ }, {
36
+ value: "angry",
37
+ label: /*#__PURE__*/_react.default.createElement("div", null, "\uD83D\uDE21"),
38
+ text: "Angry",
39
+ disabled: false
40
+ }, {
41
+ value: "sad",
42
+ label: /*#__PURE__*/_react.default.createElement("div", null, "\uD83D\uDE22"),
43
+ text: "Sad",
44
+ disabled: false
45
+ }];
46
+ };
47
+ const Template = _ref => {
48
+ let {
49
+ ...props
50
+ } = _ref;
51
+ const [selected, setSelected] = (0, _react.useState)(props.value || "");
52
+ return /*#__PURE__*/_react.default.createElement(_CustomSelect.default, _extends({}, props, {
53
+ value: selected,
54
+ onChange: value => setSelected(value)
55
+ }));
56
+ };
57
+ const meta = {
58
+ component: _CustomSelect.default,
59
+ render: Template,
60
+ tags: ["autodocs"],
61
+ args: {
62
+ name: "customSelect",
63
+ label: "Custom Select",
64
+ searchable: "auto",
65
+ initialPosition: "left"
66
+ },
67
+ argTypes: {
68
+ searchable: {
69
+ options: ["auto", "always", "never"],
70
+ control: {
71
+ type: "select"
72
+ }
73
+ },
74
+ initialPosition: {
75
+ options: ["left", "right"],
76
+ control: {
77
+ type: "select"
78
+ }
79
+ }
80
+ }
81
+ };
82
+ var _default = exports.default = meta;
83
+ /**
84
+ * If `label` is of `string` type. You do not have to do anything extra to render it.
85
+ */
86
+ const StandardOptions = exports.StandardOptions = {
87
+ args: {
88
+ options: generateStandardOptions(10)
89
+ }
90
+ };
91
+
92
+ /**
93
+ * If `label` is of `ReactNode` type. You can render custom content.
94
+ * In this case, the `text` property for each option is required and is used for display in the toggle, search and sort functionalities.
95
+ */
96
+ const CustomOptions = exports.CustomOptions = {
97
+ args: {
98
+ options: generateCustomOptions()
99
+ }
100
+ };
101
+
102
+ /**
103
+ * For each option, if `disabled` is set to `true`, the option will be disabled.
104
+ */
105
+ const DisabledOptions = exports.DisabledOptions = {
106
+ args: {
107
+ options: generateStandardOptions(5).map((option, i) => ({
108
+ ...option,
109
+ disabled: i % 2 === 0
110
+ }))
111
+ }
112
+ };
113
+
114
+ /**
115
+ * Search is enabled by default when there are 5 or more options.
116
+ */
117
+ const AutoSearchable = exports.AutoSearchable = {
118
+ args: {
119
+ options: generateStandardOptions(5),
120
+ searchable: "auto"
121
+ }
122
+ };
123
+
124
+ /**
125
+ * Search can be enabled manually by setting `searchable` to `always`.
126
+ */
127
+ const ManualSearchable = exports.ManualSearchable = {
128
+ args: {
129
+ options: generateStandardOptions(4),
130
+ searchable: "always"
131
+ }
132
+ };
@@ -0,0 +1,25 @@
1
+ import { FC, LiHTMLAttributes, ReactNode } from "react";
2
+ export type CustomSelectOption = LiHTMLAttributes<HTMLLIElement> & {
3
+ value: string;
4
+ label: ReactNode;
5
+ text?: string;
6
+ disabled?: boolean;
7
+ };
8
+ export type Props = {
9
+ searchable?: "auto" | "always" | "never";
10
+ name: string;
11
+ options: CustomSelectOption[];
12
+ onSelect: (value: string) => void;
13
+ onSearch?: (value: string) => void;
14
+ onClose: () => void;
15
+ header?: ReactNode;
16
+ toggleId: string;
17
+ };
18
+ export declare const adjustDropdownHeightBelow: (dropdown: HTMLUListElement) => void;
19
+ export declare const adjustDropdownHeightAbove: (dropdown: HTMLUListElement, search: HTMLInputElement | null) => void;
20
+ export declare const dropdownIsAbove: (dropdown: HTMLUListElement) => boolean;
21
+ export declare const adjustDropdownHeight: (dropdown: HTMLUListElement | null, search: HTMLInputElement | null) => void;
22
+ export declare const getNearestParentsZIndex: (element: HTMLElement | null) => string;
23
+ export declare const getOptionText: (option: CustomSelectOption) => string;
24
+ declare const CustomSelectDropdown: FC<Props>;
25
+ export default CustomSelectDropdown;