Skip to content
Open
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
32 changes: 31 additions & 1 deletion apps/docs/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,19 @@ import { ChangeEvent, useState } from 'react';
import '@sopt-makers/ui/dist/index.css';

import { colors } from '@sopt-makers/colors';
import { FieldBox, SearchField, Test, TextArea, TextField, SelectV2 } from '@sopt-makers/ui';
import {
FieldBox,
SearchField,
Test,
TextArea,
TextField,
SelectV2,
BottomSheetRoot,
BottomSheetTrigger,
BottomSheetContent,
BottomSheetActionButton,
BottomSheetBody,
} from '@sopt-makers/ui';

interface Option<T> {
label: string;
Expand Down Expand Up @@ -162,6 +174,24 @@ function App() {
</SelectV2.Menu>
</SelectV2.Root>
</div>
<div>
<BottomSheetRoot>
<BottomSheetTrigger>열기</BottomSheetTrigger>
<BottomSheetContent title='Title'>
<BottomSheetBody>
<p style={{ color: 'white' }}>
커뮤니티 이용규칙은 누구나 기분 좋게 참여할 수 있는 커뮤니티를 만들기 위해 제정되었습니다. 서비스 내의
모든 커뮤니티는 커뮤니티 이용규칙에 의해 운영되므로, 이용자는 커뮤니티 이용 전 반드시 모든 내용을
숙지하여야 합니다. 방송통신심의위원회의 정보통신에 관한 심의규정, 현행 법률, 서비스 이용약관 및 커뮤니티
이용규칙을 위반하거나, 사회 통념 및 관련 법령을 기준으로 타 이용자에게 악영향을 끼치는 경우, 게시물이
삭제되고 서비스 이용이 일정 기간 제한될 수 있습니다. 커뮤니티 이용규칙은 누구나 기분 좋게 참여할 수 있는
커뮤니티를 만들기 위해 제정되었습니다. 서비스 내의 모든 커뮤니티는 커뮤니티 이용규칙에 의해 운영되므로,
</p>
</BottomSheetBody>
<BottomSheetActionButton>Button</BottomSheetActionButton>
</BottomSheetContent>
</BottomSheetRoot>
</div>
</>
);
}
Expand Down
149 changes: 149 additions & 0 deletions apps/docs/src/stories/BottomSheet.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
import {
BottomSheetActionButton,
BottomSheetBody,
BottomSheetContent,
BottomSheetRoot,
BottomSheetTrigger,
Button,
} from '@sopt-makers/ui';
import type { Meta, StoryObj } from '@storybook/react';
import { useState } from 'react';

const meta: Meta = {
title: 'Components/BottomSheet',
tags: ['autodocs'],
};
export default meta;

export const BottomSheetUncontrolled: StoryObj = {
name: 'BottomSheet - Uncontrolled',
render: () => {
return (
<BottomSheetRoot>
<BottomSheetTrigger>
<Button size='sm'>열기</Button>
</BottomSheetTrigger>
<BottomSheetContent title='Title'>
<BottomSheetBody>
<p style={{ color: 'white' }}>
커뮤니티 이용규칙은 누구나 기분 좋게 참여할 수 있는 커뮤니티를 만들기 위해 제정되었습니다. 서비스 내의
모든 커뮤니티는 커뮤니티 이용규칙에 의해 운영되므로, 이용자는 커뮤니티 이용 전 반드시 모든 내용을
숙지하여야 합니다. 방송통신심의위원회의 정보통신에 관한 심의규정, 현행 법률, 서비스 이용약관 및 커뮤니티
이용규칙을 위반하거나, 사회 통념 및 관련 법령을 기준으로 타 이용자에게 악영향을 끼치는 경우, 게시물이
삭제되고 서비스 이용이 일정 기간 제한될 수 있습니다.
</p>
</BottomSheetBody>
</BottomSheetContent>
</BottomSheetRoot>
);
},
};

