@nuskin/foundation-ui-components 1.1.1 → 1.2.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 +4 -4
- package/src/common/auth-dropdown/NsAuthDropdown.tsx +8 -3
- package/src/common/autocomplete/NsAutocomplete.spec.tsx +3 -3
- package/src/common/autocomplete/mock/MockData.ts +1 -0
- package/src/common/button/NsButton.stories.tsx +0 -1
- package/src/common/card/styled/NsFfgStyled.tsx +0 -1
- package/src/common/carousal/NsCarousal.tsx +9 -3
- package/src/common/cart-icon/NsCartIcon.spec.tsx +2 -2
- package/src/common/checkbox/types.d.ts +1 -1
- package/src/common/dropdown/NsDropdown.tsx +7 -2
- package/src/common/hooks/useOnClickOutside.tsx +25 -0
- package/src/common/icon/NsIconVariants.ts +1 -1
- package/src/common/index.ts +1 -1
- package/src/common/label/NsLabel.stories.tsx +0 -3
- package/src/common/label/NsLabel.tsx +1 -1
- package/src/common/progress/NsProgress.spec.tsx +47 -0
- package/src/common/progress/NsProgress.stories.tsx +37 -0
- package/src/common/progress/NsProgress.styled.tsx +39 -0
- package/src/common/progress/NsProgress.tsx +27 -0
- package/src/common/progress/index.ts +2 -0
- package/src/common/progress/types.d.ts +21 -0
- package/src/common/radiogroup/NsRadioGroup.styled.tsx +4 -3
- package/src/common/select/NsSelect.spec.tsx +98 -17
- package/src/common/select/NsSelect.styled.tsx +53 -22
- package/src/common/select/NsSelect.tsx +109 -44
- package/src/common/select/NsSelectOption.tsx +0 -1
- package/src/common/select/stories/NsSelect.stories.tsx +23 -14
- package/src/common/select/types.d.ts +3 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nuskin/foundation-ui-components",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "It is a foundation library written in react, that provides common components to all UI pages",
|
|
5
5
|
"private": false,
|
|
6
6
|
"main": "src/common/index.ts",
|
|
@@ -41,8 +41,8 @@
|
|
|
41
41
|
"types:check": "tsc --pretty --noEmit"
|
|
42
42
|
},
|
|
43
43
|
"dependencies": {
|
|
44
|
-
"@emotion/react": "^11.
|
|
45
|
-
"@emotion/styled": "^11.
|
|
44
|
+
"@emotion/react": "^11.11.1",
|
|
45
|
+
"@emotion/styled": "^11.11.0",
|
|
46
46
|
"@mui/icons-material": "^5.11.11",
|
|
47
47
|
"@mui/material": "5.4.2",
|
|
48
48
|
"@mui/utils": "^5.14.1",
|
|
@@ -71,7 +71,7 @@
|
|
|
71
71
|
"@storybook/addon-mdx-gfm": "^7.0.7",
|
|
72
72
|
"@storybook/jest": "^0.1.0",
|
|
73
73
|
"@storybook/nextjs": "^7.0.7",
|
|
74
|
-
"@storybook/react": "^7.0.
|
|
74
|
+
"@storybook/react": "^7.0.24",
|
|
75
75
|
"@storybook/test-runner": "^0.10.0",
|
|
76
76
|
"@storybook/testing-library": "^0.1.0",
|
|
77
77
|
"@testing-library/jest-dom": "^5.16.5",
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { useCallback, useState } from 'react';
|
|
1
|
+
import { useCallback, useRef, useState } from 'react';
|
|
2
|
+
|
|
2
3
|
import { NsIcon } from '../icon';
|
|
3
4
|
import {
|
|
4
5
|
AuthDropdownContainer,
|
|
@@ -11,6 +12,7 @@ import {
|
|
|
11
12
|
SignUpLink,
|
|
12
13
|
} from './NsAuthDropdown.styled';
|
|
13
14
|
import { NsAuthDropdownProps } from './types.d';
|
|
15
|
+
import { useOnClickOutside } from '../hooks/useOnClickOutside';
|
|
14
16
|
|
|
15
17
|
const NsAuthDropdown = ({
|
|
16
18
|
signInRedirectLink,
|
|
@@ -23,11 +25,14 @@ const NsAuthDropdown = ({
|
|
|
23
25
|
darkModeEnabled = true,
|
|
24
26
|
}: NsAuthDropdownProps) => {
|
|
25
27
|
const [isOpen, setIsOpen] = useState(false);
|
|
28
|
+
const dropdowneRef = useRef<HTMLDivElement | null>(null);
|
|
26
29
|
|
|
27
30
|
const handleToggle = () => {
|
|
28
|
-
setIsOpen(
|
|
31
|
+
setIsOpen(!isOpen);
|
|
29
32
|
};
|
|
30
33
|
|
|
34
|
+
useOnClickOutside(dropdowneRef, handleToggle);
|
|
35
|
+
|
|
31
36
|
const closeDropdown = useCallback(() => {
|
|
32
37
|
if (closeOnMouseLeave) {
|
|
33
38
|
setIsOpen(false);
|
|
@@ -35,7 +40,7 @@ const NsAuthDropdown = ({
|
|
|
35
40
|
}, [closeOnMouseLeave]);
|
|
36
41
|
|
|
37
42
|
return (
|
|
38
|
-
<AuthDropdownContainer>
|
|
43
|
+
<AuthDropdownContainer ref={dropdowneRef}>
|
|
39
44
|
<AuthHead data-testid="auth-head" onClick={handleToggle} darkModeEnabled={darkModeEnabled}>
|
|
40
45
|
<Button>{titleText}</Button>
|
|
41
46
|
<NsIcon testId="chevron-icon" name={isOpen ? 'arrowUp' : 'arrowDown'} color="primary" />
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
+
import userEvent from '@testing-library/user-event';
|
|
3
|
+
import { act } from 'react-dom/test-utils';
|
|
2
4
|
import { render, screen, waitFor } from '@/test-utils/index';
|
|
3
|
-
import { NsAutocomplete } from '@/common/index';
|
|
4
5
|
import { OptionUiComponent } from './mock/MockUI';
|
|
5
6
|
import { AutocompleteMockList } from './mock/MockData';
|
|
6
|
-
import
|
|
7
|
-
import { act } from 'react-dom/test-utils';
|
|
7
|
+
import NsAutocomplete from './NsAutocomplete';
|
|
8
8
|
|
|
9
9
|
describe('CustomAutocomplete', () => {
|
|
10
10
|
it('1. should render autocomplete component', () => {
|
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
import Box from '@mui/material/Box';
|
|
2
2
|
import { styled } from '@nuskin/foundation-theme';
|
|
3
|
-
import { useRef, useCallback } from 'react';
|
|
3
|
+
import { useRef, useCallback, ReactNode } from 'react';
|
|
4
4
|
import Slider from 'react-slick';
|
|
5
5
|
import 'slick-carousel/slick/slick.css';
|
|
6
6
|
import 'slick-carousel/slick/slick-theme.css';
|
|
7
7
|
import defaultCarousalSettings from './slickSetting';
|
|
8
8
|
import CarousalControl from './CarousalControl';
|
|
9
|
-
import { NsCarousalProps } from './types';
|
|
10
9
|
|
|
11
10
|
const SlickContainer = styled(Box)(() => {
|
|
12
11
|
return {
|
|
@@ -17,8 +16,15 @@ const SlickContainer = styled(Box)(() => {
|
|
|
17
16
|
},
|
|
18
17
|
};
|
|
19
18
|
});
|
|
19
|
+
interface CarousalProps<T = any> {
|
|
20
|
+
entries: T[];
|
|
21
|
+
// eslint-disable-next-line no-unused-vars
|
|
22
|
+
renderEntry(entry: T, index: number): ReactNode;
|
|
23
|
+
allEntryLink: string;
|
|
24
|
+
settings: any;
|
|
25
|
+
}
|
|
20
26
|
|
|
21
|
-
export default function NsCarousal({ entries, renderEntry, allEntryLink, settings }:
|
|
27
|
+
export default function NsCarousal({ entries, renderEntry, allEntryLink, settings }: CarousalProps) {
|
|
22
28
|
const sliderRef = useRef();
|
|
23
29
|
const prev = useCallback(() => {
|
|
24
30
|
if (sliderRef.current) {
|
|
@@ -3,13 +3,13 @@ import { render, screen } from '@/test-utils/index';
|
|
|
3
3
|
|
|
4
4
|
const { getByTestId } = screen;
|
|
5
5
|
describe('NsCartIcon', () => {
|
|
6
|
-
it('renders Cart icon when item count is 0', () => {
|
|
6
|
+
it('1. renders Cart icon when item count is 0', () => {
|
|
7
7
|
render(<NsCartIcon itemCount={0} color="primary" size="large" />);
|
|
8
8
|
const cartIcon = getByTestId('cart-badge-icon');
|
|
9
9
|
expect(cartIcon).toBeInTheDocument();
|
|
10
10
|
});
|
|
11
11
|
|
|
12
|
-
it('renders item count badge when item count is greater than 0', () => {
|
|
12
|
+
it('2. renders item count badge when item count is greater than 0', () => {
|
|
13
13
|
const itemCount = 5;
|
|
14
14
|
const { getByText } = render(<NsCartIcon itemCount={5} color="primary" size="large" />);
|
|
15
15
|
const badgeContentElement = getByText(itemCount.toString());
|
|
@@ -3,7 +3,7 @@ import { CheckboxProps as MUICheckboxProps } from '@material-ui/core/Checkbox';
|
|
|
3
3
|
import { NsFlagProps } from '../flag';
|
|
4
4
|
|
|
5
5
|
// Define the props for the custom checkbox component
|
|
6
|
-
export type NsCheckboxProps =
|
|
6
|
+
export type NsCheckboxProps = Pick<MUICheckboxProps, 'label' | 'onChange'> & {
|
|
7
7
|
name: string;
|
|
8
8
|
className?: string;
|
|
9
9
|
checked: boolean;
|
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
import React, { useState } from 'react';
|
|
1
|
+
import React, { useRef, useState } from 'react';
|
|
2
|
+
|
|
2
3
|
import { DropdownContainer, DropdownButton, DropdownList, DropdownHead } from './NsDropdown.styled';
|
|
3
4
|
import { NsIcon } from '../icon';
|
|
4
5
|
import { DropdownProps } from './types.d';
|
|
6
|
+
import { useOnClickOutside } from '../hooks/useOnClickOutside';
|
|
5
7
|
|
|
6
8
|
const NsDropdown: React.FC<DropdownProps> = ({
|
|
7
9
|
selectedValue,
|
|
@@ -17,12 +19,15 @@ const NsDropdown: React.FC<DropdownProps> = ({
|
|
|
17
19
|
}) => {
|
|
18
20
|
const [isOpen, setIsOpen] = useState(false);
|
|
19
21
|
const [value, setValue] = useState(selectedValue || '');
|
|
22
|
+
const dropdowneRef = useRef<HTMLDivElement | null>(null);
|
|
20
23
|
|
|
21
24
|
const handleToggle = () => {
|
|
22
25
|
if (disabled) return;
|
|
23
26
|
setIsOpen(!isOpen);
|
|
24
27
|
};
|
|
25
28
|
|
|
29
|
+
useOnClickOutside(dropdowneRef, handleToggle);
|
|
30
|
+
|
|
26
31
|
const closeDropdown = () => {
|
|
27
32
|
if (closeOnMouseLeave) {
|
|
28
33
|
setIsOpen(false);
|
|
@@ -36,7 +41,7 @@ const NsDropdown: React.FC<DropdownProps> = ({
|
|
|
36
41
|
};
|
|
37
42
|
|
|
38
43
|
return (
|
|
39
|
-
<DropdownContainer disabled={disabled}>
|
|
44
|
+
<DropdownContainer disabled={disabled} ref={dropdowneRef}>
|
|
40
45
|
<DropdownHead data-testid="dropdown-head" disabled={disabled} type={type} onClick={handleToggle}>
|
|
41
46
|
<DropdownButton>{selectedLabel || placeholder}</DropdownButton>
|
|
42
47
|
<NsIcon testId="chevron-icon" name={isOpen ? 'arrowUp' : 'arrowDown'} color="primary" />
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { RefObject, useEffect } from 'react';
|
|
2
|
+
|
|
3
|
+
export type Handler = () => void;
|
|
4
|
+
|
|
5
|
+
export function useOnClickOutside<T extends HTMLElement = HTMLElement>(
|
|
6
|
+
ref: RefObject<T>,
|
|
7
|
+
handler: Handler,
|
|
8
|
+
mouseEvent: 'click' | 'mousedown' | 'mouseup' = 'click'
|
|
9
|
+
): void {
|
|
10
|
+
const handleEventListener = (event: MouseEvent) => {
|
|
11
|
+
const el = ref?.current;
|
|
12
|
+
if (!el || el.contains(event.target as Node)) {
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
handler();
|
|
16
|
+
};
|
|
17
|
+
useEffect(() => {
|
|
18
|
+
if (document) {
|
|
19
|
+
document.addEventListener(mouseEvent, handleEventListener);
|
|
20
|
+
return () => {
|
|
21
|
+
document.removeEventListener(mouseEvent, handleEventListener);
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
}, []);
|
|
25
|
+
}
|
|
@@ -11,6 +11,7 @@ import CheckVerified from './icons/CheckVerified';
|
|
|
11
11
|
import SuccessIcon from './icons/SuccessIcon';
|
|
12
12
|
import ErrorIcon from './icons/ErrorIcon';
|
|
13
13
|
import TrashIcon from './icons/TrashIcon';
|
|
14
|
+
import SearchIcon from './icons/SearchIcon';
|
|
14
15
|
import SlashIcon from './icons/SlashIcon';
|
|
15
16
|
import Home from './icons/home';
|
|
16
17
|
import AlertErrorIcon from './icons/AlertErrorIcon';
|
|
@@ -19,7 +20,6 @@ import CloseIcon from './icons/CloseIcon';
|
|
|
19
20
|
import UserIcon from './icons/UserIcon';
|
|
20
21
|
import HamburgerIcon from './icons/HamburgerIcon';
|
|
21
22
|
import UserOutline from './icons/UserOutline';
|
|
22
|
-
import SearchIcon from './icons/SearchIcon';
|
|
23
23
|
|
|
24
24
|
const IconVariants = {
|
|
25
25
|
arrowDown: ArrowDown,
|
package/src/common/index.ts
CHANGED
|
@@ -16,7 +16,6 @@ export const Default: Story = {
|
|
|
16
16
|
args: {
|
|
17
17
|
children: 'Label',
|
|
18
18
|
required: false,
|
|
19
|
-
size: 16,
|
|
20
19
|
},
|
|
21
20
|
};
|
|
22
21
|
|
|
@@ -24,7 +23,6 @@ export const Optional: Story = {
|
|
|
24
23
|
args: {
|
|
25
24
|
children: 'Label',
|
|
26
25
|
showOptional: true,
|
|
27
|
-
size: 16,
|
|
28
26
|
},
|
|
29
27
|
};
|
|
30
28
|
|
|
@@ -33,6 +31,5 @@ export const Required: Story = {
|
|
|
33
31
|
children: 'Label',
|
|
34
32
|
showOptional: true,
|
|
35
33
|
required: true,
|
|
36
|
-
size: 16,
|
|
37
34
|
},
|
|
38
35
|
};
|
|
@@ -27,7 +27,7 @@ const StyledLabel = styled(FormLabel)<{ fontSize?: number; style?: CSSProperties
|
|
|
27
27
|
const NsLabel = (props: NsLabelProps) => {
|
|
28
28
|
const { required, showOptional, children, size } = props;
|
|
29
29
|
return (
|
|
30
|
-
<StyledLabel
|
|
30
|
+
<StyledLabel fontSize={size} {...props} required={required}>
|
|
31
31
|
{children}
|
|
32
32
|
{!required && showOptional ? <span className="optional-label">{' (Optional)'}</span> : null}
|
|
33
33
|
</StyledLabel>
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { render, screen } from '@testing-library/react';
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import Progress from './NsProgress';
|
|
4
|
+
|
|
5
|
+
describe(' NsProgress Component', () => {
|
|
6
|
+
test('should render Progress bar', () => {
|
|
7
|
+
render(<Progress color="primary" value={10}></Progress>);
|
|
8
|
+
|
|
9
|
+
const pbar = screen.getByRole('progressbar');
|
|
10
|
+
|
|
11
|
+
expect(pbar).toBeInTheDocument();
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it('matches the height of the progress bar to the width of the container', () => {
|
|
15
|
+
// Define the props
|
|
16
|
+
const value = 50;
|
|
17
|
+
|
|
18
|
+
// Render the StyledProgress component with the value prop
|
|
19
|
+
const { container } = render(<Progress color="primary" value={value} />);
|
|
20
|
+
|
|
21
|
+
// Get the progress bar element
|
|
22
|
+
const progressBar = container.querySelector('.MuiLinearProgress-bar');
|
|
23
|
+
|
|
24
|
+
// Check if the progress bar element exists
|
|
25
|
+
if (progressBar) {
|
|
26
|
+
// Retrieve the computed width of the container element
|
|
27
|
+
const containerWidth = container.clientWidth;
|
|
28
|
+
|
|
29
|
+
// Retrieve the computed styles of the progress bar
|
|
30
|
+
const progressBarStyle = getComputedStyle(progressBar);
|
|
31
|
+
const progressBarHeight = parseFloat(progressBarStyle.height);
|
|
32
|
+
const progressBarPadding =
|
|
33
|
+
parseFloat(progressBarStyle.paddingTop) + parseFloat(progressBarStyle.paddingBottom);
|
|
34
|
+
const progressBarBorder =
|
|
35
|
+
parseFloat(progressBarStyle.borderTopWidth) + parseFloat(progressBarStyle.borderBottomWidth);
|
|
36
|
+
|
|
37
|
+
// Calculate the expected height of the progress bar
|
|
38
|
+
const expectedHeight = containerWidth - progressBarPadding - progressBarBorder;
|
|
39
|
+
|
|
40
|
+
// Assert that the height of the progress bar matches the width of the container
|
|
41
|
+
expect(progressBarHeight).toBe(expectedHeight);
|
|
42
|
+
} else {
|
|
43
|
+
// Handle the case where the progress bar element is not found
|
|
44
|
+
throw new Error('Progress bar element not found');
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
});
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { withCenterDecorator } from '@/stories/decorators/withCenterDecorator';
|
|
2
|
+
import type { StoryObj, Meta } from '@storybook/react';
|
|
3
|
+
import NsProgress from './NsProgress';
|
|
4
|
+
|
|
5
|
+
type ComponentType = typeof NsProgress;
|
|
6
|
+
type Story = StoryObj<ComponentType>;
|
|
7
|
+
type MetaStory = Meta<ComponentType>;
|
|
8
|
+
|
|
9
|
+
export default {
|
|
10
|
+
component: NsProgress,
|
|
11
|
+
title: 'Input/NsProgress',
|
|
12
|
+
decorators: [withCenterDecorator],
|
|
13
|
+
argTypes: {
|
|
14
|
+
color: {
|
|
15
|
+
description: 'defines the color for the completed progress',
|
|
16
|
+
},
|
|
17
|
+
height: {
|
|
18
|
+
description: 'height of the bar',
|
|
19
|
+
},
|
|
20
|
+
value: {
|
|
21
|
+
description: 'value of the progress done',
|
|
22
|
+
},
|
|
23
|
+
backgroundColor: {
|
|
24
|
+
description: 'color of incomplete progress',
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
} as MetaStory;
|
|
28
|
+
|
|
29
|
+
export const ProgressBar: Story = {
|
|
30
|
+
args: {
|
|
31
|
+
color: 'primary',
|
|
32
|
+
backgroundColor: '#19dbdb',
|
|
33
|
+
value: undefined,
|
|
34
|
+
height: '10px',
|
|
35
|
+
body: <span>{'% Complete'}</span>,
|
|
36
|
+
},
|
|
37
|
+
};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { styled, ColorUtils } from '@nuskin/foundation-theme';
|
|
2
|
+
import { LinearProgress } from '@mui/material';
|
|
3
|
+
import CheckCircleIcon from '@mui/icons-material/CheckCircle';
|
|
4
|
+
import { NsProgressProps } from './types';
|
|
5
|
+
|
|
6
|
+
const { color } = ColorUtils;
|
|
7
|
+
export const StyledCheckCircle = styled(CheckCircleIcon)`
|
|
8
|
+
color: ${color('neutral', '50')};
|
|
9
|
+
padding-top: 0.6em;
|
|
10
|
+
padding-right: 0.5em;
|
|
11
|
+
justify-self: flex-end;
|
|
12
|
+
`;
|
|
13
|
+
|
|
14
|
+
export const PercentDiv = styled('div')`
|
|
15
|
+
color: ${color('primary', '100')};
|
|
16
|
+
display: flex;
|
|
17
|
+
justify-content: flex-end;
|
|
18
|
+
font-family: Inter;
|
|
19
|
+
font-size: 12px;
|
|
20
|
+
font-style: normal;
|
|
21
|
+
font-weight: 700;
|
|
22
|
+
text-transform: uppercase;
|
|
23
|
+
`;
|
|
24
|
+
|
|
25
|
+
export const StyledLinearProgress = styled(LinearProgress)<Omit<NsProgressProps, 'value'>>(
|
|
26
|
+
({ height, backgroundColor, color }) => ({
|
|
27
|
+
width: '334px',
|
|
28
|
+
height,
|
|
29
|
+
backgroundColor,
|
|
30
|
+
borderRadius: '3px',
|
|
31
|
+
display: 'inline-block',
|
|
32
|
+
|
|
33
|
+
'.MuiLinearProgress-bar': {
|
|
34
|
+
backgroundColor: color,
|
|
35
|
+
},
|
|
36
|
+
})
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
export default StyledLinearProgress;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
|
|
3
|
+
import { NsProgressProps } from './types';
|
|
4
|
+
import StyledLinearProgress, { PercentDiv, StyledCheckCircle } from './NsProgress.styled';
|
|
5
|
+
|
|
6
|
+
const NsProgress = (props: NsProgressProps) => {
|
|
7
|
+
const { color, backgroundColor, height, value, body } = props;
|
|
8
|
+
return (
|
|
9
|
+
<>
|
|
10
|
+
<PercentDiv>
|
|
11
|
+
{value}
|
|
12
|
+
{body}
|
|
13
|
+
</PercentDiv>
|
|
14
|
+
<StyledLinearProgress
|
|
15
|
+
variant="determinate"
|
|
16
|
+
role="progressbar"
|
|
17
|
+
color={color}
|
|
18
|
+
value={value}
|
|
19
|
+
height={height}
|
|
20
|
+
backgroundColor={backgroundColor}
|
|
21
|
+
/>
|
|
22
|
+
<StyledCheckCircle />
|
|
23
|
+
</>
|
|
24
|
+
);
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export default NsProgress;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { FoundationTheme } from '@nuskin/foundation-theme';
|
|
2
|
+
import { ReactNode } from 'react';
|
|
3
|
+
|
|
4
|
+
declare module '@mui/material/LinearProgress' {
|
|
5
|
+
// eslint-disable-next-line no-unused-vars
|
|
6
|
+
interface LinearProgressPropsColorOverrides {
|
|
7
|
+
neutral: true;
|
|
8
|
+
accentColorA: true;
|
|
9
|
+
accentColorB: true;
|
|
10
|
+
accentColorC: true;
|
|
11
|
+
accentColorD: true;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export type NsProgressProps = {
|
|
16
|
+
color?: FoundationTheme.PaletteColors;
|
|
17
|
+
backgroundColor?: string;
|
|
18
|
+
height?: string;
|
|
19
|
+
value: number;
|
|
20
|
+
body?: ReactNode;
|
|
21
|
+
};
|
|
@@ -5,9 +5,10 @@ import { skipDarkModeEnabled } from '../util';
|
|
|
5
5
|
const { paletteColor, isDarkMode, color } = ColorUtils;
|
|
6
6
|
|
|
7
7
|
export const StyledHelperText = styled('div')`
|
|
8
|
-
margin-top :10px;
|
|
9
|
-
|
|
10
|
-
|
|
8
|
+
margin-top :10px;
|
|
9
|
+
padding-left: 0.5em;
|
|
10
|
+
text-align: left;
|
|
11
|
+
justify-self: flex-start;
|
|
11
12
|
)`;
|
|
12
13
|
|
|
13
14
|
export const StyledSpan = styled('span')`
|
|
@@ -8,7 +8,7 @@ function createSelectOptions(options: { value: string; label: string }[]) {
|
|
|
8
8
|
}
|
|
9
9
|
|
|
10
10
|
describe('NsSelectOption', () => {
|
|
11
|
-
|
|
11
|
+
it('1. renders NsSelectOption component without errors', () => {
|
|
12
12
|
const value = 'option1';
|
|
13
13
|
const label = 'Option 1';
|
|
14
14
|
const { getByTestId, getByText } = render(<NsSelectOption value={value} label={label} />);
|
|
@@ -21,7 +21,7 @@ describe('NsSelectOption', () => {
|
|
|
21
21
|
const optionLabel = getByText(label);
|
|
22
22
|
expect(optionLabel).toBeInTheDocument();
|
|
23
23
|
});
|
|
24
|
-
|
|
24
|
+
it('2. clicking on NsSelectOption selects the option', () => {
|
|
25
25
|
const value = 'option1';
|
|
26
26
|
const label = 'Option 1';
|
|
27
27
|
const onSelect = jest.fn();
|
|
@@ -43,7 +43,7 @@ describe('NsSelect', () => {
|
|
|
43
43
|
{ value: 'Option3', label: 'Option 3' },
|
|
44
44
|
];
|
|
45
45
|
|
|
46
|
-
it('renders the placeholder text when no value is selected', () => {
|
|
46
|
+
it('1. renders the placeholder text when no value is selected', () => {
|
|
47
47
|
render(
|
|
48
48
|
<NsSelect
|
|
49
49
|
placeholder="Select an option"
|
|
@@ -58,9 +58,7 @@ describe('NsSelect', () => {
|
|
|
58
58
|
expect(screen.getByText('Select an option')).toBeInTheDocument();
|
|
59
59
|
});
|
|
60
60
|
|
|
61
|
-
|
|
62
|
-
// const value = 'option1';
|
|
63
|
-
// const label = 'Option 1';
|
|
61
|
+
it('2. clicking outside the select container closes the dropdown', () => {
|
|
64
62
|
const onSelect = jest.fn();
|
|
65
63
|
const selectedLabel = 'Option 1';
|
|
66
64
|
const { getByTestId, queryByTestId } = render(
|
|
@@ -74,7 +72,7 @@ describe('NsSelect', () => {
|
|
|
74
72
|
expect(queryByTestId('option-Option1')).toBeNull();
|
|
75
73
|
});
|
|
76
74
|
|
|
77
|
-
it('renders the selected value label', () => {
|
|
75
|
+
it('3. renders the selected value label', () => {
|
|
78
76
|
const selectedLabel = 'Option 1';
|
|
79
77
|
render(
|
|
80
78
|
<NsSelect selectedValue="Option1" selectedLabel={selectedLabel} onSelect={onSelect}>
|
|
@@ -85,7 +83,7 @@ describe('NsSelect', () => {
|
|
|
85
83
|
expect(screen.getByText(selectedLabel)).toBeInTheDocument();
|
|
86
84
|
});
|
|
87
85
|
|
|
88
|
-
|
|
86
|
+
it('4. clicking on select button opens the dropdown', () => {
|
|
89
87
|
const { getByTestId, getByText } = render(
|
|
90
88
|
<NsSelect selectedValue="Option1" selectedLabel={selectedLabel} onSelect={onSelect}>
|
|
91
89
|
{createSelectOptions(options)}
|
|
@@ -105,7 +103,7 @@ describe('NsSelect', () => {
|
|
|
105
103
|
expect(option2).toBeInTheDocument();
|
|
106
104
|
});
|
|
107
105
|
|
|
108
|
-
|
|
106
|
+
it('5. disabling the select component prevents interaction', () => {
|
|
109
107
|
const onSelect = jest.fn();
|
|
110
108
|
const { getByTestId, queryByTestId } = render(
|
|
111
109
|
<NsSelect selectedValue="Option1" selectedLabel={selectedLabel} onSelect={onSelect} disabled>
|
|
@@ -124,7 +122,7 @@ describe('NsSelect', () => {
|
|
|
124
122
|
expect(onSelect).not.toHaveBeenCalled();
|
|
125
123
|
});
|
|
126
124
|
|
|
127
|
-
it('select closes on mouse leave when closeOnMouseLeave is set to true', async () => {
|
|
125
|
+
it('6. select closes on mouse leave when closeOnMouseLeave is set to true', async () => {
|
|
128
126
|
render(
|
|
129
127
|
<NsSelect closeOnMouseLeave selectedValue={selectedValue} selectedLabel={selectedLabel} onSelect={onSelect}>
|
|
130
128
|
{createSelectOptions(options)}
|
|
@@ -146,7 +144,7 @@ describe('NsSelect', () => {
|
|
|
146
144
|
expect(option3).not.toBeInTheDocument();
|
|
147
145
|
});
|
|
148
146
|
});
|
|
149
|
-
it('select does not open when disabled', () => {
|
|
147
|
+
it('7. select does not open when disabled', () => {
|
|
150
148
|
render(
|
|
151
149
|
<NsSelect selectedValue={selectedValue} selectedLabel={selectedLabel} onSelect={onSelect} disabled>
|
|
152
150
|
{createSelectOptions(options)}
|
|
@@ -159,7 +157,7 @@ describe('NsSelect', () => {
|
|
|
159
157
|
expect(screen.queryByTestId('select-list')).not.toBeInTheDocument();
|
|
160
158
|
});
|
|
161
159
|
|
|
162
|
-
it('opens the select on "ArrowDown" key press', () => {
|
|
160
|
+
it('8. opens the select on "ArrowDown" key press', () => {
|
|
163
161
|
render(
|
|
164
162
|
<NsSelect selectedValue={selectedValue} selectedLabel={selectedLabel} onSelect={onSelect}>
|
|
165
163
|
{createSelectOptions(options)}
|
|
@@ -172,9 +170,77 @@ describe('NsSelect', () => {
|
|
|
172
170
|
expect(selectHead).toBeInTheDocument();
|
|
173
171
|
});
|
|
174
172
|
|
|
175
|
-
|
|
176
|
-
const
|
|
173
|
+
it('9. closes the select when closeSelect is called', () => {
|
|
174
|
+
const { getByTestId } = render(
|
|
175
|
+
<NsSelect selectedValue={selectedValue} selectedLabel={selectedLabel} onSelect={onSelect} closeOnMouseLeave>
|
|
176
|
+
{createSelectOptions(options)}
|
|
177
|
+
</NsSelect>
|
|
178
|
+
);
|
|
179
|
+
|
|
180
|
+
const selectContainer = getByTestId('select-container');
|
|
181
|
+
fireEvent.mouseLeave(selectContainer);
|
|
182
|
+
|
|
183
|
+
expect(screen.queryByTestId('select-list')).not.toBeInTheDocument();
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it('10. does not open the select when disabled', () => {
|
|
177
187
|
render(
|
|
188
|
+
<NsSelect selectedValue={selectedValue} selectedLabel={selectedLabel} onSelect={onSelect} disabled>
|
|
189
|
+
{createSelectOptions(options)}
|
|
190
|
+
</NsSelect>
|
|
191
|
+
);
|
|
192
|
+
|
|
193
|
+
const selectHead = screen.getByTestId('select-head');
|
|
194
|
+
fireEvent.mouseDown(selectHead);
|
|
195
|
+
|
|
196
|
+
expect(screen.queryByTestId('select-list')).not.toBeInTheDocument();
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it('11. selects the next option on arrow right click', () => {
|
|
200
|
+
// Render the NsSelect component with options and onSelect function
|
|
201
|
+
const onSelect = jest.fn();
|
|
202
|
+
const { getByTestId } = render(
|
|
203
|
+
<NsSelect selectedValue={selectedValue} selectedLabel={selectedLabel} onSelect={onSelect} hasArrows>
|
|
204
|
+
{createSelectOptions(options)}
|
|
205
|
+
</NsSelect>
|
|
206
|
+
);
|
|
207
|
+
|
|
208
|
+
// Simulate the arrow right click on the NsSelect component
|
|
209
|
+
const selectHead = getByTestId('select-head');
|
|
210
|
+
fireEvent.mouseDown(selectHead);
|
|
211
|
+
|
|
212
|
+
const rightArrow = document.getElementsByClassName('RightArrow')[0];
|
|
213
|
+
fireEvent.click(rightArrow);
|
|
214
|
+
|
|
215
|
+
// Verify that the next option is selected and the onSelect function is called
|
|
216
|
+
expect(onSelect).toHaveBeenCalledWith('Option1');
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
it('12. selects the previous option on arrow left click', () => {
|
|
220
|
+
const { getByTestId } = render(
|
|
221
|
+
<NsSelect
|
|
222
|
+
selectedValue={selectedValue}
|
|
223
|
+
selectedLabel={selectedLabel}
|
|
224
|
+
onSelect={onSelect}
|
|
225
|
+
hasArrows
|
|
226
|
+
reverseArrowControls
|
|
227
|
+
>
|
|
228
|
+
{createSelectOptions(options)}
|
|
229
|
+
</NsSelect>
|
|
230
|
+
);
|
|
231
|
+
|
|
232
|
+
const selectHead = getByTestId('select-head');
|
|
233
|
+
fireEvent.mouseDown(selectHead);
|
|
234
|
+
|
|
235
|
+
const leftArrow = document.getElementsByClassName('LeftArrow')[0];
|
|
236
|
+
fireEvent.click(leftArrow);
|
|
237
|
+
|
|
238
|
+
expect(onSelect).toHaveBeenCalledWith('Option1');
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
it('13. renders an error message with red styles', () => {
|
|
242
|
+
const errorMessage = 'Invalid value';
|
|
243
|
+
const { getByText } = render(
|
|
178
244
|
<NsSelect
|
|
179
245
|
selectedValue={selectedValue}
|
|
180
246
|
selectedLabel={selectedLabel}
|
|
@@ -185,9 +251,24 @@ describe('NsSelect', () => {
|
|
|
185
251
|
</NsSelect>
|
|
186
252
|
);
|
|
187
253
|
|
|
188
|
-
|
|
254
|
+
// Check if the error message is rendered correctly with red styles
|
|
255
|
+
const errorText = getByText(errorMessage);
|
|
256
|
+
expect(errorText).toBeInTheDocument();
|
|
257
|
+
});
|
|
258
|
+
it('14. selecting an option calls the onSelect callback', () => {
|
|
259
|
+
const onSelect = jest.fn();
|
|
260
|
+
const { getByTestId } = render(
|
|
261
|
+
<NsSelect selectedValue={selectedValue} selectedLabel={selectedLabel} onSelect={onSelect}>
|
|
262
|
+
{createSelectOptions(options)}
|
|
263
|
+
</NsSelect>
|
|
264
|
+
);
|
|
265
|
+
|
|
266
|
+
const selectHead = getByTestId('select-head');
|
|
267
|
+
fireEvent.mouseDown(selectHead);
|
|
268
|
+
|
|
269
|
+
const option1 = getByTestId('option-Option1');
|
|
270
|
+
fireEvent.click(option1);
|
|
189
271
|
|
|
190
|
-
expect(
|
|
191
|
-
expect(selectContainer).toHaveStyle('color: red');
|
|
272
|
+
expect(onSelect).toHaveBeenCalledWith('Option1');
|
|
192
273
|
});
|
|
193
274
|
});
|
|
@@ -1,12 +1,27 @@
|
|
|
1
|
+
import { HTMLAttributes } from 'react';
|
|
1
2
|
import { styled, ColorUtils, FoundationTheme } from '@nuskin/foundation-theme';
|
|
2
3
|
import { Themed } from '../common.types';
|
|
3
4
|
import { getDropdownHeadBorder } from '../dropdown/NsDropdown.styled';
|
|
4
5
|
const { color, paletteColor, isDarkMode } = ColorUtils;
|
|
5
6
|
|
|
6
|
-
|
|
7
|
+
interface SelectLabelProps extends HTMLAttributes<HTMLLabelElement> {
|
|
8
|
+
errorMessage?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export const SelectContainer = styled.div<{ disabled: boolean; errorMessage?: string }>`
|
|
7
12
|
position: relative;
|
|
8
13
|
display: inline-block;
|
|
9
14
|
opacity: ${({ disabled }) => (disabled ? '0.5' : '1')};
|
|
15
|
+
color: ${({ theme, errorMessage }) =>
|
|
16
|
+
errorMessage ? paletteColor('error')({ theme }) : paletteColor('primary')({ theme })};
|
|
17
|
+
`;
|
|
18
|
+
export const SelectMain = styled.div`
|
|
19
|
+
position: relative;
|
|
20
|
+
display: flex;
|
|
21
|
+
`;
|
|
22
|
+
|
|
23
|
+
export const ErrorContainer = styled.div`
|
|
24
|
+
color: ${paletteColor('error')};
|
|
10
25
|
`;
|
|
11
26
|
|
|
12
27
|
export const SelectHead = styled.div<{ type: string; disabled: boolean }>`
|
|
@@ -26,6 +41,18 @@ export const SelectButton = styled.div`
|
|
|
26
41
|
padding-right: 10px;
|
|
27
42
|
`;
|
|
28
43
|
|
|
44
|
+
export const LeftArrowContainer = styled.div`
|
|
45
|
+
display: flex;
|
|
46
|
+
align-items: center;
|
|
47
|
+
padding-top: 30px;
|
|
48
|
+
`;
|
|
49
|
+
|
|
50
|
+
export const RightArrowContainer = styled.div`
|
|
51
|
+
display: flex;
|
|
52
|
+
align-items: center;
|
|
53
|
+
padding-top: 30px;
|
|
54
|
+
`;
|
|
55
|
+
|
|
29
56
|
export const SelectList = styled.div<{ borderColor: FoundationTheme.PaletteColors }>`
|
|
30
57
|
position: absolute;
|
|
31
58
|
z-index: 100;
|
|
@@ -33,18 +60,28 @@ export const SelectList = styled.div<{ borderColor: FoundationTheme.PaletteColor
|
|
|
33
60
|
padding: 0;
|
|
34
61
|
margin: 0;
|
|
35
62
|
width: -webkit-fill-available;
|
|
36
|
-
color: ${paletteColor('primary')};
|
|
63
|
+
color: ${paletteColor('primary', 'main')};
|
|
37
64
|
background-color: ${paletteColor('primary', 'contrastText')};
|
|
65
|
+
max-height: 200px;
|
|
66
|
+
overflow-y: auto;
|
|
38
67
|
`;
|
|
39
68
|
|
|
40
|
-
const getDividerColor = (
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
const getListBgColor = (props: { theme: FoundationTheme.Theme }) => {
|
|
69
|
+
const getDividerColor = ({ theme }: Themed) =>
|
|
70
|
+
isDarkMode({ theme }) ? color('neutral', '20')({ theme }) : color('neutral', '30')({ theme });
|
|
71
|
+
const getListBgColor = (props: Themed) => {
|
|
45
72
|
return !isDarkMode(props) ? color('accentColorB', '10')(props) : color('accentColorB', '50')(props);
|
|
46
73
|
};
|
|
47
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
|
+
`;
|
|
48
85
|
function createListContainerStyle({ borderBottom, bgColor, color, hoveredBg }: ContainerParam) {
|
|
49
86
|
return `
|
|
50
87
|
cursor: pointer;
|
|
@@ -62,15 +99,7 @@ function createListContainerStyle({ borderBottom, bgColor, color, hoveredBg }: C
|
|
|
62
99
|
}
|
|
63
100
|
`;
|
|
64
101
|
}
|
|
65
|
-
|
|
66
|
-
${({ showListDivider, isSelected, theme }) => {
|
|
67
|
-
const borderBottom = showListDivider ? getDividerColor({ theme }) : 'none';
|
|
68
|
-
const bgColor = isSelected ? paletteColor('primary')({ theme }) : 'white';
|
|
69
|
-
const color = isSelected ? 'white' : 'black';
|
|
70
|
-
const hoveredBg = getListBgColor({ theme });
|
|
71
|
-
return createListContainerStyle({ borderBottom, bgColor, color, hoveredBg });
|
|
72
|
-
}}
|
|
73
|
-
`;
|
|
102
|
+
|
|
74
103
|
const getListItemBgColor = ({ isSelected, theme }: { isSelected: boolean } & Themed) => {
|
|
75
104
|
if (isSelected) {
|
|
76
105
|
return paletteColor('primary')({ theme });
|
|
@@ -86,6 +115,14 @@ const getListItemBorderLeftColor = ({ isSelected, theme }: { isSelected: boolean
|
|
|
86
115
|
return 'none';
|
|
87
116
|
};
|
|
88
117
|
|
|
118
|
+
export const SelectLabel = styled.label<SelectLabelProps>`
|
|
119
|
+
display: block;
|
|
120
|
+
margin-bottom: 5px;
|
|
121
|
+
color: ${({ theme, errorMessage }) =>
|
|
122
|
+
errorMessage ? paletteColor('error')({ theme }) : paletteColor('primary')({ theme })};
|
|
123
|
+
}
|
|
124
|
+
`;
|
|
125
|
+
|
|
89
126
|
export const ListItem = styled.div<{ isSelected: boolean }>(({ isSelected, theme }) => ({
|
|
90
127
|
padding: '5px 10px',
|
|
91
128
|
borderLeft: getListItemBorderLeftColor({ isSelected, theme }),
|
|
@@ -96,9 +133,3 @@ export const ListItem = styled.div<{ isSelected: boolean }>(({ isSelected, theme
|
|
|
96
133
|
fontWeight: 'bold',
|
|
97
134
|
},
|
|
98
135
|
}));
|
|
99
|
-
|
|
100
|
-
export const SelectLabel = styled.label`
|
|
101
|
-
display: block;
|
|
102
|
-
margin-bottom: 5px;
|
|
103
|
-
color: ${paletteColor('primary')} !important;
|
|
104
|
-
`;
|
|
@@ -1,8 +1,17 @@
|
|
|
1
|
-
import React, { useState, useRef, useEffect } from 'react';
|
|
2
|
-
import {
|
|
1
|
+
import React, { useState, useRef, useEffect, Children } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
SelectContainer,
|
|
4
|
+
SelectButton,
|
|
5
|
+
SelectList,
|
|
6
|
+
SelectHead,
|
|
7
|
+
SelectLabel,
|
|
8
|
+
SelectMain,
|
|
9
|
+
LeftArrowContainer,
|
|
10
|
+
RightArrowContainer,
|
|
11
|
+
ErrorContainer,
|
|
12
|
+
} from './NsSelect.styled';
|
|
3
13
|
import { NsIcon } from '../icon';
|
|
4
14
|
import { SelectProps } from './types.d';
|
|
5
|
-
|
|
6
15
|
const NsSelect: React.FC<SelectProps> = ({
|
|
7
16
|
selectedValue,
|
|
8
17
|
selectedLabel,
|
|
@@ -17,9 +26,14 @@ const NsSelect: React.FC<SelectProps> = ({
|
|
|
17
26
|
label,
|
|
18
27
|
readOnly = false,
|
|
19
28
|
errorMessage,
|
|
29
|
+
hasArrows,
|
|
30
|
+
// eslint-disable-next-line no-unused-vars
|
|
31
|
+
rules,
|
|
32
|
+
reverseArrowControls,
|
|
20
33
|
}) => {
|
|
21
34
|
const [isOpen, setIsOpen] = useState(false);
|
|
22
35
|
const [value, setValue] = useState(selectedValue || '');
|
|
36
|
+
const [selectedIndex, setSelectedIndex] = useState(-1);
|
|
23
37
|
const selectContainerRef = useRef<HTMLDivElement>(null);
|
|
24
38
|
const selectHeadRef = useRef<HTMLDivElement>(null);
|
|
25
39
|
|
|
@@ -29,19 +43,16 @@ const NsSelect: React.FC<SelectProps> = ({
|
|
|
29
43
|
}
|
|
30
44
|
};
|
|
31
45
|
|
|
32
|
-
const handleSelect = (input: string) => {
|
|
46
|
+
const handleSelect = (input: string, index: number) => {
|
|
33
47
|
onSelect(input);
|
|
34
48
|
setIsOpen(false);
|
|
35
49
|
setValue(input);
|
|
50
|
+
setSelectedIndex(index);
|
|
36
51
|
};
|
|
37
52
|
|
|
38
53
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
39
54
|
const handleClickOutside = (event: MouseEvent) => {
|
|
40
|
-
if (
|
|
41
|
-
selectContainerRef.current &&
|
|
42
|
-
!selectContainerRef.current.contains(event.target as Node) &&
|
|
43
|
-
isOpen
|
|
44
|
-
) {
|
|
55
|
+
if (selectContainerRef.current && !selectContainerRef.current.contains(event.target as Node) && isOpen) {
|
|
45
56
|
setIsOpen(false);
|
|
46
57
|
}
|
|
47
58
|
};
|
|
@@ -55,6 +66,40 @@ const NsSelect: React.FC<SelectProps> = ({
|
|
|
55
66
|
}, 0);
|
|
56
67
|
}
|
|
57
68
|
};
|
|
69
|
+
const handleArrowRightClick = () => {
|
|
70
|
+
if (selectedIndex !== -1) {
|
|
71
|
+
const nextIndex = reverseArrowControls ? selectedIndex - 1 : selectedIndex + 1;
|
|
72
|
+
const childrenArray = Children.toArray(children);
|
|
73
|
+
const optionAtIndex = childrenArray[nextIndex];
|
|
74
|
+
const { value } = (optionAtIndex as any)?.props ?? {};
|
|
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
|
+
}
|
|
102
|
+
};
|
|
58
103
|
|
|
59
104
|
useEffect(() => {
|
|
60
105
|
document.addEventListener('click', handleClickOutside);
|
|
@@ -64,53 +109,73 @@ const NsSelect: React.FC<SelectProps> = ({
|
|
|
64
109
|
}, [handleClickOutside, isOpen]);
|
|
65
110
|
|
|
66
111
|
return (
|
|
67
|
-
<
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
112
|
+
<SelectMain>
|
|
113
|
+
{hasArrows && (
|
|
114
|
+
<LeftArrowContainer
|
|
115
|
+
onClick={handleArrowLeftClick}
|
|
116
|
+
className="LeftArrow"
|
|
117
|
+
style={{
|
|
118
|
+
visibility:
|
|
119
|
+
!reverseArrowControls && (selectedIndex === 0 || selectedIndex < 0) ? 'hidden' : 'visible',
|
|
120
|
+
}}
|
|
121
|
+
>
|
|
122
|
+
<NsIcon name="arrowLeft" color="primary" />
|
|
123
|
+
</LeftArrowContainer>
|
|
124
|
+
)}
|
|
125
|
+
<SelectContainer
|
|
126
|
+
data-testid="select-container"
|
|
79
127
|
disabled={disabled || readOnly}
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
tabIndex={0}
|
|
83
|
-
ref={selectHeadRef}
|
|
128
|
+
ref={selectContainerRef}
|
|
129
|
+
errorMessage={errorMessage}
|
|
84
130
|
>
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
onMouseLeave={closeSelect}
|
|
94
|
-
borderColor={selectBorderColor}
|
|
95
|
-
className='selectList'
|
|
131
|
+
{label && <SelectLabel errorMessage={errorMessage}>{label}</SelectLabel>}
|
|
132
|
+
<SelectHead
|
|
133
|
+
data-testid="select-head"
|
|
134
|
+
disabled={disabled || readOnly}
|
|
135
|
+
type={type}
|
|
136
|
+
onMouseDown={handleMouseClick}
|
|
137
|
+
tabIndex={0}
|
|
138
|
+
ref={selectHeadRef}
|
|
96
139
|
>
|
|
97
|
-
<
|
|
98
|
-
|
|
140
|
+
<SelectButton>{selectedLabel || placeholder}</SelectButton>
|
|
141
|
+
{!readOnly && <NsIcon name={isOpen ? 'arrowUp' : 'arrowDown'} color="primary" />}
|
|
142
|
+
</SelectHead>
|
|
143
|
+
{isOpen ? (
|
|
144
|
+
<SelectList
|
|
145
|
+
data-testid="select-list"
|
|
146
|
+
onMouseLeave={closeSelect}
|
|
147
|
+
borderColor={selectBorderColor}
|
|
148
|
+
className="selectList"
|
|
149
|
+
>
|
|
150
|
+
{React.Children.map(children, (child, index) => {
|
|
99
151
|
if (React.isValidElement(child)) {
|
|
100
152
|
return React.cloneElement(child, {
|
|
101
153
|
// @ts-ignore
|
|
102
|
-
onSelect: handleSelect,
|
|
154
|
+
onSelect: (input: string) => handleSelect(input, index),
|
|
103
155
|
selectedValue: value,
|
|
104
156
|
showListDivider,
|
|
157
|
+
index,
|
|
105
158
|
});
|
|
106
159
|
}
|
|
107
160
|
return null;
|
|
108
161
|
})}
|
|
109
|
-
</
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
162
|
+
</SelectList>
|
|
163
|
+
) : null}
|
|
164
|
+
{errorMessage && <ErrorContainer>{errorMessage}</ErrorContainer>}
|
|
165
|
+
</SelectContainer>
|
|
166
|
+
{hasArrows && (
|
|
167
|
+
<RightArrowContainer
|
|
168
|
+
onClick={handleArrowRightClick}
|
|
169
|
+
className="RightArrow"
|
|
170
|
+
style={{
|
|
171
|
+
visibility:
|
|
172
|
+
reverseArrowControls && (selectedIndex === 0 || selectedIndex < 0) ? 'hidden' : 'visible',
|
|
173
|
+
}}
|
|
174
|
+
>
|
|
175
|
+
<NsIcon name="arrowRight" color="primary" />
|
|
176
|
+
</RightArrowContainer>
|
|
177
|
+
)}
|
|
178
|
+
</SelectMain>
|
|
114
179
|
);
|
|
115
180
|
};
|
|
116
181
|
|
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
import React, { useState } from 'react';
|
|
2
|
-
import {
|
|
2
|
+
import type { Meta, StoryObj } from '@storybook/react';
|
|
3
3
|
import NsSelect from '../NsSelect';
|
|
4
4
|
import NsSelectOption from '../NsSelectOption';
|
|
5
|
+
import { withCenterDecorator } from '@/stories/decorators/withCenterDecorator';
|
|
6
|
+
|
|
7
|
+
type ComponentType = typeof NsSelect;
|
|
8
|
+
type Story = StoryObj<ComponentType>;
|
|
9
|
+
type MetaStory = Meta<ComponentType>;
|
|
5
10
|
|
|
6
11
|
export default {
|
|
7
12
|
title: 'Components/Inputs/NsSelect',
|
|
8
|
-
decorators: [
|
|
9
|
-
(story) => <div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center' }}>{story()}</div>,
|
|
10
|
-
],
|
|
13
|
+
decorators: [withCenterDecorator],
|
|
11
14
|
component: NsSelect,
|
|
12
15
|
argTypes: {
|
|
13
16
|
type: { control: 'select', options: ['unfilled', 'outlined'] },
|
|
@@ -26,6 +29,7 @@ export default {
|
|
|
26
29
|
validateOnBlur: { control: 'boolean' },
|
|
27
30
|
autoComplete: { control: 'text' },
|
|
28
31
|
rules: { control: 'array' },
|
|
32
|
+
hasArrows: { control: 'boolean' },
|
|
29
33
|
},
|
|
30
34
|
parameters: {
|
|
31
35
|
options: [
|
|
@@ -100,17 +104,16 @@ export default {
|
|
|
100
104
|
validateOnBlur: false,
|
|
101
105
|
autoComplete: '',
|
|
102
106
|
rules: [],
|
|
107
|
+
hasArrows: false,
|
|
103
108
|
},
|
|
104
|
-
} as
|
|
105
|
-
|
|
106
|
-
export const Select: ComponentStory<typeof NsSelect> = (args, context) => {
|
|
107
|
-
const [selectedLabel, setSelectedLabel] = useState(args.selectedLabel);
|
|
108
|
-
const [selectedValue, setSelectedValue] = useState(args.selectedValue);
|
|
109
|
+
} as MetaStory;
|
|
109
110
|
|
|
110
|
-
|
|
111
|
+
const AppSelect = (props: any) => {
|
|
112
|
+
const [selectedLabel, setSelectedLabel] = useState(props.selectedLabel);
|
|
113
|
+
const [selectedValue, setSelectedValue] = useState(props.selectedValue);
|
|
111
114
|
const handleSelect = (value: string) => {
|
|
112
115
|
setSelectedValue(value);
|
|
113
|
-
options.find((option: any) => {
|
|
116
|
+
props.options.find((option: any) => {
|
|
114
117
|
if (option.value === value) {
|
|
115
118
|
setSelectedLabel(option.label);
|
|
116
119
|
}
|
|
@@ -120,15 +123,21 @@ export const Select: ComponentStory<typeof NsSelect> = (args, context) => {
|
|
|
120
123
|
|
|
121
124
|
return (
|
|
122
125
|
<NsSelect
|
|
123
|
-
{...
|
|
126
|
+
{...props}
|
|
124
127
|
selectedValue={selectedValue}
|
|
125
128
|
selectedLabel={selectedLabel}
|
|
126
129
|
onSelect={handleSelect}
|
|
127
|
-
label={
|
|
130
|
+
label={props.label}
|
|
128
131
|
>
|
|
129
|
-
{options.map((option: any) => (
|
|
132
|
+
{props.options.map((option: any) => (
|
|
130
133
|
<NsSelectOption key={option.value} value={option.value} label={option.label} />
|
|
131
134
|
))}
|
|
132
135
|
</NsSelect>
|
|
133
136
|
);
|
|
134
137
|
};
|
|
138
|
+
export const Select: Story = {
|
|
139
|
+
render: (args, context) => {
|
|
140
|
+
const options = context.parameters.options;
|
|
141
|
+
return <AppSelect options={options} {...args} />;
|
|
142
|
+
},
|
|
143
|
+
};
|
|
@@ -22,7 +22,9 @@ export interface SelectProps {
|
|
|
22
22
|
itemValue?: string;
|
|
23
23
|
validateOnBlur?: boolean;
|
|
24
24
|
autoComplete?: string;
|
|
25
|
-
|
|
25
|
+
// eslint-disable-next-line no-undef
|
|
26
|
+
rules?: Array;
|
|
27
|
+
hasArrows?: boolean;
|
|
26
28
|
}
|
|
27
29
|
|
|
28
30
|
export interface SelectOptionProps {
|