diff --git a/src/components/Carousel/Carousel.stories.tsx b/src/components/Carousel/Carousel.stories.tsx index 879006d..252b9fc 100644 --- a/src/components/Carousel/Carousel.stories.tsx +++ b/src/components/Carousel/Carousel.stories.tsx @@ -2,16 +2,50 @@ import { Description, Subtitle, Title, Unstyled } from '@storybook/addon-docs/bl import type { Meta, StoryObj } from '@storybook/react'; import { useEffect, useRef, useState } from 'react'; import { Link } from '../Link'; -import { Carousel, type CarouselSlide } from './Carousel'; +import { + Carousel, + CarouselSingle, + CarouselSingleImage, + CarouselSingleLink, + type CarouselSlide, +} from './'; // @ts-expect-error: Ignore import of image files in Storybook +import image1_2x from './carousel-sample-images/image-1@2x.webp'; +// @ts-expect-error import image1 from './carousel-sample-images/image-1.webp'; // @ts-expect-error +import image2_2x from './carousel-sample-images/image-2@2x.webp'; +// @ts-expect-error import image2 from './carousel-sample-images/image-2.webp'; // @ts-expect-error +import image3_2x from './carousel-sample-images/image-3@2x.webp'; +// @ts-expect-error import image3 from './carousel-sample-images/image-3.webp'; // @ts-expect-error +import image4_2x from './carousel-sample-images/image-4@2x.webp'; +// @ts-expect-error import image4 from './carousel-sample-images/image-4.webp'; // @ts-expect-error +import image5_2x from './carousel-sample-images/image-5@2x.webp'; +// @ts-expect-error +import image5 from './carousel-sample-images/image-5.webp'; +// @ts-expect-error +import image6_2x from './carousel-sample-images/image-6@2x.webp'; +// @ts-expect-error +import image6 from './carousel-sample-images/image-6.webp'; +// @ts-expect-error +import image7_2x from './carousel-sample-images/image-7@2x.webp'; +// @ts-expect-error +import image7 from './carousel-sample-images/image-7.webp'; +// @ts-expect-error +import image8_2x from './carousel-sample-images/image-8@2x.webp'; +// @ts-expect-error +import image8 from './carousel-sample-images/image-8.webp'; +// @ts-expect-error +import image9_2x from './carousel-sample-images/image-9@2x.webp'; +// @ts-expect-error +import image9 from './carousel-sample-images/image-9.webp'; +// @ts-expect-error import carousel1024At2x from './docs/carousel-1024@2x.webp'; // @ts-expect-error import carousel1024 from './docs/carousel-1024.webp'; @@ -19,7 +53,6 @@ import carousel1024 from './docs/carousel-1024.webp'; const meta = { id: 'Component/DADS v2/Carousel', title: 'Component/カルーセル', - component: Carousel, tags: ['autodocs'], parameters: { docs: { @@ -45,9 +78,246 @@ const meta = {

コードスニペットにおける実際の動作は - NormalのStory + + Container (Multi Slides)のStory + を参照してください。

+ +

使い方

+

+ カルーセルコンポーネントは、マルチ(複数スライド)とシングル(1枚のみ)の2つの構成をサポートしています。 + 用途に応じて適切なコンポーネントを選択してください。 +

+ +

マルチ

+

+ 複数のスライドをナビゲーション付きで表示する場合は Carousel{' '} + コンポーネントを使用します。 +

+

+ 見出しを使用する場合は、Carouselchildren + に見出し要素を渡し、aria-labelledby + 属性でその見出しのIDを参照します。見出しを使用しない場合は、 + Carouselaria-label属性で直接ラベルを指定します。 +

+
+                
+                  {`// 見出しありの場合
+
+  
+
+
+// 見出しなしの場合
+
+`}
+                
+              
+

コンポーネント構成

+ + +

シングル

+

+ 単一のスライドのみを表示する場合は CarouselSingle{' '} + コンポーネントを使用します。 +

+

コンポーネント構成

+ +
+                
+                  {`import {
+  CarouselSingle,
+  CarouselSingleImage,
+  CarouselSingleLink,
+} from './Carousel';
+
+const MyKeyVisual = () => (
+  
+    
+      
+    
+  
+);`}
+                
+              
+ +

