はじめに
Qiita株式会社 アドベントカレンダー13日目は、デザインチームの@gillyが担当します!
よろしくお願いします!
最近アクセシビリティをちょっとずつ勉強しています。
今回はリニューアルしたアドベントカレンダーでも使用しているカルーセルで行った対応を記事にまとめたいと思います。
react-slickとは
react-slickとはjQueryプラグインのSlickを、Reactでも使用できるようにしたものです。
スライダーやカルーセルを簡単に実装することができます。
- GitHub
- yarn
- npm
- ドキュメント
今回作りたいもの
Qiita Advent Calendarのトップページのスポンサーカレンダーのカルーセル |
---|
該当ページはこちらから確認できます |
- 主な要件
- 複数枚のバナーを掲載したカルーセル
- ユーザーが任意で左右に動かせる
- タッチジェスチャで左右に動かせる
-
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のドキュメントと異なっている箇所があります。
コード
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にてお聞かせください
おわりに
ここまで読んでいただきありがとうございました。
明日の Qiita株式会社 Advent Calendar 2021 は、@kyntkが担当します!