@charcoal-ui/react 6.0.0-rc.3 → 6.0.0-rc.5
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 +100 -47
- 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 +100 -47
- package/dist/layered.css.map +1 -1
- package/package.json +5 -6
- package/src/__tests__/css-variables.test.ts +1 -1
- package/src/__tests__/token-v1-compat.test.ts +1 -2
- package/src/__tests__/token-v1-remap.test.ts +6 -6
- package/src/_lib/useIsomorphicLayoutEffect.ts +7 -0
- package/src/components/Carousel/MIGRATION.md +39 -39
- package/src/components/Carousel/__snapshots__/index.css.snap +39 -37
- package/src/components/Carousel/index.css +45 -44
- package/src/components/Carousel/index.story.tsx +87 -68
- package/src/components/Carousel/index.test.tsx +68 -47
- package/src/components/Carousel/index.tsx +23 -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
|
@@ -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
|
}
|
|
@@ -2,12 +2,13 @@ import { createRef } from 'react'
|
|
|
2
2
|
import { render, screen, fireEvent, act } from '@testing-library/react'
|
|
3
3
|
import { renderToString } from 'react-dom/server'
|
|
4
4
|
import { vi, describe, it, expect, beforeEach, afterEach } from 'vitest'
|
|
5
|
-
import Carousel, { type
|
|
5
|
+
import Carousel, { type CarouselHandlerRef } from '.'
|
|
6
6
|
|
|
7
|
-
const
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
7
|
+
const slides = Array.from({ length: 6 }, (_, i) => (
|
|
8
|
+
<div key={`item-${i}`} data-testid={`slide-${i}`}>
|
|
9
|
+
Slide {i}
|
|
10
|
+
</div>
|
|
11
|
+
))
|
|
11
12
|
|
|
12
13
|
function mockScrollerGeometry(
|
|
13
14
|
el: HTMLElement,
|
|
@@ -77,14 +78,14 @@ function fireIntersect(el: Element) {
|
|
|
77
78
|
describe('Carousel', () => {
|
|
78
79
|
describe('rendering', () => {
|
|
79
80
|
it('renders all items', () => {
|
|
80
|
-
render(<Carousel
|
|
81
|
+
render(<Carousel>{slides}</Carousel>)
|
|
81
82
|
for (let i = 0; i < 6; i++) {
|
|
82
83
|
expect(screen.getByTestId(`slide-${i}`)).toBeInTheDocument()
|
|
83
84
|
}
|
|
84
85
|
})
|
|
85
86
|
|
|
86
87
|
it('has carousel ARIA attributes', () => {
|
|
87
|
-
render(<Carousel
|
|
88
|
+
render(<Carousel>{slides}</Carousel>)
|
|
88
89
|
const region = screen.getByRole('region')
|
|
89
90
|
expect(region).toHaveAttribute('aria-roledescription', 'carousel')
|
|
90
91
|
expect(region).toHaveAttribute('aria-label', 'Carousel')
|
|
@@ -93,7 +94,7 @@ describe('Carousel', () => {
|
|
|
93
94
|
|
|
94
95
|
describe('Size M (default)', () => {
|
|
95
96
|
it('shows navigation buttons', () => {
|
|
96
|
-
render(<Carousel
|
|
97
|
+
render(<Carousel size="M">{slides}</Carousel>)
|
|
97
98
|
expect(
|
|
98
99
|
screen.getByRole('button', { name: 'Previous' }),
|
|
99
100
|
).toBeInTheDocument()
|
|
@@ -101,7 +102,7 @@ describe('Carousel', () => {
|
|
|
101
102
|
})
|
|
102
103
|
|
|
103
104
|
it('hides indicator by default', () => {
|
|
104
|
-
const { container } = render(<Carousel
|
|
105
|
+
const { container } = render(<Carousel size="M">{slides}</Carousel>)
|
|
105
106
|
const indicator = container.querySelector('.charcoal-carousel__indicator')
|
|
106
107
|
expect(indicator).toHaveAttribute('data-visible', 'false')
|
|
107
108
|
})
|
|
@@ -109,7 +110,7 @@ describe('Carousel', () => {
|
|
|
109
110
|
|
|
110
111
|
describe('Size S', () => {
|
|
111
112
|
it('hides navigation buttons by default', () => {
|
|
112
|
-
const { container } = render(<Carousel
|
|
113
|
+
const { container } = render(<Carousel size="S">{slides}</Carousel>)
|
|
113
114
|
const nav = container.querySelector('.charcoal-carousel__navigation')
|
|
114
115
|
expect(nav).toHaveAttribute('data-visible', 'false')
|
|
115
116
|
})
|
|
@@ -117,13 +118,13 @@ describe('Carousel', () => {
|
|
|
117
118
|
|
|
118
119
|
describe('navigation button state', () => {
|
|
119
120
|
it('disables prev button at scroll start', () => {
|
|
120
|
-
render(<Carousel
|
|
121
|
+
render(<Carousel>{slides}</Carousel>)
|
|
121
122
|
const prev = screen.getByRole('button', { name: 'Previous' })
|
|
122
123
|
expect(prev).toBeDisabled()
|
|
123
124
|
})
|
|
124
125
|
|
|
125
126
|
it('enables next button when scrollable content exists', () => {
|
|
126
|
-
render(<Carousel
|
|
127
|
+
render(<Carousel>{slides}</Carousel>)
|
|
127
128
|
const scroller = getScroller()
|
|
128
129
|
mockScrollerGeometry(scroller, { scrollLeft: 0 })
|
|
129
130
|
act(() => {
|
|
@@ -136,7 +137,7 @@ describe('Carousel', () => {
|
|
|
136
137
|
|
|
137
138
|
describe('scrollByStep (0.75x viewport)', () => {
|
|
138
139
|
it('calls scrollBy with 0.75x viewport width on next button click', () => {
|
|
139
|
-
render(<Carousel
|
|
140
|
+
render(<Carousel>{slides}</Carousel>)
|
|
140
141
|
const scroller = getScroller()
|
|
141
142
|
mockScrollerGeometry(scroller, { scrollLeft: 100 })
|
|
142
143
|
act(() => {
|
|
@@ -156,7 +157,7 @@ describe('Carousel', () => {
|
|
|
156
157
|
})
|
|
157
158
|
|
|
158
159
|
it('calls scrollBy with negative 0.75x on prev button click', () => {
|
|
159
|
-
render(<Carousel
|
|
160
|
+
render(<Carousel>{slides}</Carousel>)
|
|
160
161
|
const scroller = getScroller()
|
|
161
162
|
mockScrollerGeometry(scroller, { scrollLeft: 400 })
|
|
162
163
|
act(() => {
|
|
@@ -176,7 +177,7 @@ describe('Carousel', () => {
|
|
|
176
177
|
})
|
|
177
178
|
|
|
178
179
|
it('respects custom scrollStep ratio (number)', () => {
|
|
179
|
-
render(<Carousel
|
|
180
|
+
render(<Carousel scrollStep={0.5}>{slides}</Carousel>)
|
|
180
181
|
const scroller = getScroller()
|
|
181
182
|
mockScrollerGeometry(scroller, { scrollLeft: 100 })
|
|
182
183
|
act(() => {
|
|
@@ -199,7 +200,7 @@ describe('Carousel', () => {
|
|
|
199
200
|
const scrollStep = vi.fn(
|
|
200
201
|
({ clientWidth }: { clientWidth: number }) => clientWidth - 48,
|
|
201
202
|
)
|
|
202
|
-
render(<Carousel
|
|
203
|
+
render(<Carousel scrollStep={scrollStep}>{slides}</Carousel>)
|
|
203
204
|
const scroller = getScroller()
|
|
204
205
|
mockScrollerGeometry(scroller, { scrollLeft: 100 })
|
|
205
206
|
act(() => {
|
|
@@ -228,7 +229,7 @@ describe('Carousel', () => {
|
|
|
228
229
|
|
|
229
230
|
describe('keyboard navigation', () => {
|
|
230
231
|
it('scrolls next by 0.75x viewport on ArrowRight', () => {
|
|
231
|
-
render(<Carousel
|
|
232
|
+
render(<Carousel>{slides}</Carousel>)
|
|
232
233
|
const scroller = getScroller()
|
|
233
234
|
mockScrollerGeometry(scroller, { scrollLeft: 100 })
|
|
234
235
|
act(() => {
|
|
@@ -247,7 +248,7 @@ describe('Carousel', () => {
|
|
|
247
248
|
})
|
|
248
249
|
|
|
249
250
|
it('scrolls prev by negative 0.75x viewport on ArrowLeft', () => {
|
|
250
|
-
render(<Carousel
|
|
251
|
+
render(<Carousel>{slides}</Carousel>)
|
|
251
252
|
const scroller = getScroller()
|
|
252
253
|
mockScrollerGeometry(scroller, { scrollLeft: 400 })
|
|
253
254
|
act(() => {
|
|
@@ -266,7 +267,7 @@ describe('Carousel', () => {
|
|
|
266
267
|
})
|
|
267
268
|
|
|
268
269
|
it('ignores unrelated keys', () => {
|
|
269
|
-
render(<Carousel
|
|
270
|
+
render(<Carousel>{slides}</Carousel>)
|
|
270
271
|
const scroller = getScroller()
|
|
271
272
|
mockScrollerGeometry(scroller, { scrollLeft: 100 })
|
|
272
273
|
|
|
@@ -284,7 +285,7 @@ describe('Carousel', () => {
|
|
|
284
285
|
// jsdom には scrollIntoView が無いのでモックを定義する。
|
|
285
286
|
const scrollIntoView = vi.fn()
|
|
286
287
|
Element.prototype.scrollIntoView = scrollIntoView
|
|
287
|
-
const { container } = render(<Carousel
|
|
288
|
+
const { container } = render(<Carousel size="S">{slides}</Carousel>)
|
|
288
289
|
|
|
289
290
|
const dots = container.querySelectorAll(
|
|
290
291
|
'.charcoal-carousel__indicator__item',
|
|
@@ -311,7 +312,9 @@ describe('Carousel', () => {
|
|
|
311
312
|
it('item が中央に入ると activeIndex が更新され indicator に反映される', () => {
|
|
312
313
|
const restore = installIOMock()
|
|
313
314
|
const { container } = render(
|
|
314
|
-
<Carousel
|
|
315
|
+
<Carousel size="M" indicator>
|
|
316
|
+
{slides}
|
|
317
|
+
</Carousel>,
|
|
315
318
|
)
|
|
316
319
|
const itemEls = container.querySelectorAll('.charcoal-carousel__item')
|
|
317
320
|
act(() => {
|
|
@@ -327,7 +330,7 @@ describe('Carousel', () => {
|
|
|
327
330
|
|
|
328
331
|
describe('scroll-state data attributes (mask)', () => {
|
|
329
332
|
it('reflects canPrev/canNext on the root element', () => {
|
|
330
|
-
const { container } = render(<Carousel
|
|
333
|
+
const { container } = render(<Carousel hasGradient>{slides}</Carousel>)
|
|
331
334
|
const root = container.querySelector('.charcoal-carousel')
|
|
332
335
|
const scroller = getScroller()
|
|
333
336
|
mockScrollerGeometry(scroller, { scrollLeft: 100 })
|
|
@@ -339,7 +342,7 @@ describe('Carousel', () => {
|
|
|
339
342
|
})
|
|
340
343
|
|
|
341
344
|
it('marks data-can-prev false at the start edge', () => {
|
|
342
|
-
const { container } = render(<Carousel
|
|
345
|
+
const { container } = render(<Carousel hasGradient>{slides}</Carousel>)
|
|
343
346
|
const root = container.querySelector('.charcoal-carousel')
|
|
344
347
|
const scroller = getScroller()
|
|
345
348
|
mockScrollerGeometry(scroller, { scrollLeft: 0 })
|
|
@@ -378,26 +381,25 @@ describe('Carousel', () => {
|
|
|
378
381
|
})
|
|
379
382
|
|
|
380
383
|
it('defaults to the left edge (0)', () => {
|
|
381
|
-
render(<Carousel
|
|
384
|
+
render(<Carousel>{slides}</Carousel>)
|
|
382
385
|
expect(lastSetLeft).toBe(0)
|
|
383
386
|
})
|
|
384
387
|
|
|
385
388
|
it('centers the content when align=center (maxScroll / 2)', () => {
|
|
386
|
-
render(<Carousel
|
|
389
|
+
render(<Carousel defaultScroll={{ align: 'center' }}>{slides}</Carousel>)
|
|
387
390
|
expect(lastSetLeft).toBe(800)
|
|
388
391
|
})
|
|
389
392
|
|
|
390
393
|
it('aligns to the right edge when align=right (maxScroll)', () => {
|
|
391
|
-
render(<Carousel
|
|
394
|
+
render(<Carousel defaultScroll={{ align: 'right' }}>{slides}</Carousel>)
|
|
392
395
|
expect(lastSetLeft).toBe(1600)
|
|
393
396
|
})
|
|
394
397
|
|
|
395
398
|
it('applies offset from the base position', () => {
|
|
396
399
|
render(
|
|
397
|
-
<Carousel
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
/>,
|
|
400
|
+
<Carousel defaultScroll={{ align: 'left', offset: 100 }}>
|
|
401
|
+
{slides}
|
|
402
|
+
</Carousel>,
|
|
401
403
|
)
|
|
402
404
|
expect(lastSetLeft).toBe(100)
|
|
403
405
|
})
|
|
@@ -444,7 +446,7 @@ describe('Carousel', () => {
|
|
|
444
446
|
})
|
|
445
447
|
|
|
446
448
|
it('re-applies the initial position when content width settles later', () => {
|
|
447
|
-
render(<Carousel
|
|
449
|
+
render(<Carousel defaultScroll={{ align: 'right' }}>{slides}</Carousel>)
|
|
448
450
|
// at mount the content is not laid out yet (maxScroll 0) -> right -> 0
|
|
449
451
|
expect(lastSetLeft).toBe(0)
|
|
450
452
|
|
|
@@ -460,7 +462,7 @@ describe('Carousel', () => {
|
|
|
460
462
|
})
|
|
461
463
|
|
|
462
464
|
it('stops re-applying once the user interacts', () => {
|
|
463
|
-
render(<Carousel
|
|
465
|
+
render(<Carousel defaultScroll={{ align: 'right' }}>{slides}</Carousel>)
|
|
464
466
|
|
|
465
467
|
// user interacts (pointerdown on the scroller) before the content settles
|
|
466
468
|
fireEvent.pointerDown(getScroller())
|
|
@@ -476,16 +478,31 @@ describe('Carousel', () => {
|
|
|
476
478
|
})
|
|
477
479
|
})
|
|
478
480
|
|
|
481
|
+
describe('children (sandbox-compatible API)', () => {
|
|
482
|
+
it('renders each direct child as one slide', () => {
|
|
483
|
+
const { container } = render(<Carousel>{slides}</Carousel>)
|
|
484
|
+
expect(
|
|
485
|
+
container.querySelectorAll('.charcoal-carousel__item'),
|
|
486
|
+
).toHaveLength(6)
|
|
487
|
+
})
|
|
488
|
+
})
|
|
489
|
+
|
|
479
490
|
describe('props override defaults', () => {
|
|
480
491
|
it('shows indicator on Size M when prop is true', () => {
|
|
481
492
|
const { container } = render(
|
|
482
|
-
<Carousel
|
|
493
|
+
<Carousel size="M" indicator>
|
|
494
|
+
{slides}
|
|
495
|
+
</Carousel>,
|
|
483
496
|
)
|
|
484
497
|
expect(container.querySelector("[data-indicator='true']")).toBeTruthy()
|
|
485
498
|
})
|
|
486
499
|
|
|
487
500
|
it('shows navigation on Size S when prop is true', () => {
|
|
488
|
-
render(
|
|
501
|
+
render(
|
|
502
|
+
<Carousel size="S" navigationButtons>
|
|
503
|
+
{slides}
|
|
504
|
+
</Carousel>,
|
|
505
|
+
)
|
|
489
506
|
const nav = document.querySelector('.charcoal-carousel__navigation')
|
|
490
507
|
expect(nav).toHaveAttribute('data-visible', 'true')
|
|
491
508
|
})
|
|
@@ -493,12 +510,12 @@ describe('Carousel', () => {
|
|
|
493
510
|
|
|
494
511
|
describe('variant data attributes', () => {
|
|
495
512
|
it('sets gradient data attribute', () => {
|
|
496
|
-
const { container } = render(<Carousel
|
|
513
|
+
const { container } = render(<Carousel hasGradient>{slides}</Carousel>)
|
|
497
514
|
expect(container.querySelector("[data-has-gradient='true']")).toBeTruthy()
|
|
498
515
|
})
|
|
499
516
|
|
|
500
517
|
it('sets full-width data attribute', () => {
|
|
501
|
-
const { container } = render(<Carousel
|
|
518
|
+
const { container } = render(<Carousel fullWidth>{slides}</Carousel>)
|
|
502
519
|
expect(container.querySelector("[data-full-width='true']")).toBeTruthy()
|
|
503
520
|
})
|
|
504
521
|
})
|
|
@@ -507,30 +524,32 @@ describe('Carousel', () => {
|
|
|
507
524
|
const getRegion = () => screen.getByRole('region')
|
|
508
525
|
|
|
509
526
|
it('defaults to none / center on Size M (sandbox 同等の 0.75)', () => {
|
|
510
|
-
render(<Carousel
|
|
527
|
+
render(<Carousel size="M">{slides}</Carousel>)
|
|
511
528
|
expect(getRegion()).toHaveAttribute('data-scroll-snap-type', 'none')
|
|
512
529
|
expect(getRegion()).toHaveAttribute('data-scroll-snap-align', 'center')
|
|
513
530
|
})
|
|
514
531
|
|
|
515
532
|
it('defaults to mandatory on Size S', () => {
|
|
516
|
-
render(<Carousel
|
|
533
|
+
render(<Carousel size="S">{slides}</Carousel>)
|
|
517
534
|
expect(getRegion()).toHaveAttribute('data-scroll-snap-type', 'mandatory')
|
|
518
535
|
})
|
|
519
536
|
|
|
520
537
|
it('overrides type per prop (item-ごと mandatory)', () => {
|
|
521
538
|
render(
|
|
522
|
-
<Carousel
|
|
539
|
+
<Carousel size="M" scrollSnap={{ type: 'mandatory' }}>
|
|
540
|
+
{slides}
|
|
541
|
+
</Carousel>,
|
|
523
542
|
)
|
|
524
543
|
expect(getRegion()).toHaveAttribute('data-scroll-snap-type', 'mandatory')
|
|
525
544
|
})
|
|
526
545
|
|
|
527
546
|
it('overrides align per prop', () => {
|
|
528
|
-
render(<Carousel
|
|
547
|
+
render(<Carousel scrollSnap={{ align: 'start' }}>{slides}</Carousel>)
|
|
529
548
|
expect(getRegion()).toHaveAttribute('data-scroll-snap-align', 'start')
|
|
530
549
|
})
|
|
531
550
|
|
|
532
551
|
it('supports disabling snap (none)', () => {
|
|
533
|
-
render(<Carousel
|
|
552
|
+
render(<Carousel scrollSnap={{ type: 'none' }}>{slides}</Carousel>)
|
|
534
553
|
expect(getRegion()).toHaveAttribute('data-scroll-snap-type', 'none')
|
|
535
554
|
})
|
|
536
555
|
})
|
|
@@ -538,7 +557,7 @@ describe('Carousel', () => {
|
|
|
538
557
|
describe('react-sandbox compat (callbacks / ref)', () => {
|
|
539
558
|
it('calls onScroll with scrollLeft on scroll', () => {
|
|
540
559
|
const onScroll = vi.fn()
|
|
541
|
-
render(<Carousel
|
|
560
|
+
render(<Carousel onScroll={onScroll}>{slides}</Carousel>)
|
|
542
561
|
const scroller = getScroller()
|
|
543
562
|
mockScrollerGeometry(scroller, { scrollLeft: 240 })
|
|
544
563
|
act(() => {
|
|
@@ -550,7 +569,7 @@ describe('Carousel', () => {
|
|
|
550
569
|
it('calls onScrollStateChange when scrollability changes', () => {
|
|
551
570
|
const onScrollStateChange = vi.fn()
|
|
552
571
|
render(
|
|
553
|
-
<Carousel
|
|
572
|
+
<Carousel onScrollStateChange={onScrollStateChange}>{slides}</Carousel>,
|
|
554
573
|
)
|
|
555
574
|
const scroller = getScroller()
|
|
556
575
|
mockScrollerGeometry(scroller, { scrollLeft: 100 })
|
|
@@ -577,7 +596,9 @@ describe('Carousel', () => {
|
|
|
577
596
|
]
|
|
578
597
|
const ref = createRef<CarouselHandlerRef>()
|
|
579
598
|
render(
|
|
580
|
-
<Carousel ref={ref}
|
|
599
|
+
<Carousel ref={ref} defaultScroll={{ align: 'right' }}>
|
|
600
|
+
{slides}
|
|
601
|
+
</Carousel>,
|
|
581
602
|
)
|
|
582
603
|
lastSetLeft = undefined
|
|
583
604
|
act(() => {
|
|
@@ -603,7 +624,7 @@ describe('Carousel', () => {
|
|
|
603
624
|
.spyOn(HTMLElement.prototype, 'clientWidth', 'get')
|
|
604
625
|
.mockReturnValue(640)
|
|
605
626
|
const onResize = vi.fn()
|
|
606
|
-
render(<Carousel
|
|
627
|
+
render(<Carousel onResize={onResize}>{slides}</Carousel>)
|
|
607
628
|
act(() => {
|
|
608
629
|
roCallbacks.forEach((cb) => cb([]))
|
|
609
630
|
})
|
|
@@ -641,13 +662,13 @@ describe('Carousel', () => {
|
|
|
641
662
|
// getServerSnapshot を使うため、サーバー描画でブラウザ API に触れない。
|
|
642
663
|
expect(() =>
|
|
643
664
|
renderToString(
|
|
644
|
-
<Carousel
|
|
665
|
+
<Carousel defaultScroll={{ align: 'center' }}>{slides}</Carousel>,
|
|
645
666
|
),
|
|
646
667
|
).not.toThrow()
|
|
647
668
|
})
|
|
648
669
|
|
|
649
670
|
it('uses the server snapshot (canPrev/canNext = false) and renders items + indicator', () => {
|
|
650
|
-
const html = renderToString(<Carousel
|
|
671
|
+
const html = renderToString(<Carousel size="M">{slides}</Carousel>)
|
|
651
672
|
|
|
652
673
|
// サーバースナップショットでは canPrev/canNext は false。
|
|
653
674
|
expect(html).toContain('data-can-prev="false"')
|