const ControlledSample = () => {
const [isOpen, setOpen] = useState(false);

return (
<BottomSheetRoot open={isOpen} onOpenChange={setOpen}>
<BottomSheetTrigger>
<Button size='sm'>{isOpen ? '닫기' : '열기'}</Button>
</BottomSheetTrigger>
<BottomSheetContent title='Title'>
<BottomSheetBody>
<p style={{ color: 'white' }}>
커뮤니티 이용규칙은 누구나 기분 좋게 참여할 수 있는 커뮤니티를 만들기 위해 제정되었습니다. 서비스 내의 모든
커뮤니티는 커뮤니티 이용규칙에 의해 운영되므로, 이용자는 커뮤니티 이용 전 반드시 모든 내용을 숙지하여야
합니다. 방송통신심의위원회의 정보통신에 관한 심의규정, 현행 법률, 서비스 이용약관 및 커뮤니티 이용규칙을
위반하거나, 사회 통념 및 관련 법령을 기준으로 타 이용자에게 악영향을 끼치는 경우, 게시물이 삭제되고 서비스
이용이 일정 기간 제한될 수 있습니다.
</p>
</BottomSheetBody>
</BottomSheetContent>
</BottomSheetRoot>
);
};

export const BottomSheetControlled: StoryObj = {
name: 'BottomSheet - Controlled',
render: ControlledSample,
};

export const BottomSheetBackIcon: StoryObj = {
name: 'BottomSheet - Back Icon',
render: () => (
<BottomSheetRoot>
<BottomSheetTrigger>
<Button size='sm'>열기</Button>
</BottomSheetTrigger>
<BottomSheetContent title='Title' backIcon>
<BottomSheetBody>
<p style={{ color: 'white' }}>
커뮤니티 이용규칙은 누구나 기분 좋게 참여할 수 있는 커뮤니티를 만들기 위해 제정되었습니다. 서비스 내의 모든
커뮤니티는 커뮤니티 이용규칙에 의해 운영되므로, 이용자는 커뮤니티 이용 전 반드시 모든 내용을 숙지하여야
합니다. 방송통신심의위원회의 정보통신에 관한 심의규정, 현행 법률, 서비스 이용약관 및 커뮤니티 이용규칙을
위반하거나, 사회 통념 및 관련 법령을 기준으로 타 이용자에게 악영향을 끼치는 경우, 게시물이 삭제되고 서비스
이용이 일정 기간 제한될 수 있습니다.
</p>
</BottomSheetBody>
</BottomSheetContent>
</BottomSheetRoot>
),
};

export const BottomSheetWithActionButton: StoryObj = {
name: 'BottomSheet - Action Button',
render: () => (
<BottomSheetRoot>
<BottomSheetTrigger>
<Button size='sm'>열기</Button>
</BottomSheetTrigger>
<BottomSheetContent title='Title'>
<BottomSheetBody>
<p style={{ color: 'white' }}>
커뮤니티 이용규칙은 누구나 기분 좋게 참여할 수 있는 커뮤니티를 만들기 위해 제정되었습니다. 서비스 내의 모든
커뮤니티는 커뮤니티 이용규칙에 의해 운영되므로, 이용자는 커뮤니티 이용 전 반드시 모든 내용을 숙지하여야
합니다. 방송통신심의위원회의 정보통신에 관한 심의규정, 현행 법률, 서비스 이용약관 및 커뮤니티 이용규칙을
위반하거나, 사회 통념 및 관련 법령을 기준으로 타 이용자에게 악영향을 끼치는 경우, 게시물이 삭제되고 서비스
이용이 일정 기간 제한될 수 있습니다.
</p>
</BottomSheetBody>
<BottomSheetActionButton>Button</BottomSheetActionButton>
</BottomSheetContent>
</BottomSheetRoot>
),
};

