Skip to content

Commit

Permalink
fix: 컴포넌트 접근성 검수 (Carousel, BottomSheet) (#984)
Browse files Browse the repository at this point in the history
* fix: storybook에 globalStyle 적용

* fix: a, button tag에 focus-visible 적용

* style: focus:visible borderRadius 제거

* fix: IOS에서 svg.tsx의 rotation이 제대로 작동하도록 변경

* fix: globalStyle opacity 변경

* feat: Carousel component 접근성 개선과 반응형 대응 및 버그 수정

* fix: BottomSheet 관련 키보드 접근성 고려하여 변경

* style: lint 적용

* fix: focus-visible 속성을 더 다양한 element들에 적용

* fix: Top component tab index 제거

* style: lint 적용

* fix: useParentWidth hook으로 분리

* fix: carousel aria-live 속성 추가

* fix: Carousel 내부 버튼들에 aria 속성 추가

* style: lint 적용

* fix: 필요없는 주석 제거

* fix: focus-visible transition 제거

* chore: lodash 설치
  • Loading branch information
Todari authored Feb 5, 2025
1 parent 52c9929 commit 652d17d
Show file tree
Hide file tree
Showing 16 changed files with 373 additions and 56 deletions.
17 changes: 10 additions & 7 deletions client/.storybook/preview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import type {Preview} from '@storybook/react';
import {HDesignProvider} from '../src/components/Design';
import {css, Global} from '@emotion/react';

import {GlobalStyle} from '../src/GlobalStyle';
const preview: Preview = {
parameters: {
controls: {
Expand Down Expand Up @@ -35,12 +35,15 @@ const preview: Preview = {
return (
<div>
<Global
styles={css`
@import url('https://cdn.jsdelivr.net/gh/orioncactus/pretendard/dist/web/static/pretendard.css');
body {
font-family: 'Pretendard', sans-serif;
}
`}
styles={[
css`
@import url('https://cdn.jsdelivr.net/gh/orioncactus/pretendard/dist/web/static/pretendard.css');
body {
font-family: 'Pretendard', sans-serif;
}
`,
GlobalStyle,
]}
/>
<HDesignProvider>
<Story />
Expand Down
11 changes: 6 additions & 5 deletions client/package-lock.json

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

2 changes: 2 additions & 0 deletions client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
"@testing-library/react": "^16.0.0",
"@types/dotenv-webpack": "^7.0.7",
"@types/jest": "^29.5.12",
"@types/lodash": "^4.17.15",
"@types/react": "^18.3.3",
"@types/react-copy-to-clipboard": "^5.0.7",
"@types/react-dom": "^18.3.0",
Expand Down Expand Up @@ -96,6 +97,7 @@
"@emotion/react": "^11.11.4",
"@sentry/react": "^8.25.0",
"@tanstack/react-query": "^5.51.23",
"lodash": "^4.17.21",
"qrcode.react": "^4.1.0",
"react": "^18.3.1",
"react-copy-to-clipboard": "^5.1.0",
Expand Down
11 changes: 11 additions & 0 deletions client/src/GlobalStyle.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import {css} from '@emotion/react';

import {COLORS} from '@token/colors';

// reset css -> index css
export const GlobalStyle = css`
*:where(:not(html, iframe, canvas, img, svg, video, audio):not(svg *, symbol *)) {
Expand Down Expand Up @@ -27,6 +29,15 @@ export const GlobalStyle = css`
cursor: revert;
line-height: 0;
}
[href]:focus-visible,
[tabindex]:not([tabindex='-1']):focus-visible,
select:focus-visible,
button:focus-visible {
outline: 2px solid ${COLORS.primary};
outline-offset: 2px;
opacity: 1;
transition: all 0s;
}
button:disabled {
cursor: default;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,22 +1,63 @@
/** @jsxImportSource @emotion/react */
import type {Meta, StoryObj} from '@storybook/react';

import Box from '../Box/Box';

import Carousel from './Carousel';

const meta = {
title: 'Components/Carousel',
component: Carousel,
tags: ['autodocs'],
parameters: {
layout: 'centered',
width: 430,
viewport: {
defaultViewport: {
width: 430,
height: 930,
},
},
docs: {
description: {
component: `
Carousel 컴포넌트는 이미지들을 슬라이드 형태로 보여주는 컴포넌트입니다.
### 주요 기능
- 이미지 슬라이드 기능
- 드래그로 이미지 전환 가능
- 이미지 삭제 기능 (선택적)
- 이미지 인디케이터
- 좌우 이동 버튼
### 사용 예시
\`\`\`jsx
<Carousel
urls={[
'image1.jpg',
'image2.jpg',
'image3.jpg'
]}
onClickDelete={(index) => handleDelete(index)} // 선택적
/>
\`\`\`
`,
},
},
},
tags: ['autodocs'],
argTypes: {
urls: {
description: '캐러셀에 표시할 이미지 URL 배열',
},
onClickDelete: {
description: '이미지 삭제 핸들러 (선택적)',
},
},
argTypes: {},
args: {
urls: [
'https://wooteco-crew-wiki.s3.ap-northeast-2.amazonaws.com/%EC%BF%A0%ED%82%A4(6%EA%B8%B0)/image.png',
'https://wooteco-crew-wiki.s3.ap-northeast-2.amazonaws.com/%EC%BF%A0%ED%82%A4%286%EA%B8%B0%29/4tyq1x19rsn.jpg',
'https://img.danawa.com/images/descFiles/5/896/4895281_1_16376712347542321.gif',
'https://d1f4hb6ir36p4s.cloudfront.net/images/todari.png',
'https://d1f4hb6ir36p4s.cloudfront.net/images/cookie.png',
'https://d1f4hb6ir36p4s.cloudfront.net/images/weadie.png',
'https://d1f4hb6ir36p4s.cloudfront.net/images/soha.png',
],
},
} satisfies Meta<typeof Carousel>;
Expand All @@ -25,4 +66,20 @@ export default meta;

type Story = StoryObj<typeof meta>;

export const Playground: Story = {};
export const Default: Story = {
render: args => {
return (
<Box w={480} h={930} bg="#ffffff" p={16} b="1px solid #eee" br={8}>
<Carousel {...args} />
</Box>
);
},
};

export const WithDeleteButton: Story = {
render: args => (
<Box w={480} h={930} bg="#ffffff" p={16} b="1px solid #eee" br={8}>
<Carousel {...args} onClickDelete={index => alert(`Delete image at index ${index}`)} />
</Box>
),
};
33 changes: 28 additions & 5 deletions client/src/components/Design/components/Carousel/Carousel.style.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import {css} from '@emotion/react';

import {Theme} from '@components/Design/theme/theme.type';

import {Direction} from '../Icons/Icon.type';

export const carouselWrapperStyle = css`
position: relative;
overflow: hidden;
Expand All @@ -13,38 +15,45 @@ interface ImageCardContainerStyleProps {
length: number;
translateX: number;
isDragging: boolean;
parentWidth: number;
}

export const imageCardContainerStyle = ({
currentIndex,
length,
translateX,
isDragging,
parentWidth,
}: ImageCardContainerStyleProps) => css`
display: flex;
gap: 1rem;
margin-inline: 2rem;
transform: translateX(
transform: translate3d(
calc(
(100vw - 3rem) * ${-currentIndex} +
(${parentWidth}px - 3rem) * ${-currentIndex} +
${(currentIndex === 0 && translateX > 0) || (currentIndex === length - 1 && translateX < 0) ? 0 : translateX}px
)
),
0,
0
);
will-change: transform;
backface-visibility: hidden;
transition: ${isDragging ? 'none' : '0.2s'};
transition-timing-function: cubic-bezier(0.7, 0.62, 0.62, 1.16);
`;

interface ImageCardStyleProps {
theme: Theme;
parentWidth: number;
}

export const imageCardStyle = ({theme}: ImageCardStyleProps) => css`
export const imageCardStyle = ({theme, parentWidth}: ImageCardStyleProps) => css`
position: relative;
display: flex;
justify-content: center;
align-items: center;
clip-path: inset(0 round 1rem);
max-width: calc(768px - 4rem);
width: ${parentWidth ? `calc(${parentWidth}px - 4rem)` : 'calc(430px - 4rem)'};
background-color: ${theme.colors.gray};
`;

Expand Down Expand Up @@ -93,3 +102,17 @@ export const indicatorStyle = ({index, currentIndex, theme}: IndicatorStyleProps
transition-timing-function: cubic-bezier(0.7, 0.62, 0.62, 1.16);
content: ' ';
`;

export const changeButtonStyle = (direction: Direction) => css`
position: absolute;
${direction === ('left' as Direction) ? 'left' : 'right'}: 1rem;
top: 50%;
transform: translateY(-50%);
padding: 0.5rem;
opacity: 0.48;
background-color: rgba(0, 0, 0, 0.8);
border-radius: 50%;
display: flex;
justify-content: center;
align-items: center;
`;
70 changes: 63 additions & 7 deletions client/src/components/Design/components/Carousel/Carousel.tsx
Original file line number Diff line number Diff line change
@@ -1,30 +1,86 @@
/** @jsxImportSource @emotion/react */
import {srOnlyStyle} from '@components/Design/theme/commonStyle';

import {CarouselProps} from './Carousel.type';
import CarouselIndicator from './CarouselIndicator';
import CarouselDeleteButton from './CarouselDeleteButton';
import {carouselWrapperStyle, imageCardContainerStyle, imageCardStyle, imageStyle} from './Carousel.style';
import useCarousel from './useCarousel';
import {CarouselChangeButton} from './CarouselChangeButton';

const Carousel = ({urls, onClickDelete}: CarouselProps) => {
const {handleDragStart, handleDrag, handleDragEnd, theme, currentIndex, translateX, isDragging, handleClickDelete} =
useCarousel({urls, onClickDelete});
const {
handleDragStart,
handleDrag,
handleDragEnd,
theme,
currentIndex,
translateX,
isDragging,
handleClickDelete,
handlePreventDrag,
handleToPrev,
handleToNext,
handleKeyDown,
parentWidth,
wrapperRef,
} = useCarousel({urls, onClickDelete});

return (
<div css={carouselWrapperStyle}>
<div
css={carouselWrapperStyle}
role="region"
aria-roledescription="carousel"
aria-label="이미지 캐러샐"
onKeyDown={handleKeyDown}
ref={wrapperRef}
>
<div aria-live="polite" aria-atomic="true" css={srOnlyStyle}>
전체 {urls.length}장 중 {currentIndex + 1}번째 이미지
</div>
<div
css={imageCardContainerStyle({currentIndex, length: urls.length, translateX, isDragging})}
css={imageCardContainerStyle({currentIndex, length: urls.length, translateX, isDragging, parentWidth})}
onMouseDown={handleDragStart}
onMouseMove={handleDrag}
onMouseUp={handleDragEnd}
onTouchStart={handleDragStart}
onTouchMove={handleDrag}
onTouchEnd={handleDragEnd}
role="group"
aria-roledescription="slide"
>
{urls &&
urls.map((url, index) => (
<div key={url} css={imageCardStyle({theme})}>
<img src={url} alt={`업로드된 이미지 ${index + 1}`} css={imageStyle} />
{onClickDelete && <CarouselDeleteButton onClick={() => handleClickDelete(index)} />}
<div key={url} css={imageCardStyle({theme, parentWidth})}>
<img
src={url}
alt={`업로드된 이미지 ${index + 1}`}
loading="lazy"
decoding="async"
css={imageStyle}
onDragStart={handlePreventDrag}
onDragEnd={handlePreventDrag}
/>
{index !== 0 && (
<CarouselChangeButton
direction="left"
onClick={handleToPrev}
tabIndex={currentIndex === index ? 0 : -1}
/>
)}
{index !== urls.length - 1 && (
<CarouselChangeButton
direction="right"
onClick={handleToNext}
tabIndex={currentIndex === index ? 0 : -1}
/>
)}
{onClickDelete && (
<CarouselDeleteButton
onClick={() => handleClickDelete(index)}
tabIndex={currentIndex === index ? 0 : -1}
/>
)}
</div>
))}
</div>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/** @jsxImportSource @emotion/react */
import {Direction} from '../Icons/Icon.type';
import {IconChevron} from '../Icons/Icons/IconChevron';

import {changeButtonStyle} from './Carousel.style';

interface Props {
direction: Direction;
onClick: () => void;
tabIndex: number;
}

export const CarouselChangeButton = ({direction, onClick, tabIndex}: Props) => {
return (
<button
onClick={onClick}
css={changeButtonStyle(direction)}
tabIndex={tabIndex}
aria-label={`${direction === 'left' ? '이전' : '다음'} 이미지`}
>
<IconChevron size={16} direction={direction} />
</button>
);
};
Loading

0 comments on commit 652d17d

Please sign in to comment.