Props

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Propsデフォルト説明
+ slides + CarouselSlide[]-スライドデータの配列
+ currentIndex + number-現在表示中のスライドインデックス
+ unit + string'スライド'スライドの単位ラベル
+ isNormal + boolean-通常レイアウト(true)またはコンパクトレイアウト(false)の切り替え
+ innerRef + Ref<HTMLDivElement>-内側のdiv要素のref(ResizeObserver用)
+ onPrev + () => void-前へボタンのコールバック
+ onNext + () => void-次へボタンのコールバック
+ onStepSelect + (index: number) => void-ステップナビゲーションでのスライド選択コールバック
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
プロパティ説明
+ id + stringスライドの一意な識別子
+ label + stringスライドの表示ラベル
+ href + string?スライドのリンクURL(省略時はリンクなし)
+ target + string?リンクのtarget属性
+ image + CarouselImage画像情報(src, srcSet, alt, width, height)
+ + + + + + + + + + + + + + + + + +
Props説明
+ href + string? + リンクURL(省略時は<span>として描画) +
+ +

ブレークポイント

Carousel コンポーネントの規定のブレークポイントはコンテナ幅を基準とした1024pxです。ブレークポイントを変更するには、コンポーネント内に記述されているすべての @@ -74,13 +344,14 @@ const meta = { export default meta; -const slides: CarouselSlide[] = [ +const containerSlides: CarouselSlide[] = [ { id: 'slide-1', label: 'スライド1', href: '#link1', image: { src: image1, + srcSet: `${image1_2x} 2x`, alt: '学ぼうSDGs 偶数月の第3土曜日 環境保全の「自分事化」で学べるワークショップ開催', width: 696, height: 392, @@ -92,6 +363,7 @@ const slides: CarouselSlide[] = [ href: '#link2', image: { src: image2, + srcSet: `${image2_2x} 2x`, alt: '地産地消キャンペーン 県の名産品や体験イベントを楽しもう 期間:4月から7月までの毎週末開催!', width: 696, height: 392, @@ -103,6 +375,7 @@ const slides: CarouselSlide[] = [ href: '#link3', image: { src: image3, + srcSet: `${image3_2x} 2x`, alt: '令和 国立公園・歴史名所スタンプラリー まわろうよ 今年の週末 全国を 子どもたちの歴史理解促進と、日本各地の名産品・観光資源に親しむことを目的に、本イベントを全国開催しています!', width: 696, height: 392, @@ -114,6 +387,7 @@ const slides: CarouselSlide[] = [ href: '#link4', image: { src: image4, + srcSet: `${image4_2x} 2x`, alt: '合同健康診断のお知らせ ご自身とご家族の健康維持のため、定期的な健康診断の受診を。皆様の健やかな生活を応援します。 6月1日より受付開始 川海町および森林町にお住いの方が対象です', width: 696, height: 392, @@ -121,16 +395,67 @@ const slides: CarouselSlide[] = [ }, ]; +const containerSlidesWithoutLinks: CarouselSlide[] = containerSlides.map((slide) => ({ + ...slide, + href: undefined, +})); + +const keyVisualSlides: CarouselSlide[] = [ + { + id: 'slide-1', + label: 'スライド1', + image: { + src: image5, + srcSet: `${image5_2x} 2x`, + alt: '写真:デジタル公園の大木 - 太い幹があり、そこから伸びる多数の枝が絡み合うように広がっている。枝の間からは青空と緑の葉が見える。', + width: 696, + height: 392, + }, + }, + { + id: 'slide-2', + label: 'スライド2', + image: { + src: image6, + srcSet: `${image6_2x} 2x`, + alt: '写真:デジタル海水浴場 - 透明度が高いターコイズブルーの海と砂浜の風景。海は穏やかで、水面には細かな波紋が広がっている。', + width: 696, + height: 392, + }, + }, + { + id: 'slide-3', + label: 'スライド3', + image: { + src: image7, + srcSet: `${image7_2x} 2x`, + alt: '写真:デジタル中央通り沿いの花壇 - 白、黄色、紫、オレンジのパンジーが咲いている。背景には車のテールランプの赤い光が見える。', + width: 696, + height: 392, + }, + }, + { + id: 'slide-4', + label: 'スライド4', + image: { + src: image8, + srcSet: `${image8_2x} 2x`, + alt: '写真:デジタル県の夕焼け雲 - オレンジのグラデーションの空に、複数の濃い灰色の雲が浮かんでいる。下部には山々のシルエットが黒く連なっている。', + width: 696, + height: 392, + }, + }, +]; + /** - * コンテナ幅が1024pxの地点にブレークポイントを持つカルーセルの作例です。各スライドは横幅696pxです。 - * - * ブレークポイント前後の挙動を確認するには[NormalのStory](?path=/story/component-dads-v2-carousel--normal)を参照してください。 + * コンテナタイプのカルーセルです。見出しあり、リンクありの複数スライド構成(マルチ) * - * `Carousel`コンポーネントの規定のブレークポイントはコンテナ幅を基準とした1024pxです。ブレークポイントを変更するには、コンポーネント内に記述されているすべての`@[64rem]`の指定を同一の任意の値、またはTailwind CSSのテーマ設定で定義されたブレークポイントに置き換えてください。 + * コンテナ幅が1024pxの地点にブレークポイントを持ちます。各スライドは横幅696pxです。 */ -export const Normal: StoryObj = { +export const Container: StoryObj = { + name: 'Container (Multi Slides)', render: () => { - const CONTAINER_BREAKPOINT = 64; // rem + const CONTAINER_BREAKPOINT = 64; const [currentIndex, setCurrentIndex] = useState(0); const [isNormal, setIsNormal] = useState(false); const carouselInnerRef = useRef(null); @@ -141,7 +466,6 @@ export const Normal: StoryObj = { const observer = new ResizeObserver((entries) => { for (const entry of entries) { - // Get font size of root element to convert rem to px const fontSize = parseFloat(getComputedStyle(document.documentElement).fontSize); const widthPx = entry.borderBoxSize?.[0]?.inlineSize ?? entry.contentRect.width; setIsNormal(widthPx >= CONTAINER_BREAKPOINT * fontSize); @@ -153,22 +477,150 @@ export const Normal: StoryObj = { }, []); const handleNext = () => { - setCurrentIndex((index) => (index + 1) % slides.length); + setCurrentIndex((index) => (index + 1) % containerSlides.length); }; const handlePrev = () => { - setCurrentIndex((index) => (index + slides.length - 1) % slides.length); + setCurrentIndex((index) => (index + containerSlides.length - 1) % containerSlides.length); }; const handleSelect = (index: number) => { - setCurrentIndex((index + slides.length) % slides.length); + setCurrentIndex((index + containerSlides.length) % containerSlides.length); + }; + + return ( + +

+ + ); + }, +}; + +/** + * コンテナタイプのカルーセルです。見出しあり、リンクなしの複数スライド構成(マルチ) + */ +export const ContainerWithoutLinks: StoryObj = { + name: 'Container (Multi Slides without Links)', + render: () => { + const CONTAINER_BREAKPOINT = 64; + const [currentIndex, setCurrentIndex] = useState(0); + const [isNormal, setIsNormal] = useState(false); + const carouselInnerRef = useRef(null); + + useEffect(() => { + const element = carouselInnerRef.current; + if (!element) return; + + const observer = new ResizeObserver((entries) => { + for (const entry of entries) { + const fontSize = parseFloat(getComputedStyle(document.documentElement).fontSize); + const widthPx = entry.borderBoxSize?.[0]?.inlineSize ?? entry.contentRect.width; + setIsNormal(widthPx >= CONTAINER_BREAKPOINT * fontSize); + } + }); + + observer.observe(element, { box: 'border-box' }); + return () => observer.disconnect(); + }, []); + + const handleNext = () => { + setCurrentIndex((index) => (index + 1) % containerSlidesWithoutLinks.length); + }; + + const handlePrev = () => { + setCurrentIndex( + (index) => + (index + containerSlidesWithoutLinks.length - 1) % containerSlidesWithoutLinks.length, + ); + }; + + const handleSelect = (index: number) => { + setCurrentIndex( + (index + containerSlidesWithoutLinks.length) % containerSlidesWithoutLinks.length, + ); + }; + + return ( + + + + ); + }, +}; + +/** + * 打ち出しタイプのカルーセルです。見出しなし、リンクなしの複数スライド構成(マルチ) + */ +export const KeyVisual: StoryObj = { + name: 'Key Visual (Multi Slides without Link)', + render: () => { + const CONTAINER_BREAKPOINT = 64; + const [currentIndex, setCurrentIndex] = useState(0); + const [isNormal, setIsNormal] = useState(false); + const carouselInnerRef = useRef(null); + + useEffect(() => { + const element = carouselInnerRef.current; + if (!element) return; + + const observer = new ResizeObserver((entries) => { + for (const entry of entries) { + const fontSize = parseFloat(getComputedStyle(document.documentElement).fontSize); + const widthPx = entry.borderBoxSize?.[0]?.inlineSize ?? entry.contentRect.width; + setIsNormal(widthPx >= CONTAINER_BREAKPOINT * fontSize); + } + }); + + observer.observe(element, { box: 'border-box' }); + return () => observer.disconnect(); + }, []); + + const handleNext = () => { + setCurrentIndex((index) => (index + 1) % keyVisualSlides.length); + }; + + const handlePrev = () => { + setCurrentIndex((index) => (index + keyVisualSlides.length - 1) % keyVisualSlides.length); + }; + + const handleSelect = (index: number) => { + setCurrentIndex((index + keyVisualSlides.length) % keyVisualSlides.length); }; return ( { + return ( + + + + + + ); + }, +}; diff --git a/src/components/Carousel/Carousel.tsx b/src/components/Carousel/Carousel.tsx index 45a7ff3..c1e76fa 100644 --- a/src/components/Carousel/Carousel.tsx +++ b/src/components/Carousel/Carousel.tsx @@ -6,25 +6,18 @@ import { Disclosure, DisclosureSummary } from '../Disclosure'; type CarouselImage = { src: string; + srcSet?: string; alt: string; width: number; height: number; }; -type CarouselImageSource = { - srcSet: string; - width?: number; - height?: number; - media: string; -}; - export type CarouselSlide = { id: string; label: string; - href: string; + href?: string; target?: string; image: CarouselImage; - imageSources?: CarouselImageSource[]; }; // Carousel Sub Components @@ -163,34 +156,24 @@ const CarouselPageNav = (props: CarouselPageNavProps) => { type CarouselBackgroundLayerProps = { className?: string; image: CarouselImage; - imageSources?: CarouselImageSource[]; }; const CarouselBackgroundLayer = (props: CarouselBackgroundLayerProps) => { - const { className, image, imageSources } = props; + const { className, image } = props; + return (
- - {imageSources?.map((source) => ( - - ))} - - +
); @@ -240,38 +223,30 @@ const CarouselExpandList = (props: CarouselExpandListProps) => { {slide.label}
- - {slide.imageSources?.map((source) => ( - - ))} - {slide.image.alt} - + {slide.image.alt}
- +
); @@ -324,35 +299,27 @@ const CarouselPanelArea = (props: CarouselPanelAreaProps) => { {mainLabel}
- - {currentSlide.imageSources?.map((source) => ( - - ))} - {currentSlide.image.alt} - + {currentSlide.image.alt}
@@ -374,24 +341,14 @@ const CarouselPanelArea = (props: CarouselPanelAreaProps) => { focus-visible:outline focus-visible:outline-4 focus-visible:outline-black focus-visible:outline-offset-[calc(2/16*1rem)] focus-visible:rounded-[calc(4/16*1rem)] focus-visible:ring-[calc(2/16*1rem)] focus-visible:ring-yellow-300 `} > - - {nextSlide.imageSources?.map((source) => ( - - ))} - - + 次の{unit} @@ -400,10 +357,7 @@ const CarouselPanelArea = (props: CarouselPanelAreaProps) => {

- +
{ group-has-[[open]]/carousel:!hidden @[64rem]:block `} > - +
); @@ -435,6 +389,7 @@ export type CarouselProps = ComponentProps<'section'> & { export const Carousel = (props: CarouselProps) => { const { className, + children, slides, currentIndex, unit = 'スライド', @@ -461,11 +416,12 @@ export const Carousel = (props: CarouselProps) => {
+ {children} {
); }; + diff --git a/src/components/Carousel/CarouselSingle.tsx b/src/components/Carousel/CarouselSingle.tsx new file mode 100644 index 0000000..e65fd3e --- /dev/null +++ b/src/components/Carousel/CarouselSingle.tsx @@ -0,0 +1,53 @@ +import type { ComponentProps } from 'react'; + +export type CarouselSingleProps = ComponentProps<'div'>; + +export const CarouselSingle = (props: CarouselSingleProps) => { + const { className, children, ...rest } = props; + return ( +
+ {children} +
+ ); +}; + +export type CarouselSingleLinkProps = ComponentProps<'a'>; + +export const CarouselSingleLink = (props: CarouselSingleLinkProps) => { + const { className, href, children, ...rest } = props; + + if (href) { + return ( + + {children} + + ); + } + + return {children}; +}; + +export type CarouselSingleImageProps = ComponentProps<'img'>; + +export const CarouselSingleImage = (props: CarouselSingleImageProps) => { + const { className, alt, ...rest } = props; + return ( + {alt} + ); +}; diff --git a/src/components/Carousel/carousel-sample-images/image-1.webp b/src/components/Carousel/carousel-sample-images/image-1.webp index f535a5d..473931f 100644 Binary files a/src/components/Carousel/carousel-sample-images/image-1.webp and b/src/components/Carousel/carousel-sample-images/image-1.webp differ diff --git a/src/components/Carousel/carousel-sample-images/image-1@2x.webp b/src/components/Carousel/carousel-sample-images/image-1@2x.webp new file mode 100644 index 0000000..f4dcaaf Binary files /dev/null and b/src/components/Carousel/carousel-sample-images/image-1@2x.webp differ diff --git a/src/components/Carousel/carousel-sample-images/image-2.webp b/src/components/Carousel/carousel-sample-images/image-2.webp index 34aa780..b832dcb 100644 Binary files a/src/components/Carousel/carousel-sample-images/image-2.webp and b/src/components/Carousel/carousel-sample-images/image-2.webp differ diff --git a/src/components/Carousel/carousel-sample-images/image-2@2x.webp b/src/components/Carousel/carousel-sample-images/image-2@2x.webp new file mode 100644 index 0000000..48f9678 Binary files /dev/null and b/src/components/Carousel/carousel-sample-images/image-2@2x.webp differ diff --git a/src/components/Carousel/carousel-sample-images/image-3.webp b/src/components/Carousel/carousel-sample-images/image-3.webp index c0e436d..ae4cdae 100644 Binary files a/src/components/Carousel/carousel-sample-images/image-3.webp and b/src/components/Carousel/carousel-sample-images/image-3.webp differ diff --git a/src/components/Carousel/carousel-sample-images/image-3@2x.webp b/src/components/Carousel/carousel-sample-images/image-3@2x.webp new file mode 100644 index 0000000..83531c8 Binary files /dev/null and b/src/components/Carousel/carousel-sample-images/image-3@2x.webp differ diff --git a/src/components/Carousel/carousel-sample-images/image-4.webp b/src/components/Carousel/carousel-sample-images/image-4.webp index 2e22f6a..efce07b 100644 Binary files a/src/components/Carousel/carousel-sample-images/image-4.webp and b/src/components/Carousel/carousel-sample-images/image-4.webp differ diff --git a/src/components/Carousel/carousel-sample-images/image-4@2x.webp b/src/components/Carousel/carousel-sample-images/image-4@2x.webp new file mode 100644 index 0000000..30191d5 Binary files /dev/null and b/src/components/Carousel/carousel-sample-images/image-4@2x.webp differ diff --git a/src/components/Carousel/carousel-sample-images/image-5.webp b/src/components/Carousel/carousel-sample-images/image-5.webp new file mode 100644 index 0000000..233ddff Binary files /dev/null and b/src/components/Carousel/carousel-sample-images/image-5.webp differ diff --git a/src/components/Carousel/carousel-sample-images/image-5@2x.webp b/src/components/Carousel/carousel-sample-images/image-5@2x.webp new file mode 100644 index 0000000..3680c5d Binary files /dev/null and b/src/components/Carousel/carousel-sample-images/image-5@2x.webp differ diff --git a/src/components/Carousel/carousel-sample-images/image-6.webp b/src/components/Carousel/carousel-sample-images/image-6.webp new file mode 100644 index 0000000..c418381 Binary files /dev/null and b/src/components/Carousel/carousel-sample-images/image-6.webp differ diff --git a/src/components/Carousel/carousel-sample-images/image-6@2x.webp b/src/components/Carousel/carousel-sample-images/image-6@2x.webp new file mode 100644 index 0000000..221c499 Binary files /dev/null and b/src/components/Carousel/carousel-sample-images/image-6@2x.webp differ diff --git a/src/components/Carousel/carousel-sample-images/image-7.webp b/src/components/Carousel/carousel-sample-images/image-7.webp new file mode 100644 index 0000000..541a727 Binary files /dev/null and b/src/components/Carousel/carousel-sample-images/image-7.webp differ diff --git a/src/components/Carousel/carousel-sample-images/image-7@2x.webp b/src/components/Carousel/carousel-sample-images/image-7@2x.webp new file mode 100644 index 0000000..4989368 Binary files /dev/null and b/src/components/Carousel/carousel-sample-images/image-7@2x.webp differ diff --git a/src/components/Carousel/carousel-sample-images/image-8.webp b/src/components/Carousel/carousel-sample-images/image-8.webp new file mode 100644 index 0000000..90d2b98 Binary files /dev/null and b/src/components/Carousel/carousel-sample-images/image-8.webp differ diff --git a/src/components/Carousel/carousel-sample-images/image-8@2x.webp b/src/components/Carousel/carousel-sample-images/image-8@2x.webp new file mode 100644 index 0000000..d0a92cb Binary files /dev/null and b/src/components/Carousel/carousel-sample-images/image-8@2x.webp differ diff --git a/src/components/Carousel/carousel-sample-images/image-9.webp b/src/components/Carousel/carousel-sample-images/image-9.webp new file mode 100644 index 0000000..155675f Binary files /dev/null and b/src/components/Carousel/carousel-sample-images/image-9.webp differ diff --git a/src/components/Carousel/carousel-sample-images/image-9@2x.webp b/src/components/Carousel/carousel-sample-images/image-9@2x.webp new file mode 100644 index 0000000..8f27025 Binary files /dev/null and b/src/components/Carousel/carousel-sample-images/image-9@2x.webp differ diff --git a/src/components/Carousel/index.ts b/src/components/Carousel/index.ts index bae7d1a..b03f1b7 100644 --- a/src/components/Carousel/index.ts +++ b/src/components/Carousel/index.ts @@ -1,2 +1,8 @@ export type { CarouselProps, CarouselSlide } from './Carousel'; export { Carousel } from './Carousel'; +export type { + CarouselSingleImageProps, + CarouselSingleLinkProps, + CarouselSingleProps, +} from './CarouselSingle'; +export { CarouselSingle, CarouselSingleImage, CarouselSingleLink } from './CarouselSingle'; diff --git a/src/components/index.ts b/src/components/index.ts index 71c7acb..7c560ee 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -7,7 +7,7 @@ export { export { Blockquote } from './Blockquote'; export { BreadcrumbItem, BreadcrumbList, Breadcrumbs, BreadcrumbsLabel } from './Breadcrumbs'; export { Button, buttonBaseStyle, buttonSizeStyle, buttonVariantStyle } from './Button'; -export { Carousel } from './Carousel'; +export { Carousel, CarouselSingle, CarouselSingleImage, CarouselSingleLink } from './Carousel'; export { Checkbox } from './Checkbox'; export { ChipLabel } from './ChipLabel'; export * from './DatePicker'; diff --git a/src/index.ts b/src/index.ts index 0c48ebf..86621d4 100644 --- a/src/index.ts +++ b/src/index.ts @@ -11,6 +11,9 @@ export { buttonSizeStyle, buttonVariantStyle, Carousel, + CarouselSingle, + CarouselSingleImage, + CarouselSingleLink, Checkbox, ChipLabel, DatePicker,