@nuskin/foundation-ui-components 1.2.0 → 1.4.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/package.json +5 -5
- package/src/common/auth-dropdown/NsAuthDropdown.tsx +13 -9
- package/src/common/button/NsButton.stories.tsx +3 -2
- package/src/common/button/NsButton.tsx +27 -2
- package/src/common/button/types.d.ts +21 -17
- package/src/common/cart-icon/NsCartIcon.spec.tsx +3 -2
- package/src/common/cart-icon/NsCartIcon.stories.tsx +1 -1
- package/src/common/cart-icon/NsCartIcon.styled.tsx +14 -0
- package/src/common/cart-icon/NsCartIcon.tsx +1 -1
- package/src/common/cart-icon/types.d.ts +2 -2
- package/src/common/dropdown/NsDropdown.tsx +15 -11
- package/src/common/email-simple/NsEmailSimple.tsx +1 -1
- package/src/common/flag/NsFlag.spec.tsx +14 -2
- package/src/common/flag/NsFlag.styled.tsx +2 -5
- package/src/common/hooks/useOnClickOutside.tsx +14 -14
- package/src/common/icon/NsIcon.spec.tsx +21 -6
- package/src/common/icon/NsIcon.stories.tsx +2 -1
- package/src/common/icon/NsIcon.styled.tsx +13 -10
- package/src/common/icon/NsIcon.tsx +9 -6
- package/src/common/icon/NsIconVariants.ts +2 -0
- package/src/common/icon/icons/AlertErrorIcon.tsx +2 -1
- package/src/common/icon/icons/ArrowLeft.tsx +2 -2
- package/src/common/icon/icons/ArrowRight.tsx +2 -2
- package/src/common/icon/icons/CartBadge.tsx +2 -2
- package/src/common/icon/icons/Check.tsx +25 -0
- package/src/common/icon/icons/CloseIcon.tsx +1 -1
- package/src/common/icon/icons/ErrorIcon.tsx +3 -2
- package/src/common/icon/icons/SearchIcon.tsx +3 -2
- package/src/common/icon/icons/SuccessIcon.tsx +7 -1
- package/src/common/icon/icons/TrashIcon.tsx +2 -1
- package/src/common/icon/icons/UserIcon.tsx +8 -1
- package/src/common/icon/icons/UserOutline.tsx +4 -1
- package/src/common/icon/icons/WarningIcon.tsx +3 -2
- package/src/common/icon/types.d.ts +3 -2
- package/src/common/index.ts +8 -4
- package/src/common/progress/NsProgress.spec.tsx +2 -2
- package/src/common/progress/NsProgress.stories.tsx +10 -5
- package/src/common/progress/NsProgress.styled.tsx +30 -12
- package/src/common/progress/NsProgress.tsx +18 -16
- package/src/common/progress/types.d.ts +2 -2
- package/src/common/radiogroup/NsRadioGroup.spec.tsx +2 -2
- package/src/common/radiogroup/NsRadioGroup.stories.tsx +2 -2
- package/src/common/radiogroup/NsRadioGroup.tsx +5 -3
- package/src/common/radiogroup/types.d.ts +11 -12
- package/src/common/select/NsSelect.styled.tsx +38 -71
- package/src/common/select/NsSelect.tsx +86 -91
- package/src/common/select/NsSelectOption.tsx +10 -14
- package/src/common/select/specs/NsSelect.spec.tsx +183 -0
- package/src/common/select/specs/NsSelectOption.spec.tsx +32 -0
- package/src/common/select/stories/NsSelect.stories.tsx +25 -80
- package/src/common/select/stories/countries.ts +53 -0
- package/src/common/select/types.d.ts +21 -8
- package/src/common/table/NsTable.stories.tsx +1 -0
- package/src/common/cart-icon/NsCartIcon.styled.ts +0 -14
- package/src/common/select/NsSelect.spec.tsx +0 -274
|
@@ -1,8 +1,9 @@
|
|
|
1
|
+
import { ColorUtils, FoundationTheme, styled } from '@nuskin/foundation-theme';
|
|
1
2
|
import { HTMLAttributes } from 'react';
|
|
2
|
-
|
|
3
|
-
import { Themed } from '../common.types';
|
|
3
|
+
|
|
4
4
|
import { getDropdownHeadBorder } from '../dropdown/NsDropdown.styled';
|
|
5
|
-
|
|
5
|
+
|
|
6
|
+
const { paletteColor, themeColor } = ColorUtils;
|
|
6
7
|
|
|
7
8
|
interface SelectLabelProps extends HTMLAttributes<HTMLLabelElement> {
|
|
8
9
|
errorMessage?: string;
|
|
@@ -30,7 +31,8 @@ export const SelectHead = styled.div<{ type: string; disabled: boolean }>`
|
|
|
30
31
|
display: flex;
|
|
31
32
|
justify-content: space-between;
|
|
32
33
|
align-items: center;
|
|
33
|
-
border: ${getDropdownHeadBorder}
|
|
34
|
+
border: ${getDropdownHeadBorder};
|
|
35
|
+
background: ${paletteColor('primary', 'contrastText')};
|
|
34
36
|
color: ${paletteColor('primary')};
|
|
35
37
|
width: 207px;
|
|
36
38
|
position: relative;
|
|
@@ -41,13 +43,7 @@ export const SelectButton = styled.div`
|
|
|
41
43
|
padding-right: 10px;
|
|
42
44
|
`;
|
|
43
45
|
|
|
44
|
-
export const
|
|
45
|
-
display: flex;
|
|
46
|
-
align-items: center;
|
|
47
|
-
padding-top: 30px;
|
|
48
|
-
`;
|
|
49
|
-
|
|
50
|
-
export const RightArrowContainer = styled.div`
|
|
46
|
+
export const StyledArrowContainer = styled.div`
|
|
51
47
|
display: flex;
|
|
52
48
|
align-items: center;
|
|
53
49
|
padding-top: 30px;
|
|
@@ -66,55 +62,6 @@ export const SelectList = styled.div<{ borderColor: FoundationTheme.PaletteColor
|
|
|
66
62
|
overflow-y: auto;
|
|
67
63
|
`;
|
|
68
64
|
|
|
69
|
-
const getDividerColor = ({ theme }: Themed) =>
|
|
70
|
-
isDarkMode({ theme }) ? color('neutral', '20')({ theme }) : color('neutral', '30')({ theme });
|
|
71
|
-
const getListBgColor = (props: Themed) => {
|
|
72
|
-
return !isDarkMode(props) ? color('accentColorB', '10')(props) : color('accentColorB', '50')(props);
|
|
73
|
-
};
|
|
74
|
-
type ContainerParam = { borderBottom: string; bgColor: string; color: string; hoveredBg: string };
|
|
75
|
-
|
|
76
|
-
export const ListItemContainer = styled.div<{ showListDivider: boolean; isSelected: boolean }>`
|
|
77
|
-
${({ showListDivider, isSelected, theme }) => {
|
|
78
|
-
const borderBottom = showListDivider ? getDividerColor({ theme }) : 'none';
|
|
79
|
-
const bgColor = isSelected ? paletteColor('primary')({ theme }) : 'white';
|
|
80
|
-
const color = isSelected ? 'white' : 'black';
|
|
81
|
-
const hoveredBg = getListBgColor({ theme });
|
|
82
|
-
return createListContainerStyle({ borderBottom, bgColor, color, hoveredBg });
|
|
83
|
-
}}
|
|
84
|
-
`;
|
|
85
|
-
function createListContainerStyle({ borderBottom, bgColor, color, hoveredBg }: ContainerParam) {
|
|
86
|
-
return `
|
|
87
|
-
cursor: pointer;
|
|
88
|
-
padding: 5px 0;
|
|
89
|
-
border-bottom: 1px solid ${borderBottom};
|
|
90
|
-
background-color: ${bgColor};
|
|
91
|
-
color: ${color};
|
|
92
|
-
|
|
93
|
-
&:hover {
|
|
94
|
-
background-color: ${hoveredBg};
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
&:last-child {
|
|
98
|
-
border-bottom: none;
|
|
99
|
-
}
|
|
100
|
-
`;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
const getListItemBgColor = ({ isSelected, theme }: { isSelected: boolean } & Themed) => {
|
|
104
|
-
if (isSelected) {
|
|
105
|
-
return paletteColor('primary')({ theme });
|
|
106
|
-
}
|
|
107
|
-
return getListBgColor({ theme });
|
|
108
|
-
};
|
|
109
|
-
|
|
110
|
-
const getListItemBorderLeftColor = ({ isSelected, theme }: { isSelected: boolean } & Themed) => {
|
|
111
|
-
if (isSelected) {
|
|
112
|
-
const color = paletteColor('primary')({ theme });
|
|
113
|
-
return `2px solid ${color}`;
|
|
114
|
-
}
|
|
115
|
-
return 'none';
|
|
116
|
-
};
|
|
117
|
-
|
|
118
65
|
export const SelectLabel = styled.label<SelectLabelProps>`
|
|
119
66
|
display: block;
|
|
120
67
|
margin-bottom: 5px;
|
|
@@ -122,14 +69,34 @@ export const SelectLabel = styled.label<SelectLabelProps>`
|
|
|
122
69
|
errorMessage ? paletteColor('error')({ theme }) : paletteColor('primary')({ theme })};
|
|
123
70
|
}
|
|
124
71
|
`;
|
|
125
|
-
|
|
126
|
-
export const
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
'
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
})
|
|
72
|
+
type StyledSelectOptionType = { showListDivider: boolean };
|
|
73
|
+
export const StyledSelectOption = styled('div')<StyledSelectOptionType>(({ showListDivider, theme }) => {
|
|
74
|
+
const dividerColor = themeColor(
|
|
75
|
+
{ colorType: 'neutral', variant: '30' },
|
|
76
|
+
{ colorType: 'neutral', variant: '20' }
|
|
77
|
+
)({ theme });
|
|
78
|
+
const borderBottom = showListDivider ? dividerColor : 'none';
|
|
79
|
+
const hoveredBg = themeColor(
|
|
80
|
+
{ colorType: 'accentColorB', variant: '20' },
|
|
81
|
+
{ colorType: 'accentColorB', variant: '40' }
|
|
82
|
+
)({ theme });
|
|
83
|
+
|
|
84
|
+
return {
|
|
85
|
+
cursor: 'pointer',
|
|
86
|
+
borderBottom: `1px solid ${borderBottom}`,
|
|
87
|
+
backgroundColor: 'white',
|
|
88
|
+
color: 'black',
|
|
89
|
+
padding: '5px 10px',
|
|
90
|
+
'&.selected-option': {
|
|
91
|
+
backgroundColor: paletteColor('accentColorA')({ theme }),
|
|
92
|
+
color: 'white',
|
|
93
|
+
},
|
|
94
|
+
'&:hover': {
|
|
95
|
+
color: 'white',
|
|
96
|
+
backgroundColor: hoveredBg,
|
|
97
|
+
},
|
|
98
|
+
'&:last-child': {
|
|
99
|
+
borderBottom: 'none',
|
|
100
|
+
},
|
|
101
|
+
};
|
|
102
|
+
});
|
|
@@ -1,22 +1,53 @@
|
|
|
1
|
-
import React, {
|
|
1
|
+
import React, { MouseEvent, useCallback, useMemo, useRef, useState } from 'react';
|
|
2
|
+
|
|
3
|
+
import { useOnClickOutside } from '../hooks/useOnClickOutside';
|
|
4
|
+
import { NsIcon } from '../icon';
|
|
5
|
+
import { Show } from '../utility';
|
|
2
6
|
import {
|
|
3
|
-
|
|
7
|
+
ErrorContainer,
|
|
8
|
+
StyledArrowContainer,
|
|
4
9
|
SelectButton,
|
|
5
|
-
|
|
10
|
+
SelectContainer,
|
|
6
11
|
SelectHead,
|
|
7
12
|
SelectLabel,
|
|
13
|
+
SelectList,
|
|
8
14
|
SelectMain,
|
|
9
|
-
LeftArrowContainer,
|
|
10
|
-
RightArrowContainer,
|
|
11
|
-
ErrorContainer,
|
|
12
15
|
} from './NsSelect.styled';
|
|
13
|
-
import {
|
|
14
|
-
|
|
16
|
+
import { LabelValueType, SelectProps } from './types.d';
|
|
17
|
+
|
|
18
|
+
const factorMap: any = {
|
|
19
|
+
reverse: {
|
|
20
|
+
right: -1,
|
|
21
|
+
left: 1,
|
|
22
|
+
},
|
|
23
|
+
forward: {
|
|
24
|
+
right: 1,
|
|
25
|
+
left: -1,
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const arrowVisibility = (
|
|
30
|
+
total: number,
|
|
31
|
+
direction: 'left' | 'right',
|
|
32
|
+
reverse: boolean = false,
|
|
33
|
+
currentIndex: number | undefined = -1
|
|
34
|
+
) => {
|
|
35
|
+
if (direction === 'left') {
|
|
36
|
+
if (!reverse) {
|
|
37
|
+
return currentIndex < 1 ? 'hidden' : 'visible';
|
|
38
|
+
} else {
|
|
39
|
+
return currentIndex >= total - 1 ? 'hidden' : 'visible';
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
if (!reverse) {
|
|
43
|
+
return currentIndex >= total - 1 ? 'hidden' : 'visible';
|
|
44
|
+
} else {
|
|
45
|
+
return currentIndex < 1 ? 'hidden' : 'visible';
|
|
46
|
+
}
|
|
47
|
+
};
|
|
15
48
|
const NsSelect: React.FC<SelectProps> = ({
|
|
16
|
-
|
|
17
|
-
selectedLabel,
|
|
49
|
+
selected,
|
|
18
50
|
onSelect,
|
|
19
|
-
children,
|
|
20
51
|
disabled = false,
|
|
21
52
|
type = 'outlined',
|
|
22
53
|
placeholder = '',
|
|
@@ -30,32 +61,33 @@ const NsSelect: React.FC<SelectProps> = ({
|
|
|
30
61
|
// eslint-disable-next-line no-unused-vars
|
|
31
62
|
rules,
|
|
32
63
|
reverseArrowControls,
|
|
64
|
+
renderOption,
|
|
65
|
+
options,
|
|
33
66
|
}) => {
|
|
34
67
|
const [isOpen, setIsOpen] = useState(false);
|
|
35
|
-
const
|
|
36
|
-
|
|
68
|
+
const optionIndex = useMemo(() => {
|
|
69
|
+
if (typeof selected?.index === 'number') return selected.index;
|
|
70
|
+
return options.findIndex((option) => option.value === selected.value);
|
|
71
|
+
}, [options, selected]);
|
|
72
|
+
const [value, setValue] = useState<LabelValueType>({ ...selected, index: optionIndex });
|
|
37
73
|
const selectContainerRef = useRef<HTMLDivElement>(null);
|
|
38
74
|
const selectHeadRef = useRef<HTMLDivElement>(null);
|
|
39
75
|
|
|
40
|
-
const
|
|
76
|
+
const closeMenuOnMouseLeave = useCallback(() => {
|
|
41
77
|
if (closeOnMouseLeave) {
|
|
42
78
|
setIsOpen(false);
|
|
43
79
|
}
|
|
44
|
-
};
|
|
80
|
+
}, [closeOnMouseLeave]);
|
|
45
81
|
|
|
46
|
-
const handleSelect = (
|
|
47
|
-
onSelect(
|
|
82
|
+
const handleSelect = (val: LabelValueType) => {
|
|
83
|
+
onSelect(val);
|
|
48
84
|
setIsOpen(false);
|
|
49
|
-
setValue(
|
|
50
|
-
setSelectedIndex(index);
|
|
51
|
-
};
|
|
52
|
-
|
|
53
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
54
|
-
const handleClickOutside = (event: MouseEvent) => {
|
|
55
|
-
if (selectContainerRef.current && !selectContainerRef.current.contains(event.target as Node) && isOpen) {
|
|
56
|
-
setIsOpen(false);
|
|
57
|
-
}
|
|
85
|
+
setValue(val);
|
|
58
86
|
};
|
|
87
|
+
const closeMenu = useCallback(() => {
|
|
88
|
+
setIsOpen(false);
|
|
89
|
+
}, []);
|
|
90
|
+
useOnClickOutside(selectContainerRef.current, closeMenu);
|
|
59
91
|
|
|
60
92
|
const handleMouseClick = () => {
|
|
61
93
|
if (disabled) return;
|
|
@@ -66,61 +98,27 @@ const NsSelect: React.FC<SelectProps> = ({
|
|
|
66
98
|
}, 0);
|
|
67
99
|
}
|
|
68
100
|
};
|
|
69
|
-
const
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
handleSelect(value, nextIndex);
|
|
76
|
-
} else {
|
|
77
|
-
if (!reverseArrowControls) {
|
|
78
|
-
const nextIndex = selectedIndex + 1;
|
|
79
|
-
const childrenArray = Children.toArray(children);
|
|
80
|
-
const optionAtIndex = childrenArray[nextIndex];
|
|
81
|
-
const { value } = (optionAtIndex as any)?.props ?? {};
|
|
82
|
-
handleSelect(value, nextIndex);
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
};
|
|
86
|
-
const handleArrowLeftClick = () => {
|
|
87
|
-
if (selectedIndex !== -1) {
|
|
88
|
-
const nextIndex = reverseArrowControls ? selectedIndex + 1 : selectedIndex - 1;
|
|
89
|
-
const childrenArray = Children.toArray(children);
|
|
90
|
-
const optionAtIndex = childrenArray[nextIndex];
|
|
91
|
-
const { value } = (optionAtIndex as any)?.props ?? {};
|
|
92
|
-
handleSelect(value, nextIndex);
|
|
93
|
-
} else {
|
|
94
|
-
if (reverseArrowControls) {
|
|
95
|
-
const nextIndex = selectedIndex + 1;
|
|
96
|
-
const childrenArray = Children.toArray(children);
|
|
97
|
-
const optionAtIndex = childrenArray[nextIndex];
|
|
98
|
-
const { value } = (optionAtIndex as any)?.props ?? {};
|
|
99
|
-
handleSelect(value, nextIndex);
|
|
100
|
-
}
|
|
101
|
+
const handleArrowClick = (event: MouseEvent) => {
|
|
102
|
+
const direction = event.currentTarget?.className.includes('Right') ? 'right' : 'left';
|
|
103
|
+
const adder = factorMap[reverseArrowControls ? 'reverse' : 'forward'][direction];
|
|
104
|
+
const nextIndex = (selected?.index ?? -1) + adder;
|
|
105
|
+
if (nextIndex > -1 && nextIndex < options.length) {
|
|
106
|
+
handleSelect({ ...options[nextIndex], index: nextIndex });
|
|
101
107
|
}
|
|
102
108
|
};
|
|
103
109
|
|
|
104
|
-
useEffect(() => {
|
|
105
|
-
document.addEventListener('click', handleClickOutside);
|
|
106
|
-
return () => {
|
|
107
|
-
document.removeEventListener('click', handleClickOutside);
|
|
108
|
-
};
|
|
109
|
-
}, [handleClickOutside, isOpen]);
|
|
110
|
-
|
|
111
110
|
return (
|
|
112
111
|
<SelectMain>
|
|
113
112
|
{hasArrows && (
|
|
114
|
-
<
|
|
115
|
-
onClick={
|
|
113
|
+
<StyledArrowContainer
|
|
114
|
+
onClick={handleArrowClick}
|
|
116
115
|
className="LeftArrow"
|
|
117
116
|
style={{
|
|
118
|
-
visibility:
|
|
119
|
-
!reverseArrowControls && (selectedIndex === 0 || selectedIndex < 0) ? 'hidden' : 'visible',
|
|
117
|
+
visibility: arrowVisibility(options.length, 'left', reverseArrowControls, value?.index),
|
|
120
118
|
}}
|
|
121
119
|
>
|
|
122
120
|
<NsIcon name="arrowLeft" color="primary" />
|
|
123
|
-
</
|
|
121
|
+
</StyledArrowContainer>
|
|
124
122
|
)}
|
|
125
123
|
<SelectContainer
|
|
126
124
|
data-testid="select-container"
|
|
@@ -137,43 +135,40 @@ const NsSelect: React.FC<SelectProps> = ({
|
|
|
137
135
|
tabIndex={0}
|
|
138
136
|
ref={selectHeadRef}
|
|
139
137
|
>
|
|
140
|
-
<SelectButton>{
|
|
138
|
+
<SelectButton>{value?.label || placeholder}</SelectButton>
|
|
141
139
|
{!readOnly && <NsIcon name={isOpen ? 'arrowUp' : 'arrowDown'} color="primary" />}
|
|
142
140
|
</SelectHead>
|
|
143
|
-
{isOpen
|
|
141
|
+
<Show when={isOpen}>
|
|
144
142
|
<SelectList
|
|
145
|
-
data-testid="select-list"
|
|
146
|
-
onMouseLeave={
|
|
143
|
+
data-testid="select-menu-list"
|
|
144
|
+
onMouseLeave={closeMenuOnMouseLeave}
|
|
147
145
|
borderColor={selectBorderColor}
|
|
148
146
|
className="selectList"
|
|
149
147
|
>
|
|
150
|
-
{
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
}
|
|
160
|
-
return null;
|
|
148
|
+
{options.map((opt, optionIndex) => {
|
|
149
|
+
return renderOption({
|
|
150
|
+
value: opt.value,
|
|
151
|
+
label: opt.label,
|
|
152
|
+
index: optionIndex,
|
|
153
|
+
onClick: () => handleSelect({ label: opt.label, value: opt.value, index: optionIndex }),
|
|
154
|
+
selected: value,
|
|
155
|
+
showListDivider,
|
|
156
|
+
});
|
|
161
157
|
})}
|
|
162
158
|
</SelectList>
|
|
163
|
-
|
|
159
|
+
</Show>
|
|
164
160
|
{errorMessage && <ErrorContainer>{errorMessage}</ErrorContainer>}
|
|
165
161
|
</SelectContainer>
|
|
166
162
|
{hasArrows && (
|
|
167
|
-
<
|
|
168
|
-
onClick={
|
|
163
|
+
<StyledArrowContainer
|
|
164
|
+
onClick={handleArrowClick}
|
|
169
165
|
className="RightArrow"
|
|
170
166
|
style={{
|
|
171
|
-
visibility:
|
|
172
|
-
reverseArrowControls && (selectedIndex === 0 || selectedIndex < 0) ? 'hidden' : 'visible',
|
|
167
|
+
visibility: arrowVisibility(options.length, 'right', reverseArrowControls, selected?.index),
|
|
173
168
|
}}
|
|
174
169
|
>
|
|
175
170
|
<NsIcon name="arrowRight" color="primary" />
|
|
176
|
-
</
|
|
171
|
+
</StyledArrowContainer>
|
|
177
172
|
)}
|
|
178
173
|
</SelectMain>
|
|
179
174
|
);
|
|
@@ -1,29 +1,25 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
-
|
|
2
|
+
|
|
3
|
+
import { StyledSelectOption } from './NsSelect.styled';
|
|
3
4
|
import { SelectOptionProps } from './types.d';
|
|
4
5
|
|
|
5
6
|
export const NsSelectOption: React.FC<SelectOptionProps> = ({
|
|
6
7
|
value,
|
|
7
8
|
label,
|
|
8
|
-
|
|
9
|
-
|
|
9
|
+
onClick,
|
|
10
|
+
selected,
|
|
10
11
|
showListDivider = true,
|
|
11
12
|
}) => {
|
|
12
|
-
const
|
|
13
|
-
if (onSelect && typeof onSelect === 'function') {
|
|
14
|
-
onSelect(value);
|
|
15
|
-
}
|
|
16
|
-
};
|
|
17
|
-
|
|
13
|
+
const isSelected = value === selected?.value;
|
|
18
14
|
return (
|
|
19
|
-
<
|
|
15
|
+
<StyledSelectOption
|
|
16
|
+
className={isSelected ? 'selected-option' : ''}
|
|
20
17
|
data-testid={`option-${value}`}
|
|
21
|
-
isSelected={value === selectedValue}
|
|
22
18
|
showListDivider={showListDivider}
|
|
23
|
-
onClick={
|
|
19
|
+
onClick={onClick}
|
|
24
20
|
>
|
|
25
|
-
|
|
26
|
-
</
|
|
21
|
+
{label}
|
|
22
|
+
</StyledSelectOption>
|
|
27
23
|
);
|
|
28
24
|
};
|
|
29
25
|
export default NsSelectOption;
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import { RenderSelectOptionArgType } from '../types';
|
|
2
|
+
import { fireEvent, render, screen, waitFor } from '@/test-utils/index';
|
|
3
|
+
import React from 'react';
|
|
4
|
+
|
|
5
|
+
import { NsSelect, NsSelectOption } from '../..';
|
|
6
|
+
|
|
7
|
+
const renderSelectOption = (option: RenderSelectOptionArgType) => {
|
|
8
|
+
return (
|
|
9
|
+
<NsSelectOption
|
|
10
|
+
key={option.value}
|
|
11
|
+
value={option.value}
|
|
12
|
+
index={option.index ?? -1}
|
|
13
|
+
label={option.label}
|
|
14
|
+
onClick={option.onClick}
|
|
15
|
+
/>
|
|
16
|
+
);
|
|
17
|
+
};
|
|
18
|
+
const createOptionTestId = (value: string) => `option-${value}`;
|
|
19
|
+
const selectCommonProps = {
|
|
20
|
+
selected: { label: 'Option 1', value: 'Option1', index: 0 },
|
|
21
|
+
options: [
|
|
22
|
+
{ value: 'Option1', label: 'Option 1' },
|
|
23
|
+
{ value: 'Option2', label: 'Option 2' },
|
|
24
|
+
{ value: 'Option3', label: 'Option 3' },
|
|
25
|
+
],
|
|
26
|
+
onSelect: jest.fn(),
|
|
27
|
+
renderOption: renderSelectOption,
|
|
28
|
+
};
|
|
29
|
+
describe('NsSelect', () => {
|
|
30
|
+
const onSelect = jest.fn();
|
|
31
|
+
const selected = { label: '', value: '' };
|
|
32
|
+
|
|
33
|
+
const options = [
|
|
34
|
+
{ value: 'Option1', label: 'Option 1' },
|
|
35
|
+
{ value: 'Option2', label: 'Option 2' },
|
|
36
|
+
{ value: 'Option3', label: 'Option 3' },
|
|
37
|
+
];
|
|
38
|
+
|
|
39
|
+
it('1. renders the placeholder text when no value is selected', () => {
|
|
40
|
+
render(
|
|
41
|
+
<NsSelect
|
|
42
|
+
placeholder="Select an option"
|
|
43
|
+
selected={selected}
|
|
44
|
+
onSelect={onSelect}
|
|
45
|
+
options={options}
|
|
46
|
+
renderOption={renderSelectOption}
|
|
47
|
+
/>
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
expect(screen.getByText('Select an option')).toBeInTheDocument();
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('2. clicking outside the select container closes the dropdown', async () => {
|
|
54
|
+
const { getByTestId, queryByTestId, container } = render(
|
|
55
|
+
<div>
|
|
56
|
+
<button className="extra"></button>
|
|
57
|
+
<NsSelect {...selectCommonProps} />
|
|
58
|
+
</div>
|
|
59
|
+
);
|
|
60
|
+
const selectButton = getByTestId('select-head');
|
|
61
|
+
fireEvent.mouseDown(selectButton);
|
|
62
|
+
const outsideElement = container.querySelector('button.extra');
|
|
63
|
+
if (outsideElement) fireEvent.click(outsideElement);
|
|
64
|
+
// check if the dropdown is closed
|
|
65
|
+
expect(queryByTestId(createOptionTestId(selectCommonProps.options[1].value))).toBeNull();
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('3. renders the selected value label', () => {
|
|
69
|
+
render(<NsSelect {...selectCommonProps} />);
|
|
70
|
+
|
|
71
|
+
expect(screen.getByText(selectCommonProps.selected.label)).toBeInTheDocument();
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('4. clicking on select button opens the dropdown', () => {
|
|
75
|
+
const { getByTestId, getByText } = render(<NsSelect {...selectCommonProps} />);
|
|
76
|
+
const selectButton = getByTestId('select-head');
|
|
77
|
+
fireEvent.mouseDown(selectButton);
|
|
78
|
+
|
|
79
|
+
// Check if the dropdown is opened (isOpen state is true)
|
|
80
|
+
const dropdownList = getByTestId('select-menu-list');
|
|
81
|
+
expect(dropdownList).toBeInTheDocument();
|
|
82
|
+
|
|
83
|
+
// Check if the options are rendered in the dropdown
|
|
84
|
+
selectCommonProps.options.slice(1).forEach((option) => {
|
|
85
|
+
const optionElement = getByText(option.label);
|
|
86
|
+
expect(optionElement).toBeInTheDocument();
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('5. disabling the select component prevents interaction', () => {
|
|
91
|
+
const { getByTestId, queryByTestId } = render(<NsSelect {...selectCommonProps} disabled />);
|
|
92
|
+
const selectButton = getByTestId('select-head');
|
|
93
|
+
fireEvent.mouseDown(selectButton);
|
|
94
|
+
|
|
95
|
+
// Check if the dropdown does not open (select list is not present)
|
|
96
|
+
const dropdownList = queryByTestId('select-menu-list');
|
|
97
|
+
expect(dropdownList).toBeNull();
|
|
98
|
+
|
|
99
|
+
expect(selectCommonProps.onSelect).not.toHaveBeenCalled();
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('6. menu closes on mouse leave when closeOnMouseLeave is set to true', async () => {
|
|
103
|
+
const { getByTestId } = render(<NsSelect {...selectCommonProps} closeOnMouseLeave />);
|
|
104
|
+
|
|
105
|
+
const selectHead = getByTestId('select-head');
|
|
106
|
+
fireEvent.mouseDown(selectHead);
|
|
107
|
+
const selectList = screen.queryByTestId('select-menu-list');
|
|
108
|
+
const option1 = screen.queryByTestId(createOptionTestId(selectCommonProps.options[0].value));
|
|
109
|
+
const option2 = screen.queryByTestId(createOptionTestId(selectCommonProps.options[1].value));
|
|
110
|
+
const option3 = screen.queryByTestId(createOptionTestId(selectCommonProps.options[2].value));
|
|
111
|
+
if (selectList) fireEvent.mouseLeave(selectList);
|
|
112
|
+
|
|
113
|
+
await waitFor(() => {
|
|
114
|
+
expect(option1).not.toBeInTheDocument();
|
|
115
|
+
expect(option2).not.toBeInTheDocument();
|
|
116
|
+
expect(option3).not.toBeInTheDocument();
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('7. select does not open when disabled', () => {
|
|
121
|
+
render(<NsSelect {...selectCommonProps} disabled />);
|
|
122
|
+
|
|
123
|
+
const selectHead = screen.getByTestId('select-head');
|
|
124
|
+
fireEvent.mouseDown(selectHead);
|
|
125
|
+
|
|
126
|
+
expect(screen.queryByTestId('select-menu-list')).not.toBeInTheDocument();
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it('8. opens the select on "ArrowDown" key press', () => {
|
|
130
|
+
render(<NsSelect {...selectCommonProps} />);
|
|
131
|
+
|
|
132
|
+
const selectHead = screen.getByTestId('select-head');
|
|
133
|
+
fireEvent.mouseDown(selectHead, { key: 'arrowDown' });
|
|
134
|
+
|
|
135
|
+
expect(selectHead).toBeInTheDocument();
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it('9. selects the next option on arrow right click', () => {
|
|
139
|
+
const { getByTestId, container } = render(<NsSelect {...selectCommonProps} hasArrows />);
|
|
140
|
+
|
|
141
|
+
const selectHead = getByTestId('select-head');
|
|
142
|
+
fireEvent.mouseDown(selectHead);
|
|
143
|
+
|
|
144
|
+
const rightArrow = container.querySelector('.RightArrow');
|
|
145
|
+
if (rightArrow) fireEvent.click(rightArrow);
|
|
146
|
+
|
|
147
|
+
expect(selectCommonProps.onSelect).toHaveBeenCalledWith({ ...selectCommonProps.options[1], index: 1 });
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it('10. selects the previous option on arrow left click', () => {
|
|
151
|
+
const { getByTestId } = render(<NsSelect {...selectCommonProps} hasArrows reverseArrowControls />);
|
|
152
|
+
|
|
153
|
+
const selectHead = getByTestId('select-head');
|
|
154
|
+
fireEvent.mouseDown(selectHead);
|
|
155
|
+
const option3 = getByTestId(createOptionTestId(selectCommonProps.options[2].value));
|
|
156
|
+
fireEvent.click(option3);
|
|
157
|
+
|
|
158
|
+
const leftArrow = document.getElementsByClassName('LeftArrow')[0];
|
|
159
|
+
fireEvent.click(leftArrow);
|
|
160
|
+
|
|
161
|
+
expect(selectCommonProps.onSelect).toHaveBeenCalledWith({ ...selectCommonProps.options[1], index: 1 });
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it('11. renders an error message with red styles', () => {
|
|
165
|
+
const errorMessage = 'Invalid value';
|
|
166
|
+
|
|
167
|
+
const { getByText } = render(<NsSelect {...selectCommonProps} hasArrows errorMessage={errorMessage} />);
|
|
168
|
+
|
|
169
|
+
const errorText = getByText(errorMessage);
|
|
170
|
+
expect(errorText).toBeInTheDocument();
|
|
171
|
+
});
|
|
172
|
+
it('12. selecting an option calls the onSelect callback', () => {
|
|
173
|
+
const { getByTestId } = render(<NsSelect {...selectCommonProps} hasArrows />);
|
|
174
|
+
|
|
175
|
+
const selectHead = getByTestId('select-head');
|
|
176
|
+
fireEvent.mouseDown(selectHead);
|
|
177
|
+
|
|
178
|
+
const option1 = getByTestId(createOptionTestId(selectCommonProps.options[2].value));
|
|
179
|
+
fireEvent.click(option1);
|
|
180
|
+
|
|
181
|
+
expect(selectCommonProps.onSelect).toHaveBeenCalledWith({ ...selectCommonProps.options[2], index: 2 });
|
|
182
|
+
});
|
|
183
|
+
});
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { fireEvent, render } from '@/test-utils/index';
|
|
2
|
+
import React from 'react';
|
|
3
|
+
|
|
4
|
+
import { NsSelectOption } from '../';
|
|
5
|
+
|
|
6
|
+
describe('NsSelectOption', () => {
|
|
7
|
+
it('1. renders NsSelectOption component without errors', () => {
|
|
8
|
+
const value = 'option1';
|
|
9
|
+
const label = 'Option 1';
|
|
10
|
+
const onClick = jest.fn();
|
|
11
|
+
const { getByTestId, getByText } = render(
|
|
12
|
+
<NsSelectOption value={value} label={label} index={1} onClick={onClick} />
|
|
13
|
+
);
|
|
14
|
+
|
|
15
|
+
// Check if the option container element exists
|
|
16
|
+
const optionContainer = getByTestId(`option-${value}`);
|
|
17
|
+
expect(optionContainer).toBeInTheDocument();
|
|
18
|
+
|
|
19
|
+
// Check if the option label is rendered correctly
|
|
20
|
+
const optionLabel = getByText(label);
|
|
21
|
+
expect(optionLabel).toBeInTheDocument();
|
|
22
|
+
});
|
|
23
|
+
it('2. clicking on NsSelectOption selects the option', () => {
|
|
24
|
+
const value = 'option1';
|
|
25
|
+
const label = 'Option 1';
|
|
26
|
+
const onClick = jest.fn();
|
|
27
|
+
const { getByTestId } = render(<NsSelectOption value={value} label={label} onClick={onClick} index={1} />);
|
|
28
|
+
const optionElement = getByTestId(`option-${value}`);
|
|
29
|
+
fireEvent.click(optionElement);
|
|
30
|
+
expect(onClick).toHaveBeenCalled();
|
|
31
|
+
});
|
|
32
|
+
});
|