Skip to content

Commit 87354f7

Browse files
committed
Migrate to controlled component
1 parent 93ae42f commit 87354f7

File tree

3 files changed

+85
-89
lines changed

3 files changed

+85
-89
lines changed

features/ui/select/option.tsx

Lines changed: 10 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,25 @@
11
import React, { ReactNode } from "react";
2-
import { useSelectContext } from "./select-context";
32
import * as S from "./option.styled";
43

54
type OptionProps = {
6-
children: ReactNode | ReactNode[];
7-
value: any;
8-
handleCallback?: (value: any) => unknown;
5+
children: ReactNode;
6+
value: unknown;
7+
isSelected: boolean;
8+
onClick: (value: unknown) => void;
99
};
1010

11-
export function Option({ children, value, handleCallback }: OptionProps) {
12-
const { changeSelectedOption, selectedOption } = useSelectContext();
13-
const isCurrentlySelected = selectedOption === value;
14-
11+
export function Option({ children, value, onClick, isSelected }: OptionProps) {
1512
return (
1613
<S.ListItem
17-
isCurrentlySelected={isCurrentlySelected}
18-
aria-selected={isCurrentlySelected}
19-
onClick={() => {
20-
changeSelectedOption(value);
21-
if (handleCallback) {
22-
handleCallback(value);
23-
}
24-
}}
14+
isCurrentlySelected={isSelected}
15+
aria-selected={isSelected}
16+
onClick={() => onClick(value)}
2517
role="option"
18+
tabIndex={0}
2619
>
2720
{children}
2821
<S.ListItemIcon
29-
isCurrentlySelected={isCurrentlySelected}
22+
isCurrentlySelected={isSelected}
3023
src="/icons/checked.svg"
3124
/>
3225
</S.ListItem>

features/ui/select/select.stories.tsx

Lines changed: 18 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,36 @@
1-
import React from "react";
1+
import React, { useState } from "react";
22
import { ComponentStory, ComponentMeta } from "@storybook/react";
3-
import { Select, Option } from "./";
4-
3+
import { Select } from "./select";
54
export default {
65
title: "UI/Select",
76
component: Select,
87
parameters: {
98
layout: "fullscreen",
109
},
1110
} as ComponentMeta<typeof Select>;
12-
13-
const selectData = [
14-
"Phoenix Baker",
15-
"Olivia Rhye",
16-
"Lana Steiner",
17-
"Demi Wilkinson",
18-
"Candice Wu",
19-
"Natali Craig",
20-
"Drew Cano",
11+
const options = [
12+
{ label: "Phoenix Baker", value: 1 },
13+
{ label: "Olivia Rhye", value: 2 },
14+
{ label: "Lana Steiner", value: 3 },
15+
{ label: "Demi Wilkinson", value: 4 },
16+
{ label: "Candice Wu", value: 5 },
17+
{ label: "Natali Craig", value: 6 },
18+
{ label: "Drew Cano", value: 7 },
2119
];
22-
2320
const Template: ComponentStory<typeof Select> = (props) => {
21+
const [selectedValue, setSelectedValue] = useState<number>();
2422
return (
2523
<div style={{ padding: 50, height: 400 }}>
26-
<Select {...props}>
27-
{selectData.map((name) => (
28-
<Option key={name} value={name}>
29-
{name}
30-
</Option>
31-
))}
32-
</Select>
24+
<Select
25+
{...props}
26+
options={options}
27+
value={selectedValue}
28+
onChange={(value) => setSelectedValue(value as number)}
29+
/>
3330
</div>
3431
);
3532
};
33+
3634
export const Default = Template.bind({});
3735
Default.args = {
3836
disabled: false,

features/ui/select/select.tsx

Lines changed: 57 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,17 @@
1-
import React, {
2-
useState,
3-
ReactNode,
4-
useCallback,
5-
useMemo,
6-
useRef,
7-
SelectHTMLAttributes,
8-
} from "react";
1+
import React, { useState, useRef } from "react";
92
import { useClickAway } from "react-use";
10-
import { SelectContext } from "./select-context";
3+
import { Option } from "./option";
114
import * as S from "./select.styled";
125

13-
type SelectProps = SelectHTMLAttributes<HTMLSelectElement> & {
14-
children: ReactNode | ReactNode[];
6+
type Option = {
7+
value: unknown;
8+
label: React.ReactNode;
9+
};
10+
11+
type SelectProps = Omit<React.HTMLAttributes<HTMLDivElement>, "onChange"> & {
12+
options: Option[];
13+
value: unknown;
14+
onChange: (value: unknown) => void;
1515
errorMessage?: string;
1616
defaultValue?: string;
1717
placeholder?: string;
@@ -23,6 +23,9 @@ type SelectProps = SelectHTMLAttributes<HTMLSelectElement> & {
2323
};
2424

2525
export function Select({
26+
options,
27+
value,
28+
onChange,
2629
placeholder = "Choose an option",
2730
defaultValue = "",
2831
iconSrc = "",
@@ -31,10 +34,8 @@ export function Select({
3134
hint = "",
3235
errorMessage = "",
3336
width = "",
34-
children,
3537
...props
3638
}: SelectProps) {
37-
const [selectedOption, setSelectedOption] = useState(defaultValue || "");
3839
const [showDropdown, setShowDropdown] = useState(false);
3940
const ref = useRef(null);
4041

@@ -43,55 +44,59 @@ export function Select({
4344
setShowDropdown(false);
4445
});
4546

46-
const showDropdownHandler = useCallback(
47-
() => setShowDropdown((prevShowDropdown) => !prevShowDropdown),
48-
[]
49-
);
47+
const showDropdownHandler = () =>
48+
setShowDropdown((prevShowDropdown) => !prevShowDropdown);
5049

51-
const updateSelectedOption = useCallback((option: string) => {
52-
setSelectedOption(option);
50+
const onClickOption = (newValue: unknown) => {
51+
onChange(newValue);
5352
setShowDropdown(false);
54-
}, []);
53+
};
5554

56-
const value = useMemo(
57-
() => ({ selectedOption, changeSelectedOption: updateSelectedOption }),
58-
[selectedOption, updateSelectedOption]
55+
const selectedOption = options.find(
56+
(option) => option.value === (value || defaultValue)
5957
);
6058

6159
return (
62-
<SelectContext.Provider value={value}>
63-
<S.Container ref={ref} width={width} {...props}>
64-
{label && <S.Label>{label}</S.Label>}
60+
<S.Container ref={ref} width={width} {...props}>
61+
{label && <S.Label>{label}</S.Label>}
6562

66-
<S.SelectedOption
67-
onClick={showDropdownHandler}
68-
selectedOption={selectedOption}
69-
disabled={disabled}
70-
errorMessage={errorMessage}
71-
aria-expanded={showDropdown}
72-
width={width}
73-
>
74-
<S.LeftContainer>
75-
{iconSrc && <S.OptionalIcon src={iconSrc} />}
76-
{selectedOption || placeholder}
77-
</S.LeftContainer>
63+
<S.SelectedOption
64+
onClick={showDropdownHandler}
65+
selectedOption={selectedOption}
66+
disabled={disabled}
67+
errorMessage={errorMessage}
68+
aria-expanded={showDropdown}
69+
width={width}
70+
>
71+
<S.LeftContainer>
72+
{iconSrc && <S.OptionalIcon src={iconSrc} />}
73+
{selectedOption?.label || placeholder}
74+
</S.LeftContainer>
7875

79-
<S.SelectArrowIcon
80-
src="/icons/chevron-down.svg"
81-
showDropdown={showDropdown}
82-
/>
83-
</S.SelectedOption>
76+
<S.SelectArrowIcon
77+
src="/icons/chevron-down.svg"
78+
showDropdown={showDropdown}
79+
/>
80+
</S.SelectedOption>
8481

85-
{hint && !showDropdown && !errorMessage && <S.Hint>{hint}</S.Hint>}
82+
{hint && !showDropdown && !errorMessage && <S.Hint>{hint}</S.Hint>}
8683

87-
{errorMessage && !showDropdown && !disabled && (
88-
<S.ErrorMessage>{errorMessage}</S.ErrorMessage>
89-
)}
84+
{errorMessage && !showDropdown && !disabled && (
85+
<S.ErrorMessage>{errorMessage}</S.ErrorMessage>
86+
)}
9087

91-
<S.List showDropdown={showDropdown} role="listbox" tabIndex={-1}>
92-
{children}
93-
</S.List>
94-
</S.Container>
95-
</SelectContext.Provider>
88+
<S.List showDropdown={showDropdown} role="listbox" tabIndex={-1}>
89+
{options.map((option) => (
90+
<Option
91+
key={String(option.value)}
92+
value={option.value}
93+
isSelected={value === option.value}
94+
onClick={onClickOption}
95+
>
96+
{option.label}
97+
</Option>
98+
))}
99+
</S.List>
100+
</S.Container>
96101
);
97102
}

0 commit comments

Comments
 (0)