22
8

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

Qiita株式会社Advent Calendar 2021

Day 13

react-slickでカルーセルをコントロールするUIを改造してアクセシビリティ対応する方法

Last updated at Posted at 2021-12-12

はじめに

Qiita株式会社 アドベントカレンダー13日目は、デザインチームの@gillyが担当します!
よろしくお願いします!

最近アクセシビリティをちょっとずつ勉強しています。
今回はリニューアルしたアドベントカレンダーでも使用しているカルーセルで行った対応を記事にまとめたいと思います。

react-slickとは

react-slickとはjQueryプラグインのSlickを、Reactでも使用できるようにしたものです。
スライダーやカルーセルを簡単に実装することができます。

今回作りたいもの

Qiita Advent Calendarのトップページのスポンサーカレンダーのカルーセル
スクリーンショット 2021-12-11 11.08.17.png
該当ページはこちらから確認できます
  • 主な要件
    • 複数枚のバナーを掲載したカルーセル
    • ユーザーが任意で左右に動かせる
      • タッチジェスチャで左右に動かせる
      • Previous, Next のボタンを配置する
    • ユーザーが任意でカルーセルを停止・再生できる

react-slickの導入

私はyarnでインストールしました。
npmが良い方はnpmのドキュメントをご参照ください。

パッケージのインストール

yarn add react-slick

cssのインストール

yarn add slick-carousel

// Import css files
import "slick-carousel/slick/slick.css";
import "slick-carousel/slick/slick-theme.css";

実装

最終的にどんなコードになったのかを、最初にお見せします。

前提

前提としてQiitaではこのようにしてあります。

  • スタイルを当てるときは、CSS in JS で記述できるEmotionを採用しています。
  • react-slickで依存関係にある slick-carousel のcssのimportは別で行っているため、ここに記載してありません。
    • import "slick-carousel/slick/slick.css";
    • import "slick-carousel/slick/slick-theme.css";
  • アイコンはFont Awesome 4を使用しています。
  • React Hooksに寄せていっているので、書き方がreact-slickのドキュメントと異なっている箇所があります。

コード

carousel.jsx
import { css } from '@emotion/react'
import React, { useRef, useState } from 'react'
import Slider, { CustomArrowProps } from 'react-slick'

type Props = {
  carouselItems: {
    url: string
    image: string
    alt: string
  }[]
}

export const Carousel = (props: Props) => {
  const [isStopped, setIsStopped] = useState(false)
  const sliderRef = useRef<Slider>(null)

  const slickPlay = () => {
    sliderRef.current?.slickPlay()
    setIsStopped(false)
  }

  const slickPause = () => {
    sliderRef.current?.slickPause()
    setIsStopped(true)
  }

  const handleClickSliderPrev = () => {
    if (sliderRef?.current) {
      sliderRef.current.slickPrev()
    }
  }

  const handleClickSliderNext = () => {
    if (sliderRef?.current) {
      sliderRef.current.slickNext()
    }
  }

  const sliderSettings = {
    autoplay: true,
    autoplaySpeed: 3000,
    arrows: false,
    cssEase: 'linear',
    centerMode: true,
    dots: true,
    infinite: true,
    pauseOnFocus: true,
    pauseOnHover: true,
    responsive: [
      {
        breakpoint: 480,
        settings: {
          adaptiveHeight: true,
        },
      },
    ],
    speed: 500,
    variableWidth: true,
    appendDots: dots => (
      <div css={carouselControlerStyle}>
        <SlickArrowLeft onClick={handleClickSliderPrev} />
        <ul css={carouselDotsStyle}>{dots}</ul>
        {isStopped ? (
          <button onClick={slickPlay} css={CarouselOperationButton}>
            <span className="fa fa-fw fa-play" />
          </button>
        ) : (
          <button onClick={slickPause} css={CarouselOperationButton}>
            <span className="fa fa-fw fa-pause" />
          </button>
        )}
        <SlickArrowRight onClick={handleClickSliderNext} />
      </div>
    ),
  }

  return (
    <div>
      <Slider ref={sliderRef} {...sliderSettings}>
        {props.carouselItems.map((carouselItem, index) => (
          <div css={bunnerCalouselLinkStyle} key={`carousel-item-${index}`}>
            <a href={carouselItem.url}>
              <img
                src={carouselItem.image}
                alt={carouselItem.alt}
                width="320px"
                height="100px"
                css={{ margin: 'auto' }}
              />
            </a>
          </div>
        ))}
      </Slider>
    </div>
  )
}

