@nuskin/foundation-ui-components 1.0.0-develop.3 → 1.0.0-develop.5
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 +9 -4
- package/src/common/accordion/NsAccordion.tsx +77 -0
- package/src/common/accordion/NsAccordionHeader.tsx +56 -0
- package/src/common/accordion/NsAccordionItem.tsx +39 -0
- package/src/common/accordion/NsCollapse.tsx +56 -0
- package/src/common/accordion/NsToggleSection.tsx +30 -0
- package/src/common/accordion/ToggleSection.styled.tsx +28 -0
- package/src/common/accordion/index.ts +6 -0
- package/src/common/accordion/specs/NsAccordion.spec.tsx +93 -0
- package/src/common/accordion/specs/NsAccordionHeader.spec.tsx +63 -0
- package/src/common/accordion/specs/NsAccordionItem.spec.tsx +62 -0
- package/src/common/accordion/specs/NsCollapse.spec.tsx +37 -0
- package/src/common/accordion/specs/NsToggleSection.spec.tsx +38 -0
- package/src/common/accordion/stories/NsAccordion.stories.tsx +47 -0
- package/src/common/accordion/stories/NsAccordionHeader.stories.tsx +40 -0
- package/src/common/accordion/stories/NsAccordionItem.stories.tsx +40 -0
- package/src/common/accordion/stories/NsCollapse.stories.tsx +35 -0
- package/src/common/accordion/stories/NsToggleSection.stories.tsx +23 -0
- package/src/common/accordion/stories/data.tsx +22 -0
- package/src/common/accordion/types.d.ts +73 -0
- package/src/common/alert/NsAlert.spec.tsx +0 -1
- package/src/common/alert/NsAlert.styled.ts +10 -10
- package/src/common/alert/NsAlert.tsx +4 -3
- package/src/common/alert/types.d.ts +22 -22
- package/src/common/auth-dropdown/NsAuthDropdown.styled.ts +12 -11
- package/src/common/auth-dropdown/NsAuthDropdown.tsx +2 -1
- package/src/common/auth-dropdown/types.d.ts +3 -1
- package/src/common/autocomplete/NsAutocomplete.spec.tsx +20 -8
- package/src/common/autocomplete/NsAutocomplete.stories.tsx +3 -0
- package/src/common/autocomplete/NsAutocomplete.styled.tsx +12 -0
- package/src/common/autocomplete/NsAutocomplete.tsx +36 -18
- package/src/common/autocomplete/types.d.ts +7 -1
- package/src/common/brand-components/NsBrandComponent.tsx +3 -0
- package/src/common/breadcrumb/NsBreadcrumb.tsx +4 -2
- package/src/common/button/NsButton.spec.tsx +11 -12
- package/src/common/button/NsButton.stories.tsx +1 -1
- package/src/common/button/NsButton.styled.tsx +25 -23
- package/src/common/button/NsButton.tsx +9 -2
- package/src/common/button/types.d.ts +1 -0
- package/src/common/checkbox/NsCheckbox.styled.tsx +2 -2
- package/src/common/common.types.d.ts +2 -0
- package/src/common/dropdown/types.d.ts +3 -1
- package/src/common/email-simple/NsEmailSimple.spec.tsx +145 -0
- package/src/common/email-simple/NsEmailSimple.stories.tsx +61 -0
- package/src/common/email-simple/NsEmailSimple.tsx +71 -0
- package/src/common/email-simple/index.ts +2 -0
- package/src/common/email-simple/types.d.ts +14 -0
- package/src/common/flag/NsFlag.spec.tsx +4 -24
- package/src/common/flag/NsFlag.styled.tsx +5 -5
- package/src/common/flag/NsFlag.tsx +0 -2
- package/src/common/icon/types.d.ts +1 -0
- package/src/common/image/types.d.ts +1 -0
- package/src/common/index.ts +3 -0
- package/src/common/input/InputFieldWrapper.tsx +4 -4
- package/src/common/input/NsInputField.stories.tsx +11 -9
- package/src/common/input/NsInputField.tsx +12 -5
- package/src/common/input/getAdornmentProps.tsx +4 -0
- package/src/common/input/types.d.ts +3 -1
- package/src/common/label/NsLabel.stories.tsx +3 -0
- package/src/common/label/NsLabel.tsx +18 -17
- package/src/common/label/types.d.ts +2 -1
- package/src/common/nav-tab/NsNavTab.spec.tsx +0 -5
- package/src/common/nav-tab/NsNavTab.styled.tsx +7 -4
- package/src/common/radiogroup/NsRadioGroup.spec.tsx +64 -0
- package/src/common/radiogroup/NsRadioGroup.stories.tsx +102 -0
- package/src/common/radiogroup/NsRadioGroup.styled.tsx +50 -0
- package/src/common/radiogroup/NsRadioGroup.tsx +61 -0
- package/src/common/radiogroup/index.ts +2 -0
- package/src/common/radiogroup/types.d.ts +20 -0
- package/src/common/select/NsSelect.spec.tsx +200 -0
- package/src/common/select/NsSelect.styled.tsx +85 -0
- package/src/common/select/NsSelect.tsx +117 -0
- package/src/common/select/NsSelectOption.tsx +30 -0
- package/src/common/select/index.ts +3 -0
- package/src/common/select/stories/NsSelect.stories.tsx +134 -0
- package/src/common/select/types.d.ts +34 -0
- package/src/common/table/NsExpandableRow.tsx +68 -0
- package/src/common/table/NsTable.spec.tsx +69 -41
- package/src/common/table/NsTable.stories.tsx +45 -7
- package/src/common/table/NsTable.styled.ts +106 -12
- package/src/common/table/NsTable.tsx +78 -89
- package/src/common/table/NsTableHeader.tsx +52 -38
- package/src/common/table/NsTablePagination.tsx +21 -13
- package/src/common/table/types.d.ts +21 -8
- package/src/common/text-strip/NsTextStrip.tsx +10 -7
- package/src/common/typography/Typography.styled.tsx +3 -3
- package/src/common/validations/regex.ts +3 -0
- /package/src/common/card/{tests → specs}/BrandLogo.spec.tsx +0 -0
- /package/src/common/card/{tests → specs}/NsBgCard.spec.tsx +0 -0
- /package/src/common/card/{tests → specs}/NsBrandProductCard.spec.tsx +0 -0
- /package/src/common/card/{tests → specs}/NsFfgCard.spec.tsx +0 -0
- /package/src/common/carousal/{tests → specs}/Chevron.spec.tsx +0 -0
- /package/src/common/carousal/{tests → specs}/NsCarousal.spec.tsx +0 -0
- /package/src/common/carousal/{tests → specs}/NsScrollingCarousal.spec.tsx +0 -0
- /package/src/common/carousal/{tests → specs}/RenderBrand.spec.tsx +0 -0
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
|
|
2
|
+
import type { StoryObj, Meta } from '@storybook/react';
|
|
3
|
+
import { within, userEvent } from '@storybook/testing-library';
|
|
4
|
+
import { expect } from '@storybook/jest';
|
|
5
|
+
import { NsRadioGroup } from './'
|
|
6
|
+
import { FoundationTheme } from '@nuskin/foundation-theme';
|
|
7
|
+
|
|
8
|
+
type ComponentType = typeof NsRadioGroup;
|
|
9
|
+
type Story = StoryObj<ComponentType>;
|
|
10
|
+
type MetaStory = Meta<ComponentType>;
|
|
11
|
+
|
|
12
|
+
export default {
|
|
13
|
+
component: NsRadioGroup,
|
|
14
|
+
title: 'Components/NsRadioGroup',
|
|
15
|
+
argTypes: {
|
|
16
|
+
disable: {
|
|
17
|
+
description:
|
|
18
|
+
'Indicates whether the radio button is enabled/disabled',
|
|
19
|
+
|
|
20
|
+
},
|
|
21
|
+
items: {
|
|
22
|
+
description:
|
|
23
|
+
'Value of the component to be emitted when selected.',
|
|
24
|
+
},
|
|
25
|
+
name: {
|
|
26
|
+
description:
|
|
27
|
+
'Name of the component',
|
|
28
|
+
|
|
29
|
+
},
|
|
30
|
+
label: {
|
|
31
|
+
description:
|
|
32
|
+
'Label of the radio group',
|
|
33
|
+
|
|
34
|
+
},
|
|
35
|
+
color: {
|
|
36
|
+
description:
|
|
37
|
+
'Color of the radio button',
|
|
38
|
+
|
|
39
|
+
},
|
|
40
|
+
size: {
|
|
41
|
+
description:
|
|
42
|
+
'Margin Left for Additional Slot text',
|
|
43
|
+
|
|
44
|
+
},
|
|
45
|
+
labelFontSize: {
|
|
46
|
+
description:
|
|
47
|
+
'Used to change font-size of the label'
|
|
48
|
+
},
|
|
49
|
+
additionalSlot: {
|
|
50
|
+
description:
|
|
51
|
+
'An additional slot allows for more text',
|
|
52
|
+
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
} as MetaStory;
|
|
56
|
+
|
|
57
|
+
export const RadioGroup: Story = {
|
|
58
|
+
args: {
|
|
59
|
+
disable: false,
|
|
60
|
+
items: [
|
|
61
|
+
{
|
|
62
|
+
id: 'individual',
|
|
63
|
+
label: `Vendeur à domicile indépendant mandataire (The VDI mandataire
|
|
64
|
+
enters into sales or service agreements
|
|
65
|
+
with customers in the name and on behalf of the company.
|
|
66
|
+
His remuneration will be based on commissions made
|
|
67
|
+
on the sales of products to customers.)`,
|
|
68
|
+
value: 'individual',
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
id: ' retailer',
|
|
72
|
+
label: `Vendeur à domicile indépendant acheteur-revendeur
|
|
73
|
+
(The VDI acheteur-revendeur sells the company's products in his own name and behalf.
|
|
74
|
+
His remuneration will be based on commissions and on the margins made on the sales of
|
|
75
|
+
products to customers.)`,
|
|
76
|
+
value: 'retailer',
|
|
77
|
+
},
|
|
78
|
+
],
|
|
79
|
+
name: 'test',
|
|
80
|
+
label: 'I choose to signup as',
|
|
81
|
+
color: '#272525' as FoundationTheme.PaletteColors,
|
|
82
|
+
additionalSlot: 'An additional slot allows for more text',
|
|
83
|
+
size: 10,
|
|
84
|
+
labelFontSize: 16,
|
|
85
|
+
additionalProps: {
|
|
86
|
+
testid: 'radio-group',
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
play: async ({ canvasElement }) => {
|
|
90
|
+
const canvas = within(canvasElement);
|
|
91
|
+
await userEvent.hover(canvas.getByTestId('radio-group'));
|
|
92
|
+
await expect(
|
|
93
|
+
canvas.getByText(
|
|
94
|
+
'Vendeur à domicile indépendant mandataire (The VDI mandataire enters into sales or service agreements with customers in the name and on behalf of the company. His remuneration will be based on commissions made on the sales of products to customers.)'
|
|
95
|
+
)
|
|
96
|
+
).toBeInTheDocument();
|
|
97
|
+
},
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { Radio, FormControlLabel, FormLabel, RadioGroup } from '@mui/material';
|
|
2
|
+
import { ColorUtils, FoundationTheme, styled } from '@nuskin/foundation-theme';
|
|
3
|
+
|
|
4
|
+
const { paletteColor, isDarkMode, color } = ColorUtils;
|
|
5
|
+
|
|
6
|
+
export const StyledHelperText = styled('div')`
|
|
7
|
+
margin-top :10px;
|
|
8
|
+
padding-left: 0.5em;
|
|
9
|
+
text-align: left;
|
|
10
|
+
justify-self: flex-start;
|
|
11
|
+
)`;
|
|
12
|
+
|
|
13
|
+
export const StyledSpan = styled('span')`
|
|
14
|
+
font-size: '16px';
|
|
15
|
+
font-weight: 300;
|
|
16
|
+
color: ${paletteColor('primary')};
|
|
17
|
+
`;
|
|
18
|
+
|
|
19
|
+
export const StyledLabel = styled(FormLabel)`
|
|
20
|
+
color: ${paletteColor('primary')},
|
|
21
|
+
marginBottom: '15px',
|
|
22
|
+
backgroundColor: 'transparent',
|
|
23
|
+
`;
|
|
24
|
+
|
|
25
|
+
export const StyledRadio = styled(Radio)`
|
|
26
|
+
color: ${(props) => !isDarkMode(props) ? color('primary', 'black')(props) : color('primary', 'white')(props)}
|
|
27
|
+
`;
|
|
28
|
+
|
|
29
|
+
export const StyledFormControlLabel = styled(FormControlLabel)<{ darkModeEnabled?: boolean }>((props) => {
|
|
30
|
+
return {
|
|
31
|
+
color: paletteColor('primary')(props),
|
|
32
|
+
marginBottom: '10px',
|
|
33
|
+
'.MuiFormControlLabel-label.Mui-disabled': {
|
|
34
|
+
color: `${!isDarkMode(props) ? color('neutral', 60)(props) : color('neutral', 20)(props)}`
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
export const StyledRadioGroup = styled(RadioGroup)<
|
|
40
|
+
{ color: FoundationTheme.PaletteColors; darkModeEnabled?: boolean }
|
|
41
|
+
>(
|
|
42
|
+
({ theme, color, darkModeEnabled }) => {
|
|
43
|
+
return {
|
|
44
|
+
marginTop: '10px',
|
|
45
|
+
'.MuiRadio-root.custom-radio.Mui-checked': {
|
|
46
|
+
color: paletteColor(color)({ theme, darkModeEnabled })
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
);
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { FormControl } from '@mui/material';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
StyledHelperText,
|
|
5
|
+
StyledFormControlLabel,
|
|
6
|
+
StyledRadio,
|
|
7
|
+
StyledSpan,
|
|
8
|
+
StyledRadioGroup,
|
|
9
|
+
} from './NsRadioGroup.styled';
|
|
10
|
+
import { NsLabel } from '../label';
|
|
11
|
+
|
|
12
|
+
import { NsRadioGroupProps } from './types';
|
|
13
|
+
|
|
14
|
+
const NsRadioGroup = (props: NsRadioGroupProps) => {
|
|
15
|
+
const {
|
|
16
|
+
items,
|
|
17
|
+
additionalSlot,
|
|
18
|
+
label,
|
|
19
|
+
onChange,
|
|
20
|
+
color,
|
|
21
|
+
disable,
|
|
22
|
+
size,
|
|
23
|
+
additionalProps,
|
|
24
|
+
labelFontSize,
|
|
25
|
+
darkModeEnabled = true
|
|
26
|
+
} = props;
|
|
27
|
+
|
|
28
|
+
return (
|
|
29
|
+
<>
|
|
30
|
+
<FormControl data-testid={additionalProps?.testid}>
|
|
31
|
+
<NsLabel size={labelFontSize}>{label}</NsLabel>
|
|
32
|
+
<StyledRadioGroup
|
|
33
|
+
aria-labelledby="radio-buttons-group-label"
|
|
34
|
+
defaultValue={items?.[0]?.value}
|
|
35
|
+
name="radio-buttons-group"
|
|
36
|
+
onChange={onChange}
|
|
37
|
+
color={color}
|
|
38
|
+
darkModeEnabled={darkModeEnabled}
|
|
39
|
+
>
|
|
40
|
+
{items?.map((item, index) => (
|
|
41
|
+
<StyledFormControlLabel
|
|
42
|
+
id={`${index} `}
|
|
43
|
+
key={item?.id}
|
|
44
|
+
value={item?.value}
|
|
45
|
+
control={<StyledRadio className="custom-radio" />}
|
|
46
|
+
label={item?.label}
|
|
47
|
+
disabled={disable}
|
|
48
|
+
darkModeEnabled={darkModeEnabled}
|
|
49
|
+
/>
|
|
50
|
+
))}
|
|
51
|
+
</StyledRadioGroup>
|
|
52
|
+
</FormControl>
|
|
53
|
+
{additionalSlot ? <StyledHelperText className={`ml-${size}`}>
|
|
54
|
+
<StyledSpan className="text-sm">{additionalSlot}</StyledSpan>
|
|
55
|
+
</StyledHelperText> : null}
|
|
56
|
+
|
|
57
|
+
</>
|
|
58
|
+
);
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
export default NsRadioGroup;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { FoundationTheme } from '@nuskin/foundation-theme';
|
|
2
|
+
import { AdditionalProps } from '../common.types';
|
|
3
|
+
|
|
4
|
+
export type NsRadioGroupProps = {
|
|
5
|
+
disable : boolean ;
|
|
6
|
+
name : string;
|
|
7
|
+
additionalSlot : string;
|
|
8
|
+
label : string ;
|
|
9
|
+
items: RadioItems[] ;
|
|
10
|
+
color: FoundationTheme.PaletteColors;
|
|
11
|
+
size : number;
|
|
12
|
+
labelFontSize:number,
|
|
13
|
+
onChange : () => void;
|
|
14
|
+
} & AdditionalProps;
|
|
15
|
+
|
|
16
|
+
export type RadioItems = {
|
|
17
|
+
label : string ;
|
|
18
|
+
value :string | number ;
|
|
19
|
+
id: string | number;
|
|
20
|
+
}
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { render, fireEvent, screen, waitFor } from '@/test-utils/index';
|
|
3
|
+
import NsSelect from './NsSelect';
|
|
4
|
+
import NsSelectOption from './NsSelectOption';
|
|
5
|
+
|
|
6
|
+
function createSelectOptions(options: { value: string; label: string }[]) {
|
|
7
|
+
return options.map((option) => <NsSelectOption key={option.value} value={option.value} label={option.label} />);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
describe('NsSelectOption', () => {
|
|
11
|
+
test('renders NsSelectOption component without errors', () => {
|
|
12
|
+
const value = 'option1';
|
|
13
|
+
const label = 'Option 1';
|
|
14
|
+
const { getByTestId, getByText } = render(
|
|
15
|
+
<NsSelectOption value={value} label={label} />
|
|
16
|
+
);
|
|
17
|
+
|
|
18
|
+
// Check if the option container element exists
|
|
19
|
+
const optionContainer = getByTestId(`option-${value}`);
|
|
20
|
+
expect(optionContainer).toBeInTheDocument();
|
|
21
|
+
|
|
22
|
+
// Check if the option label is rendered correctly
|
|
23
|
+
const optionLabel = getByText(label);
|
|
24
|
+
expect(optionLabel).toBeInTheDocument();
|
|
25
|
+
});
|
|
26
|
+
test('clicking on NsSelectOption selects the option', () => {
|
|
27
|
+
const value = 'option1';
|
|
28
|
+
const label = 'Option 1';
|
|
29
|
+
const onSelect = jest.fn();
|
|
30
|
+
const { getByTestId } = render(
|
|
31
|
+
<NsSelectOption value={value} label={label} onSelect={onSelect} />
|
|
32
|
+
);
|
|
33
|
+
const optionElement = getByTestId(`option-${value}`);
|
|
34
|
+
fireEvent.click(optionElement);
|
|
35
|
+
expect(onSelect).toHaveBeenCalledWith(value);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
describe('NsSelect', () => {
|
|
41
|
+
const onSelect = jest.fn();
|
|
42
|
+
const selectedValue = '';
|
|
43
|
+
const selectedLabel = '';
|
|
44
|
+
|
|
45
|
+
const options = [
|
|
46
|
+
{ value: 'Option1', label: 'Option 1' },
|
|
47
|
+
{ value: 'Option2', label: 'Option 2' },
|
|
48
|
+
{ value: 'Option3', label: 'Option 3' },
|
|
49
|
+
];
|
|
50
|
+
|
|
51
|
+
it('renders the placeholder text when no value is selected', () => {
|
|
52
|
+
render(
|
|
53
|
+
<NsSelect
|
|
54
|
+
placeholder="Select an option"
|
|
55
|
+
selectedValue={selectedValue}
|
|
56
|
+
selectedLabel={selectedLabel}
|
|
57
|
+
onSelect={onSelect}
|
|
58
|
+
>
|
|
59
|
+
{createSelectOptions(options)}
|
|
60
|
+
</NsSelect>
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
expect(screen.getByText('Select an option')).toBeInTheDocument();
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test('clicking outside the select container closes the dropdown', () => {
|
|
67
|
+
// const value = 'option1';
|
|
68
|
+
// const label = 'Option 1';
|
|
69
|
+
const onSelect = jest.fn();
|
|
70
|
+
const selectedLabel = 'Option 1';
|
|
71
|
+
const { getByTestId, queryByTestId } = render(
|
|
72
|
+
<NsSelect selectedValue="Option1" selectedLabel={selectedLabel} onSelect={onSelect}>
|
|
73
|
+
</NsSelect>
|
|
74
|
+
);
|
|
75
|
+
const selectButton = getByTestId('select-head');
|
|
76
|
+
fireEvent.mouseDown(selectButton);
|
|
77
|
+
const outsideElement = document.createElement('div');
|
|
78
|
+
fireEvent.mouseDown(outsideElement);
|
|
79
|
+
// Add assertions to check if the dropdown is closed (isOpen state is false)
|
|
80
|
+
expect(queryByTestId('option-Option1')).toBeNull();
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('renders the selected value label', () => {
|
|
84
|
+
const selectedLabel = 'Option 1';
|
|
85
|
+
render(
|
|
86
|
+
<NsSelect selectedValue="Option1" selectedLabel={selectedLabel} onSelect={onSelect}>
|
|
87
|
+
{createSelectOptions(options)}
|
|
88
|
+
</NsSelect>
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
expect(screen.getByText(selectedLabel)).toBeInTheDocument();
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test('clicking on select button opens the dropdown', () => {
|
|
95
|
+
const { getByTestId, getByText } = render(
|
|
96
|
+
<NsSelect selectedValue="Option1" selectedLabel={selectedLabel} onSelect={onSelect}>
|
|
97
|
+
{createSelectOptions(options)}
|
|
98
|
+
</NsSelect>
|
|
99
|
+
);
|
|
100
|
+
const selectButton = getByTestId('select-head');
|
|
101
|
+
fireEvent.mouseDown(selectButton);
|
|
102
|
+
|
|
103
|
+
// Check if the dropdown is opened (isOpen state is true)
|
|
104
|
+
const dropdownList = getByTestId('select-list');
|
|
105
|
+
expect(dropdownList).toBeInTheDocument();
|
|
106
|
+
|
|
107
|
+
// Check if the options are rendered in the dropdown
|
|
108
|
+
const option1 = getByText('Option 1');
|
|
109
|
+
const option2 = getByText('Option 2');
|
|
110
|
+
expect(option1).toBeInTheDocument();
|
|
111
|
+
expect(option2).toBeInTheDocument();
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test('disabling the select component prevents interaction', () => {
|
|
115
|
+
const onSelect = jest.fn();
|
|
116
|
+
const { getByTestId, queryByTestId } = render(
|
|
117
|
+
<NsSelect selectedValue="Option1" selectedLabel={selectedLabel} onSelect={onSelect} disabled>
|
|
118
|
+
<NsSelectOption value="option1" label="Option 1" onSelect={onSelect} />
|
|
119
|
+
<NsSelectOption value="option2" label="Option 2" onSelect={onSelect} />
|
|
120
|
+
</NsSelect>
|
|
121
|
+
);
|
|
122
|
+
const selectButton = getByTestId('select-head');
|
|
123
|
+
fireEvent.mouseDown(selectButton);
|
|
124
|
+
|
|
125
|
+
// Check if the dropdown does not open (select list is not present)
|
|
126
|
+
const dropdownList = queryByTestId('select-list');
|
|
127
|
+
expect(dropdownList).toBeNull();
|
|
128
|
+
|
|
129
|
+
// Check if the onSelect callback is not called
|
|
130
|
+
expect(onSelect).not.toHaveBeenCalled();
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it('select closes on mouse leave when closeOnMouseLeave is set to true', async () => {
|
|
134
|
+
render(
|
|
135
|
+
<NsSelect
|
|
136
|
+
closeOnMouseLeave
|
|
137
|
+
selectedValue={selectedValue}
|
|
138
|
+
selectedLabel={selectedLabel}
|
|
139
|
+
onSelect={onSelect}
|
|
140
|
+
>
|
|
141
|
+
{createSelectOptions(options)}
|
|
142
|
+
</NsSelect>
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
const selectHead = screen.getByTestId('select-head');
|
|
146
|
+
fireEvent.click(selectHead);
|
|
147
|
+
|
|
148
|
+
// const selectList = screen.queryByTestId ('select-list');
|
|
149
|
+
const option1 = screen.queryByTestId('option-Option1');
|
|
150
|
+
const option2 = screen.queryByTestId('option-Option2');
|
|
151
|
+
const option3 = screen.queryByTestId('option-Option3');
|
|
152
|
+
fireEvent.click(selectHead);
|
|
153
|
+
|
|
154
|
+
await waitFor(() => {
|
|
155
|
+
expect(option1).not.toBeInTheDocument();
|
|
156
|
+
expect(option2).not.toBeInTheDocument();
|
|
157
|
+
expect(option3).not.toBeInTheDocument();
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
it('select does not open when disabled', () => {
|
|
161
|
+
render(
|
|
162
|
+
<NsSelect selectedValue={selectedValue} selectedLabel={selectedLabel} onSelect={onSelect} disabled>
|
|
163
|
+
{createSelectOptions(options)}
|
|
164
|
+
</NsSelect>
|
|
165
|
+
);
|
|
166
|
+
|
|
167
|
+
const selectHead = screen.getByTestId('select-head');
|
|
168
|
+
fireEvent.click(selectHead);
|
|
169
|
+
|
|
170
|
+
expect(screen.queryByTestId('select-list')).not.toBeInTheDocument();
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it('opens the select on "ArrowDown" key press', () => {
|
|
174
|
+
render(
|
|
175
|
+
<NsSelect selectedValue={selectedValue} selectedLabel={selectedLabel} onSelect={onSelect}>
|
|
176
|
+
{createSelectOptions(options)}
|
|
177
|
+
</NsSelect>
|
|
178
|
+
);
|
|
179
|
+
|
|
180
|
+
const selectHead = screen.getByTestId('select-head');
|
|
181
|
+
fireEvent.keyDown(selectHead, { key: 'arrowDown' });
|
|
182
|
+
|
|
183
|
+
expect(selectHead).toBeInTheDocument();
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
test('renders SelectContainer with error styles when errorMessage is present', () => {
|
|
187
|
+
const errorMessage = 'Invalid value';
|
|
188
|
+
render(
|
|
189
|
+
<NsSelect selectedValue={selectedValue} selectedLabel={selectedLabel} onSelect={onSelect} errorMessage={errorMessage}>
|
|
190
|
+
{createSelectOptions(options)}
|
|
191
|
+
</NsSelect>
|
|
192
|
+
);
|
|
193
|
+
|
|
194
|
+
const selectContainer = screen.getByTestId('select-container');
|
|
195
|
+
|
|
196
|
+
expect(selectContainer).toHaveStyle('borderColor: red');
|
|
197
|
+
expect(selectContainer).toHaveStyle('color: red');
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
});
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { styled, ColorUtils, FoundationTheme } from '@nuskin/foundation-theme';
|
|
2
|
+
const { color, paletteColor, isDarkMode } = ColorUtils;
|
|
3
|
+
|
|
4
|
+
export const SelectContainer = styled.div<{ disabled: boolean }>`
|
|
5
|
+
position: relative;
|
|
6
|
+
display: inline-block;
|
|
7
|
+
opacity: ${({ disabled }) => (disabled ? '0.5' : '1')};
|
|
8
|
+
`;
|
|
9
|
+
|
|
10
|
+
export const SelectHead = styled.div<{ type: string; disabled: boolean }>`
|
|
11
|
+
cursor: ${({ disabled }) => (disabled ? 'inherit' : 'pointer')};
|
|
12
|
+
padding: 5px 10px;
|
|
13
|
+
display: flex;
|
|
14
|
+
justify-content: space-between;
|
|
15
|
+
align-items: center;
|
|
16
|
+
border: ${({ type, theme }) =>
|
|
17
|
+
type === 'outlined' ? `1px solid ${color('primary', 'black')({ theme })}` : 'none'};
|
|
18
|
+
color: ${paletteColor('primary')};
|
|
19
|
+
width: 207px;
|
|
20
|
+
position: relative;
|
|
21
|
+
z-index: 2;
|
|
22
|
+
`;
|
|
23
|
+
|
|
24
|
+
export const SelectButton = styled.div`
|
|
25
|
+
padding-right: 10px;
|
|
26
|
+
`;
|
|
27
|
+
|
|
28
|
+
export const SelectList = styled.div<{ borderColor: FoundationTheme.PaletteColors }>`
|
|
29
|
+
position: absolute;
|
|
30
|
+
z-index: 100;
|
|
31
|
+
border: 1px solid ${({ borderColor, theme }) => paletteColor(borderColor)({ theme })};
|
|
32
|
+
padding: 0;
|
|
33
|
+
margin: 0;
|
|
34
|
+
width: -webkit-fill-available;
|
|
35
|
+
color: ${paletteColor('primary')};
|
|
36
|
+
background-color: ${paletteColor('primary', 'contrastText')};
|
|
37
|
+
`;
|
|
38
|
+
|
|
39
|
+
const getDividerColor = (props: { theme: FoundationTheme.Theme}) => {
|
|
40
|
+
const dividerColor = !isDarkMode(props) ? color('neutral', '30')(props) : color('neutral', '20')(props);
|
|
41
|
+
return dividerColor;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const getListBgColor = (props: { theme: FoundationTheme.Theme }) => {
|
|
45
|
+
const bgColor = !isDarkMode(props) ? color('accentColorB', '10')(props) : color('accentColorB', '50')(props);
|
|
46
|
+
return bgColor;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
export const ListItemContainer = styled.div<{ showListDivider: boolean; isSelected: boolean }>`
|
|
50
|
+
${({ showListDivider, isSelected, theme }) => `
|
|
51
|
+
cursor: pointer;
|
|
52
|
+
padding: 5px 0;
|
|
53
|
+
border-bottom: 1px solid ${showListDivider ? getDividerColor({ theme }) : 'none'};
|
|
54
|
+
background-color: ${isSelected ? paletteColor('primary')({ theme }) : 'white'};
|
|
55
|
+
color: ${isSelected ? 'white' : 'black'};
|
|
56
|
+
|
|
57
|
+
&:hover {
|
|
58
|
+
background-color: ${getListBgColor({ theme })};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
&:last-child {
|
|
62
|
+
border-bottom: none;
|
|
63
|
+
}
|
|
64
|
+
`}
|
|
65
|
+
`;
|
|
66
|
+
|
|
67
|
+
export const ListItem = styled.div<{ isSelected: boolean}>`
|
|
68
|
+
padding: 5px 10px;
|
|
69
|
+
border-left: ${({ isSelected, theme }) =>
|
|
70
|
+
isSelected ? `2px solid ${paletteColor('primary')({ theme })}` : 'none'};
|
|
71
|
+
background-color: ${({ isSelected, theme }) =>
|
|
72
|
+
isSelected ? paletteColor('primary', 'main')({ theme }) : 'white'};
|
|
73
|
+
color: ${({ isSelected }) => (isSelected ? 'white' : 'black')};
|
|
74
|
+
&:hover {
|
|
75
|
+
background-color: ${({ isSelected, theme }) =>
|
|
76
|
+
isSelected ? paletteColor('primary')({ theme }) : getListBgColor({ theme })};
|
|
77
|
+
font-weight: bold;
|
|
78
|
+
}
|
|
79
|
+
`;
|
|
80
|
+
|
|
81
|
+
export const SelectLabel = styled.label`
|
|
82
|
+
display: block;
|
|
83
|
+
margin-bottom: 5px;
|
|
84
|
+
color: ${paletteColor('primary')} !important;
|
|
85
|
+
`;
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import React, { useState, useRef, useEffect } from 'react';
|
|
2
|
+
import { SelectContainer, SelectButton, SelectList, SelectHead, SelectLabel } from './NsSelect.styled';
|
|
3
|
+
import { NsIcon } from '../icon';
|
|
4
|
+
import { SelectProps } from './types.d';
|
|
5
|
+
|
|
6
|
+
const NsSelect: React.FC<SelectProps> = ({
|
|
7
|
+
selectedValue,
|
|
8
|
+
selectedLabel,
|
|
9
|
+
onSelect,
|
|
10
|
+
children,
|
|
11
|
+
disabled = false,
|
|
12
|
+
type = 'outlined',
|
|
13
|
+
placeholder = '',
|
|
14
|
+
selectBorderColor = 'accentColorB',
|
|
15
|
+
closeOnMouseLeave = false,
|
|
16
|
+
showListDivider = true,
|
|
17
|
+
label,
|
|
18
|
+
readOnly = false,
|
|
19
|
+
errorMessage,
|
|
20
|
+
}) => {
|
|
21
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
22
|
+
const [value, setValue] = useState(selectedValue || '');
|
|
23
|
+
const selectContainerRef = useRef<HTMLDivElement>(null);
|
|
24
|
+
const selectHeadRef = useRef<HTMLDivElement>(null);
|
|
25
|
+
|
|
26
|
+
const closeSelect = () => {
|
|
27
|
+
if (closeOnMouseLeave) {
|
|
28
|
+
setIsOpen(false);
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const handleSelect = (input: string) => {
|
|
33
|
+
onSelect(input);
|
|
34
|
+
setIsOpen(false);
|
|
35
|
+
setValue(input);
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
39
|
+
const handleClickOutside = (event: MouseEvent) => {
|
|
40
|
+
if (
|
|
41
|
+
selectContainerRef.current &&
|
|
42
|
+
!selectContainerRef.current.contains(event.target as Node) &&
|
|
43
|
+
isOpen
|
|
44
|
+
) {
|
|
45
|
+
setIsOpen(false);
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const handleMouseClick = () => {
|
|
50
|
+
if (disabled) return;
|
|
51
|
+
setIsOpen(!isOpen);
|
|
52
|
+
if (!isOpen) {
|
|
53
|
+
setTimeout(() => {
|
|
54
|
+
selectHeadRef.current?.focus();
|
|
55
|
+
}, 0);
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
useEffect(() => {
|
|
60
|
+
document.addEventListener('click', handleClickOutside);
|
|
61
|
+
return () => {
|
|
62
|
+
document.removeEventListener('click', handleClickOutside);
|
|
63
|
+
};
|
|
64
|
+
}, [handleClickOutside, isOpen]);
|
|
65
|
+
|
|
66
|
+
return (
|
|
67
|
+
<SelectContainer
|
|
68
|
+
data-testid="select-container"
|
|
69
|
+
disabled={disabled || readOnly}
|
|
70
|
+
ref={selectContainerRef}
|
|
71
|
+
style={{
|
|
72
|
+
borderColor: errorMessage ? 'red' : undefined,
|
|
73
|
+
color: errorMessage ? 'red' : 'inherit',
|
|
74
|
+
}}
|
|
75
|
+
>
|
|
76
|
+
{label && <SelectLabel style={{ color: errorMessage ? 'red' : 'inherit' }}>{label}</SelectLabel>}
|
|
77
|
+
<SelectHead
|
|
78
|
+
data-testid="select-head"
|
|
79
|
+
disabled={disabled || readOnly}
|
|
80
|
+
type={type}
|
|
81
|
+
onMouseDown={handleMouseClick}
|
|
82
|
+
tabIndex={0}
|
|
83
|
+
ref={selectHeadRef}
|
|
84
|
+
>
|
|
85
|
+
<SelectButton>{selectedLabel || placeholder}</SelectButton>
|
|
86
|
+
{!readOnly && (
|
|
87
|
+
<NsIcon testId="chevron-icon" name={isOpen ? 'arrowUp' : 'arrowDown'} color="primary" />
|
|
88
|
+
)}
|
|
89
|
+
</SelectHead>
|
|
90
|
+
{isOpen ? (
|
|
91
|
+
<SelectList
|
|
92
|
+
data-testid="select-list"
|
|
93
|
+
onMouseLeave={closeSelect}
|
|
94
|
+
borderColor={selectBorderColor}
|
|
95
|
+
className='selectList'
|
|
96
|
+
>
|
|
97
|
+
<div style={{ maxHeight: '200px', overflowY: 'auto' }}>
|
|
98
|
+
{React.Children.map(children, (child) => {
|
|
99
|
+
if (React.isValidElement(child)) {
|
|
100
|
+
return React.cloneElement(child, {
|
|
101
|
+
// @ts-ignore
|
|
102
|
+
onSelect: handleSelect,
|
|
103
|
+
selectedValue: value,
|
|
104
|
+
showListDivider,
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
return null;
|
|
108
|
+
})}
|
|
109
|
+
</div>
|
|
110
|
+
</SelectList>
|
|
111
|
+
) : null}
|
|
112
|
+
{errorMessage && <div style={{ color: 'red' }}>{errorMessage}</div>}
|
|
113
|
+
</SelectContainer>
|
|
114
|
+
);
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
export default NsSelect;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { ListItem, ListItemContainer } from './NsSelect.styled';
|
|
3
|
+
import { SelectOptionProps } from './types.d';
|
|
4
|
+
|
|
5
|
+
export const NsSelectOption: React.FC<SelectOptionProps> = ({
|
|
6
|
+
value,
|
|
7
|
+
label,
|
|
8
|
+
onSelect,
|
|
9
|
+
selectedValue,
|
|
10
|
+
showListDivider = true,
|
|
11
|
+
}) => {
|
|
12
|
+
const handleClick = () => {
|
|
13
|
+
if (onSelect && typeof onSelect === 'function') {
|
|
14
|
+
onSelect(value);
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
return (
|
|
19
|
+
<ListItemContainer
|
|
20
|
+
data-testid={`option-${value}`}
|
|
21
|
+
isSelected={value === selectedValue}
|
|
22
|
+
showListDivider={showListDivider}
|
|
23
|
+
onClick={handleClick}
|
|
24
|
+
>
|
|
25
|
+
<ListItem isSelected={value === selectedValue}>{label}</ListItem>
|
|
26
|
+
</ListItemContainer>
|
|
27
|
+
|
|
28
|
+
);
|
|
29
|
+
};
|
|
30
|
+
export default NsSelectOption;
|