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
3 changes: 3 additions & 0 deletions iconsAsset/static/IcSearchLine.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 iconsAsset/static/icSearchFilled.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
105 changes: 105 additions & 0 deletions src/components/SearchBar/SearchBar.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import { Canvas, Meta, Controls } from '@storybook/blocks';
import * as SearchBarStories from './SearchBar.stories';
import { SearchBar } from './SearchBar';

<Meta of={SearchBarStories} />

# Searchbar

SearchBar는 사용자가 검색어를 입력하기 위한 필드를 구성하기 위해 사용합니다.<br />

<Canvas of={SearchBarStories.Primary} />
<Controls />

<br />
<br />

## 사용법

```tsx
import { SearchBar } from '@yourssu/design-system-react';

<SearchBar>
<SearchBar.Input placeholder="검색어를 입력해주세요" />
</SearchBar>;
```

<br />
<br />

### SearchBar.ClearButton

SearchBar에 입력된 검색어를 초기화하기 위한 UI 버튼을 제공합니다.<br />

```tsx
import { SearchBar } from '@yourssu/design-system-react';

<SearchBar>
<SearchBar.Input placeholder="검색어를 입력해주세요" />
<SearchBar.ClearButton />
</SearchBar>;
```

실제 초기화 동작을 위해서는 `onClick` 이벤트로 초기화 로직을 구현해야 합니다.

> **왜 내부적으로 입력 값을 초기화하지 않나요?**<br />
> SearchBar 입력 값으로 컴포넌트 외부의 상태를 사용하는 경우, 외부 상태의 초기화 여부를 판단하기 어렵기 때문이에요.

```tsx
const Component = () => {
const [value, setValue] = useState('');

return (
<SearchBar>
<SearchBar.Input
placeholder="검색어를 입력해주세요"
value={value}
onChange={(e) => setValue(e.target.value)}
/>
<SearchBar.ClearButton onClick={() => setValue('')} />
</SearchBar>
);
};
```

<Canvas of={SearchBarStories.WithClearButton} withSource="none" />

<br />
<br />

### 가변 너비

SearchBar 컴포넌트는 가변 너비 컴포넌트입니다.
부모 컴포넌트의 너비에 따라 자동으로 너비가 조절됩니다.

```tsx
import { SearchBar } from '@yourssu/design-system-react';

<div style={{ width: '800px' }}>
<SearchBar>
<SearchBar.Input placeholder="검색어를 입력해주세요" />
</SearchBar>
</div>;
```

<Canvas of={SearchBarStories.Width} withSource="none" />

<br />
<br />

### Form 제출

SearchBar 컴포넌트는 HTML form 요소를 기반으로 구성되어 있습니다.
올바른 입력 값 제출을 위해서 `SearchBar` 컴포넌트의 `onSubmit` 이벤트를 사용해주세요.

> ❗ **`SearchBar.Input` 컴포넌트에 keydown 이벤트를 통한 폼 제출은 지양해주세요.**

```tsx
import { SearchBar } from '@yourssu/design-system-react';

<SearchBar onSubmit={() => alert('제출')}>
<SearchBar.Input placeholder="검색어를 입력해주세요" />
</SearchBar>;
```

<Canvas of={SearchBarStories.Form} withSource="none" />
61 changes: 61 additions & 0 deletions src/components/SearchBar/SearchBar.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { useState } from 'react';

import type { Meta, StoryObj } from '@storybook/react';

import { SearchBar } from './SearchBar';

const meta: Meta<typeof SearchBar> = {
title: 'Components/SearchBar',
component: SearchBar,
parameters: {
layout: 'centered',
},
};

export default meta;
type Story = StoryObj<typeof SearchBar>;

const WithClearButtonComponent = () => {
const [value, setValue] = useState('');

return (
<SearchBar>
<SearchBar.Input
placeholder="검색어를 입력해주세요"
value={value}
onChange={(e) => setValue(e.target.value)}
/>
<SearchBar.ClearButton onClick={() => setValue('')} />
</SearchBar>
);
};

export const Primary: Story = {
render: () => (
<SearchBar>
<SearchBar.Input placeholder="검색어를 입력해주세요" />
</SearchBar>
),
};

export const WithClearButton: Story = {
render: WithClearButtonComponent,
};

export const Width: Story = {
render: () => (
<div style={{ width: '800px' }}>
<SearchBar>
<SearchBar.Input placeholder="검색어를 입력해주세요" />
</SearchBar>
</div>
),
};

export const Form: Story = {
render: () => (
<SearchBar onSubmit={() => alert('제출')}>
<SearchBar.Input placeholder="검색어를 입력해주세요" />
</SearchBar>
),
};
57 changes: 57 additions & 0 deletions src/components/SearchBar/SearchBar.style.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { styled } from 'styled-components';

export const StyledContainer = styled.form`
position: relative;

width: 100%;
height: 48px;
border-radius: ${({ theme }) => theme.semantic.radius.m}px;
background-color: ${({ theme }) => theme.semantic.color.bgBasicLight};

display: flex;
align-items: center;
gap: 8px;

padding: 12px;

&:has(.searchbar-close-button) {
padding-right: 44px;
}

.searchbar-icon {
color: ${({ theme }) => theme.semantic.color.iconBasicTertiary};
width: 20px;
height: 20px;
}
`;

export const StyledInput = styled.input`
all: unset;
${({ theme }) => theme.typo.B1_Rg_16};

width: 100%;
color: ${({ theme }) => theme.semantic.color.textBasicPrimary};

&::placeholder {
color: ${({ theme }) => theme.semantic.color.textBasicTertiary};
}
`;