const bunnerCalouselLinkStyle = css({
  padding: `0 8px`,
})

const SlickArrowLeft = ({ onClick }: CustomArrowProps): JSX.Element => {
  return (
    <button css={prevArrowStyle} onClick={onClick}>
      <span className="fa fa-fw fa-chevron-left"></span>
    </button>
  )
}

const SlickArrowRight = ({ onClick }: CustomArrowProps): JSX.Element => {
  return (
    <button css={nextArrowStyle} onClick={onClick}>
      <span className="fa fa-fw fa-chevron-right"></span>
    </button>
  )
}

const carouselControlerStyle = css({
  display: 'flex',
  justifyContent: 'center',
  alignItems: 'center',
  position: 'initial',
})

const prevArrowStyle = css({
  color: 'rgba(0, 0, 0, 0.87)',
  cursor: 'pointer',
  padding: 8,
})

const nextArrowStyle = css({
  color: 'rgba(0, 0, 0, 0.6)',
  cursor: 'pointer',
  padding: 8,
})

const carouselDotsStyle = css({
  display: 'flex',
  margin: '0 8px',
  li: {
    alignItems: 'center',
    display: 'flex',
    height: 8,
    justifyContent: 'center',
    margin: `0 8px`,
    width: 8,
    '@media (max-width: 769px)':{
      margin: `0 4px`,
    },
  },
  'li button': {
    alignItems: 'center',
    display: 'flex',
    height: 8,
    margin: 0,
    width: 8,
  },
  'li button:focus::before': {
    color: 'rgba(0, 0, 0, 0.87)',
  },
  'li button::before': {
    color: '#C2C2C3',
    height: 8,
    lineHeight: 1,
    opacity: 1.0,
    width: 8,
  },
  'li.slick-active button::before': {
    color: '#8A8B8B',
  },
  '@media (max-width: 479px)': {
    display: 'none',
  }
})

const CarouselOperationButton = css({
  background: 'none',
  border: 'none',
  color: 'rgba(0, 0, 0, 0.6)',
  cursor: 'pointer',
  padding: 8,
})

解説

今回の解説はアクセシビリティに対応した箇所を中心に行います。装飾で使ったreact-slickの設定は解説しませんのでご了承ください。

【解説1】カルーセルをコントロールするUIの下準備

この部分

appendDots を応用します。
Ref https://react-slick.neostack.com/docs/api/#appendDots

Custom dots templates. Works same as customPaging

とあるように、カルーセルの枚数を表示するドットを拡張できるようにするオプションです。

Previous, Next のボタンとドットと再生・停止ボタンを一列に並べられるようにマークアップをしています。

  const sliderSettings = {
    appendDots: dots => (
      <div css={carouselControlerStyle}>
        <SlickArrowLeft onClick={handleClickSliderPrev} /> //Previpousボタン
        <ul css={carouselDotsStyle}>{dots}</ul> //ドット
        {isStopped ? (
          <button onClick={slickPlay} css={CarouselOperationButton}>
            <span className="fa fa-fw fa-play" />
          </button>
        ) : (
          <button onClick={slickPause} css={CarouselOperationButton}>
            <span className="fa fa-fw fa-pause" />
          </button>
        )} //停止・再生のボタン
        <SlickArrowRight onClick={handleClickSliderNext} /> //Neaxtボタン
      </div>
    ),
}

【解説2】タップ操作で左右に動かせる

これは特に何もせず、react-slickではデフォルトで実装可能です。

