Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
</head>
<body>
<div id="root"></div>
<div id="dropdown"></div>
<script type="module" src="/src/main.jsx"></script>
</body>
</html>
38 changes: 38 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
"axios": "^1.11.0",
"react": "^19.1.1",
"react-dom": "^19.1.1",
"react-router": "^7.8.0",
"styled-components": "^6.1.19"
},
"devDependencies": {
Expand Down
16 changes: 13 additions & 3 deletions src/app.jsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,19 @@
// import TestPage from "./pages/test-page";
import { BrowserRouter, Route, Routes } from "react-router";
import DropdownProvider from "./components/text-field/dropdown-input/dropdown-provider";
import MessagePage from "./pages/message-list";
import TestPage from "./pages/test-page";

function App() {
// return <TestPage />;
return <MessagePage />;
return (
<DropdownProvider>
<BrowserRouter>
<Routes>
<Route path="/test-components" element={<TestPage />} />
<Route path="/list" element={<MessagePage />} />
</Routes>
</BrowserRouter>
</DropdownProvider>
);
}

export default App;
3 changes: 3 additions & 0 deletions src/assets/ic-chevron-down.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions src/assets/ic-chevron-up.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 5 additions & 0 deletions src/components/text-field/dropdown-input/dropdown-context.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { createContext } from "react";

const DropdownContext = createContext(null);

export default DropdownContext;
145 changes: 145 additions & 0 deletions src/components/text-field/dropdown-input/dropdown-input.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
import styled from "styled-components";
import arrowDownImg from "../../../assets/ic-chevron-down.svg";
import arrowUpImg from "../../../assets/ic-chevron-up.svg";
import { useDropdown } from "../../../hooks/dropdown/use-dropdown";
import INPUT_STYLES from "../input-styles";
import Dropdown from "./dropdown";
import DropdownOption from "./dropdown-option";

const PlaceholderText = styled.span`
${INPUT_STYLES.font}
color: ${INPUT_STYLES.textColor.placeholder};
flex-grow: 1;
text-align: left;
`;

const InputText = styled.span`
${INPUT_STYLES.font}
color: ${INPUT_STYLES.textColor.normal};
flex-grow: 1;
text-align: left;
`;

function Text({ value, placeholder }) {
return value ? (
<InputText>{value}</InputText>
) : (
<PlaceholderText>{placeholder}</PlaceholderText>
);
}

const Icon = styled.div`
width: 16px;
height: 16px;

img {
width: 100%;
height: 100%;
}
`;

const StyledDropdownInput = styled.button`
background-color: ${INPUT_STYLES.backgroundColor.normal};
border: none;
border-radius: 8px;
box-shadow: 0 0 0 1px
${({ $error }) => INPUT_STYLES.borderColor.normal($error)} inset;
padding: 12px 16px;
min-width: 320px;
display: flex;
align-items: center;
gap: 8px;
cursor: pointer;
position: relative;

&:hover {
box-shadow: 0 0 0 1px
${({ $error }) => INPUT_STYLES.borderColor.hover($error)} inset;
}

&:active {
box-shadow: 0 0 0 2px
${({ $error }) => INPUT_STYLES.borderColor.active($error)} inset;
}

&:focus {
box-shadow: 0 0 0 2px
${({ $error }) => INPUT_STYLES.borderColor.focus($error)} inset;
}

&:disabled {
background-color: ${INPUT_STYLES.backgroundColor.disabled};
box-shadow: 0 0 0 1px ${INPUT_STYLES.borderColor.disabled} inset;
cursor: default;
}
`;

function DropdownInput({
dropdownId,
error,
placeholder,
value,
options,
onSelect,
...props
}) {
const {
targetRef,
dropdownRect,
showsDropdown,
setShowsDropdown,
handleTargetClick,
} = useDropdown({
id: dropdownId,
type: "dropdown-input",
});

const handleInputClick = () => {
handleTargetClick(!showsDropdown);
};

const handleOptionClick = (event) => {
onSelect(event.target.textContent);
};

const handleDropdownClose = () => {
setShowsDropdown(false);
};

return (
<>
<StyledDropdownInput
$error={error}
onClick={handleInputClick}
{...props}
ref={targetRef}
>
<Text value={value} placeholder={placeholder} />
<Icon>
<img
src={showsDropdown ? arrowUpImg : arrowDownImg}
alt="Dropdown 화살표"
/>
</Icon>
{showsDropdown && (
<Dropdown
origin={dropdownRect.origin}
size={dropdownRect.size}
onClose={handleDropdownClose}
>
{options.map((option, index) => (
<DropdownOption
key={`${index}-${option}`}
onClick={handleOptionClick}
>
{option}
</DropdownOption>
))}
</Dropdown>
)}
</StyledDropdownInput>
</>
);
}

export default DropdownInput;
20 changes: 20 additions & 0 deletions src/components/text-field/dropdown-input/dropdown-option.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import styled from "styled-components";
import Colors from "../../color/colors";

const DropdownOption = styled.div`
width: calc(100% - 2px);
border: none;
background: none;
padding: 12px 16px;
font-size: 16px;
font-weight: 400;
line-height: 26px;
color: ${Colors.gray(900)};
cursor: pointer;

&:hover {
background-color: ${Colors.gray(100)};
}
`;

export default DropdownOption;
10 changes: 10 additions & 0 deletions src/components/text-field/dropdown-input/dropdown-provider.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { useState } from "react";
import DropdownContext from "./dropdown-context";

function DropdownProvider({ children }) {
const [dropdownState, setDropdownState] = useState({});
const value = { dropdownState, setDropdownState };
return <DropdownContext value={value}>{children}</DropdownContext>;
}

export default DropdownProvider;
66 changes: 66 additions & 0 deletions src/components/text-field/dropdown-input/dropdown.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { createPortal } from "react-dom";
import styled from "styled-components";
import Colors from "../../color/colors";

const BACKDROP_CLASS_NAME = "dropdown-backdrop";

const DropdownContainer = styled.div`
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 111;

& > *:not(.${BACKDROP_CLASS_NAME}) {
z-index: 114;
}
`;

const DropdownBackdrop = styled(DropdownContainer)`
z-index: 112;
position: fixed;
`;

const DropdownContent = styled(DropdownContainer)`
z-index: 113;
position: relative;
height: 100%;
`;

const StyledDropdown = styled.div`
background-color: white;
box-shadow: 0 0 0 1px ${Colors.gray(300)} inset,
0 2px 12px 0 rgba(0, 0, 0, 0.08);
border-radius: 8px;
display: flex;
flex-direction: column;
align-items: center;
padding: 10px 0;
position: absolute;
top: ${({ $origin }) => $origin.y}px;
left: ${({ $origin }) => $origin.x}px;
width: ${({ $size }) => $size.width}px;
`;

function Dropdown({ children, origin, size, onClose }) {
const DropdownPortal = ({ children }) => {
return createPortal(children, document.getElementById("dropdown"));
};

return (
<DropdownPortal>
<DropdownContainer>
<DropdownBackdrop className={BACKDROP_CLASS_NAME} onClick={onClose}>
<DropdownContent>
<StyledDropdown $origin={origin} $size={size}>
{children}
</StyledDropdown>
</DropdownContent>
</DropdownBackdrop>
</DropdownContainer>
</DropdownPortal>
);
}

export default Dropdown;
27 changes: 27 additions & 0 deletions src/components/text-field/input-styles.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { css } from "styled-components";
import Colors from "../color/colors";

const INPUT_STYLES = Object.freeze({
font: css`
font-size: 16px;
font-weight: 400;
line-height: 26px;
`,
borderColor: {
normal: (error) => (error ? Colors.error : Colors.gray(300)),
hover: (error) => (error ? Colors.error : Colors.gray(500)),
active: (error) => (error ? Colors.error : Colors.gray(700)),
focus: (error) => (error ? Colors.error : Colors.gray(500)),
disabled: Colors.gray(300),
},
textColor: {
normal: Colors.gray(900),
placeholder: Colors.gray(500),
},
backgroundColor: {
normal: "#ffffff",
disabled: Colors.gray(100),
},
});

export default INPUT_STYLES;
6 changes: 6 additions & 0 deletions src/components/text-field/text-field-type.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
const TEXT_FIELD_TYPE = Object.freeze({
input: "input",
dropdown: "dropdown",
});

export default TEXT_FIELD_TYPE;
Loading