@hyphen/hyphen-components 8.0.1 → 9.0.0-beta.1
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/css/utilities.css +1 -1
- package/dist/css/utilities.css.map +1 -1
- package/dist/hyphen-components.cjs.development.js +353 -313
- package/dist/hyphen-components.cjs.development.js.map +1 -1
- package/dist/hyphen-components.cjs.production.min.js +2 -2
- package/dist/hyphen-components.cjs.production.min.js.map +1 -1
- package/dist/hyphen-components.esm.js +357 -314
- package/dist/hyphen-components.esm.js.map +1 -1
- package/dist/index.d.ts +222 -212
- package/package.json +6 -5
- package/src/components/Alert/Alert.tsx +5 -6
- package/src/components/Badge/Badge.test.tsx +12 -0
- package/src/components/Badge/Badge.tsx +52 -20
- package/src/components/Box/Box.mdx +1 -1
- package/src/components/Box/Box.stories.tsx +1 -1
- package/src/components/Box/Box.test.tsx +24 -1
- package/src/components/Box/Box.tsx +108 -82
- package/src/components/Card/Card.tsx +5 -1
- package/src/components/Card/components/CardFooter/CardFooter.tsx +0 -4
- package/src/components/Card/components/CardHeader/CardHeader.tsx +4 -1
- package/src/components/Card/components/CardSection/CardSection.tsx +4 -5
- package/src/components/CheckboxInput/CheckboxInput.test.tsx +17 -0
- package/src/components/CheckboxInput/CheckboxInput.tsx +15 -4
- package/src/components/CheckboxInput/components/Checkbox.test.tsx +36 -0
- package/src/components/CheckboxInput/components/Checkbox.tsx +8 -2
- package/src/components/Details/Details.test.tsx +8 -0
- package/src/components/Details/Details.tsx +5 -2
- package/src/components/Details/DetailsSummary.tsx +4 -1
- package/src/components/Drawer/Drawer.test.tsx +8 -0
- package/src/components/Drawer/Drawer.tsx +1 -1
- package/src/components/FormLabel/FormLabel.tsx +6 -3
- package/src/components/Formik/FormikCheckboxInput/FormikCheckboxInput.test.tsx +9 -0
- package/src/components/Formik/FormikCheckboxInput/FormikCheckboxInput.tsx +1 -0
- package/src/components/Formik/FormikSelectInput/FormikSelectInput.tsx +21 -11
- package/src/components/Formik/FormikSelectInputNative/FormikSelectInputNative.tsx +1 -1
- package/src/components/Formik/FormikTimePicker/FormikTimePicker.tsx +1 -1
- package/src/components/Heading/Heading.tsx +5 -6
- package/src/components/Icon/Icon.test.tsx +8 -0
- package/src/components/Icon/Icon.tsx +6 -7
- package/src/components/Modal/Modal.test.tsx +31 -1
- package/src/components/Modal/Modal.tsx +13 -7
- package/src/components/Popover/Popover.test.tsx +3 -4
- package/src/components/RadioGroup/RadioGroup.tsx +11 -6
- package/src/components/RadioGroup/RadioInput/RadioInputIcon.tsx +0 -4
- package/src/components/SelectInput/SelectInput.test.tsx +18 -0
- package/src/components/SelectInput/SelectInput.tsx +47 -27
- package/src/components/SelectInputInset/SelectInputInset.tsx +8 -9
- package/src/components/SelectInputNative/SelectInputNative.test.tsx +24 -0
- package/src/components/SelectInputNative/SelectInputNative.tsx +79 -14
- package/src/components/Switch/Switch.tsx +1 -1
- package/src/components/Table/Table.mdx +21 -1
- package/src/components/Table/Table.stories.tsx +5 -1
- package/src/components/Table/Table.tsx +7 -7
- package/src/components/Table/TableBody/TableBody.test.tsx +89 -1
- package/src/components/Table/TableBody/TableBody.tsx +23 -8
- package/src/components/Table/TableHead/TableHead.tsx +6 -6
- package/src/components/Table/TableHead/TableHeaderCell/TableHeaderCell.tsx +6 -6
- package/src/components/Table/common/TableRow/TableRow.test.tsx +17 -1
- package/src/components/Table/common/TableRow/TableRow.tsx +30 -10
- package/src/components/TextInput/TextInput.tsx +10 -8
- package/src/components/TextInputInset/TextInputInset.tsx +5 -6
- package/src/components/TextareaInput/TextareaInput.tsx +4 -5
- package/src/components/TextareaInputInset/TextareaInputInset.tsx +10 -17
- package/src/components/Toast/Toast.stories.tsx +5 -1
- package/src/lib/getColumnKeys.ts +5 -3
- package/src/types/index.ts +6 -7
|
@@ -7,7 +7,9 @@ import Select, {
|
|
|
7
7
|
OptionsOrGroups,
|
|
8
8
|
OnChangeValue,
|
|
9
9
|
} from 'react-select';
|
|
10
|
-
import AsyncCreatableSelect
|
|
10
|
+
import AsyncCreatableSelect, {
|
|
11
|
+
AsyncCreatableProps,
|
|
12
|
+
} from 'react-select/async-creatable';
|
|
11
13
|
import AsyncSelect from 'react-select/async';
|
|
12
14
|
import CreatableSelect from 'react-select/creatable';
|
|
13
15
|
import { ResponsiveProp } from '../../types';
|
|
@@ -42,7 +44,7 @@ export type TextInputSize =
|
|
|
42
44
|
| 'lg'
|
|
43
45
|
| ResponsiveProp<'sm' | 'md' | 'lg'>;
|
|
44
46
|
|
|
45
|
-
|
|
47
|
+
interface SelectInputOwnProps {
|
|
46
48
|
/**
|
|
47
49
|
* The id attribute of the input.
|
|
48
50
|
*/
|
|
@@ -59,10 +61,6 @@ export interface SelectInputProps {
|
|
|
59
61
|
* The value(s) of select.
|
|
60
62
|
*/
|
|
61
63
|
value: any | any[]; // eslint-disable-line @typescript-eslint/no-explicit-any
|
|
62
|
-
/**
|
|
63
|
-
* Options for dropdown list.
|
|
64
|
-
*/
|
|
65
|
-
options: SelectInputOptions | AsyncOptions;
|
|
66
64
|
/**
|
|
67
65
|
* Autofocus select input on render.
|
|
68
66
|
*/
|
|
@@ -84,10 +82,6 @@ export interface SelectInputProps {
|
|
|
84
82
|
* Visually hide the label.
|
|
85
83
|
*/
|
|
86
84
|
hideLabel?: boolean;
|
|
87
|
-
/**
|
|
88
|
-
* Load the input asynchronously.
|
|
89
|
-
*/
|
|
90
|
-
isAsync?: boolean;
|
|
91
85
|
/**
|
|
92
86
|
* If the input value is clearable programmatically.
|
|
93
87
|
*/
|
|
@@ -137,18 +131,36 @@ export interface SelectInputProps {
|
|
|
137
131
|
* The size of the text input.
|
|
138
132
|
*/
|
|
139
133
|
size?: TextInputSize;
|
|
140
|
-
/**
|
|
141
|
-
* Additional props to be spread. These will be applied specifically to
|
|
142
|
-
* the `react-select` component that powers the select. For full docs on
|
|
143
|
-
* react-select props, [Click Here](https://react-select.com/props)
|
|
144
|
-
*/
|
|
145
|
-
[x: string]: any; // eslint-disable-line
|
|
146
134
|
}
|
|
147
135
|
|
|
136
|
+
type ReactSelectAsyncProps = AsyncCreatableProps<
|
|
137
|
+
SelectOption,
|
|
138
|
+
boolean,
|
|
139
|
+
SelectGroupOptions
|
|
140
|
+
>;
|
|
141
|
+
|
|
142
|
+
type ControlledReactSelectProps =
|
|
143
|
+
| 'aria-label'
|
|
144
|
+
| 'aria-labelledby'
|
|
145
|
+
| 'cacheOptions'
|
|
146
|
+
| 'classNamePrefix'
|
|
147
|
+
| 'components'
|
|
148
|
+
| 'defaultOptions'
|
|
149
|
+
| 'inputId'
|
|
150
|
+
| 'loadOptions'
|
|
151
|
+
| 'options'
|
|
152
|
+
| 'styles';
|
|
153
|
+
|
|
154
|
+
type SelectInputBaseProps = SelectInputOwnProps &
|
|
155
|
+
Omit<
|
|
156
|
+
ReactSelectAsyncProps,
|
|
157
|
+
keyof SelectInputOwnProps | ControlledReactSelectProps
|
|
158
|
+
>;
|
|
159
|
+
|
|
148
160
|
type AsyncOptions = (
|
|
149
161
|
inputValue: string
|
|
150
162
|
) => Promise<OptionsOrGroups<SelectOption, SelectGroupOptions>>;
|
|
151
|
-
type AsyncSelectInputProps =
|
|
163
|
+
export type AsyncSelectInputProps = SelectInputBaseProps & {
|
|
152
164
|
/**
|
|
153
165
|
* Load the input asynchronously.
|
|
154
166
|
*/
|
|
@@ -160,14 +172,14 @@ type AsyncSelectInputProps = SelectInputProps & {
|
|
|
160
172
|
/**
|
|
161
173
|
* If cacheOptions is passed, then the loaded data will be cached.
|
|
162
174
|
*/
|
|
163
|
-
cacheOptions?:
|
|
175
|
+
cacheOptions?: ReactSelectAsyncProps['cacheOptions'];
|
|
164
176
|
/**
|
|
165
177
|
* The default set of options to show before the user starts searching.
|
|
166
178
|
*/
|
|
167
|
-
defaultOptions?:
|
|
179
|
+
defaultOptions?: ReactSelectAsyncProps['defaultOptions'];
|
|
168
180
|
};
|
|
169
181
|
|
|
170
|
-
type SyncSelectInputProps =
|
|
182
|
+
export type SyncSelectInputProps = SelectInputBaseProps & {
|
|
171
183
|
/**
|
|
172
184
|
* Load the input synchronously.
|
|
173
185
|
*/
|
|
@@ -176,10 +188,12 @@ type SyncSelectInputProps = SelectInputProps & {
|
|
|
176
188
|
* Options for dropdown list.
|
|
177
189
|
*/
|
|
178
190
|
options: SelectInputOptions;
|
|
191
|
+
cacheOptions?: never;
|
|
192
|
+
defaultOptions?: never;
|
|
179
193
|
};
|
|
180
194
|
|
|
181
|
-
export
|
|
182
|
-
|
|
195
|
+
export type SelectInputProps = AsyncSelectInputProps | SyncSelectInputProps;
|
|
196
|
+
|
|
183
197
|
export function SelectInput(props: SelectInputProps): JSX.Element {
|
|
184
198
|
const {
|
|
185
199
|
id,
|
|
@@ -205,6 +219,8 @@ export function SelectInput(props: SelectInputProps): JSX.Element {
|
|
|
205
219
|
placeholder = undefined,
|
|
206
220
|
requiredIndicator = ' *',
|
|
207
221
|
size = 'md',
|
|
222
|
+
cacheOptions,
|
|
223
|
+
defaultOptions,
|
|
208
224
|
...restProps
|
|
209
225
|
} = props;
|
|
210
226
|
|
|
@@ -262,13 +278,17 @@ export function SelectInput(props: SelectInputProps): JSX.Element {
|
|
|
262
278
|
isCreatable && isAsync
|
|
263
279
|
? AsyncCreatableSelect
|
|
264
280
|
: isCreatable
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
281
|
+
? CreatableSelect
|
|
282
|
+
: isAsync
|
|
283
|
+
? AsyncSelect
|
|
284
|
+
: Select;
|
|
269
285
|
|
|
270
286
|
const selectOptions = isAsync
|
|
271
|
-
?
|
|
287
|
+
? {
|
|
288
|
+
loadOptions: options as AsyncOptions,
|
|
289
|
+
cacheOptions,
|
|
290
|
+
defaultOptions,
|
|
291
|
+
}
|
|
272
292
|
: ({ options } as { options: SelectInputOptions });
|
|
273
293
|
|
|
274
294
|
return (
|
|
@@ -6,7 +6,6 @@ import React, {
|
|
|
6
6
|
FocusEvent,
|
|
7
7
|
ForwardRefExoticComponent,
|
|
8
8
|
ReactNode,
|
|
9
|
-
HTMLProps,
|
|
10
9
|
} from 'react';
|
|
11
10
|
import classNames from 'classnames';
|
|
12
11
|
import { ResponsiveProp } from '../../types';
|
|
@@ -36,11 +35,15 @@ export interface SelectInputInsetProps {
|
|
|
36
35
|
/**
|
|
37
36
|
* Callback function to call on change event.
|
|
38
37
|
*/
|
|
39
|
-
onChange: (event: ChangeEvent<
|
|
38
|
+
onChange: (event: ChangeEvent<HTMLSelectElement>) => void;
|
|
40
39
|
/**
|
|
41
40
|
* Value of selected option. Should match the value key in the option object.
|
|
42
41
|
*/
|
|
43
42
|
value: string | number | null;
|
|
43
|
+
/**
|
|
44
|
+
* The select's autocomplete behavior.
|
|
45
|
+
*/
|
|
46
|
+
autoComplete?: boolean | string;
|
|
44
47
|
/**
|
|
45
48
|
* Automatically focus the input when the page is loaded.
|
|
46
49
|
*/
|
|
@@ -61,7 +64,7 @@ export interface SelectInputInsetProps {
|
|
|
61
64
|
/**
|
|
62
65
|
* Props passed directly to the input element of the component
|
|
63
66
|
*/
|
|
64
|
-
inputProps?: BoxProps
|
|
67
|
+
inputProps?: Omit<BoxProps<'select'>, 'as'>;
|
|
65
68
|
/**
|
|
66
69
|
* The input's disabled attribute
|
|
67
70
|
*/
|
|
@@ -77,7 +80,7 @@ export interface SelectInputInsetProps {
|
|
|
77
80
|
/**
|
|
78
81
|
* Callback function to call on blur event.
|
|
79
82
|
*/
|
|
80
|
-
onBlur?: (event: FocusEvent<
|
|
83
|
+
onBlur?: (event: FocusEvent<HTMLSelectElement>) => void;
|
|
81
84
|
/**
|
|
82
85
|
* Callback function to call when input us cleared. When this is passed,
|
|
83
86
|
* the input will display an icon on the right side, for triggering this callback.
|
|
@@ -88,7 +91,7 @@ export interface SelectInputInsetProps {
|
|
|
88
91
|
/**
|
|
89
92
|
* Callback function to call on focus event.
|
|
90
93
|
*/
|
|
91
|
-
onFocus?: (event: FocusEvent<
|
|
94
|
+
onFocus?: (event: FocusEvent<HTMLSelectElement>) => void;
|
|
92
95
|
/**
|
|
93
96
|
* The input placeholder attribute.
|
|
94
97
|
*/
|
|
@@ -101,10 +104,6 @@ export interface SelectInputInsetProps {
|
|
|
101
104
|
* The size of the text input.
|
|
102
105
|
*/
|
|
103
106
|
size?: SelectInputInsetSize | ResponsiveProp<SelectInputInsetSize>;
|
|
104
|
-
/**
|
|
105
|
-
* Additional props to be spread to rendered element
|
|
106
|
-
*/
|
|
107
|
-
[x: string]: any; // eslint-disable-line
|
|
108
107
|
}
|
|
109
108
|
|
|
110
109
|
export const SelectInputInset: ForwardRefExoticComponent<SelectInputInsetProps> =
|
|
@@ -102,6 +102,30 @@ describe('SelectInputNative', () => {
|
|
|
102
102
|
|
|
103
103
|
expect(mockedHandleBlur).toBeCalledTimes(1);
|
|
104
104
|
});
|
|
105
|
+
|
|
106
|
+
test('it forwards native select attributes to the select element', () => {
|
|
107
|
+
render(
|
|
108
|
+
<SelectInputNative
|
|
109
|
+
id="testId"
|
|
110
|
+
label="native attributes"
|
|
111
|
+
onChange={() => undefined}
|
|
112
|
+
options={selectOptions}
|
|
113
|
+
value={null}
|
|
114
|
+
form="settings-form"
|
|
115
|
+
disabled
|
|
116
|
+
required
|
|
117
|
+
data-testid="native-select"
|
|
118
|
+
inputProps={{ title: 'Select a flavor' }}
|
|
119
|
+
/>
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
const select = screen.getByTestId('native-select');
|
|
123
|
+
expect(select).toHaveAttribute('form', 'settings-form');
|
|
124
|
+
expect(select).toHaveAttribute('title', 'Select a flavor');
|
|
125
|
+
expect(select).toBeDisabled();
|
|
126
|
+
expect(select).toBeRequired();
|
|
127
|
+
expect(select.parentElement).not.toHaveAttribute('form');
|
|
128
|
+
});
|
|
105
129
|
});
|
|
106
130
|
|
|
107
131
|
describe('States', () => {
|
|
@@ -2,7 +2,7 @@ import React from 'react';
|
|
|
2
2
|
import classNames from 'classnames';
|
|
3
3
|
import { generateResponsiveClasses } from '../../lib/generateResponsiveClasses';
|
|
4
4
|
import { ResponsiveProp } from '../../types';
|
|
5
|
-
import { Box, BoxProps } from '../Box/Box';
|
|
5
|
+
import { Box, BoxOwnProps, BoxProps, boxPropsKeys } from '../Box/Box';
|
|
6
6
|
import { FormControl, FormControlProps } from '../FormControl/FormControl';
|
|
7
7
|
import styles from './SelectInputNative.module.scss';
|
|
8
8
|
|
|
@@ -13,7 +13,7 @@ export interface SelectInputNativeOption {
|
|
|
13
13
|
disabled?: boolean;
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
interface SelectInputNativeOwnProps {
|
|
17
17
|
/**
|
|
18
18
|
* List of options for the select input.
|
|
19
19
|
*/
|
|
@@ -25,11 +25,36 @@ export interface SelectInputNativeProps extends BoxProps, FormControlProps {
|
|
|
25
25
|
/**
|
|
26
26
|
* Value of selected option. Should match the value key in the option object.
|
|
27
27
|
*/
|
|
28
|
-
value:
|
|
28
|
+
value: React.ComponentPropsWithoutRef<'select'>['value'] | null;
|
|
29
|
+
/**
|
|
30
|
+
* Props passed directly to the select element of the component.
|
|
31
|
+
*/
|
|
32
|
+
inputProps?: Omit<
|
|
33
|
+
BoxProps<'select'>,
|
|
34
|
+
| 'aria-label'
|
|
35
|
+
| 'aria-labelledby'
|
|
36
|
+
| 'aria-required'
|
|
37
|
+
| 'as'
|
|
38
|
+
| 'autoFocus'
|
|
39
|
+
| 'children'
|
|
40
|
+
| 'color'
|
|
41
|
+
| 'defaultValue'
|
|
42
|
+
| 'disabled'
|
|
43
|
+
| 'id'
|
|
44
|
+
| 'name'
|
|
45
|
+
| 'onChange'
|
|
46
|
+
| 'required'
|
|
47
|
+
| 'size'
|
|
48
|
+
| 'value'
|
|
49
|
+
>;
|
|
29
50
|
/**
|
|
30
51
|
* The input's 'name' attribute.
|
|
31
52
|
*/
|
|
32
53
|
name?: string;
|
|
54
|
+
/**
|
|
55
|
+
* Placeholder option displayed before a value is selected.
|
|
56
|
+
*/
|
|
57
|
+
placeholder?: string;
|
|
33
58
|
/**
|
|
34
59
|
* Visual indicator that the field is required, that gets appended to the label
|
|
35
60
|
*/
|
|
@@ -42,12 +67,38 @@ export interface SelectInputNativeProps extends BoxProps, FormControlProps {
|
|
|
42
67
|
* Whether the input is autofocused on initial render.
|
|
43
68
|
*/
|
|
44
69
|
autoFocus?: HTMLSelectElement['autofocus'];
|
|
45
|
-
/**
|
|
46
|
-
* Additional props to be spread.
|
|
47
|
-
*/
|
|
48
|
-
[x: string]: any; // eslint-disable-line
|
|
49
70
|
}
|
|
50
71
|
|
|
72
|
+
type SelectInputNativeFormControlProps = Pick<
|
|
73
|
+
FormControlProps,
|
|
74
|
+
| 'error'
|
|
75
|
+
| 'helpText'
|
|
76
|
+
| 'hideLabel'
|
|
77
|
+
| 'id'
|
|
78
|
+
| 'isDisabled'
|
|
79
|
+
| 'isRequired'
|
|
80
|
+
| 'label'
|
|
81
|
+
>;
|
|
82
|
+
|
|
83
|
+
type SelectInputNativeElementProps = Omit<
|
|
84
|
+
React.ComponentPropsWithoutRef<'select'>,
|
|
85
|
+
| keyof SelectInputNativeOwnProps
|
|
86
|
+
| keyof SelectInputNativeFormControlProps
|
|
87
|
+
| keyof BoxOwnProps
|
|
88
|
+
| 'aria-label'
|
|
89
|
+
| 'aria-labelledby'
|
|
90
|
+
| 'aria-required'
|
|
91
|
+
| 'children'
|
|
92
|
+
| 'dangerouslySetInnerHTML'
|
|
93
|
+
| 'defaultValue'
|
|
94
|
+
| 'size'
|
|
95
|
+
>;
|
|
96
|
+
|
|
97
|
+
export type SelectInputNativeProps = SelectInputNativeOwnProps &
|
|
98
|
+
SelectInputNativeFormControlProps &
|
|
99
|
+
Omit<BoxOwnProps, 'children'> &
|
|
100
|
+
SelectInputNativeElementProps;
|
|
101
|
+
|
|
51
102
|
export const SelectInputNative: React.FC<SelectInputNativeProps> = ({
|
|
52
103
|
autoFocus = false,
|
|
53
104
|
label,
|
|
@@ -55,6 +106,7 @@ export const SelectInputNative: React.FC<SelectInputNativeProps> = ({
|
|
|
55
106
|
helpText,
|
|
56
107
|
error,
|
|
57
108
|
id,
|
|
109
|
+
inputProps = {},
|
|
58
110
|
isDisabled,
|
|
59
111
|
isRequired,
|
|
60
112
|
name,
|
|
@@ -66,6 +118,17 @@ export const SelectInputNative: React.FC<SelectInputNativeProps> = ({
|
|
|
66
118
|
size = 'md',
|
|
67
119
|
...restProps
|
|
68
120
|
}) => {
|
|
121
|
+
const isBoxProp = (key: string) =>
|
|
122
|
+
boxPropsKeys.includes(key as keyof BoxOwnProps);
|
|
123
|
+
const formControlProps = Object.fromEntries(
|
|
124
|
+
Object.entries(restProps).filter(([key]) => isBoxProp(key))
|
|
125
|
+
) as Omit<BoxOwnProps, 'children'>;
|
|
126
|
+
const selectProps = Object.fromEntries(
|
|
127
|
+
Object.entries(restProps).filter(([key]) => !isBoxProp(key))
|
|
128
|
+
) as SelectInputNativeElementProps;
|
|
129
|
+
const selectIsDisabled = isDisabled || selectProps.disabled;
|
|
130
|
+
const selectIsRequired = isRequired || selectProps.required;
|
|
131
|
+
|
|
69
132
|
const placeholderOption: SelectInputNativeOption = {
|
|
70
133
|
value: '',
|
|
71
134
|
label: placeholder,
|
|
@@ -83,7 +146,7 @@ export const SelectInputNative: React.FC<SelectInputNativeProps> = ({
|
|
|
83
146
|
styles['select-input-native-wrapper'],
|
|
84
147
|
...responsiveClasses.map((className) => styles[className]),
|
|
85
148
|
{
|
|
86
|
-
[styles.disabled]:
|
|
149
|
+
[styles.disabled]: selectIsDisabled,
|
|
87
150
|
[styles.error]: error,
|
|
88
151
|
}
|
|
89
152
|
);
|
|
@@ -95,25 +158,27 @@ export const SelectInputNative: React.FC<SelectInputNativeProps> = ({
|
|
|
95
158
|
id={id}
|
|
96
159
|
error={error}
|
|
97
160
|
helpText={helpText}
|
|
98
|
-
isDisabled={
|
|
99
|
-
isRequired={
|
|
161
|
+
isDisabled={selectIsDisabled}
|
|
162
|
+
isRequired={selectIsRequired}
|
|
100
163
|
requiredIndicator={requiredIndicator}
|
|
101
|
-
{...
|
|
164
|
+
{...formControlProps}
|
|
102
165
|
>
|
|
103
166
|
<Box className={selectWrapperClasses}>
|
|
104
167
|
<Box
|
|
105
168
|
as="select"
|
|
169
|
+
{...inputProps}
|
|
170
|
+
{...selectProps}
|
|
106
171
|
aria-label={label}
|
|
107
172
|
aria-labelledby={label && !hideLabel ? `${id}Label` : undefined}
|
|
108
|
-
aria-required={
|
|
173
|
+
aria-required={selectIsRequired}
|
|
109
174
|
value={value ?? ''}
|
|
110
175
|
onChange={onChange}
|
|
111
176
|
color={!value ? 'disabled' : 'base'}
|
|
112
177
|
autoFocus={autoFocus}
|
|
113
|
-
disabled={
|
|
178
|
+
disabled={selectIsDisabled}
|
|
114
179
|
name={name}
|
|
115
180
|
id={id}
|
|
116
|
-
required={
|
|
181
|
+
required={selectIsRequired}
|
|
117
182
|
>
|
|
118
183
|
{optionsWithPlaceholder.map((option) => (
|
|
119
184
|
<Box
|
|
@@ -139,7 +139,7 @@ export const Switch: FC<SwitchProps> = ({
|
|
|
139
139
|
alignItems: helpText ? 'flex-start' : ('center' as BoxProps['alignItems']),
|
|
140
140
|
isFieldRequired: isRequired,
|
|
141
141
|
requiredIndicator,
|
|
142
|
-
size,
|
|
142
|
+
...(typeof size === 'string' && { size }),
|
|
143
143
|
};
|
|
144
144
|
|
|
145
145
|
return (
|
|
@@ -30,6 +30,26 @@ An example showcasing the props required to render a list of customers in a tabl
|
|
|
30
30
|
|
|
31
31
|
<Canvas of={Stories.CommonExample} />
|
|
32
32
|
|
|
33
|
+
## TypeScript row types
|
|
34
|
+
|
|
35
|
+
`Table` accepts interfaces and infers its row type from `rows`. Type separately declared columns with `Column<TRow>[]` to validate `dataKey` values and type the `row` argument passed to render callbacks.
|
|
36
|
+
|
|
37
|
+
```tsx
|
|
38
|
+
interface Customer {
|
|
39
|
+
id: string;
|
|
40
|
+
name: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const columns: Column<Customer>[] = [
|
|
44
|
+
{
|
|
45
|
+
dataKey: 'name',
|
|
46
|
+
render: (_cell, row) => row?.name,
|
|
47
|
+
},
|
|
48
|
+
];
|
|
49
|
+
|
|
50
|
+
<Table columns={columns} rows={customers} rowKey="id" />;
|
|
51
|
+
```
|
|
52
|
+
|
|
33
53
|
## Loading
|
|
34
54
|
|
|
35
55
|
Use the `isLoading` prop on the `<Table />` component to display an overlay that prevents user interaction. Useful for when
|
|
@@ -146,7 +166,7 @@ The `Column` object is equiped with a `render` method. When used, the cells will
|
|
|
146
166
|
|
|
147
167
|
The render method also exposes data about the cell and row, namely:
|
|
148
168
|
|
|
149
|
-
`cell: the content in the cell,`
|
|
169
|
+
`cell: the content in the cell, typed as Cell — narrow it before rendering,`
|
|
150
170
|
|
|
151
171
|
`row: the entire row object,`
|
|
152
172
|
|
|
@@ -106,7 +106,11 @@ export const Column = () =>
|
|
|
106
106
|
{
|
|
107
107
|
heading: 'Type',
|
|
108
108
|
dataKey: 'type',
|
|
109
|
-
render: (cell
|
|
109
|
+
render: (cell?: Cell) => (
|
|
110
|
+
<code style={codePreviewStyle}>
|
|
111
|
+
{typeof cell === 'string' || typeof cell === 'number' ? cell : null}
|
|
112
|
+
</code>
|
|
113
|
+
),
|
|
110
114
|
},
|
|
111
115
|
{ heading: 'Description', dataKey: 'description' },
|
|
112
116
|
];
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React
|
|
1
|
+
import React from 'react';
|
|
2
2
|
import classNames from 'classnames';
|
|
3
3
|
import { Column, Row, EventWithColumnKey } from '../../types';
|
|
4
4
|
import { Spinner } from '../Spinner/Spinner';
|
|
@@ -6,20 +6,20 @@ import styles from './Table.module.scss';
|
|
|
6
6
|
import { TableBody } from './TableBody/TableBody';
|
|
7
7
|
import { TableHead } from './TableHead/TableHead';
|
|
8
8
|
|
|
9
|
-
export interface TableProps {
|
|
9
|
+
export interface TableProps<TRow extends object = Row> {
|
|
10
10
|
/**
|
|
11
11
|
* Columns for the table. See Column definition below for details.
|
|
12
12
|
*/
|
|
13
|
-
columns: Column[];
|
|
13
|
+
columns: Column<TRow>[];
|
|
14
14
|
/**
|
|
15
15
|
* The data rows to be displayed
|
|
16
16
|
*/
|
|
17
|
-
rows:
|
|
17
|
+
rows: TRow[];
|
|
18
18
|
/**
|
|
19
19
|
* Key that represents a unique value for a row. This is necessary in
|
|
20
20
|
* order to supply React with a node key on each row.
|
|
21
21
|
*/
|
|
22
|
-
rowKey: string
|
|
22
|
+
rowKey: Extract<keyof TRow, string>;
|
|
23
23
|
/**
|
|
24
24
|
* Additional classes to add.
|
|
25
25
|
*/
|
|
@@ -90,7 +90,7 @@ export interface TableProps {
|
|
|
90
90
|
truncateOverflow?: boolean;
|
|
91
91
|
}
|
|
92
92
|
|
|
93
|
-
export const Table
|
|
93
|
+
export const Table = <TRow extends object = Row,>({
|
|
94
94
|
columns,
|
|
95
95
|
rows,
|
|
96
96
|
rowKey,
|
|
@@ -108,7 +108,7 @@ export const Table: FC<TableProps> = ({
|
|
|
108
108
|
sortedColumn = undefined,
|
|
109
109
|
useFixedTableLayout = false,
|
|
110
110
|
truncateOverflow = false,
|
|
111
|
-
}) => {
|
|
111
|
+
}: TableProps<TRow>) => {
|
|
112
112
|
const containerClasses = classNames(styles.container, {
|
|
113
113
|
[styles['full-height']]: !!isScrollable?.y,
|
|
114
114
|
});
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
-
import { render, screen } from '@testing-library/react';
|
|
2
|
+
import { fireEvent, render, screen } from '@testing-library/react';
|
|
3
|
+
import type { Column } from '../../../types';
|
|
3
4
|
import { TableBody } from './TableBody';
|
|
4
5
|
|
|
5
6
|
const columns = [
|
|
@@ -14,6 +15,18 @@ const rows = [
|
|
|
14
15
|
{ id: 3, color: 'green', flavor: 'strawberry' },
|
|
15
16
|
];
|
|
16
17
|
|
|
18
|
+
const inputColumns: Column<(typeof rows)[number]>[] = [
|
|
19
|
+
{
|
|
20
|
+
dataKey: 'color',
|
|
21
|
+
render: (_cell, row) => (
|
|
22
|
+
<input
|
|
23
|
+
aria-label={`Color for row ${row?.id}`}
|
|
24
|
+
defaultValue={row?.color}
|
|
25
|
+
/>
|
|
26
|
+
),
|
|
27
|
+
},
|
|
28
|
+
];
|
|
29
|
+
|
|
17
30
|
describe('TableBody', () => {
|
|
18
31
|
test("It renders with striped rows if passed 'isStriped' prop", () => {
|
|
19
32
|
render(<TableBody columns={columns} rows={rows} rowKey="id" isStriped />);
|
|
@@ -35,4 +48,79 @@ describe('TableBody', () => {
|
|
|
35
48
|
const tableBody = screen.getByRole('rowgroup');
|
|
36
49
|
expect(tableBody).toHaveClass('my-custom-class');
|
|
37
50
|
});
|
|
51
|
+
|
|
52
|
+
test('It preserves row identity when rows are reordered', () => {
|
|
53
|
+
const { rerender } = render(
|
|
54
|
+
<table>
|
|
55
|
+
<TableBody columns={inputColumns} rows={rows} rowKey="id" />
|
|
56
|
+
</table>
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
fireEvent.change(screen.getByLabelText('Color for row 1'), {
|
|
60
|
+
target: { value: 'purple' },
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
rerender(
|
|
64
|
+
<table>
|
|
65
|
+
<TableBody
|
|
66
|
+
columns={inputColumns}
|
|
67
|
+
rows={[...rows].reverse()}
|
|
68
|
+
rowKey="id"
|
|
69
|
+
/>
|
|
70
|
+
</table>
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
expect(screen.getByLabelText('Color for row 1')).toHaveValue('purple');
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test('It avoids duplicate keys when row key values cannot be used', () => {
|
|
77
|
+
const consoleError = jest.spyOn(console, 'error').mockImplementation();
|
|
78
|
+
const rowsWithoutUsableKeys = [
|
|
79
|
+
{ id: undefined, color: 'red' },
|
|
80
|
+
{ id: undefined, color: 'blue' },
|
|
81
|
+
{ id: null, color: 'green' },
|
|
82
|
+
{ id: null, color: 'yellow' },
|
|
83
|
+
{ id: {}, color: 'black' },
|
|
84
|
+
{ id: {}, color: 'white' },
|
|
85
|
+
];
|
|
86
|
+
|
|
87
|
+
render(
|
|
88
|
+
<table>
|
|
89
|
+
<TableBody
|
|
90
|
+
columns={[{ dataKey: 'color' }]}
|
|
91
|
+
rows={rowsWithoutUsableKeys}
|
|
92
|
+
rowKey="id"
|
|
93
|
+
/>
|
|
94
|
+
</table>
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
const duplicateKeyWarnings = consoleError.mock.calls.filter((call) =>
|
|
98
|
+
call.some(
|
|
99
|
+
(argument) =>
|
|
100
|
+
typeof argument === 'string' &&
|
|
101
|
+
argument.includes('Encountered two children with the same key')
|
|
102
|
+
)
|
|
103
|
+
);
|
|
104
|
+
consoleError.mockRestore();
|
|
105
|
+
|
|
106
|
+
expect(duplicateKeyWarnings).toHaveLength(0);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
test('It handles nullish row values at runtime', () => {
|
|
110
|
+
const rowsWithNullishValues = [undefined, null] as unknown as typeof rows;
|
|
111
|
+
|
|
112
|
+
expect(() =>
|
|
113
|
+
render(
|
|
114
|
+
<table>
|
|
115
|
+
<TableBody
|
|
116
|
+
columns={columns}
|
|
117
|
+
rows={rowsWithNullishValues}
|
|
118
|
+
rowKey="id"
|
|
119
|
+
/>
|
|
120
|
+
</table>
|
|
121
|
+
)
|
|
122
|
+
).not.toThrow();
|
|
123
|
+
|
|
124
|
+
expect(screen.getAllByRole('row')).toHaveLength(2);
|
|
125
|
+
});
|
|
38
126
|
});
|