@spark-web/select 1.0.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/CHANGELOG.md ADDED
@@ -0,0 +1,22 @@
1
+ # @spark-web/select
2
+
3
+ ## 1.0.0
4
+
5
+ ### Major Changes
6
+
7
+ - [#27](https://github.com/brighte-labs/spark-web/pull/27)
8
+ [`4c8e398`](https://github.com/brighte-labs/spark-web/commit/4c8e3988f8a59d3dab60a6b67b1128b6ff2a5f2c)
9
+ Thanks [@JedWatson](https://github.com/JedWatson)! - Initial Version
10
+
11
+ ### Patch Changes
12
+
13
+ - Updated dependencies
14
+ [[`4c8e398`](https://github.com/brighte-labs/spark-web/commit/4c8e3988f8a59d3dab60a6b67b1128b6ff2a5f2c)]:
15
+ - @spark-web/a11y@1.0.0
16
+ - @spark-web/box@1.0.0
17
+ - @spark-web/field@1.0.0
18
+ - @spark-web/icon@1.0.0
19
+ - @spark-web/text@1.0.0
20
+ - @spark-web/text-input@1.0.0
21
+ - @spark-web/theme@1.0.0
22
+ - @spark-web/utils@1.0.0
package/README.md ADDED
@@ -0,0 +1,139 @@
1
+ ---
2
+ title: Select
3
+ storybookPath: forms-select--default
4
+ ---
5
+
6
+ Allows the user to make a single selection from a list of values — usually in a
7
+ form. If only a few options are provided, consider using a `RadioButton`
8
+ instead.
9
+
10
+ ## Usage
11
+
12
+ ### Field
13
+
14
+ Each select input must be accompanied by a Field with a label. Effective form
15
+ labeling helps inform users which selection to make.
16
+
17
+ ## Examples
18
+
19
+ ### Controlled
20
+
21
+ A `<Select>` can be both controlled and uncontrolled. To control a `<Select>`
22
+ provide a `value`, as well as an `onChange` function to set the new value when
23
+ the select is updated.
24
+
25
+ ```jsx live
26
+ const [selectedOption, setSelectedOption] = React.useState('');
27
+
28
+ const options = [
29
+ { label: 'NSW', value: 'nsw' },
30
+ { label: 'VIC', value: 'vic' },
31
+ { label: 'QLD', value: 'qld' },
32
+ { label: 'SA', value: 'sa' },
33
+ { label: 'WA', value: 'wa' },
34
+ { label: 'TAS', value: 'tas' },
35
+ { label: 'NT', value: 'nt' },
36
+ { label: 'ACT', value: 'act' },
37
+ ];
38
+
39
+ return (
40
+ <Stack gap="large">
41
+ <Field label="State">
42
+ <Select
43
+ placeholder="Choose a state..."
44
+ value={selectedOption}
45
+ onChange={event => setSelectedOption(event.target.value)}
46
+ options={options}
47
+ required
48
+ />
49
+ </Field>
50
+ {selectedOption && (
51
+ <Text>
52
+ You have selected{' '}
53
+ {options.find(option => option.value === selectedOption).label}
54
+ </Text>
55
+ )}
56
+ </Stack>
57
+ );
58
+ ```
59
+
60
+ ### Uncontrolled
61
+
62
+ The `<Select>`, by default, is an uncontrolled component, meaning that the form
63
+ data is controlled directly by the DOM itself. To access the value, instead of
64
+ writing an `onChange` handler, you would use a `ref` to get form values from the
65
+ DOM.
66
+
67
+ ```jsx live
68
+ <Field label="Breaking Bad Characters">
69
+ <Select
70
+ options={[
71
+ { label: 'Walter White', value: 'walter-white' },
72
+ { label: 'Jesse Pinkman', value: 'jesse-pinkman' },
73
+ { label: 'Saul Goodman', value: 'saul-goodman' },
74
+ { label: 'Gus Fring', value: 'gus-fring' },
75
+ { label: 'Hank Schrader', value: 'hank-schrader' },
76
+ { label: 'Mike Ehrmantraut', value: 'mike-ehrmantraut' },
77
+ ]}
78
+ />
79
+ </Field>
80
+ ```
81
+
82
+ ### Groups
83
+
84
+ Related options can be grouped by passing in an array of objects with a label
85
+ and option key — where each option is an array of objects with label, value and
86
+ (optionally) disabled keys. Internally this uses the
87
+ [`<optgroup>` element](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/optgroup).
88
+
89
+ ```jsx live
90
+ const [selectedOption, setSelectedOption] = React.useState('');
91
+
92
+ return (
93
+ <Field label="Select">
94
+ <Select
95
+ placeholder="TV Characters"
96
+ options={[
97
+ {
98
+ label: 'Mad Men',
99
+ options: [
100
+ { label: 'Don Draper', value: 'don-draper' },
101
+ { label: 'Peggy Olson', value: 'peggy-olson' },
102
+ { label: 'Joan Harris', value: 'joan-harris' },
103
+ { label: 'Roger Sterling', value: 'roger-sterling' },
104
+ { label: 'Pete Campbell', value: 'pete-campbell' },
105
+ ],
106
+ },
107
+ {
108
+ label: 'Breaking Bad',
109
+ options: [
110
+ { label: 'Walter White', value: 'walter-white' },
111
+ { label: 'Jesse Pinkman', value: 'jesse-pinkman' },
112
+ { label: 'Saul Goodman', value: 'saul-goodman' },
113
+ { label: 'Gus Fring', value: 'gus-fring' },
114
+ { label: 'Hank Schrader', value: 'hank-schrader' },
115
+ { label: 'Mike Ehrmantraut', value: 'mike-ehrmantraut' },
116
+ ],
117
+ },
118
+ ]}
119
+ />
120
+ </Field>
121
+ );
122
+ ```
123
+
124
+ ## Props
125
+
126
+ | Prop | Type | Default | Description |
127
+ | ------------- | -------------------------------------------- | ------- | -------------------------------------------------------------------------------------- |
128
+ | data? | [DataAttributeMap][data-attribute-map] | | Sets data attributes on the component. |
129
+ | options | Readonly<Array<Option \| Group\>> | | The values that can be selected by the input. |
130
+ | placeholder? | string | | Placeholder text for when the input does not have an initial value. |
131
+ | defaultValue? | string \| number \| readonly string[] | | Default value of the select. |
132
+ | name? | string | | This attribute is used to specify the name of the control. |
133
+ | onBlur? | React.FocusEventHandler<HTMLSelectElement\> | | Function for handling change events. |
134
+ | onChange? | React.ChangeEventHandler<HTMLSelectElement\> | | Function for handling blur events. |
135
+ | required? | boolean | | Boolean that indicating that an option with a non-empty string value must be selected. |
136
+ | value | string \| number \| readonly string[] | | Value of the select. |
137
+
138
+ [data-attribute-map]:
139
+ https://bitbucket.org/brighte-energy/energy/src/14a694872cc43bb454981bada65f5f12b56f77c9/spark-web/packages/utils-spark/src/buildDataAttributes.ts#spark-web/packages/utils-spark/src/buildDataAttributes.ts-1
@@ -0,0 +1,23 @@
1
+ import type { DataAttributeMap } from '@spark-web/utils/internal';
2
+ import * as React from 'react';
3
+ declare type Option = {
4
+ disabled?: boolean;
5
+ label: string;
6
+ value: string | number;
7
+ };
8
+ declare type Group = {
9
+ options: Array<Option>;
10
+ label: string;
11
+ };
12
+ export declare type OptionsOrGroups = Array<Option | Group>;
13
+ export declare type SelectProps = Pick<React.SelectHTMLAttributes<HTMLSelectElement>, 'defaultValue' | 'name' | 'onBlur' | 'onChange' | 'required' | 'value'> & {
14
+ data?: DataAttributeMap;
15
+ options: OptionsOrGroups;
16
+ placeholder?: string;
17
+ };
18
+ export declare const Select: React.ForwardRefExoticComponent<Pick<React.SelectHTMLAttributes<HTMLSelectElement>, "value" | "defaultValue" | "onBlur" | "onChange" | "name" | "required"> & {
19
+ data?: DataAttributeMap | undefined;
20
+ options: OptionsOrGroups;
21
+ placeholder?: string | undefined;
22
+ } & React.RefAttributes<HTMLSelectElement>>;
23
+ export {};
@@ -0,0 +1,2 @@
1
+ export { Select } from './Select';
2
+ export type { SelectProps } from './Select';
@@ -0,0 +1 @@
1
+ export * from "./declarations/src/index";
@@ -0,0 +1,144 @@
1
+ 'use strict';
2
+
3
+ Object.defineProperty(exports, '__esModule', { value: true });
4
+
5
+ var _defineProperty = require('@babel/runtime/helpers/esm/defineProperty');
6
+ var _extends = require('@babel/runtime/helpers/esm/extends');
7
+ var _objectWithoutProperties = require('@babel/runtime/helpers/esm/objectWithoutProperties');
8
+ var css = require('@emotion/css');
9
+ var box = require('@spark-web/box');
10
+ var field = require('@spark-web/field');
11
+ var icon = require('@spark-web/icon');
12
+ var textInput = require('@spark-web/text-input');
13
+ var theme = require('@spark-web/theme');
14
+ var internal = require('@spark-web/utils/internal');
15
+ var React = require('react');
16
+
17
+ function _interopNamespace(e) {
18
+ if (e && e.__esModule) return e;
19
+ var n = Object.create(null);
20
+ if (e) {
21
+ Object.keys(e).forEach(function (k) {
22
+ if (k !== 'default') {
23
+ var d = Object.getOwnPropertyDescriptor(e, k);
24
+ Object.defineProperty(n, k, d.get ? d : {
25
+ enumerable: true,
26
+ get: function () { return e[k]; }
27
+ });
28
+ }
29
+ });
30
+ }
31
+ n["default"] = e;
32
+ return Object.freeze(n);
33
+ }
34
+
35
+ var React__namespace = /*#__PURE__*/_interopNamespace(React);
36
+
37
+ var _excluded = ["disabled", "invalid"];
38
+ var __jsx = React__namespace.createElement;
39
+
40
+ function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); enumerableOnly && (symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; })), keys.push.apply(keys, symbols); } return keys; }
41
+
42
+ function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = null != arguments[i] ? arguments[i] : {}; i % 2 ? ownKeys(Object(source), !0).forEach(function (key) { _defineProperty(target, key, source[key]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)) : ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } return target; }
43
+ var Select = /*#__PURE__*/React__namespace.forwardRef(function (_ref, forwardedRef) {
44
+ var data = _ref.data,
45
+ defaultValue = _ref.defaultValue,
46
+ name = _ref.name,
47
+ onBlur = _ref.onBlur,
48
+ onChange = _ref.onChange,
49
+ optionsOrGroups = _ref.options,
50
+ placeholder = _ref.placeholder,
51
+ required = _ref.required,
52
+ value = _ref.value;
53
+
54
+ var _useFieldContext = field.useFieldContext(),
55
+ disabled = _useFieldContext.disabled,
56
+ invalid = _useFieldContext.invalid,
57
+ a11yProps = _objectWithoutProperties(_useFieldContext, _excluded);
58
+
59
+ var styles = useSelectStyles({
60
+ disabled: disabled,
61
+ invalid: invalid
62
+ });
63
+ var mapOptions = React__namespace.useCallback(function (opt) {
64
+ return __jsx("option", {
65
+ key: opt.value,
66
+ value: opt.value,
67
+ disabled: opt.disabled
68
+ }, opt.label);
69
+ }, []);
70
+ return __jsx(box.Box, {
71
+ position: "relative"
72
+ }, __jsx(box.Box, _extends({}, a11yProps, data ? internal.buildDataAttributes(data) : null, {
73
+ as: "select",
74
+ defaultValue: (defaultValue !== null && defaultValue !== void 0 ? defaultValue : placeholder) ? '' : undefined,
75
+ disabled: disabled,
76
+ name: name,
77
+ onBlur: onBlur,
78
+ onChange: onChange,
79
+ ref: forwardedRef,
80
+ required: required,
81
+ value: value // Styles
82
+ ,
83
+ background: disabled ? 'inputDisabled' : 'input',
84
+ border: invalid ? 'critical' : 'field',
85
+ borderRadius: "small",
86
+ paddingX: "medium",
87
+ height: "medium",
88
+ width: "full",
89
+ className: css.css(styles)
90
+ }), placeholder && __jsx("option", {
91
+ value: "",
92
+ disabled: true
93
+ }, placeholder), optionsOrGroups.map(function (optionOrGroup) {
94
+ if ('options' in optionOrGroup) {
95
+ return __jsx("optgroup", {
96
+ key: optionOrGroup.label,
97
+ label: optionOrGroup.label
98
+ }, optionOrGroup.options.map(function (option) {
99
+ return mapOptions(option);
100
+ }));
101
+ }
102
+
103
+ return mapOptions(optionOrGroup);
104
+ })), __jsx(box.Box, {
105
+ position: "absolute",
106
+ top: 0,
107
+ bottom: 0,
108
+ right: 0,
109
+ display: "flex",
110
+ alignItems: "center",
111
+ padding: "medium",
112
+ className: css.css({
113
+ pointerEvents: 'none'
114
+ })
115
+ }, __jsx(icon.ChevronDownIcon, {
116
+ size: "xxsmall",
117
+ tone: "placeholder"
118
+ })));
119
+ });
120
+ Select.displayName = 'Select';
121
+
122
+ function useSelectStyles(_ref2) {
123
+ var disabled = _ref2.disabled,
124
+ invalid = _ref2.invalid;
125
+ var theme$1 = theme.useTheme();
126
+ var inputStyles = textInput.useInput({
127
+ disabled: disabled,
128
+ invalid: invalid
129
+ });
130
+ return _objectSpread(_objectSpread({}, inputStyles), {}, {
131
+ overflow: 'hidden',
132
+ // fix for Safari to prevent unwanted scrolling of parent container to occur
133
+ textOverflow: 'ellipsis',
134
+ // Prevent text going underneath the chevron icon
135
+ paddingRight: theme$1.sizing.xxsmall + // size of chevron icon
136
+ theme$1.spacing.medium * 2,
137
+ // paddingX value
138
+ ':invalid': {
139
+ color: theme$1.color.foreground.muted
140
+ }
141
+ });
142
+ }
143
+
144
+ exports.Select = Select;
@@ -0,0 +1,7 @@
1
+ 'use strict';
2
+
3
+ if (process.env.NODE_ENV === "production") {
4
+ module.exports = require("./spark-web-select.cjs.prod.js");
5
+ } else {
6
+ module.exports = require("./spark-web-select.cjs.dev.js");
7
+ }
@@ -0,0 +1,144 @@
1
+ 'use strict';
2
+
3
+ Object.defineProperty(exports, '__esModule', { value: true });
4
+
5
+ var _defineProperty = require('@babel/runtime/helpers/esm/defineProperty');
6
+ var _extends = require('@babel/runtime/helpers/esm/extends');
7
+ var _objectWithoutProperties = require('@babel/runtime/helpers/esm/objectWithoutProperties');
8
+ var css = require('@emotion/css');
9
+ var box = require('@spark-web/box');
10
+ var field = require('@spark-web/field');
11
+ var icon = require('@spark-web/icon');
12
+ var textInput = require('@spark-web/text-input');
13
+ var theme = require('@spark-web/theme');
14
+ var internal = require('@spark-web/utils/internal');
15
+ var React = require('react');
16
+
17
+ function _interopNamespace(e) {
18
+ if (e && e.__esModule) return e;
19
+ var n = Object.create(null);
20
+ if (e) {
21
+ Object.keys(e).forEach(function (k) {
22
+ if (k !== 'default') {
23
+ var d = Object.getOwnPropertyDescriptor(e, k);
24
+ Object.defineProperty(n, k, d.get ? d : {
25
+ enumerable: true,
26
+ get: function () { return e[k]; }
27
+ });
28
+ }
29
+ });
30
+ }
31
+ n["default"] = e;
32
+ return Object.freeze(n);
33
+ }
34
+
35
+ var React__namespace = /*#__PURE__*/_interopNamespace(React);
36
+
37
+ var _excluded = ["disabled", "invalid"];
38
+ var __jsx = React__namespace.createElement;
39
+
40
+ function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); enumerableOnly && (symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; })), keys.push.apply(keys, symbols); } return keys; }
41
+
42
+ function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = null != arguments[i] ? arguments[i] : {}; i % 2 ? ownKeys(Object(source), !0).forEach(function (key) { _defineProperty(target, key, source[key]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)) : ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } return target; }
43
+ var Select = /*#__PURE__*/React__namespace.forwardRef(function (_ref, forwardedRef) {
44
+ var data = _ref.data,
45
+ defaultValue = _ref.defaultValue,
46
+ name = _ref.name,
47
+ onBlur = _ref.onBlur,
48
+ onChange = _ref.onChange,
49
+ optionsOrGroups = _ref.options,
50
+ placeholder = _ref.placeholder,
51
+ required = _ref.required,
52
+ value = _ref.value;
53
+
54
+ var _useFieldContext = field.useFieldContext(),
55
+ disabled = _useFieldContext.disabled,
56
+ invalid = _useFieldContext.invalid,
57
+ a11yProps = _objectWithoutProperties(_useFieldContext, _excluded);
58
+
59
+ var styles = useSelectStyles({
60
+ disabled: disabled,
61
+ invalid: invalid
62
+ });
63
+ var mapOptions = React__namespace.useCallback(function (opt) {
64
+ return __jsx("option", {
65
+ key: opt.value,
66
+ value: opt.value,
67
+ disabled: opt.disabled
68
+ }, opt.label);
69
+ }, []);
70
+ return __jsx(box.Box, {
71
+ position: "relative"
72
+ }, __jsx(box.Box, _extends({}, a11yProps, data ? internal.buildDataAttributes(data) : null, {
73
+ as: "select",
74
+ defaultValue: (defaultValue !== null && defaultValue !== void 0 ? defaultValue : placeholder) ? '' : undefined,
75
+ disabled: disabled,
76
+ name: name,
77
+ onBlur: onBlur,
78
+ onChange: onChange,
79
+ ref: forwardedRef,
80
+ required: required,
81
+ value: value // Styles
82
+ ,
83
+ background: disabled ? 'inputDisabled' : 'input',
84
+ border: invalid ? 'critical' : 'field',
85
+ borderRadius: "small",
86
+ paddingX: "medium",
87
+ height: "medium",
88
+ width: "full",
89
+ className: css.css(styles)
90
+ }), placeholder && __jsx("option", {
91
+ value: "",
92
+ disabled: true
93
+ }, placeholder), optionsOrGroups.map(function (optionOrGroup) {
94
+ if ('options' in optionOrGroup) {
95
+ return __jsx("optgroup", {
96
+ key: optionOrGroup.label,
97
+ label: optionOrGroup.label
98
+ }, optionOrGroup.options.map(function (option) {
99
+ return mapOptions(option);
100
+ }));
101
+ }
102
+
103
+ return mapOptions(optionOrGroup);
104
+ })), __jsx(box.Box, {
105
+ position: "absolute",
106
+ top: 0,
107
+ bottom: 0,
108
+ right: 0,
109
+ display: "flex",
110
+ alignItems: "center",
111
+ padding: "medium",
112
+ className: css.css({
113
+ pointerEvents: 'none'
114
+ })
115
+ }, __jsx(icon.ChevronDownIcon, {
116
+ size: "xxsmall",
117
+ tone: "placeholder"
118
+ })));
119
+ });
120
+ Select.displayName = 'Select';
121
+
122
+ function useSelectStyles(_ref2) {
123
+ var disabled = _ref2.disabled,
124
+ invalid = _ref2.invalid;
125
+ var theme$1 = theme.useTheme();
126
+ var inputStyles = textInput.useInput({
127
+ disabled: disabled,
128
+ invalid: invalid
129
+ });
130
+ return _objectSpread(_objectSpread({}, inputStyles), {}, {
131
+ overflow: 'hidden',
132
+ // fix for Safari to prevent unwanted scrolling of parent container to occur
133
+ textOverflow: 'ellipsis',
134
+ // Prevent text going underneath the chevron icon
135
+ paddingRight: theme$1.sizing.xxsmall + // size of chevron icon
136
+ theme$1.spacing.medium * 2,
137
+ // paddingX value
138
+ ':invalid': {
139
+ color: theme$1.color.foreground.muted
140
+ }
141
+ });
142
+ }
143
+
144
+ exports.Select = Select;
@@ -0,0 +1,120 @@
1
+ import _defineProperty from '@babel/runtime/helpers/esm/defineProperty';
2
+ import _extends from '@babel/runtime/helpers/esm/extends';
3
+ import _objectWithoutProperties from '@babel/runtime/helpers/esm/objectWithoutProperties';
4
+ import { css } from '@emotion/css';
5
+ import { Box } from '@spark-web/box';
6
+ import { useFieldContext } from '@spark-web/field';
7
+ import { ChevronDownIcon } from '@spark-web/icon';
8
+ import { useInput } from '@spark-web/text-input';
9
+ import { useTheme } from '@spark-web/theme';
10
+ import { buildDataAttributes } from '@spark-web/utils/internal';
11
+ import * as React from 'react';
12
+
13
+ var _excluded = ["disabled", "invalid"];
14
+ var __jsx = React.createElement;
15
+
16
+ function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); enumerableOnly && (symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; })), keys.push.apply(keys, symbols); } return keys; }
17
+
18
+ function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = null != arguments[i] ? arguments[i] : {}; i % 2 ? ownKeys(Object(source), !0).forEach(function (key) { _defineProperty(target, key, source[key]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)) : ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } return target; }
19
+ var Select = /*#__PURE__*/React.forwardRef(function (_ref, forwardedRef) {
20
+ var data = _ref.data,
21
+ defaultValue = _ref.defaultValue,
22
+ name = _ref.name,
23
+ onBlur = _ref.onBlur,
24
+ onChange = _ref.onChange,
25
+ optionsOrGroups = _ref.options,
26
+ placeholder = _ref.placeholder,
27
+ required = _ref.required,
28
+ value = _ref.value;
29
+
30
+ var _useFieldContext = useFieldContext(),
31
+ disabled = _useFieldContext.disabled,
32
+ invalid = _useFieldContext.invalid,
33
+ a11yProps = _objectWithoutProperties(_useFieldContext, _excluded);
34
+
35
+ var styles = useSelectStyles({
36
+ disabled: disabled,
37
+ invalid: invalid
38
+ });
39
+ var mapOptions = React.useCallback(function (opt) {
40
+ return __jsx("option", {
41
+ key: opt.value,
42
+ value: opt.value,
43
+ disabled: opt.disabled
44
+ }, opt.label);
45
+ }, []);
46
+ return __jsx(Box, {
47
+ position: "relative"
48
+ }, __jsx(Box, _extends({}, a11yProps, data ? buildDataAttributes(data) : null, {
49
+ as: "select",
50
+ defaultValue: (defaultValue !== null && defaultValue !== void 0 ? defaultValue : placeholder) ? '' : undefined,
51
+ disabled: disabled,
52
+ name: name,
53
+ onBlur: onBlur,
54
+ onChange: onChange,
55
+ ref: forwardedRef,
56
+ required: required,
57
+ value: value // Styles
58
+ ,
59
+ background: disabled ? 'inputDisabled' : 'input',
60
+ border: invalid ? 'critical' : 'field',
61
+ borderRadius: "small",
62
+ paddingX: "medium",
63
+ height: "medium",
64
+ width: "full",
65
+ className: css(styles)
66
+ }), placeholder && __jsx("option", {
67
+ value: "",
68
+ disabled: true
69
+ }, placeholder), optionsOrGroups.map(function (optionOrGroup) {
70
+ if ('options' in optionOrGroup) {
71
+ return __jsx("optgroup", {
72
+ key: optionOrGroup.label,
73
+ label: optionOrGroup.label
74
+ }, optionOrGroup.options.map(function (option) {
75
+ return mapOptions(option);
76
+ }));
77
+ }
78
+
79
+ return mapOptions(optionOrGroup);
80
+ })), __jsx(Box, {
81
+ position: "absolute",
82
+ top: 0,
83
+ bottom: 0,
84
+ right: 0,
85
+ display: "flex",
86
+ alignItems: "center",
87
+ padding: "medium",
88
+ className: css({
89
+ pointerEvents: 'none'
90
+ })
91
+ }, __jsx(ChevronDownIcon, {
92
+ size: "xxsmall",
93
+ tone: "placeholder"
94
+ })));
95
+ });
96
+ Select.displayName = 'Select';
97
+
98
+ function useSelectStyles(_ref2) {
99
+ var disabled = _ref2.disabled,
100
+ invalid = _ref2.invalid;
101
+ var theme = useTheme();
102
+ var inputStyles = useInput({
103
+ disabled: disabled,
104
+ invalid: invalid
105
+ });
106
+ return _objectSpread(_objectSpread({}, inputStyles), {}, {
107
+ overflow: 'hidden',
108
+ // fix for Safari to prevent unwanted scrolling of parent container to occur
109
+ textOverflow: 'ellipsis',
110
+ // Prevent text going underneath the chevron icon
111
+ paddingRight: theme.sizing.xxsmall + // size of chevron icon
112
+ theme.spacing.medium * 2,
113
+ // paddingX value
114
+ ':invalid': {
115
+ color: theme.color.foreground.muted
116
+ }
117
+ });
118
+ }
119
+
120
+ export { Select };
package/package.json ADDED
@@ -0,0 +1,26 @@
1
+ {
2
+ "name": "@spark-web/select",
3
+ "license": "MIT",
4
+ "version": "1.0.0",
5
+ "main": "dist/spark-web-select.cjs.js",
6
+ "module": "dist/spark-web-select.esm.js",
7
+ "devDependencies": {
8
+ "@types/react": "^17.0.12"
9
+ },
10
+ "dependencies": {
11
+ "@babel/runtime": "^7.14.6",
12
+ "@emotion/css": "^11.7.1",
13
+ "@spark-web/a11y": "^1.0.0",
14
+ "@spark-web/box": "^1.0.0",
15
+ "@spark-web/field": "^1.0.0",
16
+ "@spark-web/icon": "^1.0.0",
17
+ "@spark-web/text": "^1.0.0",
18
+ "@spark-web/text-input": "^1.0.0",
19
+ "@spark-web/theme": "^1.0.0",
20
+ "@spark-web/utils": "^1.0.0",
21
+ "react": "^17.0.2"
22
+ },
23
+ "engines": {
24
+ "node": ">= 14.13"
25
+ }
26
+ }
@@ -0,0 +1,37 @@
1
+ import { Field } from '@spark-web/field';
2
+ import { InformationCircleIcon } from '@spark-web/icon';
3
+ import { Inline } from '@spark-web/inline';
4
+ import { Stack } from '@spark-web/stack';
5
+ import { Text } from '@spark-web/text';
6
+ import type { ComponentMeta, ComponentStory } from '@storybook/react';
7
+
8
+ import type { SelectProps } from './Select';
9
+ import { Select } from './Select';
10
+
11
+ export default {
12
+ title: 'Forms / Select',
13
+ component: Select,
14
+ } as ComponentMeta<typeof Select>;
15
+
16
+ const SelectStory: ComponentStory<typeof Select> = (args: SelectProps) => (
17
+ <Stack gap="large">
18
+ <Inline gap="xsmall" alignY="center">
19
+ <InformationCircleIcon tone="info" size="xsmall" />
20
+ <Text weight="medium" tone="info" baseline={false}>
21
+ {`Must be used inside of a <Field/>`}
22
+ </Text>
23
+ </Inline>
24
+ <Field label="Select input">
25
+ <Select options={args.options} />
26
+ </Field>
27
+ </Stack>
28
+ );
29
+ export const Default = SelectStory.bind({});
30
+
31
+ Default.args = {
32
+ options: [
33
+ { value: 'one', label: 'One' },
34
+ { value: 'two', label: 'Two' },
35
+ { value: 'three', label: 'Three' },
36
+ ],
37
+ } as SelectProps;
@@ -0,0 +1,135 @@
1
+ import '@testing-library/jest-dom';
2
+
3
+ import { Field, useFieldContext } from '@spark-web/field';
4
+ import type { DataAttributeMap } from '@spark-web/utils/internal';
5
+ import { cleanup, fireEvent, render, screen } from '@testing-library/react';
6
+
7
+ import type { OptionsOrGroups } from './Select';
8
+ import { Select } from './Select';
9
+
10
+ jest.mock('@spark-web/field', () => {
11
+ const original = jest.requireActual('@spark-web/field');
12
+ return {
13
+ ...original,
14
+ useFieldContext: jest.fn(),
15
+ };
16
+ });
17
+
18
+ const useFieldContextMock = useFieldContext as jest.Mock;
19
+
20
+ const renderComponent = ({
21
+ options,
22
+ name,
23
+ placeholder,
24
+ data,
25
+ }: {
26
+ options: OptionsOrGroups;
27
+ name: string;
28
+ placeholder?: string;
29
+ data?: DataAttributeMap;
30
+ }) =>
31
+ render(
32
+ <Field label={name}>
33
+ <Select
34
+ options={options}
35
+ {...(placeholder && { placeholder })}
36
+ {...(data && { data })}
37
+ />
38
+ </Field>
39
+ );
40
+
41
+ describe('Select component', () => {
42
+ const name = 'test select';
43
+ beforeEach(() => {
44
+ useFieldContextMock.mockReturnValue({
45
+ disabled: false,
46
+ invalid: false,
47
+ 'aria-label': name,
48
+ });
49
+ });
50
+
51
+ afterEach(cleanup);
52
+
53
+ it('should display select label', () => {
54
+ renderComponent({ options: [], name });
55
+ screen.getByText(name);
56
+ });
57
+
58
+ it('should display placeholder with empty value and disabled', () => {
59
+ const placeholder = 'select placeholder';
60
+ renderComponent({ options: [], name, placeholder });
61
+ const placeholderOption = screen.getByRole('option', {
62
+ name: placeholder,
63
+ }) as HTMLOptionElement;
64
+ expect(placeholderOption.selected).toBe(true);
65
+ expect(placeholderOption.value).toBe('');
66
+ expect(placeholderOption).toBeDisabled();
67
+ });
68
+
69
+ it('should have options to select', () => {
70
+ const options = [
71
+ { label: 'foo', value: 'bar' },
72
+ { label: 'foo1', value: 'bar1' },
73
+ { label: 'foo2', value: 'bar2' },
74
+ ];
75
+ renderComponent({ options, name });
76
+ expect(
77
+ (screen.getByRole('option', { name: 'foo' }) as HTMLOptionElement)
78
+ .selected
79
+ ).toBe(true);
80
+
81
+ fireEvent.change(screen.getByLabelText(name), {
82
+ target: { value: 'bar1' },
83
+ });
84
+ expect(
85
+ (screen.getByRole('option', { name: 'foo1' }) as HTMLOptionElement)
86
+ .selected
87
+ ).toBe(true);
88
+
89
+ fireEvent.change(screen.getByLabelText(name), {
90
+ target: { value: 'bar2' },
91
+ });
92
+ expect(
93
+ (screen.getByRole('option', { name: 'foo2' }) as HTMLOptionElement)
94
+ .selected
95
+ ).toBe(true);
96
+ });
97
+
98
+ it('should have attributes built by data', () => {
99
+ const data = { foo: 'bar', foo1: 'bar1' };
100
+
101
+ renderComponent({ data, name, options: [] });
102
+ expect(screen.getByLabelText(name)).toHaveAttribute('data-foo', 'bar');
103
+ expect(screen.getByLabelText(name)).toHaveAttribute('data-foo1', 'bar1');
104
+ });
105
+
106
+ it('should be disabled by field context', () => {
107
+ useFieldContextMock.mockReturnValue({
108
+ disabled: true,
109
+ invalid: true,
110
+ 'aria-label': name,
111
+ });
112
+ renderComponent({ name, options: [] });
113
+ expect(screen.getByLabelText(name)).toBeDisabled();
114
+ });
115
+
116
+ it('should have option in optGroup', () => {
117
+ const optGroupOption = { label: 'foo1-0', value: 'bar1-0' };
118
+ const options = [
119
+ { label: 'foo', value: 'bar' },
120
+ {
121
+ label: 'foo1',
122
+ value: 'bar1',
123
+ options: [optGroupOption],
124
+ },
125
+ ];
126
+ renderComponent({ name, options });
127
+ expect(
128
+ (
129
+ screen.getByRole('option', {
130
+ name: optGroupOption.label,
131
+ }) as HTMLOptionElement
132
+ ).value
133
+ ).toBe(optGroupOption.value);
134
+ });
135
+ });
package/src/Select.tsx ADDED
@@ -0,0 +1,134 @@
1
+ import { css } from '@emotion/css';
2
+ import { Box } from '@spark-web/box';
3
+ import { useFieldContext } from '@spark-web/field';
4
+ import { ChevronDownIcon } from '@spark-web/icon';
5
+ import type { UseInputProps } from '@spark-web/text-input';
6
+ import { useInput } from '@spark-web/text-input';
7
+ import { useTheme } from '@spark-web/theme';
8
+ import type { DataAttributeMap } from '@spark-web/utils/internal';
9
+ import { buildDataAttributes } from '@spark-web/utils/internal';
10
+ import * as React from 'react';
11
+
12
+ type Option = {
13
+ disabled?: boolean;
14
+ label: string;
15
+ value: string | number;
16
+ };
17
+ type Group = { options: Array<Option>; label: string };
18
+ export type OptionsOrGroups = Array<Option | Group>;
19
+
20
+ export type SelectProps = Pick<
21
+ React.SelectHTMLAttributes<HTMLSelectElement>,
22
+ 'defaultValue' | 'name' | 'onBlur' | 'onChange' | 'required' | 'value'
23
+ > & {
24
+ data?: DataAttributeMap;
25
+ options: OptionsOrGroups;
26
+ placeholder?: string;
27
+ };
28
+
29
+ export const Select = React.forwardRef<HTMLSelectElement, SelectProps>(
30
+ (
31
+ {
32
+ data,
33
+ defaultValue,
34
+ name,
35
+ onBlur,
36
+ onChange,
37
+ options: optionsOrGroups,
38
+ placeholder,
39
+ required,
40
+ value,
41
+ },
42
+ forwardedRef
43
+ ) => {
44
+ const { disabled, invalid, ...a11yProps } = useFieldContext();
45
+ const styles = useSelectStyles({ disabled, invalid });
46
+
47
+ const mapOptions = React.useCallback(
48
+ (opt: Option) => (
49
+ <option key={opt.value} value={opt.value} disabled={opt.disabled}>
50
+ {opt.label}
51
+ </option>
52
+ ),
53
+ []
54
+ );
55
+
56
+ return (
57
+ <Box position="relative">
58
+ <Box
59
+ {...a11yProps}
60
+ {...(data ? buildDataAttributes(data) : null)}
61
+ as="select"
62
+ defaultValue={defaultValue ?? placeholder ? '' : undefined}
63
+ disabled={disabled}
64
+ name={name}
65
+ onBlur={onBlur}
66
+ onChange={onChange}
67
+ ref={forwardedRef}
68
+ required={required}
69
+ value={value}
70
+ // Styles
71
+ background={disabled ? 'inputDisabled' : 'input'}
72
+ border={invalid ? 'critical' : 'field'}
73
+ borderRadius="small"
74
+ paddingX="medium"
75
+ height="medium"
76
+ width="full"
77
+ className={css(styles)}
78
+ >
79
+ {placeholder && (
80
+ <option value="" disabled>
81
+ {placeholder}
82
+ </option>
83
+ )}
84
+ {optionsOrGroups.map(optionOrGroup => {
85
+ if ('options' in optionOrGroup) {
86
+ return (
87
+ <optgroup key={optionOrGroup.label} label={optionOrGroup.label}>
88
+ {optionOrGroup.options.map(option => mapOptions(option))}
89
+ </optgroup>
90
+ );
91
+ }
92
+ return mapOptions(optionOrGroup);
93
+ })}
94
+ </Box>
95
+ <Box
96
+ position="absolute"
97
+ top={0}
98
+ bottom={0}
99
+ right={0}
100
+ display="flex"
101
+ alignItems="center"
102
+ padding="medium"
103
+ className={css({ pointerEvents: 'none' })}
104
+ >
105
+ <ChevronDownIcon size="xxsmall" tone="placeholder" />
106
+ </Box>
107
+ </Box>
108
+ );
109
+ }
110
+ );
111
+
112
+ Select.displayName = 'Select';
113
+
114
+ function useSelectStyles({ disabled, invalid }: UseInputProps) {
115
+ const theme = useTheme();
116
+ const inputStyles = useInput({
117
+ disabled,
118
+ invalid,
119
+ });
120
+ return {
121
+ ...inputStyles,
122
+ overflow: 'hidden', // fix for Safari to prevent unwanted scrolling of parent container to occur
123
+ textOverflow: 'ellipsis',
124
+
125
+ // Prevent text going underneath the chevron icon
126
+ paddingRight:
127
+ theme.sizing.xxsmall + // size of chevron icon
128
+ theme.spacing.medium * 2, // paddingX value
129
+
130
+ ':invalid': {
131
+ color: theme.color.foreground.muted,
132
+ },
133
+ };
134
+ }
package/src/index.ts ADDED
@@ -0,0 +1,5 @@
1
+ export { Select } from './Select';
2
+
3
+ // types
4
+
5
+ export type { SelectProps } from './Select';