Skip to content

Commit 8ae036c

Browse files
committed
Create initial select
1 parent f2ee67c commit 8ae036c

File tree

13 files changed

+802
-23
lines changed

13 files changed

+802
-23
lines changed

features/ui/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
export * from "./badge";
22
export * from "./button";
3+
export * from "./select";

features/ui/select/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export { Option } from "./option";
2+
export { Select } from "./select";
3+
export { SelectContext, useSelectContext } from "./select-context";
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import styled, { css } from "styled-components";
2+
3+
export const ListItem = styled.li.attrs(() => ({
4+
tabIndex: 0,
5+
}))<{ isCurrentlySelected: boolean }>`
6+
margin: 0;
7+
padding: 0;
8+
display: flex;
9+
width: 100%;
10+
justify-content: space-between;
11+
align-items: center;
12+
list-style-type: none;
13+
padding-inline: 0.75rem;
14+
padding-block: calc(0.75rem - 0.1rem);
15+
font-size: 1rem;
16+
line-height: 1.5rem;
17+
font-weight: 400;
18+
cursor: pointer;
19+
z-index: 100;
20+
color: #1d2939;
21+
background-color: #fff;
22+
box-sizing: border-box;
23+
${({ isCurrentlySelected }) =>
24+
isCurrentlySelected &&
25+
css`
26+
background-color: #fcfaff;
27+
`};
28+
29+
&:hover {
30+
background: #f4ebff;
31+
}
32+
`;
33+
34+
export const ListItemIcon = styled.img<{ isCurrentlySelected: boolean }>`
35+
display: ${({ isCurrentlySelected }) =>
36+
isCurrentlySelected ? "block" : "none"};
37+
padding: 0;
38+
width: 1rem;
39+
height: 1rem;
40+
`;

features/ui/select/option.tsx

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import React, { ReactNode } from "react";
2+
import { useSelectContext } from "./select-context";
3+
import * as S from "./option.styled";
4+
5+
type OptionProps = {
6+
children: ReactNode | ReactNode[];
7+
value: any;
8+
handleCallback?: (value: any) => unknown;
9+
};
10+
11+
export function Option({ children, value, handleCallback }: OptionProps) {
12+
const { changeSelectedOption, selectedOption } = useSelectContext();
13+
const isCurrentlySelected = selectedOption === value;
14+
15+
return (
16+
<S.ListItem
17+
isCurrentlySelected={isCurrentlySelected}
18+
aria-selected={isCurrentlySelected}
19+
onClick={() => {
20+
changeSelectedOption(value);
21+
if (handleCallback) {
22+
handleCallback(value);
23+
}
24+
}}
25+
role="option"
26+
>
27+
{children}
28+
<S.ListItemIcon
29+
isCurrentlySelected={isCurrentlySelected}
30+
src="/icons/checked.svg"
31+
/>
32+
</S.ListItem>
33+
);
34+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { createContext, useContext } from "react";
2+
3+
export const SelectContext = createContext<{
4+
selectedOption: string;
5+
changeSelectedOption: (option: string) => void;
6+
}>({
7+
selectedOption: "",
8+
// eslint-disable-next-line @typescript-eslint/no-empty-function, @typescript-eslint/no-unused-vars
9+
changeSelectedOption: (option: string) => {},
10+
});
11+
12+
export const useSelectContext = () => {
13+
const context = useContext(SelectContext);
14+
if (!context) {
15+
throw new Error("Error in creating the context");
16+
}
17+
return context;
18+
};
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import React from "react";
2+
import { ComponentStory, ComponentMeta } from "@storybook/react";
3+
import { Select, Option } from "./";
4+
5+
export default {
6+
title: "UI/Select",
7+
component: Select,
8+
parameters: {
9+
layout: "fullscreen",
10+
},
11+
} 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",
21+
];
22+
23+
const Template: ComponentStory<typeof Select> = (props) => {
24+
return (
25+
<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>
33+
</div>
34+
);
35+
};
36+
export const Default = Template.bind({});
37+
Default.args = {
38+
disabled: false,
39+
placeholder: "Select team member",
40+
iconSrc: "/icons/person.svg",
41+
label: "Team member",
42+
hint: "This is a hint text to help user.",
43+
defaultValue: "Demi Wilkinson",
44+
errorMessage: "",
45+
width: "",
46+
};
47+
Default.parameters = {
48+
viewMode: "docs",
49+
};
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
import styled, { css } from "styled-components";
2+
3+
export const Container = styled.div<any>`
4+
position: relative;
5+
display: block;
6+
width: ${({ width }) => width || `calc(5rem * 4)`};
7+
background-color: #fff;
8+
`;
9+
10+
export const SelectedOption = styled.div.attrs(() => ({
11+
tabIndex: 0,
12+
ariaHasPopup: "listbox",
13+
}))<any>`
14+
border: 1px solid;
15+
border-color: ${({ disabled, errorMessage }) =>
16+
!disabled && errorMessage ? "#FDA29B" : "#D0D5DD"};
17+
border-radius: 7px;
18+
width: ${({ width }) => width || `calc(5rem * 4 - 1.5rem)`};
19+
padding: 0.5rem 0.75rem;
20+
color: ${({ selectedOption }) => (selectedOption ? "#101828" : "#667085")};
21+
cursor: pointer;
22+
display: flex;
23+
justify-content: space-between;
24+
letter-spacing: 0.052rem;
25+
font-size: 1rem;
26+
line-height: 1.5rem;
27+
font-weight: 400;
28+
&:focus {
29+
outline: 3px solid;
30+
outline-color: ${({ disabled, errorMessage }) =>
31+
!disabled && errorMessage ? "#FEE4E2" : "#E9D7FE"};
32+
}
33+
${({ disabled }) =>
34+
disabled &&
35+
css`
36+
color: #667085;
37+
background-color: #f2f4f7;
38+
pointer-events: none;
39+
`}
40+
`;
41+
42+
export const SelectArrowIcon = styled.img<{
43+
showDropdown: boolean;
44+
}>`
45+
transform: ${({ showDropdown }) =>
46+
showDropdown ? "rotate(180deg)" : "none"};
47+
padding-inline: 0.25rem;
48+
`;
49+
50+
export const OptionalIcon = styled.img`
51+
width: 1.25rem;
52+
height: 1.25rem;
53+
padding-inline: 0.25rem 0.5rem;
54+
`;
55+
56+
export const LeftContainer = styled.div`
57+
display: flex;
58+
align-items: center;
59+
`;
60+
61+
export const Label = styled.p`
62+
margin: 0;
63+
margin-bottom: 0.25rem;
64+
color: #344054;
65+
font-size: 1rem;
66+
line-height: 1.5rem;
67+
font-weight: 400;
68+
`;
69+
70+
export const List = styled.ul<{ showDropdown: boolean }>`
71+
display: block;
72+
width: 100%;
73+
margin: 0.5rem 0 0;
74+
padding: 0;
75+
position: absolute;
76+
background: white;
77+
box-shadow: 0 7px 12px -6px #d0d5dd;
78+
box-shadow: 0px 12px 16px -4px rgba(16, 24, 40, 0.1),
79+
0px 4px 6px -2px rgba(16, 24, 40, 0.05);
80+
border-radius: 8px;
81+
overflow: hidden;
82+
83+
${({ showDropdown }) =>
84+
showDropdown
85+
? css`
86+
opacity: 1;
87+
visibility: visible;
88+
position: absolute;
89+
height: auto;
90+
z-index: 200;
91+
`
92+
: css`
93+
opacity: 0;
94+
visibility: hidden;
95+
`};
96+
`;
97+
98+
export const Hint = styled.p`
99+
margin: 0;
100+
margin-top: 0.25rem;
101+
color: #667085;
102+
font-size: 0.875rem;
103+
line-height: 1.25rem;
104+
font-weight: 400;
105+
letter-spacing: 0.05rem;
106+
`;
107+
108+
export const ErrorMessage = styled.p`
109+
margin: 0;
110+
margin-top: 0.25rem;
111+
color: #f04438;
112+
font-size: 0.875rem;
113+
line-height: 1.25rem;
114+
font-weight: 400;
115+
letter-spacing: 0.05rem;
116+
`;

