@charcoal-ui/react 6.0.0-rc.4 → 6.0.0-rc.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/_lib/useIsomorphicLayoutEffect.d.ts +6 -0
- package/dist/_lib/useIsomorphicLayoutEffect.d.ts.map +1 -0
- package/dist/components/Carousel/index.d.ts +2 -6
- package/dist/components/Carousel/index.d.ts.map +1 -1
- package/dist/components/Carousel/useCarouselScroller.d.ts.map +1 -1
- package/dist/components/Modal/index.d.ts.map +1 -1
- package/dist/components/Modal/useTransitionPresence.d.ts +19 -0
- package/dist/components/Modal/useTransitionPresence.d.ts.map +1 -0
- package/dist/index.cjs +2 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.css +124 -49
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/layered.css +124 -49
- package/dist/layered.css.map +1 -1
- package/package.json +5 -6
- package/src/__tests__/token-v1-compat.test.ts +0 -1
- package/src/_lib/useIsomorphicLayoutEffect.ts +7 -0
- package/src/components/Carousel/MIGRATION.md +39 -39
- package/src/components/Carousel/__snapshots__/index.css.snap +63 -39
- package/src/components/Carousel/index.browser.test.tsx +102 -0
- package/src/components/Carousel/index.css +68 -46
- package/src/components/Carousel/index.story.tsx +87 -68
- package/src/components/Carousel/index.test.tsx +120 -47
- package/src/components/Carousel/index.tsx +30 -14
- package/src/components/Carousel/useCarouselScroller.ts +8 -12
- package/src/components/FieldLabel/__snapshots__/index.css.snap +1 -1
- package/src/components/FieldLabel/index.css +1 -1
- package/src/components/IconButton/__snapshots__/index.css.snap +1 -1
- package/src/components/IconButton/index.css +1 -1
- package/src/components/Modal/__snapshots__/index.css.snap +32 -1
- package/src/components/Modal/index.css +34 -0
- package/src/components/Modal/index.test.tsx +200 -0
- package/src/components/Modal/index.tsx +48 -78
- package/src/components/Modal/useTransitionPresence.ts +95 -0
- package/src/index.ts +0 -1
|
@@ -12,10 +12,19 @@
|
|
|
12
12
|
position: relative;
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
+
/* フォーカスリングは scroller ではなく viewport に描く。scroller に置くと
|
|
16
|
+
hasGradient の mask(mask-clip: border-box)で外側の box-shadow ごと消えるため。 */
|
|
17
|
+
|
|
18
|
+
.charcoal-carousel__viewport[data-focus-visible] {
|
|
19
|
+
box-shadow: 0 0 0 4px rgba(0, 150, 250, 0.32);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/* スライド間隔は所有しない(sandbox 同様、利用者が children 側で注入する)。
|
|
23
|
+
このコンポーネントの責務はスクロールと indicator / arrow によるページ送りのみ。 */
|
|
24
|
+
|
|
15
25
|
.charcoal-carousel__scroller {
|
|
16
26
|
position: relative;
|
|
17
27
|
display: flex;
|
|
18
|
-
gap: 24px;
|
|
19
28
|
overflow-x: auto;
|
|
20
29
|
overflow-y: hidden;
|
|
21
30
|
overscroll-behavior-x: contain;
|
|
@@ -28,14 +37,6 @@
|
|
|
28
37
|
display: none;
|
|
29
38
|
}
|
|
30
39
|
|
|
31
|
-
.charcoal-carousel__scroller[data-focus-visible] {
|
|
32
|
-
box-shadow: 0 0 0 4px rgba(0, 150, 250, 0.32);
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
.charcoal-carousel[data-size='S'] .charcoal-carousel__scroller {
|
|
36
|
-
gap: 0;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
40
|
/* スクロールスナップ type は prop(scrollSnap.type)で scroller に出し分ける。
|
|
40
41
|
align は基底 .charcoal-carousel__item の後(後述)に置く(no-descending-specificity 回避)。
|
|
41
42
|
未指定時の既定(M=none / S=mandatory / align=center)は JS 側で data 属性に解決する。 */
|
|
@@ -52,38 +53,39 @@
|
|
|
52
53
|
scroll-snap-type: x mandatory;
|
|
53
54
|
}
|
|
54
55
|
|
|
55
|
-
/* グラデーション:
|
|
56
|
-
|
|
57
|
-
|
|
56
|
+
/* グラデーション: mask で端のアイテム自体を透明へフェードさせる(下の背景色を
|
|
57
|
+
問わない)。mask の色はアルファとしてだけ効く(#000=表示 / transparent=透過)。
|
|
58
|
+
左フェード・右フェードの 2 レイヤーを intersect で合成し、はみ出して
|
|
59
|
+
スクロール可能な側のみ data-can-prev/next で 72px のフェード帯を枠内に置く。
|
|
60
|
+
無効な側はフェード帯ごと枠外へスライドして隠す(mask-image と違い
|
|
61
|
+
mask-position は補間可能なので、切替が 0.2s のフェードになる)。 */
|
|
58
62
|
|
|
59
|
-
.charcoal-carousel[data-has-gradient='true'] .charcoal-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
63
|
+
.charcoal-carousel[data-has-gradient='true'] .charcoal-carousel__scroller {
|
|
64
|
+
mask-image: linear-gradient(to right, transparent, #000 72px),
|
|
65
|
+
linear-gradient(to left, transparent, #000 72px);
|
|
66
|
+
/* 枠外へ 72px スライドしても反対側の端が欠けないよう、レイヤー幅に 72px 足す */
|
|
67
|
+
mask-size: calc(100% + 72px) 100%;
|
|
68
|
+
mask-repeat: no-repeat;
|
|
69
|
+
mask-composite: intersect;
|
|
70
|
+
mask-position: -72px 0,
|
|
71
|
+
0 0;
|
|
72
|
+
/* 0.2s ease-in は react-sandbox の LeftGradient と同じ値 */
|
|
73
|
+
transition: 0.2s ease-in mask-position;
|
|
69
74
|
}
|
|
70
75
|
|
|
71
|
-
.charcoal-carousel[data-has-gradient='true'] .charcoal-
|
|
72
|
-
|
|
73
|
-
|
|
76
|
+
.charcoal-carousel[data-has-gradient='true'][data-can-prev='true'] .charcoal-carousel__scroller {
|
|
77
|
+
mask-position: 0 0,
|
|
78
|
+
0 0;
|
|
74
79
|
}
|
|
75
80
|
|
|
76
|
-
.charcoal-carousel[data-has-gradient='true'] .charcoal-
|
|
77
|
-
|
|
78
|
-
|
|
81
|
+
.charcoal-carousel[data-has-gradient='true'][data-can-next='true'] .charcoal-carousel__scroller {
|
|
82
|
+
mask-position: -72px 0,
|
|
83
|
+
-72px 0;
|
|
79
84
|
}
|
|
80
85
|
|
|
81
|
-
.charcoal-carousel[data-has-gradient='true'][data-can-prev='true'] .charcoal-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
.charcoal-carousel[data-has-gradient='true'][data-can-next='true'] .charcoal-carousel__viewport::after {
|
|
86
|
-
opacity: 1;
|
|
86
|
+
.charcoal-carousel[data-has-gradient='true'][data-can-prev='true'][data-can-next='true'] .charcoal-carousel__scroller {
|
|
87
|
+
mask-position: 0 0,
|
|
88
|
+
-72px 0;
|
|
87
89
|
}
|
|
88
90
|
|
|
89
91
|
.charcoal-carousel__item {
|
|
@@ -108,7 +110,7 @@
|
|
|
108
110
|
|
|
109
111
|
/* ── Navigation Buttons (charcoal IconButton, variant=Overlay size=S) ── */
|
|
110
112
|
|
|
111
|
-
/*
|
|
113
|
+
/* マスクの透過域と同じ 72px 1fr 72px グリッド。各ボタンは 72px ゾーンの中央に置く。 */
|
|
112
114
|
|
|
113
115
|
.charcoal-carousel__navigation {
|
|
114
116
|
position: absolute;
|
|
@@ -117,8 +119,8 @@
|
|
|
117
119
|
display: grid;
|
|
118
120
|
grid-template-columns: 72px 1fr 72px;
|
|
119
121
|
align-items: center;
|
|
120
|
-
/*
|
|
121
|
-
z-index:
|
|
122
|
+
/* アイテム内のコンテンツより上にボタンを表示する */
|
|
123
|
+
z-index: 1;
|
|
122
124
|
/* sandbox 同様、通常は隠してカルーセル hover 時にフェードイン表示する。 */
|
|
123
125
|
opacity: 0;
|
|
124
126
|
transition: 0.4s opacity;
|
|
@@ -128,9 +130,11 @@
|
|
|
128
130
|
display: none;
|
|
129
131
|
}
|
|
130
132
|
|
|
131
|
-
/* hover
|
|
133
|
+
/* hover で表示。キーボード操作でも到達できるよう、キーボード由来のフォーカスが
|
|
134
|
+
カルーセル内にある間も表示する(:focus-within だとクリックで残ったフォーカスでも
|
|
135
|
+
表示され続けるため、react-aria が付与する data 属性で判定する)。 */
|
|
132
136
|
|
|
133
|
-
.charcoal-carousel:hover .charcoal-carousel__navigation, .charcoal-carousel
|
|
137
|
+
.charcoal-carousel:hover .charcoal-carousel__navigation, .charcoal-carousel[data-focus-visible-within] .charcoal-carousel__navigation {
|
|
134
138
|
opacity: 1;
|
|
135
139
|
}
|
|
136
140
|
|
|
@@ -138,16 +142,36 @@
|
|
|
138
142
|
pointer-events: auto;
|
|
139
143
|
}
|
|
140
144
|
|
|
145
|
+
/* ヒットエリアをグリッド端列(= mask のフェード帯と同じ 72px)全体へ広げる。
|
|
146
|
+
擬似要素上のクリックは元の button に届くため、帯のどこを押してもページ送りになる。
|
|
147
|
+
position: absolute の基準は positioned な .charcoal-carousel__navigation(inset: 0)。 */
|
|
148
|
+
|
|
149
|
+
.charcoal-carousel__navigation__item::after {
|
|
150
|
+
content: '';
|
|
151
|
+
position: absolute;
|
|
152
|
+
top: 0;
|
|
153
|
+
bottom: 0;
|
|
154
|
+
width: 72px;
|
|
155
|
+
}
|
|
156
|
+
|
|
141
157
|
.charcoal-carousel__navigation__item[data-direction='prev'] {
|
|
142
158
|
grid-column: 1;
|
|
143
159
|
justify-self: center;
|
|
144
160
|
}
|
|
145
161
|
|
|
162
|
+
.charcoal-carousel__navigation__item[data-direction='prev']::after {
|
|
163
|
+
left: 0;
|
|
164
|
+
}
|
|
165
|
+
|
|
146
166
|
.charcoal-carousel__navigation__item[data-direction='next'] {
|
|
147
167
|
grid-column: 3;
|
|
148
168
|
justify-self: center;
|
|
149
169
|
}
|
|
150
170
|
|
|
171
|
+
.charcoal-carousel__navigation__item[data-direction='next']::after {
|
|
172
|
+
right: 0;
|
|
173
|
+
}
|
|
174
|
+
|
|
151
175
|
/* スクロール端では非表示。IconButton の disabled スタイル(opacity:0.32)より
|
|
152
176
|
高い詳細度で打ち消すため、ルートを前置する。 */
|
|
153
177
|
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ナビゲーションボタンのヒットエリア検証(実ブラウザ)
|
|
3
|
+
*
|
|
4
|
+
* hasGradient の 72px フェード帯はページ送り操作の領域に見えるが、
|
|
5
|
+
* クリック可能域が中央の 32px 円形ボタンだけだと、帯の残り部分の
|
|
6
|
+
* クリックが背後のスライド(リンク等)に素通りしてしまう。
|
|
7
|
+
* 帯全体がナビゲーションボタンのヒットエリアであることを
|
|
8
|
+
* document.elementFromPoint のヒットテストで検証する。
|
|
9
|
+
* (pointer-events / 擬似要素のヒットテストは jsdom では再現できない)
|
|
10
|
+
*/
|
|
11
|
+
import { userEvent } from '@vitest/browser/context'
|
|
12
|
+
import { render } from '@testing-library/react'
|
|
13
|
+
import Carousel from '.'
|
|
14
|
+
|
|
15
|
+
const EDGE_ZONE_WIDTH = 72
|
|
16
|
+
|
|
17
|
+
const slides = Array.from({ length: 10 }, (_, i) => (
|
|
18
|
+
<div key={`slide-${i}`} style={{ width: 200, height: 160 }}>
|
|
19
|
+
Slide {i}
|
|
20
|
+
</div>
|
|
21
|
+
))
|
|
22
|
+
|
|
23
|
+
function renderCarousel() {
|
|
24
|
+
const { container } = render(
|
|
25
|
+
<div style={{ width: 400 }}>
|
|
26
|
+
<Carousel size="M" hasGradient>
|
|
27
|
+
{slides}
|
|
28
|
+
</Carousel>
|
|
29
|
+
</div>,
|
|
30
|
+
)
|
|
31
|
+
const viewport = container.querySelector(
|
|
32
|
+
'.charcoal-carousel__viewport',
|
|
33
|
+
) as HTMLElement
|
|
34
|
+
return { container, viewport, rect: viewport.getBoundingClientRect() }
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function hitNavigationItem(x: number, y: number) {
|
|
38
|
+
return document
|
|
39
|
+
.elementFromPoint(x, y)
|
|
40
|
+
?.closest('.charcoal-carousel__navigation__item')
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
describe('navigation button hit area', () => {
|
|
44
|
+
it('右端 72px 帯のクリックはスライドではなく next ボタンに届く', () => {
|
|
45
|
+
const { rect } = renderCarousel()
|
|
46
|
+
const centerY = rect.top + rect.height / 2
|
|
47
|
+
|
|
48
|
+
// ボタン円(32px、帯の中央)の外側かつ帯の内側の点
|
|
49
|
+
const nearEdge = hitNavigationItem(rect.right - 8, centerY)
|
|
50
|
+
expect(nearEdge?.getAttribute('data-direction')).toBe('next')
|
|
51
|
+
|
|
52
|
+
// ボタンの上下方向の外側(帯の上端付近)
|
|
53
|
+
const aboveButton = hitNavigationItem(
|
|
54
|
+
rect.right - EDGE_ZONE_WIDTH / 2,
|
|
55
|
+
rect.top + 8,
|
|
56
|
+
)
|
|
57
|
+
expect(aboveButton?.getAttribute('data-direction')).toBe('next')
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
it('スクロール不能な側(初期位置の prev)の帯はスライドへ素通りする', () => {
|
|
61
|
+
const { rect } = renderCarousel()
|
|
62
|
+
const centerY = rect.top + rect.height / 2
|
|
63
|
+
|
|
64
|
+
const hit = document.elementFromPoint(rect.left + 8, centerY)
|
|
65
|
+
expect(hit?.closest('.charcoal-carousel__navigation__item')).toBeNull()
|
|
66
|
+
expect(hit?.closest('.charcoal-carousel__scroller')).not.toBeNull()
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
it('帯のボタン外の点をクリックすると next へページ送りされる', async () => {
|
|
70
|
+
const { container, viewport, rect } = renderCarousel()
|
|
71
|
+
const scroller = container.querySelector(
|
|
72
|
+
'.charcoal-carousel__scroller',
|
|
73
|
+
) as HTMLElement
|
|
74
|
+
expect(scroller.scrollLeft).toBe(0)
|
|
75
|
+
|
|
76
|
+
// trusted click とその後の scroll イベントは React の act() 管理外で
|
|
77
|
+
// 発火するため、この区間だけ act 警告を無効化する。
|
|
78
|
+
const g = globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }
|
|
79
|
+
g.IS_REACT_ACT_ENVIRONMENT = false
|
|
80
|
+
try {
|
|
81
|
+
// viewport 左上基準・帯の内側かつボタン円の外側の座標を実クリックする
|
|
82
|
+
await userEvent.click(viewport, {
|
|
83
|
+
position: { x: rect.width - 8, y: 8 },
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
await vi.waitFor(() => {
|
|
87
|
+
expect(scroller.scrollLeft).toBeGreaterThan(0)
|
|
88
|
+
})
|
|
89
|
+
} finally {
|
|
90
|
+
g.IS_REACT_ACT_ENVIRONMENT = true
|
|
91
|
+
}
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
it('帯の外側(中央領域)のクリックはスライドに届く', () => {
|
|
95
|
+
const { rect } = renderCarousel()
|
|
96
|
+
const centerY = rect.top + rect.height / 2
|
|
97
|
+
|
|
98
|
+
const hit = document.elementFromPoint(rect.left + rect.width / 2, centerY)
|
|
99
|
+
expect(hit?.closest('.charcoal-carousel__navigation__item')).toBeNull()
|
|
100
|
+
expect(hit?.closest('.charcoal-carousel__scroller')).not.toBeNull()
|
|
101
|
+
})
|
|
102
|
+
})
|
|
@@ -10,12 +10,19 @@
|
|
|
10
10
|
|
|
11
11
|
.charcoal-carousel__viewport {
|
|
12
12
|
position: relative;
|
|
13
|
+
|
|
14
|
+
/* フォーカスリングは scroller ではなく viewport に描く。scroller に置くと
|
|
15
|
+
hasGradient の mask(mask-clip: border-box)で外側の box-shadow ごと消えるため。 */
|
|
16
|
+
&[data-focus-visible] {
|
|
17
|
+
box-shadow: 0 0 0 4px rgba(0, 150, 250, 0.32);
|
|
18
|
+
}
|
|
13
19
|
}
|
|
14
20
|
|
|
21
|
+
/* スライド間隔は所有しない(sandbox 同様、利用者が children 側で注入する)。
|
|
22
|
+
このコンポーネントの責務はスクロールと indicator / arrow によるページ送りのみ。 */
|
|
15
23
|
.charcoal-carousel__scroller {
|
|
16
24
|
position: relative;
|
|
17
25
|
display: flex;
|
|
18
|
-
gap: 24px;
|
|
19
26
|
overflow-x: auto;
|
|
20
27
|
overflow-y: hidden;
|
|
21
28
|
overscroll-behavior-x: contain;
|
|
@@ -26,14 +33,6 @@
|
|
|
26
33
|
&::-webkit-scrollbar {
|
|
27
34
|
display: none;
|
|
28
35
|
}
|
|
29
|
-
|
|
30
|
-
&[data-focus-visible] {
|
|
31
|
-
box-shadow: 0 0 0 4px rgba(0, 150, 250, 0.32);
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
.charcoal-carousel[data-size='S'] .charcoal-carousel__scroller {
|
|
36
|
-
gap: 0;
|
|
37
36
|
}
|
|
38
37
|
|
|
39
38
|
/* スクロールスナップ type は prop(scrollSnap.type)で scroller に出し分ける。
|
|
@@ -53,44 +52,46 @@
|
|
|
53
52
|
scroll-snap-type: x mandatory;
|
|
54
53
|
}
|
|
55
54
|
|
|
56
|
-
/* グラデーション:
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
.
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
.
|
|
75
|
-
.charcoal-carousel__viewport::before {
|
|
76
|
-
left: 0;
|
|
77
|
-
background: linear-gradient(to right, #fff, transparent);
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
.charcoal-carousel[data-has-gradient='true']
|
|
81
|
-
.charcoal-carousel__viewport::after {
|
|
82
|
-
right: 0;
|
|
83
|
-
background: linear-gradient(to left, #fff, transparent);
|
|
55
|
+
/* グラデーション: mask で端のアイテム自体を透明へフェードさせる(下の背景色を
|
|
56
|
+
問わない)。mask の色はアルファとしてだけ効く(#000=表示 / transparent=透過)。
|
|
57
|
+
左フェード・右フェードの 2 レイヤーを intersect で合成し、はみ出して
|
|
58
|
+
スクロール可能な側のみ data-can-prev/next で 72px のフェード帯を枠内に置く。
|
|
59
|
+
無効な側はフェード帯ごと枠外へスライドして隠す(mask-image と違い
|
|
60
|
+
mask-position は補間可能なので、切替が 0.2s のフェードになる)。 */
|
|
61
|
+
.charcoal-carousel[data-has-gradient='true'] .charcoal-carousel__scroller {
|
|
62
|
+
mask-image:
|
|
63
|
+
linear-gradient(to right, transparent, #000 72px),
|
|
64
|
+
linear-gradient(to left, transparent, #000 72px);
|
|
65
|
+
/* 枠外へ 72px スライドしても反対側の端が欠けないよう、レイヤー幅に 72px 足す */
|
|
66
|
+
mask-size: calc(100% + 72px) 100%;
|
|
67
|
+
mask-repeat: no-repeat;
|
|
68
|
+
mask-composite: intersect;
|
|
69
|
+
mask-position:
|
|
70
|
+
-72px 0,
|
|
71
|
+
0 0;
|
|
72
|
+
/* 0.2s ease-in は react-sandbox の LeftGradient と同じ値 */
|
|
73
|
+
transition: 0.2s ease-in mask-position;
|
|
84
74
|
}
|
|
85
75
|
|
|
86
76
|
.charcoal-carousel[data-has-gradient='true'][data-can-prev='true']
|
|
87
|
-
.charcoal-
|
|
88
|
-
|
|
77
|
+
.charcoal-carousel__scroller {
|
|
78
|
+
mask-position:
|
|
79
|
+
0 0,
|
|
80
|
+
0 0;
|
|
89
81
|
}
|
|
90
82
|
|
|
91
83
|
.charcoal-carousel[data-has-gradient='true'][data-can-next='true']
|
|
92
|
-
.charcoal-
|
|
93
|
-
|
|
84
|
+
.charcoal-carousel__scroller {
|
|
85
|
+
mask-position:
|
|
86
|
+
-72px 0,
|
|
87
|
+
-72px 0;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
.charcoal-carousel[data-has-gradient='true'][data-can-prev='true'][data-can-next='true']
|
|
91
|
+
.charcoal-carousel__scroller {
|
|
92
|
+
mask-position:
|
|
93
|
+
0 0,
|
|
94
|
+
-72px 0;
|
|
94
95
|
}
|
|
95
96
|
|
|
96
97
|
.charcoal-carousel__item {
|
|
@@ -114,7 +115,7 @@
|
|
|
114
115
|
|
|
115
116
|
/* ── Navigation Buttons (charcoal IconButton, variant=Overlay size=S) ── */
|
|
116
117
|
|
|
117
|
-
/*
|
|
118
|
+
/* マスクの透過域と同じ 72px 1fr 72px グリッド。各ボタンは 72px ゾーンの中央に置く。 */
|
|
118
119
|
.charcoal-carousel__navigation {
|
|
119
120
|
position: absolute;
|
|
120
121
|
inset: 0;
|
|
@@ -122,8 +123,8 @@
|
|
|
122
123
|
display: grid;
|
|
123
124
|
grid-template-columns: 72px 1fr 72px;
|
|
124
125
|
align-items: center;
|
|
125
|
-
/*
|
|
126
|
-
z-index:
|
|
126
|
+
/* アイテム内のコンテンツより上にボタンを表示する */
|
|
127
|
+
z-index: 1;
|
|
127
128
|
/* sandbox 同様、通常は隠してカルーセル hover 時にフェードイン表示する。 */
|
|
128
129
|
opacity: 0;
|
|
129
130
|
transition: 0.4s opacity;
|
|
@@ -133,23 +134,44 @@
|
|
|
133
134
|
}
|
|
134
135
|
}
|
|
135
136
|
|
|
136
|
-
/* hover
|
|
137
|
+
/* hover で表示。キーボード操作でも到達できるよう、キーボード由来のフォーカスが
|
|
138
|
+
カルーセル内にある間も表示する(:focus-within だとクリックで残ったフォーカスでも
|
|
139
|
+
表示され続けるため、react-aria が付与する data 属性で判定する)。 */
|
|
137
140
|
.charcoal-carousel:hover .charcoal-carousel__navigation,
|
|
138
|
-
.charcoal-carousel
|
|
141
|
+
.charcoal-carousel[data-focus-visible-within] .charcoal-carousel__navigation {
|
|
139
142
|
opacity: 1;
|
|
140
143
|
}
|
|
141
144
|
|
|
142
145
|
.charcoal-carousel__navigation__item {
|
|
143
146
|
pointer-events: auto;
|
|
144
147
|
|
|
148
|
+
/* ヒットエリアをグリッド端列(= mask のフェード帯と同じ 72px)全体へ広げる。
|
|
149
|
+
擬似要素上のクリックは元の button に届くため、帯のどこを押してもページ送りになる。
|
|
150
|
+
position: absolute の基準は positioned な .charcoal-carousel__navigation(inset: 0)。 */
|
|
151
|
+
&::after {
|
|
152
|
+
content: '';
|
|
153
|
+
position: absolute;
|
|
154
|
+
top: 0;
|
|
155
|
+
bottom: 0;
|
|
156
|
+
width: 72px;
|
|
157
|
+
}
|
|
158
|
+
|
|
145
159
|
&[data-direction='prev'] {
|
|
146
160
|
grid-column: 1;
|
|
147
161
|
justify-self: center;
|
|
162
|
+
|
|
163
|
+
&::after {
|
|
164
|
+
left: 0;
|
|
165
|
+
}
|
|
148
166
|
}
|
|
149
167
|
|
|
150
168
|
&[data-direction='next'] {
|
|
151
169
|
grid-column: 3;
|
|
152
170
|
justify-self: center;
|
|
171
|
+
|
|
172
|
+
&::after {
|
|
173
|
+
right: 0;
|
|
174
|
+
}
|
|
153
175
|
}
|
|
154
176
|
}
|
|
155
177
|
|
|
@@ -1,45 +1,52 @@
|
|
|
1
1
|
import { Meta, StoryObj } from '@storybook/react-vite'
|
|
2
|
-
import
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
objectFit: 'cover',
|
|
12
|
-
display: 'block',
|
|
13
|
-
}}
|
|
14
|
-
/>
|
|
15
|
-
)
|
|
16
|
-
|
|
17
|
-
const items: CarouselItem[] = Array.from({ length: 6 }, (_, i) => ({
|
|
18
|
-
id: `item-${i + 1}`,
|
|
19
|
-
children: sampleImage,
|
|
20
|
-
}))
|
|
21
|
-
|
|
22
|
-
// 横幅が確定したスロット(defaultScroll の初期位置計算と 0.75 ページ送りを確認するため)
|
|
23
|
-
const numberedItems: CarouselItem[] = Array.from({ length: 20 }, (_, i) => ({
|
|
24
|
-
id: `num-${i + 1}`,
|
|
25
|
-
children: (
|
|
26
|
-
<div
|
|
2
|
+
import type { CSSProperties } from 'react'
|
|
3
|
+
import Carousel from '.'
|
|
4
|
+
|
|
5
|
+
const makeSampleImages = (style: CSSProperties) =>
|
|
6
|
+
Array.from({ length: 6 }, (_, i) => (
|
|
7
|
+
<img
|
|
8
|
+
key={`item-${i + 1}`}
|
|
9
|
+
src="/carousel-sample.png"
|
|
10
|
+
alt="サンプル画像"
|
|
27
11
|
style={{
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
alignItems: 'center',
|
|
32
|
-
justifyContent: 'center',
|
|
33
|
-
background: i % 2 === 0 ? '#cfe3ff' : '#ffe3cf',
|
|
34
|
-
borderRadius: 8,
|
|
35
|
-
font: 'bold 32px sans-serif',
|
|
36
|
-
color: '#333',
|
|
12
|
+
objectFit: 'cover',
|
|
13
|
+
display: 'block',
|
|
14
|
+
...style,
|
|
37
15
|
}}
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
16
|
+
/>
|
|
17
|
+
))
|
|
18
|
+
|
|
19
|
+
// M 用: スライド間隔はコンポーネントが所有しないので、利用者側の margin で注入する。
|
|
20
|
+
// width: 100% と margin の併用はラッパーとの循環サイズ解決で間隔が潰れるため、明示サイズにする。
|
|
21
|
+
const spacedImages = makeSampleImages({
|
|
22
|
+
width: 280,
|
|
23
|
+
height: 210,
|
|
24
|
+
marginInlineEnd: 24,
|
|
25
|
+
})
|
|
26
|
+
// S 用: 1 枚全幅ページングのため間隔なし
|
|
27
|
+
const fullWidthImages = makeSampleImages({ width: '100%', height: '100%' })
|
|
28
|
+
|
|
29
|
+
// 横幅が確定したスロット(defaultScroll の初期位置計算と 0.75 ページ送りを確認するため)。
|
|
30
|
+
// スライド間隔はコンポーネントが所有しないので、利用者側の margin で注入する。
|
|
31
|
+
const numberedSlides = Array.from({ length: 20 }, (_, i) => (
|
|
32
|
+
<div
|
|
33
|
+
key={`num-${i + 1}`}
|
|
34
|
+
style={{
|
|
35
|
+
width: 200,
|
|
36
|
+
height: 120,
|
|
37
|
+
marginInlineEnd: 24,
|
|
38
|
+
display: 'flex',
|
|
39
|
+
alignItems: 'center',
|
|
40
|
+
justifyContent: 'center',
|
|
41
|
+
background: i % 2 === 0 ? '#cfe3ff' : '#ffe3cf',
|
|
42
|
+
borderRadius: 8,
|
|
43
|
+
font: 'bold 32px sans-serif',
|
|
44
|
+
color: '#333',
|
|
45
|
+
}}
|
|
46
|
+
>
|
|
47
|
+
{i + 1}
|
|
48
|
+
</div>
|
|
49
|
+
))
|
|
43
50
|
|
|
44
51
|
export default {
|
|
45
52
|
title: 'react/Carousel',
|
|
@@ -48,13 +55,14 @@ export default {
|
|
|
48
55
|
layout: 'padded',
|
|
49
56
|
},
|
|
50
57
|
args: {
|
|
51
|
-
|
|
58
|
+
children: spacedImages,
|
|
52
59
|
size: 'M',
|
|
53
60
|
hasGradient: false,
|
|
54
61
|
fullWidth: false,
|
|
55
62
|
scrollStep: 0.75,
|
|
56
63
|
},
|
|
57
64
|
argTypes: {
|
|
65
|
+
children: { control: false },
|
|
58
66
|
size: {
|
|
59
67
|
control: { type: 'radio' },
|
|
60
68
|
options: ['S', 'M'],
|
|
@@ -80,7 +88,7 @@ export const SizeM: StoryObj<typeof Carousel> = {
|
|
|
80
88
|
}
|
|
81
89
|
|
|
82
90
|
export const SizeS: StoryObj<typeof Carousel> = {
|
|
83
|
-
args: { size: 'S' },
|
|
91
|
+
args: { size: 'S', children: fullWidthImages },
|
|
84
92
|
}
|
|
85
93
|
|
|
86
94
|
export const WithGradient: StoryObj<typeof Carousel> = {
|
|
@@ -92,7 +100,7 @@ export const FullWidth: StoryObj<typeof Carousel> = {
|
|
|
92
100
|
}
|
|
93
101
|
|
|
94
102
|
export const NavigationButtonsOnSizeS: StoryObj<typeof Carousel> = {
|
|
95
|
-
args: { size: 'S', navigationButtons: true },
|
|
103
|
+
args: { size: 'S', navigationButtons: true, children: fullWidthImages },
|
|
96
104
|
}
|
|
97
105
|
|
|
98
106
|
export const IndicatorOnSizeM: StoryObj<typeof Carousel> = {
|
|
@@ -101,42 +109,53 @@ export const IndicatorOnSizeM: StoryObj<typeof Carousel> = {
|
|
|
101
109
|
|
|
102
110
|
// 横幅が確定したスロット(番号付き)
|
|
103
111
|
export const DefaultScrollCenter: StoryObj<typeof Carousel> = {
|
|
104
|
-
args: {
|
|
112
|
+
args: {
|
|
113
|
+
size: 'M',
|
|
114
|
+
children: numberedSlides,
|
|
115
|
+
defaultScroll: { align: 'center' },
|
|
116
|
+
},
|
|
105
117
|
}
|
|
106
118
|
|
|
107
119
|
export const DefaultScrollRight: StoryObj<typeof Carousel> = {
|
|
108
|
-
args: {
|
|
120
|
+
args: {
|
|
121
|
+
size: 'M',
|
|
122
|
+
children: numberedSlides,
|
|
123
|
+
defaultScroll: { align: 'right' },
|
|
124
|
+
},
|
|
109
125
|
}
|
|
110
126
|
|
|
111
127
|
// 遅延読み込み画像(マウント時に幅が未確定)でも初期位置が効くことの確認
|
|
112
128
|
export const DefaultScrollCenterAsyncImages: StoryObj<typeof Carousel> = {
|
|
113
|
-
args: {
|
|
129
|
+
args: {
|
|
130
|
+
size: 'M',
|
|
131
|
+
children: spacedImages,
|
|
132
|
+
defaultScroll: { align: 'center' },
|
|
133
|
+
},
|
|
114
134
|
}
|
|
115
135
|
|
|
116
136
|
// 白フェードが見えることの確認用(暗色コンテンツなら白フェードが明確に出る)
|
|
117
|
-
const darkTiles
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
}))
|
|
137
|
+
const darkTiles = Array.from({ length: 10 }, (_, i) => (
|
|
138
|
+
<div
|
|
139
|
+
key={`dark-${i + 1}`}
|
|
140
|
+
style={{
|
|
141
|
+
width: 220,
|
|
142
|
+
height: 140,
|
|
143
|
+
marginInlineEnd: 24,
|
|
144
|
+
display: 'flex',
|
|
145
|
+
alignItems: 'center',
|
|
146
|
+
justifyContent: 'center',
|
|
147
|
+
background: '#2a3b8f',
|
|
148
|
+
color: '#fff',
|
|
149
|
+
borderRadius: 4,
|
|
150
|
+
font: 'bold 28px sans-serif',
|
|
151
|
+
}}
|
|
152
|
+
>
|
|
153
|
+
{i + 1}
|
|
154
|
+
</div>
|
|
155
|
+
))
|
|
137
156
|
|
|
138
157
|
export const GradientOnDarkContent: StoryObj<typeof Carousel> = {
|
|
139
|
-
args: { size: 'M', hasGradient: true,
|
|
158
|
+
args: { size: 'M', hasGradient: true, children: darkTiles },
|
|
140
159
|
}
|
|
141
160
|
|
|
142
161
|
// scrollStep に関数を渡してフル制御する例(送り量を clientWidth - 48px に固定)。
|
|
@@ -144,7 +163,7 @@ export const GradientOnDarkContent: StoryObj<typeof Carousel> = {
|
|
|
144
163
|
export const ScrollStepFunction: StoryObj<typeof Carousel> = {
|
|
145
164
|
args: {
|
|
146
165
|
size: 'M',
|
|
147
|
-
|
|
166
|
+
children: numberedSlides,
|
|
148
167
|
scrollStep: ({ clientWidth }) => clientWidth - 48,
|
|
149
168
|
},
|
|
150
169
|
}
|
|
@@ -153,7 +172,7 @@ export const ScrollStepFunction: StoryObj<typeof Carousel> = {
|
|
|
153
172
|
export const ScrollSnapPerItem: StoryObj<typeof Carousel> = {
|
|
154
173
|
args: {
|
|
155
174
|
size: 'M',
|
|
156
|
-
|
|
175
|
+
children: numberedSlides,
|
|
157
176
|
scrollSnap: { type: 'mandatory', align: 'start' },
|
|
158
177
|
},
|
|
159
178
|
}
|