export const StyledClearButton = styled.button`
all: unset;
cursor: pointer;

width: 20px;
height: 20px;

position: absolute;
top: 50%;
right: 12px;
transform: translateY(-50%);

svg {
fill: ${({ theme }) => theme.semantic.color.iconBasicTertiary};
width: 100%;
height: 100%;
}
`;
44 changes: 44 additions & 0 deletions src/components/SearchBar/SearchBar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { forwardRef } from 'react';

import { IcCancelFilled, IcSearchLine } from '@/style';

import { StyledClearButton, StyledContainer, StyledInput } from './SearchBar.style';

const SearchBarClearButton = forwardRef<
HTMLButtonElement,
React.ButtonHTMLAttributes<HTMLButtonElement>
>(({ className, ...props }, ref) => {
return (
<StyledClearButton ref={ref} className={`searchbar-close-button ${className}`} {...props}>
<IcCancelFilled />
</StyledClearButton>
);
});

const SearchBarInput = forwardRef<HTMLInputElement, React.InputHTMLAttributes<HTMLInputElement>>(
(props, ref) => {
return <StyledInput ref={ref} {...props} />;
}
);

SearchBarInput.displayName = 'SearchBarInput';

export const SearchBar = Object.assign(
forwardRef<HTMLFormElement, React.FormHTMLAttributes<HTMLFormElement>>((props, ref) => {
const onSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
props.onSubmit?.(e);
};

return (
<StyledContainer ref={ref} {...props} onSubmit={onSubmit}>
<IcSearchLine className="searchbar-icon" size="20px" />
{props.children}
</StyledContainer>
);
}),
{
Input: SearchBarInput,
ClearButton: SearchBarClearButton,
}
);
1 change: 1 addition & 0 deletions src/components/SearchBar/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { SearchBar } from './SearchBar';
2 changes: 2 additions & 0 deletions src/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,3 +61,5 @@ export type * from './Snackbar';

export { LoadingIndicator } from './LoadingIndicator';
export type { LoadingIndicatorProps } from './LoadingIndicator';

export { SearchBar } from './SearchBar';
24 changes: 24 additions & 0 deletions src/style/foundation/icons/generated/IcSearchFilled.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/**
* 이 파일은 iconsAsset/convert.js에 의해 자동 생성되었습니다.
* 직접 수정하는 대신 iconsAsset/convert.js를 수정하세요.
*/

import { memo, forwardRef } from 'react';

import { IconBase } from '../icon.base';
import { IconProps } from '../icon.type';

export const IcSearchFilled = memo(
forwardRef<SVGSVGElement, IconProps>((props, ref) => (
<IconBase ref={ref} viewBox="0 0 24 24" {...props}>
<path
d="M19.9382 7.66937C18.512 4.24473 15.1704 2.01008 11.4607 2.00017C7.45562 1.9761 3.89312 4.54041 2.64466 8.34596C1.39619 12.1515 2.74734 16.3278 5.98831 18.6809C9.22928 21.034 13.6187 21.0256 16.8507 18.6602L19.7207 21.5302C20.0135 21.8226 20.4878 21.8226 20.7807 21.5302C21.0731 21.2374 21.0731 20.763 20.7807 20.4702L17.9907 17.6802C20.5968 15.04 21.3644 11.094 19.9382 7.66937ZM9.13996 8.04179C8.89594 8.62562 8.32342 9.00421 7.69066 9.00017C6.83462 9.00017 6.14066 8.30621 6.14066 7.45017C6.13662 6.81741 6.51521 6.24488 7.09904 6.00086C7.68287 5.75684 8.35627 5.88967 8.80371 6.33711C9.25115 6.78455 9.38398 7.45796 9.13996 8.04179Z"
fillRule="evenodd"
clipRule="evenodd"
xmlns="http://www.w3.org/2000/svg"
/>
</IconBase>
))
);

IcSearchFilled.displayName = 'IcSearchFilled';
24 changes: 24 additions & 0 deletions src/style/foundation/icons/generated/IcSearchLine.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/**
* 이 파일은 iconsAsset/convert.js에 의해 자동 생성되었습니다.
* 직접 수정하는 대신 iconsAsset/convert.js를 수정하세요.
*/

import { memo, forwardRef } from 'react';

import { IconBase } from '../icon.base';
import { IconProps } from '../icon.type';

export const IcSearchLine = memo(
forwardRef<SVGSVGElement, IconProps>((props, ref) => (
<IconBase ref={ref} viewBox="0 0 24 24" {...props}>
<path
d="M19.9382 7.66933C18.512 4.24469 15.1704 2.01004 11.4607 2.00013C7.46246 1.97907 3.90737 4.54006 2.66105 8.3391C1.41473 12.1381 2.7619 16.3074 5.99559 18.6588C9.22927 21.0103 13.6107 21.0068 16.8407 18.6501L19.7207 21.5301C20.0135 21.8226 20.4878 21.8226 20.7807 21.5301C21.0731 21.2373 21.0731 20.7629 20.7807 20.4701L17.9907 17.6801C20.5968 15.04 21.3644 11.094 19.9382 7.66933ZM18.6011 14.1691C17.4036 17.0547 14.5849 18.9342 11.4607 18.9301V18.8901C7.22037 18.8847 3.77812 15.4603 3.75066 11.2201C3.74662 8.09586 5.62605 5.27717 8.51172 4.0797C11.3974 2.88223 14.7203 3.54208 16.9295 5.75127C19.1387 7.96046 19.7986 11.2834 18.6011 14.1691Z"
fillRule="evenodd"
clipRule="evenodd"
xmlns="http://www.w3.org/2000/svg"
/>
</IconBase>
))
);

IcSearchLine.displayName = 'IcSearchLine';
Loading