features/ui/select/select.tsx

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
import React, {
2+
useState,
3+
ReactNode,
4+
useCallback,
5+
useMemo,
6+
useRef,
7+
SelectHTMLAttributes,
8+
} from "react";
9+
import { useClickAway } from "react-use";
10+
import { SelectContext } from "./select-context";
11+
import * as S from "./select.styled";
12+
13+
type SelectProps = SelectHTMLAttributes<HTMLSelectElement> & {
14+
children: ReactNode | ReactNode[];
15+
errorMessage?: string;
16+
defaultValue?: string;
17+
placeholder?: string;
18+
disabled?: boolean;
19+
iconSrc?: string;
20+
width?: string | number;
21+
label?: string;
22+
hint?: string;
23+
};
24+
25+
export function Select({
26+
placeholder = "Choose an option",
27+
defaultValue = "",
28+
iconSrc = "",
29+
disabled = false,
30+
label = "",
31+
hint = "",
32+
errorMessage = "",
33+
width = "",
34+
children,
35+
...props
36+
}: SelectProps) {
37+
const [selectedOption, setSelectedOption] = useState(defaultValue || "");
38+
const [showDropdown, setShowDropdown] = useState(false);
39+
const ref = useRef(null);
40+
41+
// hides dropdown when user clicks outside the select component
42+
useClickAway(ref, () => {
43+
setShowDropdown(false);
44+
});
45+
46+
const showDropdownHandler = useCallback(
47+
() => setShowDropdown((prevShowDropdown) => !prevShowDropdown),
48+
[]
49+
);
50+
51+
const updateSelectedOption = useCallback((option: string) => {
52+
setSelectedOption(option);
53+
setShowDropdown(false);
54+
}, []);
55+
56+
const value = useMemo(
57+
() => ({ selectedOption, changeSelectedOption: updateSelectedOption }),
58+
[selectedOption, updateSelectedOption]
59+
);
60+
61+
return (
62+
<SelectContext.Provider value={value}>
63+
<S.Container ref={ref} width={width} {...props}>
64+
{label && <S.Label>{label}</S.Label>}
65+
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>
78+
79+
<S.SelectArrowIcon
80+
src="/icons/chevron-down.svg"
81+
showDropdown={showDropdown}
82+
/>
83+
</S.SelectedOption>
84+
85+
{hint && !showDropdown && !errorMessage && <S.Hint>{hint}</S.Hint>}
86+
87+
{errorMessage && !showDropdown && !disabled && (
88+
<S.ErrorMessage>{errorMessage}</S.ErrorMessage>
89+
)}
90+
91+
<S.List showDropdown={showDropdown} role="listbox" tabIndex={-1}>
92+
{children}
93+
</S.List>
94+
</S.Container>
95+
</SelectContext.Provider>
96+
);
97+
}

0 commit comments

Comments
 (0)