https://react-slick.neostack.com/docs/api#swipe に書いてあるように、 Default: true になっています。
もし、タップ操作やドラックで操作出来ないようにしたい場合は false にすれば止まります。

【解説3】Previous, Next のボタンを配置する

https://react-slick.neostack.com/docs/example/previous-next-methods を応用します。

react-slickのデフォルトの Previous, Next のボタンは、カルーセルのすぐ横に表示されます。
今回はバナー下のdotsを挟むように配置したいため、デフォルトのPrevious, Next のボタンを非表示にして任意のアイコンをボタンとして利用します。

前後の状態を参照できるようにする

  const sliderRef = useRef<Slider>(null)

  const handleClickSliderPrev = () => {
    if (sliderRef?.current) {
      sliderRef.current.slickPrev()
    }
  }

  const handleClickSliderNext = () => {
    if (sliderRef?.current) {
      sliderRef.current.slickNext()
    }
  }

react-slickのsettingsの該当オプション

  const sliderSettings = {
    arrows: false,
    appendDots: dots => (
      <div css={carouselControlerStyle}>
        <SlickArrowLeft onClick={handleClickSliderPrev} />
        <SlickArrowRight onClick={handleClickSliderNext} />
      </div>
    ),
  }

ボタンを <, > アイコンとして配置する

const SlickArrowLeft = ({ onClick }: CustomArrowProps): JSX.Element => {
  return (
    <button css={prevArrowStyle} onClick={onClick}>
      <span className="fa fa-fw fa-chevron-left"></span>
    </button>
  )
}

const SlickArrowRight = ({ onClick }: CustomArrowProps): JSX.Element => {
  return (
    <button css={nextArrowStyle} onClick={onClick}>
      <span className="fa fa-fw fa-chevron-right"></span>
    </button>
  )
}

【解説4】ユーザーが任意でカルーセルを停止・再生できる

達成基準 2.2.2: 一時停止、停止、非表示を理解するに下記のように記載があります。

動きのある、点滅している、又はスクロールしている情報が、(1) 自動的に開始し、(2) 5 秒よりも長く継続し、かつ、(3) その他のコンテンツと並行して提示される場合、利用者がそれらを一時停止、停止、又は非表示にすることのできるメカニズムがある。

今回はユーザーが任意に一時停止できるようにしました。
また、ユーザーがバナーにhoverしている間も停止するようにしています。

停止の状態を取得できるようにする

  const [isStopped, setIsStopped] = useState(false)

  const slickPlay = () => {
    sliderRef.current?.slickPlay()
    setIsStopped(false)
  }

  const slickPause = () => {
    sliderRef.current?.slickPause()
    setIsStopped(true)
  }

react-slickのsettingsの該当オプション

  const sliderSettings = {
    autoplay: true,
    autoplaySpeed: 3000,
    pauseOnFocus: true,
    pauseOnHover: true,
    appendDots: dots => (
      <div css={carouselControlerStyle}>
        {isStopped ? (
          <button onClick={slickPlay} css={CarouselOperationButton}>
            <span className="fa fa-fw fa-play" />
          </button>
        ) : (
          <button onClick={slickPause} css={CarouselOperationButton}>
            <span className="fa fa-fw fa-pause" />
          </button>
        )}
      </div>
    ),
  }

まとめ

要件としていた下記について解説いたしました。

  • ユーザーが任意で左右に動かせる
    • タッチジェスチャで左右に動かせる
    • Previous, Next のボタンを配置する
  • ユーザーが任意でカルーセルを停止・再生できる

Qiitaのデザイナー達は、少しずつアクセシビリティについて学びながらサイトに反映していきたいと考えています。
たくさんの人がQiitaをより快適にご利用いただけるよう、日々精進してまいります。

ご意見・ご要望がございましたら、ぜひFooterのご意見や、Discussions · increments/qiita-discussionsにてお聞かせください :pray:

おわりに

ここまで読んでいただきありがとうございました。

明日の Qiita株式会社 Advent Calendar 2021 は、@kyntkが担当します! :santa:

22
8
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
22
8

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?