Skip to content

Commit

Permalink
feat(react): AspectRatio Slot 적용 (#579)
Browse files Browse the repository at this point in the history
* feat(react): AspectRatio Slot 적용

* fix: docs
  • Loading branch information
ssi02014 authored Nov 11, 2024
1 parent 1d18dd5 commit b14a556
Show file tree
Hide file tree
Showing 4 changed files with 132 additions and 51 deletions.
5 changes: 5 additions & 0 deletions .changeset/mighty-buckets-drum.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@modern-kit/react': minor
---

feat(react): AspectRatio Slot 적용 - @ssi0214
60 changes: 47 additions & 13 deletions docs/docs/react/components/AspectRatio.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@ import { AspectRatio } from '@modern-kit/react';

# AspectRatio

자식 요소의 가로, 세로 비율 **[aspect-ratio](https://developer.mozilla.org/en-US/docs/Web/CSS/aspect-ratio)** 을 편리하게 맞춰 줄 수 있는 컴포넌트입니다.
주어진 **[aspect-ratio](https://developer.mozilla.org/en-US/docs/Web/CSS/aspect-ratio)** 비율에 맞게 가로/세로 비율을 편리하게 맞춰주는 컴포넌트입니다.

미리 영역을 확보하여 Layout Shift를 방지하는데 효과적입니다.

<br />

Expand All @@ -12,17 +14,19 @@ import { AspectRatio } from '@modern-kit/react';

## Interface
```ts title="typescript"
interface AspectRatioProps extends ComponentProps<'div'> {
ratio: number;
}

const AspectRatio: React.ForwardRefExoticComponent<
Omit<PropsWithChildren<AspectRatioProps>, 'ref'> &
React.RefAttributes<HTMLDivElement>
>;
const AspectRatio: <
T extends React.ElementType = 'div'
>({
asChild,
ratio,
children,
...props
}: React.PropsWithChildren<AspectRatioProps<T>>) => JSX.Element;
```

## Usage
### Basic
- 기본적으로 div 태그를 부모로 두고 자식 요소를 렌더링 하며, div 태그에 aspect-ratio 속성을 적용합니다.
```tsx title="typescript"
import { AspectRatio } from '@modern-kit/react'

Expand All @@ -37,9 +41,7 @@ const Example = () => {
};
```

## Example

export const Example = () => {
export const Example1 = () => {
return (
<div style={{ width: '500px' }}>
<AspectRatio ratio={427 / 640}>
Expand All @@ -49,4 +51,36 @@ export const Example = () => {
);
};

<Example />
<Example1 />

### asChild
- asChild 속성을 true로 설정하면 자식 요소에 aspect-ratio 속성을 적용합니다.
```tsx title="typescript"
import { AspectRatio } from '@modern-kit/react'

const Example = () => {
return (
<div style={{ width: '500px' }}>
<AspectRatio ratio={427 / 640} asChild>
<section> {/* section 태그에 aspect-ratio 속성이 적용됩니다. */}
<img src="https://github.com/user-attachments/assets/dd60ec12-afd7-44c9-bd6b-0069e16bf2c9" />
</section>
</AspectRatio>
</div>
);
};
```

export const Example2 = () => {
return (
<div style={{ width: '500px' }}>
<AspectRatio ratio={427 / 640} asChild>
<section>
<img src="https://github.com/user-attachments/assets/dd60ec12-afd7-44c9-bd6b-0069e16bf2c9" />
</section>
</AspectRatio>
</div>
);
};

<Example2 />
36 changes: 27 additions & 9 deletions packages/react/src/components/AspectRatio/AspectRatio.spec.tsx
Original file line number Diff line number Diff line change
@@ -1,23 +1,43 @@
import { render } from '@testing-library/react';
import { render, screen } from '@testing-library/react';
import { describe, it, expect } from 'vitest';
import { AspectRatio } from '.';

describe('AspectRatio component', () => {
it('should render correctly with the given ratio', () => {
it('기본적으로 div 태그를 부모 요소로 두고 자식 요소를 렌더링하며, div 태그에 aspect-ratio 설정이 적용되어야 합니다.', () => {
render(
<AspectRatio ratio={16 / 9} role="wrapper">
<p>Content</p>
</AspectRatio>
);

const wrapper = screen.getByRole('wrapper');

expect(wrapper).toHaveStyle(
'padding-top: calc(100% * (1 / 1.7777777777777777))'
);
expect(wrapper.tagName).toBe('DIV');
});

it('asChild 속성이 true라면 aspect-ratio 설정이 자식 요소에 적용됩니다.', () => {
const { container } = render(
<AspectRatio ratio={16 / 9}>
<div>Content</div>
<AspectRatio ratio={16 / 9} asChild>
<section role="wrapper">Content</section>
</AspectRatio>
);

const wrapper = container.firstChild as HTMLElement;
const divWrapper = container.querySelector('div');

expect(divWrapper).not.toBeInTheDocument();

const wrapper = screen.getByRole('wrapper');

expect(wrapper).toHaveStyle(
'padding-top: calc(100% * (1 / 1.7777777777777777))'
);
expect(wrapper.tagName).toBe('SECTION');
});

it('should throw an error if more than one child is passed', () => {
it('하나 이상의 하위 요소가 있을 때 오류를 발생시켜야 합니다.', () => {
const renderComponent = () => {
render(
<AspectRatio ratio={4 / 3}>
Expand All @@ -27,8 +47,6 @@ describe('AspectRatio component', () => {
);
};

expect(renderComponent).toThrow(
'AspectRatio component expects exactly one child element.'
);
expect(renderComponent).toThrow();
});
});
82 changes: 53 additions & 29 deletions packages/react/src/components/AspectRatio/index.tsx
Original file line number Diff line number Diff line change
@@ -1,42 +1,66 @@
import {
CSSProperties,
Children,
ComponentProps,
PropsWithChildren,
forwardRef,
useMemo,
} from 'react';
import classNames from 'classnames';
import styles from './AspectRatio.module.css';
import { Slot } from '../Slot';
import React from 'react';

interface AspectRatioProps extends ComponentProps<'div'> {
ratio: number;
}
type AspectRatioProps<T extends React.ElementType = 'div'> =
React.ComponentPropsWithoutRef<T> & {
asChild?: boolean;
ratio: number;
};

export const AspectRatio = forwardRef<
HTMLDivElement,
PropsWithChildren<AspectRatioProps>
>(({ ratio, children, className, style, ...props }, ref) => {
const customStyle: CSSProperties = useMemo(
/**
* @description 주어진 aspect-ratio 비율에 맞게 가로/세로 비율을 편리하게 맞춰주는 컴포넌트입니다.
*
* 미리 영역을 확보하여 Layout Shift를 방지하는데 효과적입니다.
*
* @template T - 사용할 HTML 요소의 타입을 지정합니다. 기본값은 'div'입니다.
*
* @param {PropsWithChildren<AspectRatioProps<T>>} aspectRatioProps - 컴포넌트에 전달되는 속성들입니다.
* @param {boolean} [aspectRatioProps.asChild=false] - `true`로 설정하면 `Slot` 컴포넌트를 사용해 자식 요소에 aspect-ratio 속성을 적용합니다.
* @param {number} aspectRatioProps.ratio - 자식 요소의 가로 세로 비율을 지정합니다.
* @param {React.ReactNode} aspectRatioProps.children - 렌더링 할 자식요소 입니다.
* @param {Object} [aspectRatioProps.props] - 기타 추가할 나머지 속성들입니다.
*
* @returns {JSX.Element} 주어진 비율에 맞춰 스타일이 적용된 래퍼 요소를 반환합니다.
*
* @example
* // 기본적으로 div를 생성 후 aspect-ratio 속성을 적용합니다.
* ```tsx
* <AspectRatio ratio={16 / 9}>
* <p>Content</p>
* </AspectRatio>
* ```
*
* @example
* // asChild 속성을 true로 설정하면 자식 요소에 aspect-ratio 속성을 적용합니다.
* ```tsx
* <AspectRatio ratio={16 / 9} asChild>
* <section>Content</section>
* </AspectRatio>
* ```
*/
export const AspectRatio = <T extends React.ElementType = 'div'>({
asChild = false,
ratio,
children,
...props
}: React.PropsWithChildren<AspectRatioProps<T>>): JSX.Element => {
const Wrapper = asChild ? Slot : 'div';
const customStyle: React.CSSProperties = React.useMemo(
() => ({
paddingTop: `calc(100% * (1 / ${ratio}))`,
...style,
...props.style,
}),
[ratio, style]
[ratio, props.style]
);

if (Children.count(children) > 1) {
throw new Error('AspectRatio component expects exactly one child element.');
}
return (
<div
ref={ref}
className={classNames(styles['wrapper'], className)}
<Wrapper
className={classNames(styles['wrapper'], props.className)}
style={customStyle}
{...props}>
{Children.only(children)}
</div>
{React.Children.only(children)}
</Wrapper>
);
});

AspectRatio.displayName = 'AspectRatio';
};

0 comments on commit b14a556

Please sign in to comment.