@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 +22 -0
- package/README.md +139 -0
- package/dist/declarations/src/Select.d.ts +23 -0
- package/dist/declarations/src/index.d.ts +2 -0
- package/dist/spark-web-select.cjs.d.ts +1 -0
- package/dist/spark-web-select.cjs.dev.js +144 -0
- package/dist/spark-web-select.cjs.js +7 -0
- package/dist/spark-web-select.cjs.prod.js +144 -0
- package/dist/spark-web-select.esm.js +120 -0
- package/package.json +26 -0
- package/src/Select.stories.tsx +37 -0
- package/src/Select.test.tsx +135 -0
- package/src/Select.tsx +134 -0
- package/src/index.ts +5 -0
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 @@
|
|
|
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,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
|
+
}
|