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
|
@@ -1,40 +1,119 @@
|
|
|
1
1
|
import { describe, expect, test } from 'vitest';
|
|
2
2
|
import { render } from '@testing-library/react';
|
|
3
|
+
import userEvent from '@testing-library/user-event';
|
|
3
4
|
import Select from '../Select';
|
|
4
5
|
import { ThemeContextProvider } from '../../../theme/useTheme';
|
|
5
6
|
|
|
7
|
+
const defaultOptions = [
|
|
8
|
+
{ text: 'Option 1', value: '1' },
|
|
9
|
+
{ text: 'Option 2', value: '2' },
|
|
10
|
+
{ text: 'Option 3', value: '3' },
|
|
11
|
+
];
|
|
12
|
+
|
|
6
13
|
describe('Select', () => {
|
|
7
14
|
// Snapshot tests
|
|
8
15
|
|
|
9
|
-
test('
|
|
16
|
+
test('Snapshot: default', () => {
|
|
17
|
+
const renderResult = render(
|
|
18
|
+
<ThemeContextProvider>
|
|
19
|
+
<Select
|
|
20
|
+
options={defaultOptions}
|
|
21
|
+
value=''
|
|
22
|
+
onChange={() => {}}
|
|
23
|
+
/>
|
|
24
|
+
</ThemeContextProvider>
|
|
25
|
+
);
|
|
26
|
+
expect(renderResult.container.firstChild).toMatchSnapshot();
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
// Interation tests
|
|
30
|
+
test('Can be found by default testId', () => {
|
|
31
|
+
const defaultTestId = 'ucl-uikit-select';
|
|
32
|
+
|
|
33
|
+
const renderResult = render(
|
|
34
|
+
<ThemeContextProvider>
|
|
35
|
+
<Select
|
|
36
|
+
options={defaultOptions}
|
|
37
|
+
value=''
|
|
38
|
+
onChange={() => {}}
|
|
39
|
+
/>
|
|
40
|
+
</ThemeContextProvider>
|
|
41
|
+
);
|
|
42
|
+
const select = renderResult.getByTestId(defaultTestId);
|
|
43
|
+
expect(select).toBeDefined();
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test('Returns a native select when native prop is true', () => {
|
|
47
|
+
const renderResult = render(
|
|
48
|
+
<ThemeContextProvider>
|
|
49
|
+
<Select
|
|
50
|
+
native
|
|
51
|
+
options={defaultOptions}
|
|
52
|
+
value=''
|
|
53
|
+
onChange={() => {}}
|
|
54
|
+
/>
|
|
55
|
+
</ThemeContextProvider>
|
|
56
|
+
);
|
|
57
|
+
const select = renderResult.getByTestId('ucl-uikit-select--native');
|
|
58
|
+
expect(select).toBeDefined();
|
|
59
|
+
expect(select.tagName).toBe('SELECT');
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test('Opens the panel when clicked', async () => {
|
|
63
|
+
const user = userEvent.setup();
|
|
64
|
+
const renderResult = render(
|
|
65
|
+
<ThemeContextProvider>
|
|
66
|
+
<Select
|
|
67
|
+
options={defaultOptions}
|
|
68
|
+
value=''
|
|
69
|
+
onChange={() => {}}
|
|
70
|
+
/>
|
|
71
|
+
</ThemeContextProvider>
|
|
72
|
+
);
|
|
73
|
+
const select = renderResult.getByTestId('ucl-uikit-select');
|
|
74
|
+
await user.click(select);
|
|
75
|
+
// Panel should open
|
|
76
|
+
const panel = renderResult.getByTestId('ucl-uikit-select__panel');
|
|
77
|
+
expect(panel).toBeInTheDocument();
|
|
78
|
+
// Options should be present
|
|
79
|
+
const options = renderResult.getAllByTestId('ucl-uikit-select__option');
|
|
80
|
+
expect(options.length).toBe(defaultOptions.length);
|
|
81
|
+
expect(options[0].textContent).toBe(defaultOptions[0].text);
|
|
82
|
+
expect(options[1].textContent).toBe(defaultOptions[1].text);
|
|
83
|
+
expect(options[2].textContent).toBe(defaultOptions[2].text);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test('Cannot be used when disabled', async () => {
|
|
87
|
+
const user = userEvent.setup();
|
|
10
88
|
const renderResult = render(
|
|
11
89
|
<ThemeContextProvider>
|
|
12
|
-
<Select
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
90
|
+
<Select
|
|
91
|
+
disabled
|
|
92
|
+
options={defaultOptions}
|
|
93
|
+
value=''
|
|
94
|
+
onChange={() => {}}
|
|
95
|
+
/>
|
|
18
96
|
</ThemeContextProvider>
|
|
19
97
|
);
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
98
|
+
const select = renderResult.getByTestId('ucl-uikit-select');
|
|
99
|
+
await user.click(select);
|
|
100
|
+
// Panel should not open
|
|
101
|
+
const panel = renderResult.queryByTestId('ucl-uikit-select__panel');
|
|
102
|
+
expect(panel).not.toBeInTheDocument();
|
|
23
103
|
});
|
|
24
104
|
|
|
25
|
-
test('
|
|
105
|
+
test('Cannot be used when native and disabled', async () => {
|
|
26
106
|
const renderResult = render(
|
|
27
107
|
<ThemeContextProvider>
|
|
28
|
-
<Select
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
108
|
+
<Select
|
|
109
|
+
native
|
|
110
|
+
options={defaultOptions}
|
|
111
|
+
value='1'
|
|
112
|
+
onChange={() => {}}
|
|
113
|
+
/>
|
|
34
114
|
</ThemeContextProvider>
|
|
35
115
|
);
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
).toMatchSnapshot();
|
|
116
|
+
const select = renderResult.getByTestId('ucl-uikit-select--native');
|
|
117
|
+
expect(select).toHaveProperty('disabled');
|
|
39
118
|
});
|
|
40
119
|
});
|
|
@@ -1,42 +1,38 @@
|
|
|
1
1
|
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
|
2
2
|
|
|
3
|
-
exports[`Select >
|
|
4
|
-
<
|
|
5
|
-
|
|
3
|
+
exports[`Select > Snapshot: default 1`] = `
|
|
4
|
+
<div
|
|
5
|
+
aria-expanded="false"
|
|
6
|
+
aria-haspopup="listbox"
|
|
7
|
+
class="ucl-uikit-select css-1ur0y0n"
|
|
6
8
|
data-testid="ucl-uikit-select"
|
|
7
|
-
|
|
9
|
+
role="combobox"
|
|
10
|
+
tabindex="0"
|
|
8
11
|
>
|
|
9
|
-
<
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
>
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
<option>
|
|
36
|
-
Option 3
|
|
37
|
-
</option>
|
|
38
|
-
<option>
|
|
39
|
-
Option 4
|
|
40
|
-
</option>
|
|
41
|
-
</select>
|
|
12
|
+
<div
|
|
13
|
+
class="ucl-uikit-select__visible-field css-4o4quu"
|
|
14
|
+
data-testid="ucl-uikit-select__visible-field"
|
|
15
|
+
>
|
|
16
|
+
<span
|
|
17
|
+
class="css-1oagzrk"
|
|
18
|
+
/>
|
|
19
|
+
<svg
|
|
20
|
+
class="ucl-uikit-icon css-1ieo112"
|
|
21
|
+
data-testid="ucl-uikit-icon"
|
|
22
|
+
fill="none"
|
|
23
|
+
height="24"
|
|
24
|
+
stroke="currentColor"
|
|
25
|
+
stroke-linecap="round"
|
|
26
|
+
stroke-linejoin="round"
|
|
27
|
+
stroke-width="2"
|
|
28
|
+
viewBox="0 0 24 24"
|
|
29
|
+
width="24"
|
|
30
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
31
|
+
>
|
|
32
|
+
<polyline
|
|
33
|
+
points="6 9 12 15 18 9"
|
|
34
|
+
/>
|
|
35
|
+
</svg>
|
|
36
|
+
</div>
|
|
37
|
+
</div>
|
|
42
38
|
`;
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
export { default } from './Select';
|
|
2
|
-
export type { SelectProps } from './Select';
|
|
2
|
+
export type { SelectProps, OptionData, SelectEvent } from './Select.types';
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { useEffect, useRef } from 'react';
|
|
2
|
+
import { css, cx } from '@emotion/css';
|
|
3
|
+
import { useTheme } from '../../../theme';
|
|
4
|
+
import { CustomOptionProps } from '../Select.types';
|
|
5
|
+
|
|
6
|
+
const NAME = 'ucl-uikit-select__option';
|
|
7
|
+
|
|
8
|
+
const CustomOption = ({
|
|
9
|
+
value,
|
|
10
|
+
isSelected = false,
|
|
11
|
+
onSelect,
|
|
12
|
+
testId = NAME,
|
|
13
|
+
className,
|
|
14
|
+
children,
|
|
15
|
+
...props
|
|
16
|
+
}: CustomOptionProps) => {
|
|
17
|
+
const [theme] = useTheme();
|
|
18
|
+
const internalRef = useRef<HTMLDivElement>(null);
|
|
19
|
+
|
|
20
|
+
// Scroll into view when selected & the Panel opens
|
|
21
|
+
useEffect(() => {
|
|
22
|
+
if (isSelected && internalRef.current) {
|
|
23
|
+
internalRef.current.scrollIntoView({
|
|
24
|
+
behavior: 'auto',
|
|
25
|
+
block: 'nearest',
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
}, [isSelected]);
|
|
29
|
+
|
|
30
|
+
const handleClick = (event: React.MouseEvent<HTMLDivElement>) => {
|
|
31
|
+
onSelect(event, value);
|
|
32
|
+
event.stopPropagation(); // Otherwise the panel will open again instantaneously
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const baseStyle = css`
|
|
36
|
+
display: flex;
|
|
37
|
+
align-items: center;
|
|
38
|
+
justify-content: left;
|
|
39
|
+
gap: 16px;
|
|
40
|
+
width: 100%;
|
|
41
|
+
min-height: 40px;
|
|
42
|
+
box-sizing: border-box;
|
|
43
|
+
padding: 8px 16px;
|
|
44
|
+
font-family: ${theme.font.family.primary};
|
|
45
|
+
font-size: ${theme.font.size.f16};
|
|
46
|
+
background-color: ${theme.color.neutral.white};
|
|
47
|
+
overflow: hidden;
|
|
48
|
+
white-space: nowrap;
|
|
49
|
+
|
|
50
|
+
&:hover {
|
|
51
|
+
background-color: ${theme.color.neutral.grey5};
|
|
52
|
+
}
|
|
53
|
+
`;
|
|
54
|
+
|
|
55
|
+
const selectedStyle = css`
|
|
56
|
+
background-color: ${theme.color.neutral.grey5};
|
|
57
|
+
`;
|
|
58
|
+
|
|
59
|
+
const style = cx(NAME, baseStyle, isSelected && selectedStyle, className);
|
|
60
|
+
|
|
61
|
+
return (
|
|
62
|
+
<div
|
|
63
|
+
onClick={handleClick}
|
|
64
|
+
className={style}
|
|
65
|
+
data-testid={testId}
|
|
66
|
+
ref={internalRef}
|
|
67
|
+
{...props}
|
|
68
|
+
>
|
|
69
|
+
{children}
|
|
70
|
+
</div>
|
|
71
|
+
);
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
export default CustomOption;
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import { useState, useRef, useEffect } from 'react';
|
|
2
|
+
import { css, cx } from '@emotion/css';
|
|
3
|
+
import { VisibleField, Panel, CustomOption } from '.';
|
|
4
|
+
import { useTheme } from '../../../theme';
|
|
5
|
+
import type { CustomSelectProps } from '../Select.types';
|
|
6
|
+
|
|
7
|
+
const NAME = 'ucl-uikit-select';
|
|
8
|
+
|
|
9
|
+
const CustomSelect = ({
|
|
10
|
+
value,
|
|
11
|
+
options = [],
|
|
12
|
+
onChange,
|
|
13
|
+
disabled,
|
|
14
|
+
placeholder,
|
|
15
|
+
width = 325,
|
|
16
|
+
testId = NAME,
|
|
17
|
+
className,
|
|
18
|
+
ref,
|
|
19
|
+
...props
|
|
20
|
+
}: CustomSelectProps) => {
|
|
21
|
+
const internalRef = useRef<HTMLDivElement>(null);
|
|
22
|
+
const effectiveRef = ref || internalRef;
|
|
23
|
+
|
|
24
|
+
const [theme] = useTheme();
|
|
25
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
26
|
+
|
|
27
|
+
useEffect(() => {
|
|
28
|
+
const handleClickOutside = (event: MouseEvent) => {
|
|
29
|
+
if (
|
|
30
|
+
effectiveRef.current &&
|
|
31
|
+
!effectiveRef.current.contains(event.target as Node)
|
|
32
|
+
) {
|
|
33
|
+
setIsOpen(false);
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
document.addEventListener('mousedown', handleClickOutside);
|
|
37
|
+
return () => {
|
|
38
|
+
document.removeEventListener('mousedown', handleClickOutside);
|
|
39
|
+
};
|
|
40
|
+
}, [effectiveRef]);
|
|
41
|
+
|
|
42
|
+
useEffect(() => {
|
|
43
|
+
// Close the dropdown if it becomes disabled
|
|
44
|
+
if (disabled && isOpen) setIsOpen(false);
|
|
45
|
+
}, [disabled, isOpen]);
|
|
46
|
+
|
|
47
|
+
const togglePanel = () => {
|
|
48
|
+
if (!disabled) setIsOpen((prev) => !prev);
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const openPanel = () => {
|
|
52
|
+
if (!disabled) setIsOpen(true);
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const closePanel = () => {
|
|
56
|
+
if (!disabled) setIsOpen(false);
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
// Used by <CustomOption> and passed as prop
|
|
60
|
+
const handleSelect = (event: React.MouseEvent, optionValue: string) => {
|
|
61
|
+
if (onChange) onChange(event, optionValue);
|
|
62
|
+
closePanel();
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const selectedOption = options.find((option) => option.value === value);
|
|
66
|
+
|
|
67
|
+
const handleKeyDown = (event: React.KeyboardEvent) => {
|
|
68
|
+
// Prevent scrolling the page when the select is open
|
|
69
|
+
if (
|
|
70
|
+
event.key === 'ArrowUp' ||
|
|
71
|
+
event.key === 'ArrowDown' ||
|
|
72
|
+
event.key === 'ArrowLeft' ||
|
|
73
|
+
event.key === 'ArrowRight'
|
|
74
|
+
)
|
|
75
|
+
event.preventDefault();
|
|
76
|
+
|
|
77
|
+
if (disabled) return;
|
|
78
|
+
|
|
79
|
+
if (event.key === 'Enter') {
|
|
80
|
+
togglePanel();
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
if (!isOpen && event.code === 'Space') {
|
|
84
|
+
openPanel();
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
if (isOpen && event.key === 'Escape') {
|
|
88
|
+
closePanel();
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
// Select the previous option
|
|
92
|
+
if (event.key === 'ArrowUp' || event.key === 'ArrowLeft') {
|
|
93
|
+
if (!isOpen) {
|
|
94
|
+
openPanel();
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
if (!value) {
|
|
98
|
+
// Initialise at the last option if no value provided
|
|
99
|
+
onChange(event, options[options.length - 1].value);
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
const currentOptionIndex = options.findIndex(
|
|
103
|
+
(option) => option.value === value
|
|
104
|
+
);
|
|
105
|
+
const previousOptionIndex =
|
|
106
|
+
(currentOptionIndex - 1 + options.length) % options.length;
|
|
107
|
+
onChange(event, options[previousOptionIndex].value);
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
// Select the next option
|
|
111
|
+
if (event.key === 'ArrowDown' || event.key === 'ArrowRight') {
|
|
112
|
+
if (!isOpen) {
|
|
113
|
+
openPanel();
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
if (!value) {
|
|
117
|
+
// Initialise at the first option if no value provided
|
|
118
|
+
onChange(event, options[0].value);
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
const currentOptionIndex = options.findIndex(
|
|
122
|
+
(option) => option.value === value
|
|
123
|
+
);
|
|
124
|
+
const nextOptionIndex = (currentOptionIndex + 1) % options.length;
|
|
125
|
+
onChange(event, options[nextOptionIndex].value);
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
const baseStyle = css`
|
|
131
|
+
display: flex;
|
|
132
|
+
align-items: center;
|
|
133
|
+
justify-content: space-between;
|
|
134
|
+
position: relative;
|
|
135
|
+
width: ${width}px;
|
|
136
|
+
min-width: 80px;
|
|
137
|
+
max-width: 624px;
|
|
138
|
+
height: 48px;
|
|
139
|
+
box-sizing: border-box;
|
|
140
|
+
padding: 0 16px;
|
|
141
|
+
background-color: ${theme.color.neutral.white};
|
|
142
|
+
color: ${theme.color.text.primary};
|
|
143
|
+
font-family: ${theme.font.family.primary};
|
|
144
|
+
font-size: ${theme.font.size.f16};
|
|
145
|
+
border: ${theme.border.b1} solid ${theme.color.neutral.grey60};
|
|
146
|
+
cursor: pointer;
|
|
147
|
+
user-select: none;
|
|
148
|
+
|
|
149
|
+
&:hover {
|
|
150
|
+
${!isOpen && `background-color: ${theme.color.neutral.grey5};`}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
&:focus-visible {
|
|
154
|
+
outline: none;
|
|
155
|
+
box-shadow: ${theme.boxShadow.focus};
|
|
156
|
+
}
|
|
157
|
+
`;
|
|
158
|
+
|
|
159
|
+
const disabledStyle = css`
|
|
160
|
+
color: ${theme.color.text.disabled};
|
|
161
|
+
border: ${theme.border.b1} solid ${theme.color.neutral.grey20};
|
|
162
|
+
cursor: not-allowed;
|
|
163
|
+
|
|
164
|
+
&:hover {
|
|
165
|
+
background-color: ${theme.color.neutral.white};
|
|
166
|
+
}
|
|
167
|
+
`;
|
|
168
|
+
|
|
169
|
+
const style = cx(NAME, baseStyle, disabled && disabledStyle, className);
|
|
170
|
+
|
|
171
|
+
return (
|
|
172
|
+
<div
|
|
173
|
+
onClick={togglePanel}
|
|
174
|
+
onKeyDown={handleKeyDown}
|
|
175
|
+
tabIndex={disabled ? -1 : 0}
|
|
176
|
+
className={style}
|
|
177
|
+
data-testid={testId}
|
|
178
|
+
ref={effectiveRef}
|
|
179
|
+
role='combobox'
|
|
180
|
+
aria-haspopup='listbox'
|
|
181
|
+
aria-expanded={isOpen}
|
|
182
|
+
{...props}
|
|
183
|
+
>
|
|
184
|
+
<VisibleField
|
|
185
|
+
isOpen={isOpen}
|
|
186
|
+
selectedOption={selectedOption}
|
|
187
|
+
placeholder={placeholder}
|
|
188
|
+
disabled={disabled}
|
|
189
|
+
/>
|
|
190
|
+
{isOpen && (
|
|
191
|
+
<Panel role='listbox'>
|
|
192
|
+
{options.map((option) => (
|
|
193
|
+
<CustomOption
|
|
194
|
+
key={option.value}
|
|
195
|
+
value={option.value}
|
|
196
|
+
isSelected={value === option.value}
|
|
197
|
+
onSelect={handleSelect}
|
|
198
|
+
role='option'
|
|
199
|
+
aria-selected={value === option.value}
|
|
200
|
+
>
|
|
201
|
+
{option.icon}
|
|
202
|
+
{option.text}
|
|
203
|
+
</CustomOption>
|
|
204
|
+
))}
|
|
205
|
+
</Panel>
|
|
206
|
+
)}
|
|
207
|
+
</div>
|
|
208
|
+
);
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
export default CustomSelect;
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { css, cx } from '@emotion/css';
|
|
2
|
+
import { useTheme } from '../../../theme';
|
|
3
|
+
import { dataUri as chevronDownSvgDataUri } from '../../Icon/svgs/ChevronDownSvg';
|
|
4
|
+
import { NativeSelectProps } from '../Select.types';
|
|
5
|
+
|
|
6
|
+
const NAME = 'ucl-uikit-select--native';
|
|
7
|
+
|
|
8
|
+
const NativeSelect = ({
|
|
9
|
+
options,
|
|
10
|
+
width = 325,
|
|
11
|
+
disabled,
|
|
12
|
+
placeholder,
|
|
13
|
+
testId = NAME,
|
|
14
|
+
className,
|
|
15
|
+
...props
|
|
16
|
+
}: NativeSelectProps) => {
|
|
17
|
+
const [theme] = useTheme();
|
|
18
|
+
|
|
19
|
+
const chevronColour = disabled
|
|
20
|
+
? theme.color.neutral.grey20
|
|
21
|
+
: theme.color.interaction.blue70;
|
|
22
|
+
const chevronDownSvg = chevronDownSvgDataUri(chevronColour);
|
|
23
|
+
|
|
24
|
+
const baseStyle = css`
|
|
25
|
+
width: ${width}px;
|
|
26
|
+
padding: 0 ${theme.padding.p40} 0 ${theme.padding.p16};
|
|
27
|
+
height: ${theme.padding.p48};
|
|
28
|
+
line-height: ${theme.font.lineHeight.h150};
|
|
29
|
+
font-family: ${theme.font.family.primary};
|
|
30
|
+
font-size: ${theme.font.size.f16};
|
|
31
|
+
background-color: ${theme.color.neutral.white};
|
|
32
|
+
border: ${theme.border.b1} solid ${theme.color.neutral.grey60};
|
|
33
|
+
color: ${theme.color.text.primary};
|
|
34
|
+
appearance: none;
|
|
35
|
+
-webkit-appearance: none;
|
|
36
|
+
-moz-appearance: none;
|
|
37
|
+
outline: none;
|
|
38
|
+
cursor: pointer;
|
|
39
|
+
|
|
40
|
+
background-image: url(${chevronDownSvg});
|
|
41
|
+
background-size: 24px 24px;
|
|
42
|
+
background-position: right 16px center;
|
|
43
|
+
background-repeat: no-repeat;
|
|
44
|
+
`;
|
|
45
|
+
|
|
46
|
+
const hoverStyle = css`
|
|
47
|
+
&:hover {
|
|
48
|
+
border-color: ${theme.color.neutral.grey60};
|
|
49
|
+
background-color: ${theme.color.neutral.grey5};
|
|
50
|
+
}
|
|
51
|
+
`;
|
|
52
|
+
|
|
53
|
+
const focusStyle = css`
|
|
54
|
+
&:focus-visible {
|
|
55
|
+
box-shadow: ${theme.boxShadow.focus};
|
|
56
|
+
}
|
|
57
|
+
`;
|
|
58
|
+
|
|
59
|
+
const disabledStyle = css`
|
|
60
|
+
color: ${theme.color.text.disabled};
|
|
61
|
+
border: ${theme.border.b1} solid ${theme.color.neutral.grey20};
|
|
62
|
+
opacity: 1; // Override user-agent default
|
|
63
|
+
cursor: not-allowed;
|
|
64
|
+
|
|
65
|
+
&:hover {
|
|
66
|
+
background-color: ${theme.color.neutral.white};
|
|
67
|
+
}
|
|
68
|
+
`;
|
|
69
|
+
|
|
70
|
+
const style = cx(
|
|
71
|
+
NAME,
|
|
72
|
+
baseStyle,
|
|
73
|
+
!disabled && hoverStyle,
|
|
74
|
+
!disabled && focusStyle,
|
|
75
|
+
disabled && disabledStyle,
|
|
76
|
+
className
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
return (
|
|
80
|
+
<select
|
|
81
|
+
className={style}
|
|
82
|
+
data-testid={testId}
|
|
83
|
+
disabled={disabled}
|
|
84
|
+
{...props}
|
|
85
|
+
>
|
|
86
|
+
{placeholder && (
|
|
87
|
+
<option
|
|
88
|
+
value=''
|
|
89
|
+
disabled
|
|
90
|
+
selected
|
|
91
|
+
>
|
|
92
|
+
{placeholder}
|
|
93
|
+
</option>
|
|
94
|
+
)}
|
|
95
|
+
{options
|
|
96
|
+
? options.map((option) => (
|
|
97
|
+
<option
|
|
98
|
+
key={option.value}
|
|
99
|
+
value={option.value}
|
|
100
|
+
>
|
|
101
|
+
{option.text}
|
|
102
|
+
</option>
|
|
103
|
+
))
|
|
104
|
+
: null}
|
|
105
|
+
</select>
|
|
106
|
+
);
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
export default NativeSelect;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { css, cx } from '@emotion/css';
|
|
2
|
+
import { useTheme } from '../../../theme';
|
|
3
|
+
|
|
4
|
+
const NAME = 'ucl-uikit-select__panel';
|
|
5
|
+
|
|
6
|
+
type PanelProps = React.ComponentPropsWithoutRef<'div'>;
|
|
7
|
+
|
|
8
|
+
const Panel = (props: PanelProps) => {
|
|
9
|
+
const [theme] = useTheme();
|
|
10
|
+
|
|
11
|
+
const handleClick = (event: React.MouseEvent) => {
|
|
12
|
+
// Missed clicks on the panel should not close the dropdown
|
|
13
|
+
event.stopPropagation();
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const baseStyle = css`
|
|
17
|
+
display: flex;
|
|
18
|
+
flex-direction: column;
|
|
19
|
+
position: absolute;
|
|
20
|
+
top: 46px;
|
|
21
|
+
left: -1px; // -1px to align with the border of the field
|
|
22
|
+
|
|
23
|
+
z-index: 10; // Required: panel must be 'above' subsquent DOM elements
|
|
24
|
+
width: 100%;
|
|
25
|
+
max-height: 200px;
|
|
26
|
+
overflow-y: auto;
|
|
27
|
+
overflow-x: hidden;
|
|
28
|
+
box-sizing: content-box;
|
|
29
|
+
padding: 16px 0 24px 0;
|
|
30
|
+
border: ${theme.border.b1} solid ${theme.color.neutral.grey20};
|
|
31
|
+
background-color: ${theme.color.neutral.white};
|
|
32
|
+
`;
|
|
33
|
+
|
|
34
|
+
const style = cx(NAME, baseStyle);
|
|
35
|
+
|
|
36
|
+
return (
|
|
37
|
+
<div
|
|
38
|
+
className={style}
|
|
39
|
+
data-testid={NAME}
|
|
40
|
+
onClick={handleClick}
|
|
41
|
+
{...props}
|
|
42
|
+
/>
|
|
43
|
+
);
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export default Panel;
|