uikit-react-public 0.11.13 → 0.11.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/Button/Button.d.ts +21 -3
- package/dist/components/Button/Button.stories.d.ts +1 -1
- package/dist/components/Button/buttonPrimaryStyle.d.ts +2 -2
- package/dist/components/Button/buttonSecondaryStyle.d.ts +2 -2
- package/dist/components/Button/buttonTertiaryStyle.d.ts +2 -2
- package/dist/components/Button/index.d.ts +1 -1
- package/dist/components/Select/Select.d.ts +3 -8
- package/dist/components/Select/Select.stories.d.ts +50 -2
- package/dist/components/Select/Select.types.d.ts +122 -0
- package/dist/components/Select/index.d.ts +1 -1
- package/dist/components/Select/subcomponents/CustomOption.d.ts +3 -0
- package/dist/components/Select/subcomponents/CustomSelect.d.ts +3 -0
- package/dist/components/Select/subcomponents/NativeSelect.d.ts +3 -0
- package/dist/components/Select/subcomponents/Panel.d.ts +3 -0
- package/dist/components/Select/subcomponents/VisibleField.d.ts +9 -0
- package/dist/components/Select/subcomponents/index.d.ts +5 -0
- package/dist/index.js +2875 -2549
- package/lib/Welcome.mdx +7 -7
- package/lib/components/Button/Button.tsx +36 -7
- package/lib/components/Button/buttonPrimaryStyle.ts +4 -4
- package/lib/components/Button/buttonSecondaryStyle.ts +4 -4
- package/lib/components/Button/buttonTertiaryStyle.ts +3 -3
- package/lib/components/Button/index.ts +1 -1
- package/lib/components/Icon/svgs/AvatarSvg.tsx +2 -2
- package/lib/components/Icon/svgs/ChevronDownSvg.tsx +2 -5
- package/lib/components/Select/Select.stories.tsx +192 -13
- package/lib/components/Select/Select.tsx +33 -76
- package/lib/components/Select/Select.types.ts +146 -0
- package/lib/components/Select/__tests__/Select.test.tsx +99 -20
- package/lib/components/Select/__tests__/__snapshots__/Select.test.tsx.snap +33 -37
- package/lib/components/Select/index.ts +1 -1
- package/lib/components/Select/subcomponents/CustomOption.tsx +74 -0
- package/lib/components/Select/subcomponents/CustomSelect.tsx +211 -0
- package/lib/components/Select/subcomponents/NativeSelect.tsx +109 -0
- package/lib/components/Select/subcomponents/Panel.tsx +46 -0
- package/lib/components/Select/subcomponents/VisibleField.tsx +69 -0
- package/lib/components/Select/subcomponents/index.tsx +5 -0
- package/package.json +1 -1
- package/dist/components/Button/Button.types.d.ts +0 -26
- package/lib/components/Button/Button.types.ts +0 -46
package/lib/Welcome.mdx
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { Meta } from
|
|
2
|
-
import UclLogo from
|
|
1
|
+
import { Meta } from "@storybook/blocks";
|
|
2
|
+
import { UclLogo } from "./components/";
|
|
3
3
|
|
|
4
4
|
<Meta title="Welcome" />
|
|
5
5
|
|
|
6
6
|
<style>
|
|
7
|
-
{`
|
|
7
|
+
{`
|
|
8
8
|
.logo-container {
|
|
9
9
|
display: flex;
|
|
10
10
|
align-items: flex-end;
|
|
@@ -18,13 +18,13 @@ import UclLogo from './components/UclLogo';
|
|
|
18
18
|
`}
|
|
19
19
|
</style>
|
|
20
20
|
|
|
21
|
-
<div className=
|
|
22
|
-
<UclLogo className=
|
|
21
|
+
<div className="logo-container">
|
|
22
|
+
<UclLogo className="ucl-logo" />
|
|
23
23
|
</div>
|
|
24
24
|
|
|
25
|
-
# UIKit-React
|
|
25
|
+
# UIKit-React
|
|
26
26
|
|
|
27
|
-
This component library of React components implements the UCL Design System.
|
|
27
|
+
This component library of React components implements the UCL Design System.
|
|
28
28
|
|
|
29
29
|
- [GitHub repo](https://github.com/ucl-isd/uikit-react)
|
|
30
30
|
- [Design System Figma](https://www.figma.com/design/8Sm5PxWOWJYpYXAzhRzUzt/UCL-Design-System-UI-Kit)
|
|
@@ -1,16 +1,43 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
useState,
|
|
3
|
+
useRef,
|
|
4
|
+
ReactElement,
|
|
5
|
+
ElementType,
|
|
6
|
+
ComponentPropsWithRef,
|
|
7
|
+
memo,
|
|
8
|
+
} from 'react';
|
|
2
9
|
import { css, cx } from '@emotion/css';
|
|
3
10
|
import useTheme from '../../theme/useTheme';
|
|
4
11
|
import buttonPrimaryStyle from './buttonPrimaryStyle';
|
|
5
12
|
import buttonSecondaryStyle from './buttonSecondaryStyle';
|
|
6
13
|
import buttonTertiaryStyle from './buttonTertiaryStyle';
|
|
7
|
-
import { ButtonProps } from './Button.types';
|
|
8
14
|
import { Spinner, Overlay, Tooltip } from '../';
|
|
9
15
|
import marginsStyle from '../common/marginsStyle';
|
|
10
16
|
|
|
11
17
|
export const NAME = 'ucl-uikit-button';
|
|
12
18
|
|
|
13
|
-
|
|
19
|
+
type PolymorphicRef<C extends ElementType> = ComponentPropsWithRef<C>['ref'];
|
|
20
|
+
|
|
21
|
+
export interface ButtonBaseProps {
|
|
22
|
+
variant?: 'primary' | 'secondary' | 'tertiary';
|
|
23
|
+
destructive?: boolean;
|
|
24
|
+
size?: 'large' | 'default' | 'small';
|
|
25
|
+
disabled?: boolean;
|
|
26
|
+
icon?: ReactElement;
|
|
27
|
+
iconPosition?: 'left' | 'right';
|
|
28
|
+
tooltip?: string;
|
|
29
|
+
loading?: boolean;
|
|
30
|
+
fullWidth?: boolean;
|
|
31
|
+
testId?: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export type ButtonProps<C extends ElementType = 'button'> = {
|
|
35
|
+
as?: C;
|
|
36
|
+
ref?: PolymorphicRef<C>;
|
|
37
|
+
} & ButtonBaseProps &
|
|
38
|
+
Omit<ComponentPropsWithRef<C>, keyof ButtonBaseProps | 'as'>;
|
|
39
|
+
|
|
40
|
+
const Button = <C extends ElementType = 'button'>({
|
|
14
41
|
as,
|
|
15
42
|
variant = 'primary',
|
|
16
43
|
destructive = false,
|
|
@@ -26,7 +53,9 @@ const Button = ({
|
|
|
26
53
|
children,
|
|
27
54
|
className,
|
|
28
55
|
...props
|
|
29
|
-
}: ButtonProps) => {
|
|
56
|
+
}: ButtonProps<C>) => {
|
|
57
|
+
const Component = as || 'button';
|
|
58
|
+
|
|
30
59
|
if (variant === 'tertiary' && destructive) {
|
|
31
60
|
console.warn("Button variant 'tertiary' cannot also be 'destructive'.");
|
|
32
61
|
}
|
|
@@ -99,8 +128,6 @@ const Button = ({
|
|
|
99
128
|
className
|
|
100
129
|
);
|
|
101
130
|
|
|
102
|
-
const Component = as || 'button';
|
|
103
|
-
|
|
104
131
|
return (
|
|
105
132
|
<>
|
|
106
133
|
<Component
|
|
@@ -143,4 +170,6 @@ const Button = ({
|
|
|
143
170
|
);
|
|
144
171
|
};
|
|
145
172
|
|
|
146
|
-
|
|
173
|
+
const MemoizedButton = memo(Button) as typeof Button;
|
|
174
|
+
|
|
175
|
+
export default MemoizedButton;
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { css, cx } from '@emotion/css';
|
|
2
2
|
import { ThemeType } from '../../theme';
|
|
3
|
-
import {
|
|
3
|
+
import { ButtonBaseProps } from './Button';
|
|
4
4
|
|
|
5
5
|
const buttonPrimaryStyle = (
|
|
6
6
|
theme: ThemeType,
|
|
7
|
-
destructive:
|
|
8
|
-
disabled:
|
|
9
|
-
loading:
|
|
7
|
+
destructive: ButtonBaseProps['destructive'],
|
|
8
|
+
disabled: ButtonBaseProps['disabled'],
|
|
9
|
+
loading: ButtonBaseProps['loading']
|
|
10
10
|
) => {
|
|
11
11
|
const {
|
|
12
12
|
color: { system, interaction },
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { css, cx } from '@emotion/css';
|
|
2
2
|
import { ThemeType } from '../../theme';
|
|
3
|
-
import {
|
|
3
|
+
import { ButtonBaseProps } from './Button';
|
|
4
4
|
|
|
5
5
|
const buttonSecondaryStyle = (
|
|
6
6
|
theme: ThemeType,
|
|
7
|
-
destructive:
|
|
8
|
-
disabled:
|
|
9
|
-
loading:
|
|
7
|
+
destructive: ButtonBaseProps['destructive'],
|
|
8
|
+
disabled: ButtonBaseProps['disabled'],
|
|
9
|
+
loading: ButtonBaseProps['loading']
|
|
10
10
|
) => {
|
|
11
11
|
const {
|
|
12
12
|
color: { system, interaction },
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { css, cx } from '@emotion/css';
|
|
2
2
|
import { ThemeType } from '../../theme';
|
|
3
|
-
import {
|
|
3
|
+
import { ButtonBaseProps } from './Button';
|
|
4
4
|
|
|
5
5
|
const buttonTertiaryStyle = (
|
|
6
6
|
theme: ThemeType,
|
|
7
|
-
disabled:
|
|
8
|
-
loading:
|
|
7
|
+
disabled: ButtonBaseProps['disabled'],
|
|
8
|
+
loading: ButtonBaseProps['loading']
|
|
9
9
|
) => {
|
|
10
10
|
const {
|
|
11
11
|
color: { interaction },
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
export { default } from './Button';
|
|
2
|
-
export type { ButtonProps } from './Button
|
|
2
|
+
export type { ButtonProps, ButtonBaseProps } from './Button';
|
|
@@ -13,8 +13,8 @@ const AvatarSvg = ({ ...props }) => {
|
|
|
13
13
|
>
|
|
14
14
|
{title && <title>{title}</title>}
|
|
15
15
|
<path
|
|
16
|
-
|
|
17
|
-
|
|
16
|
+
fillRule='evenodd'
|
|
17
|
+
clipRule='evenodd'
|
|
18
18
|
d='M4 20.9995C4.49232 17.0534 7.8586 14 11.938 14C16.0175 14 19.5077 17.0539 20 21C18 23 15 24 12 24C9 24 6 23 4 20.9995Z'
|
|
19
19
|
/>
|
|
20
20
|
<circle
|
|
@@ -22,11 +22,8 @@ const ChevronDownSvg = ({ ...props }) => {
|
|
|
22
22
|
|
|
23
23
|
export default memo(ChevronDownSvg);
|
|
24
24
|
|
|
25
|
-
// Used by Select
|
|
25
|
+
// Used by <Select>
|
|
26
26
|
const DATA_URI =
|
|
27
27
|
'data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2024%2024%22%20fill%3D%22none%22%20stroke%3D%22{{COLOUR}}%22%20stroke-width%3D%222%22%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%20width%3D%2224%22%20height%3D%2224%22%20class%3D%22ucl-icon%20css-4bxmzw%22%3E%3Cpolyline%20points%3D%226%209%2012%2015%2018%209%22%3E%3C%2Fpolyline%3E%3C%2Fsvg%3E';
|
|
28
28
|
export const dataUri = (colour: string) =>
|
|
29
|
-
DATA_URI.replace(
|
|
30
|
-
'{{COLOUR}}',
|
|
31
|
-
encodeURIComponent(colour)
|
|
32
|
-
);
|
|
29
|
+
DATA_URI.replace('{{COLOUR}}', encodeURIComponent(colour));
|
|
@@ -1,27 +1,206 @@
|
|
|
1
1
|
import type { Meta, StoryObj } from '@storybook/react';
|
|
2
|
-
|
|
2
|
+
import { useArgs } from '@storybook/preview-api';
|
|
3
3
|
import Select from './Select';
|
|
4
|
+
import { Icon } from '../';
|
|
5
|
+
import { SelectEvent } from './Select.types';
|
|
4
6
|
|
|
5
7
|
const meta = {
|
|
6
|
-
title: 'Components/
|
|
8
|
+
title: 'Components/Ready to use/Select',
|
|
7
9
|
component: Select,
|
|
10
|
+
argTypes: {
|
|
11
|
+
value: { control: { type: 'text' } },
|
|
12
|
+
native: {
|
|
13
|
+
control: { type: 'boolean' },
|
|
14
|
+
},
|
|
15
|
+
disabled: {
|
|
16
|
+
control: { type: 'boolean' },
|
|
17
|
+
},
|
|
18
|
+
placeholder: {
|
|
19
|
+
control: { type: 'text' },
|
|
20
|
+
},
|
|
21
|
+
testId: {
|
|
22
|
+
control: { type: 'text' },
|
|
23
|
+
},
|
|
24
|
+
width: {
|
|
25
|
+
control: { type: 'range', min: 80, max: 624, step: 1 },
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
args: {
|
|
29
|
+
value: undefined,
|
|
30
|
+
onChange: () => {},
|
|
31
|
+
options: [
|
|
32
|
+
{ text: 'Option one', value: '1' },
|
|
33
|
+
{ text: 'Option two', value: '2' },
|
|
34
|
+
{ text: 'Option three', value: '3' },
|
|
35
|
+
],
|
|
36
|
+
},
|
|
8
37
|
} satisfies Meta<typeof Select>;
|
|
9
38
|
|
|
10
39
|
export default meta;
|
|
11
|
-
|
|
12
40
|
type Story = StoryObj<typeof meta>;
|
|
13
41
|
|
|
14
42
|
export const Default: Story = {
|
|
15
|
-
render: () =>
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
43
|
+
render: () => {
|
|
44
|
+
const [args, updateArgs] = useArgs();
|
|
45
|
+
const onChange = (_event: SelectEvent, value: string) => {
|
|
46
|
+
updateArgs({ value });
|
|
47
|
+
};
|
|
48
|
+
return (
|
|
49
|
+
<Select
|
|
50
|
+
{...args}
|
|
51
|
+
options={args.options}
|
|
52
|
+
value={args.value}
|
|
53
|
+
onChange={onChange}
|
|
54
|
+
/>
|
|
55
|
+
);
|
|
56
|
+
},
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
export const Native: Story = {
|
|
60
|
+
args: {
|
|
61
|
+
native: true,
|
|
62
|
+
},
|
|
63
|
+
render: () => {
|
|
64
|
+
const [args, updateArgs] = useArgs();
|
|
65
|
+
const onChange = (_event: SelectEvent, value: string) => {
|
|
66
|
+
updateArgs({ value });
|
|
67
|
+
};
|
|
68
|
+
return (
|
|
69
|
+
<Select
|
|
70
|
+
{...args}
|
|
71
|
+
options={args.options}
|
|
72
|
+
value={args.value}
|
|
73
|
+
onChange={onChange}
|
|
74
|
+
/>
|
|
75
|
+
);
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
export const OptionsWithIcons: Story = {
|
|
80
|
+
name: 'Options with icons',
|
|
81
|
+
args: {
|
|
82
|
+
options: [
|
|
83
|
+
{ text: 'Option 1', value: '1', icon: <Icon.Printer /> },
|
|
84
|
+
{ text: 'Option 2', value: '2', icon: <Icon.User /> },
|
|
85
|
+
{ text: 'Option 3', value: '3', icon: <Icon.Info /> },
|
|
86
|
+
],
|
|
87
|
+
},
|
|
88
|
+
render: () => {
|
|
89
|
+
const [args, updateArgs] = useArgs();
|
|
90
|
+
const onChange = (_event: SelectEvent, value: string) => {
|
|
91
|
+
updateArgs({ value });
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
return (
|
|
95
|
+
<Select
|
|
96
|
+
{...args}
|
|
97
|
+
options={args.options}
|
|
98
|
+
value={args.value}
|
|
99
|
+
onChange={onChange}
|
|
100
|
+
/>
|
|
101
|
+
);
|
|
102
|
+
},
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
export const Disabled: Story = {
|
|
106
|
+
args: {
|
|
107
|
+
disabled: true,
|
|
108
|
+
},
|
|
109
|
+
render: () => {
|
|
110
|
+
const [args, updateArgs] = useArgs();
|
|
111
|
+
const onChange = (_event: SelectEvent, value: string) => {
|
|
112
|
+
updateArgs({ value });
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
return (
|
|
116
|
+
<Select
|
|
117
|
+
{...args}
|
|
118
|
+
options={args.options}
|
|
119
|
+
value={args.value}
|
|
120
|
+
onChange={onChange}
|
|
121
|
+
/>
|
|
122
|
+
);
|
|
123
|
+
},
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
export const WithPlaceholder: Story = {
|
|
127
|
+
args: { placeholder: 'Please select an option' },
|
|
128
|
+
render: () => {
|
|
129
|
+
const [args, updateArgs] = useArgs();
|
|
130
|
+
const onChange = (_event: SelectEvent, value: string) => {
|
|
131
|
+
updateArgs({ value });
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
return (
|
|
135
|
+
<Select
|
|
136
|
+
{...args}
|
|
137
|
+
options={args.options}
|
|
138
|
+
value={args.value}
|
|
139
|
+
onChange={onChange}
|
|
140
|
+
/>
|
|
141
|
+
);
|
|
142
|
+
},
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
export const SingleLongOption: Story = {
|
|
146
|
+
name: 'Long option',
|
|
147
|
+
args: {
|
|
148
|
+
options: [
|
|
149
|
+
{ text: 'Option 1', value: '1' },
|
|
150
|
+
{ text: 'Option 2', value: '2' },
|
|
151
|
+
{ text: 'Option 3 long long long long ', value: '3' },
|
|
152
|
+
],
|
|
153
|
+
},
|
|
154
|
+
render: () => {
|
|
155
|
+
const [args, updateArgs] = useArgs();
|
|
156
|
+
const onChange = (_event: SelectEvent, value: string) => {
|
|
157
|
+
updateArgs({ value });
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
return (
|
|
161
|
+
<Select
|
|
162
|
+
{...args}
|
|
163
|
+
options={args.options}
|
|
164
|
+
value={args.value}
|
|
165
|
+
onChange={onChange}
|
|
166
|
+
/>
|
|
167
|
+
);
|
|
168
|
+
},
|
|
22
169
|
};
|
|
23
170
|
|
|
24
|
-
export const
|
|
25
|
-
name: '
|
|
26
|
-
|
|
171
|
+
export const ManyOptions: Story = {
|
|
172
|
+
name: 'Many options',
|
|
173
|
+
args: {
|
|
174
|
+
options: [
|
|
175
|
+
{ text: 'Option one', value: '1' },
|
|
176
|
+
{ text: 'Option two', value: '2' },
|
|
177
|
+
{ text: 'Option three', value: '3' },
|
|
178
|
+
{ text: 'Option four', value: '4' },
|
|
179
|
+
{ text: 'Option five', value: '5' },
|
|
180
|
+
{ text: 'Option six', value: '6' },
|
|
181
|
+
{ text: 'Option seven', value: '7' },
|
|
182
|
+
{ text: 'Option eight', value: '8' },
|
|
183
|
+
{ text: 'Option nine', value: '9' },
|
|
184
|
+
{ text: 'Option ten', value: '10' },
|
|
185
|
+
{ text: 'Option eleven', value: '11' },
|
|
186
|
+
{ text: 'Option twelve', value: '12' },
|
|
187
|
+
{ text: 'Option thirteen', value: '13' },
|
|
188
|
+
{ text: 'Option fourteen', value: '14' },
|
|
189
|
+
{ text: 'Option fifteen', value: '15' },
|
|
190
|
+
],
|
|
191
|
+
},
|
|
192
|
+
render: () => {
|
|
193
|
+
const [args, updateArgs] = useArgs();
|
|
194
|
+
const onChange = (_event: SelectEvent, value: string) => {
|
|
195
|
+
updateArgs({ value });
|
|
196
|
+
};
|
|
197
|
+
return (
|
|
198
|
+
<Select
|
|
199
|
+
{...args}
|
|
200
|
+
options={args.options}
|
|
201
|
+
value={args.value}
|
|
202
|
+
onChange={onChange}
|
|
203
|
+
/>
|
|
204
|
+
);
|
|
205
|
+
},
|
|
27
206
|
};
|
|
@@ -1,82 +1,39 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
({ testId = NAME, className, ...props }, ref) => {
|
|
21
|
-
const [theme] = useTheme();
|
|
22
|
-
|
|
23
|
-
const baseStyle = css`
|
|
24
|
-
height: ${theme.padding.p48};
|
|
25
|
-
line-height: ${theme.padding.p48};
|
|
26
|
-
font-family: ${theme.font.family.primary};
|
|
27
|
-
padding: 0 ${theme.padding.p40} 0 ${theme.padding.p16};
|
|
28
|
-
background-color: ${theme.color.neutral.white};
|
|
29
|
-
border: ${theme.border.b1} solid
|
|
30
|
-
${theme.color.neutral.grey40};
|
|
31
|
-
appearance: none;
|
|
32
|
-
-webkit-appearance: none;
|
|
33
|
-
-moz-appearance: none;
|
|
34
|
-
outline: none;
|
|
35
|
-
cursor: pointer;
|
|
36
|
-
|
|
37
|
-
background-image: url(${chevronDownSvgDataUri(
|
|
38
|
-
theme.color.interaction.blue70
|
|
39
|
-
)});
|
|
40
|
-
background-size: 24px 24px;
|
|
41
|
-
background-position: right 8px center;
|
|
42
|
-
background-repeat: no-repeat;
|
|
43
|
-
`;
|
|
44
|
-
|
|
45
|
-
const hoverStyle = css`
|
|
46
|
-
&:hover {
|
|
47
|
-
border-color: ${theme.color.neutral.grey60};
|
|
48
|
-
background-color: ${theme.color.neutral.grey5};
|
|
49
|
-
}
|
|
50
|
-
`;
|
|
51
|
-
|
|
52
|
-
const focusStyle = css`
|
|
53
|
-
&:focus {
|
|
54
|
-
box-shadow: ${theme.boxShadow.focus};
|
|
55
|
-
}
|
|
56
|
-
`;
|
|
57
|
-
|
|
58
|
-
const disabledStyle = css`
|
|
59
|
-
cursor: not-allowed;
|
|
60
|
-
`;
|
|
61
|
-
|
|
62
|
-
const style = cx(
|
|
63
|
-
NAME,
|
|
64
|
-
baseStyle,
|
|
65
|
-
!props.disabled && hoverStyle,
|
|
66
|
-
!props.disabled && focusStyle,
|
|
67
|
-
props.disabled && disabledStyle,
|
|
68
|
-
className
|
|
1
|
+
import { NativeSelect, CustomSelect } from './subcomponents';
|
|
2
|
+
import { SelectProps } from './Select.types';
|
|
3
|
+
|
|
4
|
+
const Select = (props: SelectProps) => {
|
|
5
|
+
if (props.native) {
|
|
6
|
+
// We can throw away `native` prop before passing to internal components
|
|
7
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
8
|
+
const { onChange, value, native, ...rest } = props;
|
|
9
|
+
const handleNativeChange = (
|
|
10
|
+
event: React.ChangeEvent<HTMLSelectElement>
|
|
11
|
+
) => {
|
|
12
|
+
if (onChange) onChange(event, event.target.value);
|
|
13
|
+
};
|
|
14
|
+
return (
|
|
15
|
+
<NativeSelect
|
|
16
|
+
onChange={handleNativeChange}
|
|
17
|
+
value={value || ''}
|
|
18
|
+
{...rest}
|
|
19
|
+
/>
|
|
69
20
|
);
|
|
70
|
-
|
|
21
|
+
} else {
|
|
22
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
23
|
+
const { onChange, native, ...rest } = props;
|
|
24
|
+
const handleCustomChange = (
|
|
25
|
+
event: React.MouseEvent | React.KeyboardEvent,
|
|
26
|
+
value: string
|
|
27
|
+
) => {
|
|
28
|
+
if (onChange) onChange(event, value);
|
|
29
|
+
};
|
|
71
30
|
return (
|
|
72
|
-
<
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
data-testid={testId}
|
|
76
|
-
{...props}
|
|
31
|
+
<CustomSelect
|
|
32
|
+
onChange={handleCustomChange}
|
|
33
|
+
{...rest}
|
|
77
34
|
/>
|
|
78
35
|
);
|
|
79
36
|
}
|
|
80
|
-
|
|
37
|
+
};
|
|
81
38
|
|
|
82
|
-
export default
|
|
39
|
+
export default Select;
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Represents an 'option' in the <Select>
|
|
3
|
+
* Maps to a native <option> element in <NativeSelect>
|
|
4
|
+
* or a <CustomOption> in <CustomSelect>
|
|
5
|
+
*/
|
|
6
|
+
export type OptionData = {
|
|
7
|
+
/**
|
|
8
|
+
* Display text shown to the user
|
|
9
|
+
*/
|
|
10
|
+
text: string;
|
|
11
|
+
/**
|
|
12
|
+
* Data-friendly value that is returned when the option is selected
|
|
13
|
+
* We assume this will be submitted to a server or used in some other way
|
|
14
|
+
*/
|
|
15
|
+
value: string;
|
|
16
|
+
/**
|
|
17
|
+
* Optional icon to be displayed to the left of the option text.
|
|
18
|
+
* Only used by <CustomOption>
|
|
19
|
+
* Takes in an actual <Icon> component
|
|
20
|
+
*/
|
|
21
|
+
icon?: React.ReactNode;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Utility type we expose to developers
|
|
26
|
+
* We expect this to be used in typing event handlers
|
|
27
|
+
* This only applies to the top-level <Select> component
|
|
28
|
+
*/
|
|
29
|
+
export type SelectEvent =
|
|
30
|
+
| React.ChangeEvent<HTMLSelectElement>
|
|
31
|
+
| React.MouseEvent
|
|
32
|
+
| React.KeyboardEvent;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Top level props that <Select> accepts when implemented
|
|
36
|
+
*/
|
|
37
|
+
interface BaseSelectProps {
|
|
38
|
+
/**
|
|
39
|
+
* An array of option data, to be rendered either natively or custom
|
|
40
|
+
*/
|
|
41
|
+
options: OptionData[];
|
|
42
|
+
/**
|
|
43
|
+
* The currently selected value
|
|
44
|
+
* This determines which option is shown when the select is closed
|
|
45
|
+
*/
|
|
46
|
+
value: string | undefined | null;
|
|
47
|
+
/**
|
|
48
|
+
* Generic onChange that splits into native and custom versions
|
|
49
|
+
* The original event is exposed, and the value always returns the value of the selected option
|
|
50
|
+
*/
|
|
51
|
+
onChange: (event: SelectEvent, value: string) => void;
|
|
52
|
+
/**
|
|
53
|
+
* Prevents use, including focus events
|
|
54
|
+
*/
|
|
55
|
+
disabled?: boolean | undefined;
|
|
56
|
+
/**
|
|
57
|
+
* Placeholder text shown when no option is selected
|
|
58
|
+
* Displayed in visible field of custom implementation
|
|
59
|
+
* or as a disabled option in the native implementation
|
|
60
|
+
*/
|
|
61
|
+
placeholder?: string;
|
|
62
|
+
/**
|
|
63
|
+
* Specify a specific width if the default (325px) is not suitable
|
|
64
|
+
*/
|
|
65
|
+
width?: number;
|
|
66
|
+
/**
|
|
67
|
+
* Test ID for testing purposes
|
|
68
|
+
* This is passed to the root element of the component
|
|
69
|
+
* as `data-testid`
|
|
70
|
+
*/
|
|
71
|
+
testId?: string;
|
|
72
|
+
/**
|
|
73
|
+
* Optional className for styling
|
|
74
|
+
* This is passed to the root element of the component
|
|
75
|
+
* for additional CSS styling via Emotion.
|
|
76
|
+
*/
|
|
77
|
+
className?: string;
|
|
78
|
+
/**
|
|
79
|
+
* Native flag determines which implementation to use
|
|
80
|
+
*/
|
|
81
|
+
native?: boolean;
|
|
82
|
+
// `ref` prop added in the discriminated union below
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export type SelectProps = BaseSelectProps &
|
|
86
|
+
// Discriminated union to determine which implementation to use
|
|
87
|
+
(| ({ native: true; ref?: React.RefObject<HTMLSelectElement | null> } & Omit<
|
|
88
|
+
React.SelectHTMLAttributes<HTMLSelectElement>,
|
|
89
|
+
keyof BaseSelectProps
|
|
90
|
+
>)
|
|
91
|
+
| ({ native?: false; ref?: React.RefObject<HTMLDivElement | null> } & Omit<
|
|
92
|
+
React.HTMLAttributes<HTMLDivElement>,
|
|
93
|
+
keyof BaseSelectProps
|
|
94
|
+
>)
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
// Props interface for the two variants are separated.
|
|
98
|
+
// We expose SelectProps for developers to use, and handle discrepancies internally.
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Internal props for the custom implementation, with <div> as root element
|
|
102
|
+
* onChange already exists on <div>. We override it.
|
|
103
|
+
*/
|
|
104
|
+
export interface CustomSelectProps
|
|
105
|
+
extends Omit<React.HTMLAttributes<HTMLDivElement>, 'onChange'> {
|
|
106
|
+
options: OptionData[];
|
|
107
|
+
value: string | undefined | null;
|
|
108
|
+
onChange: (
|
|
109
|
+
// Stripping out the native <select> event signature
|
|
110
|
+
event: React.MouseEvent | React.KeyboardEvent,
|
|
111
|
+
// Value returned by <CustomOption>: we validate there
|
|
112
|
+
value: string
|
|
113
|
+
) => void;
|
|
114
|
+
disabled?: boolean;
|
|
115
|
+
placeholder?: string;
|
|
116
|
+
width?: number;
|
|
117
|
+
testId?: string;
|
|
118
|
+
className?: string;
|
|
119
|
+
ref?: React.RefObject<HTMLDivElement | null>;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Internal props for native implementation, with <select> as root element
|
|
124
|
+
* Default props like value and onChange are passed to the <select> element automatically
|
|
125
|
+
*/
|
|
126
|
+
export interface NativeSelectProps
|
|
127
|
+
extends React.SelectHTMLAttributes<HTMLSelectElement> {
|
|
128
|
+
options: OptionData[];
|
|
129
|
+
placeholder?: string;
|
|
130
|
+
width?: number;
|
|
131
|
+
testId?: string;
|
|
132
|
+
className?: string;
|
|
133
|
+
ref?: React.RefObject<HTMLSelectElement | null>;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Each option as displayed in the Panel of <CustomSelect>
|
|
138
|
+
* Roughly equivalent to a custom version of <option>
|
|
139
|
+
*/
|
|
140
|
+
export interface CustomOptionProps
|
|
141
|
+
extends Omit<React.HTMLAttributes<HTMLDivElement>, 'onSelect'> {
|
|
142
|
+
value: string;
|
|
143
|
+
testId?: string;
|
|
144
|
+
isSelected?: boolean;
|
|
145
|
+
onSelect: (event: React.MouseEvent, value: string) => void;
|
|
146
|
+
}
|