export const BottomSheetBodyScrolled: StoryObj = {
name: 'BottomSheet - Body Scrolled',
render: () => (
<BottomSheetRoot>
<BottomSheetTrigger>
<Button size='sm'>열기</Button>
</BottomSheetTrigger>
<BottomSheetContent title='Title'>
<BottomSheetBody>
<p style={{ color: 'white' }}>
커뮤니티 이용규칙은 누구나 기분 좋게 참여할 수 있는 커뮤니티를 만들기 위해 제정되었습니다. 서비스 내의 모든
커뮤니티는 커뮤니티 이용규칙에 의해 운영되므로, 이용자는 커뮤니티 이용 전 반드시 모든 내용을 숙지하여야
합니다. 방송통신심의위원회의 정보통신에 관한 심의규정, 현행 법률, 서비스 이용약관 및 커뮤니티 이용규칙을
위반하거나, 사회 통념 및 관련 법령을 기준으로 타 이용자에게 악영향을 끼치는 경우, 게시물이 삭제되고 서비스
이용이 일정 기간 제한될 수 있습니다. 커뮤니티 이용규칙은 누구나 기분 좋게 참여할 수 있는 커뮤니티를 만들기
위해 제정되었습니다. 서비스 내의 모든 커뮤니티는 커뮤니티 이용규칙에 의해 운영되므로, 이용자는 커뮤니티 이용
전 반드시 모든 내용을 숙지하여야 합니다. 방송통신심의위원회의 정보통신에 관한 심의규정, 현행 법률, 서비스
이용약관 및 커뮤니티 이용규칙을 위반하거나, 사회 통념 및 관련 법령을 기준으로 타 이용자에게 악영향을 끼치는
경우, 게시물이 삭제되고 서비스 이용이 일정 기간 제한될 수 있습니다. 커뮤니티 이용규칙은 누구나 기분 좋게
참여할 수 있는 커뮤니티를 만들기 위해 제정되었습니다. 서비스 내의 모든 커뮤니티는 커뮤니티 이용규칙에 의해
운영되므로, 이용자는 커뮤니티 이용 전 반드시 모든 내용을 숙지하여야 합니다. 방송통신심의위원회의 정보통신에
관한 심의규정, 현행 법률, 서비스 이용약관 및 커뮤니티 이용규칙을 위반하거나, 사회 통념 및 관련 법령을
기준으로 타 이용자에게 악영향을 끼치는 경우, 게시물이 삭제되고 서비스 이용이 일정 기간 제한될 수 있습니다.
커뮤니티 이용규칙은 누구나 기분 좋게 참여할 수 있는 커뮤니티를 만들기 위해 제정되었습니다. 서비스 내의 모든
커뮤니티는 커뮤니티 이용규칙에 의해 운영되므로, 이용자는 커뮤니티 이용 전 반드시 모든 내용을 숙지하여야
합니다. 방송통신심의위원회의 정보통신에 관한 심의규정, 현행 법률, 서비스 이용약관 및 커뮤니티 이용규칙을
위반하거나, 사회 통념 및 관련 법령을 기준으로 타 이용자에게 악영향을 끼치는 경우, 게시물이 삭제되고 서비스
이용이 일정 기간 제한될 수 있습니다.
</p>
</BottomSheetBody>
<BottomSheetActionButton>Button</BottomSheetActionButton>
</BottomSheetContent>
</BottomSheetRoot>
),
};
1 change: 1 addition & 0 deletions packages/eslint-config-custom/react-internal.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ module.exports = {
ignorePatterns: ['node_modules/', 'dist/', '.eslintrc.js'],

rules: {
'import/no-extraneous-dependencies': 'off',
'import/no-default-export': 'off',
'import/prefer-default-export': 'warn',
'@typescript-eslint/explicit-function-return-type': 'off',
Expand Down
31 changes: 31 additions & 0 deletions packages/ui/BottomSheet/context.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { createContext, PropsWithChildren, useContext } from 'react';

interface ContextProps {
open: boolean;
onOpenChange: (open: boolean) => void;
}

export const BottomSheetContext = createContext<ContextProps | null>(null);

export function BottomSheetProvider({ open, onOpenChange, children }: PropsWithChildren<ContextProps>) {
return (
<BottomSheetContext.Provider
value={{
open,
onOpenChange,
}}
>
{children}
</BottomSheetContext.Provider>
);
}

export function useBottomSheetContext() {
const context = useContext(BottomSheetContext);

if (context === null) {
throw new Error('this hook muse be used within a BottomSheetProvider');
}

return context;
}
123 changes: 123 additions & 0 deletions packages/ui/BottomSheet/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
/* eslint-disable -- able to click dim sesction jsx-a11y/click-events-have-key-events */
/* eslint-disable -- able to click dim sesction jsx-a11y/no-static-element-interactions */

import { HTMLAttributes, PropsWithChildren, ReactNode } from 'react';
import { BottomSheetProvider, useBottomSheetContext } from './context';
import { useBooleanState } from '@toss/react';
import { IconChevronLeft } from '@sopt-makers/icons';
import {
actionButtonStyle,
bodyWrapperStyle,
buttonWrapperStyle,
contentWrapperStyle,
dimStyle,
iconStyle,
overlayStyle,
titleTextStyle,
titleWrapperStyle,
} from './style.css';
import Button from '../Button';

export function BottomSheetTrigger({ children }: HTMLAttributes<HTMLButtonElement>) {
const { open, onOpenChange } = useBottomSheetContext();

const handleOpenChange = () => {
onOpenChange(!open);
};

return <div onClick={handleOpenChange}>{children}</div>;
}

interface RootProps {
open?: boolean;
defaultOpen?: boolean;
onOpenChange?: (open: boolean) => void;
}

export function BottomSheetRoot({
open: _open,
defaultOpen = false,
onOpenChange,
children,
}: PropsWithChildren<RootProps>) {
const [internalOpenValue, internalOpen, internalClose] = useBooleanState(defaultOpen);

const uncontrolled = _open === undefined;

const open = uncontrolled ? internalOpenValue : _open;
const handleOpenChange = (value: boolean) => {
if (uncontrolled) {
if (value) {
internalOpen();
} else {
internalClose();
}
} else {
onOpenChange?.(value);
}
};

const handleDimClick = () => {
if (uncontrolled) {
internalClose();
} else {
onOpenChange?.(false);
}
};

return (
<BottomSheetProvider open={open} onOpenChange={handleOpenChange}>
{open && <div className={dimStyle} onClick={handleDimClick} />}
{children}
</BottomSheetProvider>
);
}

interface ContentProps {
title?: string;
backIcon?: boolean;
}

export function BottomSheetContent({ title, backIcon, children }: PropsWithChildren<ContentProps>) {
const { open, onOpenChange } = useBottomSheetContext();

return (
open && (
<div className={overlayStyle}>
{title && (
<div className={titleWrapperStyle}>
{backIcon && <IconChevronLeft onClick={() => onOpenChange(false)} className={iconStyle} />}
<p className={titleTextStyle}>{title}</p>
</div>
)}
{children}
</div>
)
);
}

interface BodyProps extends HTMLAttributes<HTMLDivElement> {
maxHeight?: string;
}

export const BottomSheetBody = ({ children, ...props }: PropsWithChildren<BodyProps>) => {
const { maxHeight = '400px', ...restProps } = props;

return (
<div className={bodyWrapperStyle} {...restProps}>
<div className={contentWrapperStyle} style={{ maxHeight }}>
{children}
</div>
</div>
);
};

export function BottomSheetActionButton({ children }: PropsWithChildren) {
return (
<div className={buttonWrapperStyle}>
<Button size='lg' variant='fill' className={actionButtonStyle}>
{children}
</Button>
</div>
);
}
Loading