Radix UIを利用してエディターのコンポーネントを実装した話

  • URLをコピーしました!

こんにちは。2023年の7月に中途で開発本部に入社しました。フロントエンドエンジニアの夛田(@unachang113)です

昨年12月にリリースされたPR TIMESの新エディターの開発でRadix UIを使用したので、
本エントリーではどのように活用したかを紹介します。

目次

Radix UIとは

Radix UIはReact製のUIライブラリです。現在はThemesとPrimitivesの2つを提供されています。
Themesの場合、styleが予め設定されていて、自分たちが実装したいUIとフィットしない場合が多いため、PR TIMESではheadlessUIのPrimitivesの方を採用しています。

有名なサービスだとvercelやcodesandboxでもRadix UIを採用しているようです。

弊社では元々ReachUIを使用していましたが、PR TIMES本体で使用しているReactのバージョンを18にアップデートしようとしたタイミングで、ReachUIの特定のコンポーネントがReact18に対応していなかった経緯から、2022年の夏頃よりRadix UIを採用し、デザインシステムのコンポーネントやプロダクトのコンポーネント内でも幅広く利用されています。

どのように利用したか

利用を決めた経緯

12/19にリリースされた新エディターのUIは、以前の旧エディターのUIとデザインが全く異なっており、既存で運用しているデザインシステムが全て流用できない状態でした。そこで、既存のデザインシステムでも導入実績のあったRadix UIを採用した結果、うまく今回の開発にハマる結果となりました。

旧エディター(before)

新エディター(after)

利用箇所

主にエディターのコンポーネント全般で利用しました。

具体的には、アラートのモーダル、目的選択のプルダウンメニュー、ツールバーのPopOverメニューなど、使用用途は多岐にわたります。

alertはradix UIのDialogを使用

メニューの表示はradix UIのPopoverを使用

どうやって組み込んでいったか

radix-uiをwrapしたコンポーネントを作成することで組み込みを行っていきました。

RadixUIのパーツをwrapした共通コンポーネントを作成することにより、
万が一UIライブラリを変更することになった場合もコードが捨てやすくなるように設計をしています。

実装例(Popoverの場合)

import {css} from '@emotion/react';
import * as RadixPopover from '@radix-ui/react-popover';
import type {ReactNode} from 'react';

const defaultSideOffset = 4;

type SideType = 'top' | 'right' | 'bottom' | 'left';
type AlignType = 'start' | 'center' | 'end';

type Props = {
  /** popoverを表示させるトリガー */
  readonly trigger: ReactNode;
  /** popoverの中身 */
  readonly children: ReactNode;
  /** popoverを開いた状態にするか否か */
  readonly isOpen?: boolean;
  /** popoverをopenの状態を切り替えす関数 */
  readonly setIsOpen?: (isOpen: boolean) => void;
  /** popoverの外側がクリックされたときのイベント */
  readonly onInteractOutside?: () => void;
  /** popoverを表示させる位置(横軸 | default: top) */
  readonly side?: SideType;
  /** popoverを表示させる位置(縦軸 | default: start) */
  readonly align?: AlignType;
  /** triggerとpopoverのx軸の距離(px) */
  readonly sideOffset?: number;
  /** triggerとpopoverのy軸の距離(px) */
  readonly alignOffset?: number;
  /** popoverを最初からopenの状態にするか */
  readonly isDefaultOpen?: boolean;
};

export function Popover({
  trigger,
  children,
  isOpen,
  setIsOpen,
  onInteractOutside,
  side = 'top',
  align = 'start',
  alignOffset = 0,
  sideOffset = defaultSideOffset,
  isDefaultOpen = false,
  verticalSize = 'medium',
}: Props) {
  return (
    <RadixPopover.Root
      open={isOpen}
      defaultOpen={isDefaultOpen}
      onOpenChange={setIsOpen}
    >
      <RadixPopover.Trigger asChild>{trigger}</RadixPopover.Trigger>
        <RadixPopover.Portal>
          <RadixPopover.Content
            css={[styles.popover, verticalSizeThemes[verticalSize]]}
            side={side}
            align={align}
            alignOffset={alignOffset}
            sideOffset={sideOffset}
            onInteractOutside={onInteractOutside}
          >
            {children}
          </RadixPopover.Content>
        </RadixPopover.Portal>
    </RadixPopover.Root>
  );
}

const styles = {
	...
};
呼び出し元(ex. ドキュメント編集用のpopover)
import {Popover} from './components/popover';
import {
  ImageGalleryIcon,
  ImageIcon,
  ImageWithTextIcon,
  LargeImageIcon,
  MediumImageIcon,
} from './icons/image';

export function DocumentPopover() {
	return (
    <Popover
      trigger={
        <button
          type='button'
          css={styles.trigger}
          aria-label='ドキュメントのメニューを開く'
        >
          <MenuIcon aria-hidden='true' />
        </button>
      }
      side='right'
      align='start'
    >
      <ul>
        <li key='downloadFile'>
          <a
            href={filePath(fileName)}
            css={styles.button}
            target='_blank'
            rel='noreferrer'
          >
            <span css={styles.textWrapper}>
              <DownloadIcon />
              <span css={styles.buttonText}>ファイルをダウンロード</span>
            </span>
          </a>
        </li>
      </ul>		
		</Popover>
	);
}

実際使ってみてどうだったか

CSSや独自のロジックの作成に注力できるため、開発速度が上がった

コアな機能のロジックはライブラリが担保しているため、1からUIコンポーネントを作成する場合に比べ、CSSの作成やプロジェクト独自のロジックの作成に注力できることができ、結果として共通のUIコンポーネントを手早く作ることができました。

正直これがなかったらエディターが無事にリリースできていただろうか…という気持ちになる程度には個人的に助かっていました…

アクセシビリティを担保できるのが良かった

Radix UIはariaやrole属性、フォーカス管理、キーボードナビゲーションにデフォルトで対応しており、Radix UIを利用すればある程度アクセシビリティが担保したコンポーネントを提供が可能です。

アクセシビリティ対応に関してはは社内だとまだ実装にムラがあり、関心のある人が少しずつ始めているような状態なので、UIライブラリである程度担保できるのは品質を一定に保つ事にも繋がるので良いなと感じました。

Radix UIで対応しなかったコンポーネントは?

Form周り(input, textarea)はまだpreview版だったこともあり、RadixUIを採用せず、自前でシンプルに実装しました。
また、checkboxに関しても特に大幅なカスタマイズが必要なく、
CSSのスタイリングで要件を満たすことができたので、通常のcheckboxタグを用いて実装しています。

まとめ

Radix UIを採用することにより、素早くコンポーネントを作成することができました。
この記事がRadixUIを導入するきっかけになれば幸いです。

We are hiring!

フロントエンドはもちろん、各種ポジションで採用を行っています!

  • URLをコピーしました!

この記事を書いた人

株式会社PR TIMES 開発部 フロントエンドエンジニア

目次