Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Re-implement Modal component using Dialog in Radix UI #1036

Merged
merged 27 commits into from
Dec 6, 2022

Conversation

sungik-choi
Copy link
Contributor

@sungik-choi sungik-choi commented Nov 18, 2022

Self Checklist

  • I wrote a PR title in English.
  • I added an appropriate label to the PR.
  • I wrote a commit message in English.
  • I wrote a commit message according to the Conventional Commits specification.
  • I added the appropriate changeset for the changes.
  • [Component] I wrote a unit test about the implementation.
  • [Component] I wrote a storybook document about the implementation.
  • [Component] I tested the implementation in various browsers.
    • Windows: Chrome, Edge, (Optional) Firefox
    • macOS: Chrome, Edge, Safari, (Optional) Firefox
  • [New Component] I added my username to the correct directory in the CODEOWNERS file.

Related Issue

Summary

Radix UI의 Dialog 컴포넌트를 사용하여 Modal 컴포넌트를 재구현합니다.

Details

  • 컴포넌트가 렌더되면 포커스가 자동으로 트랩됩니다. 기존 구현과 다르게 포커스가 모달 바깥으로 빠져나가지 않습니다.
  • 제어형 컴포넌트만 가능했던 기존 구현과 다르게 제어형/비제어형 컴포넌트 방식으로 모두 사용할 수 있습니다.
  • 사용처에서 모달 스타일링을 styled 사용 없이 쉽게 할 수 있도록 ModalContentPropswidth, height 속성을 추가합니다.
  • 언마운트 시 fade out 애니메이션이 추가됩니다.
  • 제목과 설명, 트리거 버튼 등에 적절한 접근성 속성이 추가됩니다.
  • 적절한 시맨틱 엘리먼트를 사용하도록 변경합니다.

Anatomy

AS-IS

import { 
  Modal, 
  ModalContent, 
  ModalAction, 
} from '@channel.io/bezier-react'

export default () => (
  <Modal>
    <ModalContent>
      { content }
    </ModalContent>
    <ModalAction />
  </Modal>
)

TO-BE

import { 
  Modal, 
  ModalTrigger, 
  ModalContent, 
  ModalHeader, 
  ModalBody, 
  ModalFooter 
} from '@channel.io/bezier-react'

export default () => (
  <Modal>
    <ModalTrigger />
    <ModalContent>
      <ModalHeader />
      <ModalBody>
        { content }
      </ModalBody>
      <ModalFooter />
    </ModalContent>
  </Modal>
)
  • Modal : Radix Dialog.Root 를 추상화한 컴포넌트입니다. Context의 역할만 담당하고, 컴포넌트를 렌더하지는 않습니다. 모달의 상태와 핸들러를 관리합니다.
  • ModalContent : Radix Dialog.Portal, Dialog.Overlay, Dialog.Content 를 추상화한 컴포넌트입니다. 모달 전체가 렌더되는 위치, 스타일을 관리합니다.
  • ModalHeader : 기존 ModalContent 가 담당하던 역할 중 제목에 대한 역할만을 담당하는 컴포넌트입니다. 제목, 부제목, 설명을 렌더하는 책임을 가집니다.
  • ModalFooter : 기존 ModalAction 과 동일한 책임을 가진 컴포넌트입니다.
  • ModalBody : 모달 내용물을 감싸는 단순한 Wrapper 프리셋 컴포넌트입니다.
  • ModalTrigger : 모달을 여는 버튼을 감쌀 때 사용할 수 있는 헬퍼 컴포넌트입니다. 자신은 렌더하지 않고 하위 컴포넌트(children)에게 자신의 속성을 전달합니다. 속성에는 모달의 상태에 관련한 접근성 속성이 포함되어 있습니다. 하위 컴포넌트를 클릭하면 ModalonShow 핸들러를 호출합니다. 비제어 컴포넌트로 사용할 경우 모달을 엽니다.
  • ModalClose : 모달을 닫는 버튼을 감쌀 때 사용할 수 있는 헬퍼 컴포넌트입니다. 자신은 렌더하지 않고 하위 컴포넌트(children)에게 자신의 속성을 전달합니다. 하위 컴포넌트를 클릭하면 ModalonHide 핸들러를 호출합니다. 비제어 컴포넌트로 사용할 경우 모달을 닫습니다.

Design Decision

컴포넌트 추가 및 네이밍 변경

Radix의 Dialog의 구현상 기존의 Modal(=BaseModal), ModalContent, ModalAction 3가지 컴포넌트로 이루어진 구성으로는 사용하기 어려웠습니다. 특히 ModalModalContent 컴포넌트의 인터페이스를 그대로 유지한 채 Radix Dialog를 사용해서 구현하게 되면 Dialog.Trigger 사용이 불가능한 점이 걸렸습니다. 기존엔 Modal 컴포넌트가 모달 Wrapper를 그리는 책임, Portal을 열고 렌더하는 책임을 담당하고 있었는데 이 경우 children으로 기존 렌더 트리아래 그려져야 할 트리거 컴포넌트를 넘겨줄 수 없었기 때문입니다. 마찬가지로, ModalPortal 의 역할을 담당하게 하지 않으려면(= 트리거 컴포넌트를 사용하려면) 기존처럼 ModalContentModalAction 컴포넌트를 병렬로 배치하는 방식도 사용이 불가능했습니다.

새로운 모달 컴포넌트는 비제어형 컴포넌트로도 쓰일 수 있었으면 했고, 접근성 속성도 꼭 챙겼으면 했습니다. 추후 확장 가능성, 마이그레이션 비용을 고려해보면 어느정도 브레이킹 체인지가 있더라도 괜찮다고 생각했습니다. 상기한대로 ModalContent 가 Portal, Overlay의 역할을 가지게 되면서 기존 구조를 그대로 가져가기가 어려웠으므로, 기존 피그마와 통일했었던 구조에서(#734 (review)) 다시 ModalHeader, ModalBody, ModalFooter 를 가지는 구조로 변경하게 되었습니다(#). 구조 변경에 따른 인터페이스 변경도 함께 진행했습니다.

Examples

Controlled

채널 데스크 어플리케이션에서 사용하는 패턴입니다. 트리거 컴포넌트와 모달 컴포넌트의 거리가 먼 경우, 같은 컨텍스트를 공유하지 못하는 경우엔 ModalTrigger 없이도 사용할 수 있습니다(접근성 속성을 챙기지 못한다는 점은 아쉽습니다).

export default () => {
  const [show, setShow] = useState(false)

  return (
    <>
      <Button
        text="Open Modal"
        onClick={() => setShow(true)}
      />

      <Modal 
        show={show}
        onHide={() => setShow(false)}
      >
        <ModalTrigger>
          <Button text="Open Modal" />
        </ModalTrigger>
    
        <ModalContent showCloseIcon>
          <ModalHeader
            title="Foo"
            subTitle="Foo"
            description="Foo"
            titleSize={ModalTitleSize.L}
          />
    
          <ModalBody>
            <FormControl labelPosition="left">
              <FormLabel>Name</FormLabel>
              <TextField />
            </FormControl>
          </ModalBody>
    
          <ModalFooter
            rightContent={(
              <ButtonGroup>
                <ModalClose>
                  <Button
                    colorVariant={ButtonColorVariant.MonochromeLight}
                    styleVariant={ButtonStyleVariant.Secondary}
                    text="Cancel"
                  />
                </ModalClose>
                <ModalClose>
                  <Button
                    colorVariant={ButtonColorVariant.Blue}
                    styleVariant={ButtonStyleVariant.Primary}
                    text="Save"
                  />
                </ModalClose>
              </ButtonGroup>
            )}
          />
        </ModalContent>
      </Modal>
    </>
  )
}

Uncontrolled

비동기 로직 등이 없는 간단한 모달의 경우 별도의 상태 없이도 구현이 가능합니다.

export default () => (
  <Modal>
    <ModalTrigger>
      <Button text="Open Modal" />
    </ModalTrigger>

    <ModalContent showCloseIcon>
      <ModalHeader
        title="Foo"
        subTitle="Foo"
        description="Foo"
        titleSize={ModalTitleSize.L}
      />

      <ModalBody>
        <FormControl labelPosition="left">
          <FormLabel>Name</FormLabel>
          <TextField />
        </FormControl>
      </ModalBody>

      <ModalFooter
        rightContent={(
          <ButtonGroup>
            <ModalClose>
              <Button
                colorVariant={ButtonColorVariant.MonochromeLight}
                styleVariant={ButtonStyleVariant.Secondary}
                text="Cancel"
              />
            </ModalClose>
            <ModalClose>
              <Button
                colorVariant={ButtonColorVariant.Blue}
                styleVariant={ButtonStyleVariant.Primary}
                text="Save"
              />
            </ModalClose>
          </ButtonGroup>
        )}
      />
    </ModalContent>
  </Modal>
)

TODO

  • 유닛 테스트 작성
  • 개별 컴포넌트에 대한 JSDoc 작성

Breaking change or not (Yes/No)

Yes

  • 더 이상 Modal 의 내부 구현에 BaseModal 을 사용하지 않습니다.
  • 기존 ModalLegacyModal 로 이름이 변경되며, 후속 PR에서 제거됩니다.
  • ModalAction 컴포넌트의 이름이 ModalFooter 로 변경됩니다.
  • ModalPropstargetElement 속성의 이름이 container 로 변경됩니다.
  • showCloseIcon 속성이 ModalProps 에서 ModalContentProps 로 옮겨집니다.
  • title, subTitle, description, titleSize 속성이 ModalContentProps 에서 새로운 ModalHeaderProps 로 옮겨집니다.

References

@sungik-choi sungik-choi added enhancement Issues or PR related to making existing features better component a11y Issue or PR related to accessibility labels Nov 18, 2022
@sungik-choi sungik-choi self-assigned this Nov 18, 2022
@changeset-bot
Copy link

changeset-bot bot commented Nov 18, 2022

🦋 Changeset detected

Latest commit: 3b2e1a4

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 2 packages
Name Type
@channel.io/bezier-react Minor
bezier-figma-plugin Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@codecov
Copy link

codecov bot commented Nov 18, 2022

Codecov Report

Base: 71.18% // Head: 71.72% // Increases project coverage by +0.53% 🎉

Coverage data is based on head (3b2e1a4) compared to base (cb677dd).
Patch coverage: 88.69% of modified lines in pull request are covered.

Additional details and impacted files
@@             Coverage Diff             @@
##           next-v1    #1036      +/-   ##
===========================================
+ Coverage    71.18%   71.72%   +0.53%     
===========================================
  Files          208      217       +9     
  Lines         2978     3063      +85     
  Branches       818      842      +24     
===========================================
+ Hits          2120     2197      +77     
- Misses         739      742       +3     
- Partials       119      124       +5     
Impacted Files Coverage Δ
...rc/components/Modals/ConfirmModal/ConfirmModal.tsx 0.00% <ø> (ø)
.../src/components/Modals/LegacyModal/ModalAction.tsx 100.00% <ø> (ø)
.../src/components/Modals/LegacyModal/ModalContext.ts 50.00% <ø> (ø)
...ages/bezier-react/src/components/Button/Button.tsx 92.18% <60.00%> (ø)
...-react/src/components/Modals/Modal/ModalHeader.tsx 78.94% <78.94%> (ø)
...src/components/Modals/LegacyModal/ModalContent.tsx 80.00% <80.00%> (ø)
...c/components/Modals/Modal/ModalAnimation.styled.ts 83.33% <83.33%> (ø)
...react/src/components/Modals/Modal/ModalContent.tsx 84.61% <83.33%> (+4.61%) ⬆️
.../src/components/Modals/LegacyModal/Modal.styled.ts 100.00% <100.00%> (ø)
...-react/src/components/Modals/LegacyModal/Modal.tsx 100.00% <100.00%> (ø)
... and 6 more

Help us with your feedback. Take ten seconds to tell us how you rate us. Have a feature suggestion? Share it here.

☔ View full report at Codecov.
📢 Do you have feedback about the report comment? Let us know in this issue.

@github-actions
Copy link
Contributor

github-actions bot commented Nov 18, 2022

Chromatic Report

🚀 Congratulations! Your build was successful!

@sungik-choi sungik-choi changed the title [WIP] Re-implement Modal component using Dialog in Radix UI Re-implement Modal component using Dialog in Radix UI Nov 23, 2022
@sungik-choi sungik-choi marked this pull request as draft November 23, 2022 11:47
@sungik-choi sungik-choi marked this pull request as ready for review November 23, 2022 11:47
Comment on lines 48 to 110
<Styled.HeadingGroup
role="group"
aria-roledescription="Heading group"
>
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

leftContent={leftContent}
rightContent={rightContent}
titleSize={titleSize}
hidden={hidden}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hidden prop의 경우에는 사용처에서 어떤 식으로 활용할 수 있는걸까요? (어떻게 사용해야 접근성 측면에서 올바른 사용법인지)
관련 링크가 있다면 알려주심 감사하겠습니다

Copy link
Contributor Author

@sungik-choi sungik-choi Dec 5, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

스크린샷 2022-12-05 오후 4 27 46

위 스크린샷같이 제목이 없는 케이스에서 사용되길 의도했습니다.

<Modal>
  <ModalContent>
    <ModalHeader title="할인 쿠폰 추가" hidden />
    <ModalBody>
      <FormControl>
        <FormLabel>할인 쿠폰 코드</FormLabel>
        <TextField placeholder="쿠폰 코드를 입력하세요" />
      </FormControl>
    </ModalBody>
  {/* ... */}
  </ModalContent>
</Modal>

Copy link
Contributor

@Dogdriip Dogdriip left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚀

@sungik-choi sungik-choi merged commit e23c54a into channel-io:next-v1 Dec 6, 2022
@sungik-choi sungik-choi deleted the feat/implement-modal branch December 6, 2022 11:20
@sungik-choi sungik-choi added the #reimplementation Issue or PR related to Reimplementation of existing components (#1105) label Jan 17, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
a11y Issue or PR related to accessibility enhancement Issues or PR related to making existing features better #reimplementation Issue or PR related to Reimplementation of existing components (#1105)
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants