@charcoal-ui/react 6.0.0-rc.4 → 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.
Files changed (37) hide show
  1. package/dist/_lib/useIsomorphicLayoutEffect.d.ts +6 -0
  2. package/dist/_lib/useIsomorphicLayoutEffect.d.ts.map +1 -0
  3. package/dist/components/Carousel/index.d.ts +2 -6
  4. package/dist/components/Carousel/index.d.ts.map +1 -1
  5. package/dist/components/Carousel/useCarouselScroller.d.ts.map +1 -1
  6. package/dist/components/Modal/index.d.ts.map +1 -1
  7. package/dist/components/Modal/useTransitionPresence.d.ts +19 -0
  8. package/dist/components/Modal/useTransitionPresence.d.ts.map +1 -0
  9. package/dist/index.cjs +2 -2
  10. package/dist/index.cjs.map +1 -1
  11. package/dist/index.css +100 -47
  12. package/dist/index.d.ts +1 -1
  13. package/dist/index.d.ts.map +1 -1
  14. package/dist/index.js +2 -2
  15. package/dist/index.js.map +1 -1
  16. package/dist/layered.css +100 -47
  17. package/dist/layered.css.map +1 -1
  18. package/package.json +5 -6
  19. package/src/__tests__/token-v1-compat.test.ts +0 -1
  20. package/src/_lib/useIsomorphicLayoutEffect.ts +7 -0
  21. package/src/components/Carousel/MIGRATION.md +39 -39
  22. package/src/components/Carousel/__snapshots__/index.css.snap +39 -37
  23. package/src/components/Carousel/index.css +45 -44
  24. package/src/components/Carousel/index.story.tsx +87 -68
  25. package/src/components/Carousel/index.test.tsx +68 -47
  26. package/src/components/Carousel/index.tsx +23 -14
  27. package/src/components/Carousel/useCarouselScroller.ts +8 -12
  28. package/src/components/FieldLabel/__snapshots__/index.css.snap +1 -1
  29. package/src/components/FieldLabel/index.css +1 -1
  30. package/src/components/IconButton/__snapshots__/index.css.snap +1 -1
  31. package/src/components/IconButton/index.css +1 -1
  32. package/src/components/Modal/__snapshots__/index.css.snap +32 -1
  33. package/src/components/Modal/index.css +34 -0
  34. package/src/components/Modal/index.test.tsx +200 -0
  35. package/src/components/Modal/index.tsx +48 -78
  36. package/src/components/Modal/useTransitionPresence.ts +95 -0
  37. package/src/index.ts +0 -1
@@ -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 CarouselItem, type CarouselHandlerRef } from '.'
5
+ import Carousel, { type CarouselHandlerRef } from '.'
6
6
 
7
- const items: CarouselItem[] = Array.from({ length: 6 }, (_, i) => ({
8
- id: `item-${i}`,
9
- children: <div data-testid={`slide-${i}`}>Slide {i}</div>,
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 items={items} />)
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 items={items} />)
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 items={items} size="M" />)
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 items={items} size="M" />)
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 items={items} size="S" />)
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 items={items} />)
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 items={items} />)
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 items={items} />)
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 items={items} />)
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 items={items} scrollStep={0.5} />)
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 items={items} scrollStep={scrollStep} />)
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 items={items} />)
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 items={items} />)
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 items={items} />)
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 items={items} size="S" />)
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 items={items} size="M" indicator />,
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 items={items} hasGradient />)
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 items={items} hasGradient />)
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 items={items} />)
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 items={items} defaultScroll={{ align: 'center' }} />)
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 items={items} defaultScroll={{ align: 'right' }} />)
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
- items={items}
399
- defaultScroll={{ align: 'left', offset: 100 }}
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 items={items} defaultScroll={{ align: 'right' }} />)
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 items={items} defaultScroll={{ align: 'right' }} />)
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 items={items} size="M" indicator />,
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(<Carousel items={items} size="S" navigationButtons />)
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 items={items} hasGradient />)
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 items={items} fullWidth />)
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 items={items} size="M" />)
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 items={items} size="S" />)
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 items={items} size="M" scrollSnap={{ type: 'mandatory' }} />,
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 items={items} scrollSnap={{ align: 'start' }} />)
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 items={items} scrollSnap={{ type: 'none' }} />)
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 items={items} onScroll={onScroll} />)
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 items={items} onScrollStateChange={onScrollStateChange} />,
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} items={items} defaultScroll={{ align: 'right' }} />,
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 items={items} onResize={onResize} />)
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 items={items} defaultScroll={{ align: 'center' }} />,
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 items={items} size="M" />)
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"')
@@ -1,7 +1,9 @@
1
1
  import './index.css'
2
2
 
3
3
  import {
4
+ Children,
4
5
  forwardRef,
6
+ isValidElement,
5
7
  memo,
6
8
  useCallback,
7
9
  useImperativeHandle,
@@ -23,11 +25,6 @@ import { useCarouselScroller } from './useCarouselScroller'
23
25
 
24
26
  const getServerSnapshot = (): CarouselState => INITIAL_CAROUSEL_STATE
25
27
 
26
- export type CarouselItem = {
27
- id: string
28
- children: ReactNode
29
- }
30
-
31
28
  export type ScrollAlign = 'left' | 'center' | 'right'
32
29
 
33
30
  export type ScrollSnapType = 'none' | 'proximity' | 'mandatory'
@@ -70,7 +67,8 @@ export type CarouselProps = Readonly<{
70
67
  onResize?: (width: number) => void
71
68
  onScrollStateChange?: (canScroll: boolean) => void
72
69
  defaultScroll?: { align?: ScrollAlign; offset?: number }
73
- items: CarouselItem[]
70
+ // 1 直接子要素 = 1 スライド(react-sandbox 互換)。
71
+ children: ReactNode
74
72
  }>
75
73
 
76
74
  type Direction = 'prev' | 'next'
@@ -149,6 +147,7 @@ const Carousel = forwardRef<CarouselHandlerRef, CarouselProps>(function Render(
149
147
  onResize,
150
148
  onScrollStateChange,
151
149
  defaultScroll: { align = 'left', offset = 0 } = {},
150
+ children,
152
151
  ...props
153
152
  }: CarouselProps,
154
153
  ref,
@@ -159,13 +158,20 @@ const Carousel = forwardRef<CarouselHandlerRef, CarouselProps>(function Render(
159
158
  const snapType = scrollSnap?.type ?? (size === 'S' ? 'mandatory' : 'none')
160
159
  const snapAlign = scrollSnap?.align ?? 'center'
161
160
 
161
+ // 直接子要素 1 つを 1 スライドとして数える。key は子要素の key を引き継ぐ
162
+ // (toArray が付与する接頭辞付き key。無ければ index)。
163
+ const slides = Children.toArray(children)
164
+ const slideKeys = slides.map((slide, i) =>
165
+ isValidElement(slide) && slide.key != null ? slide.key : i,
166
+ )
167
+
162
168
  const scrollerRef = useRef<HTMLDivElement>(null)
163
169
  const [store] = useState(createCarouselStore)
164
170
 
165
171
  const { scrollByStep, onItemResize, resetScroll } = useCarouselScroller(
166
172
  scrollerRef,
167
173
  store,
168
- props.items.length,
174
+ slides.length,
169
175
  { align, offset, scrollStep, onScroll, onResize, onScrollStateChange },
170
176
  )
171
177
 
@@ -217,22 +223,25 @@ const Carousel = forwardRef<CarouselHandlerRef, CarouselProps>(function Render(
217
223
  aria-roledescription="carousel"
218
224
  aria-label="Carousel"
219
225
  >
220
- <div className="charcoal-carousel__viewport">
226
+ {/* フォーカスリングは viewport に描く(理由は index.css の同セレクタ参照) */}
227
+ <div
228
+ className="charcoal-carousel__viewport"
229
+ data-focus-visible={scrollerFocusVisible || undefined}
230
+ >
221
231
  <div
222
232
  {...mergeProps(scrollerFocusProps, keyboardProps)}
223
233
  ref={scrollerRef}
224
234
  className="charcoal-carousel__scroller"
225
235
  tabIndex={0}
226
- data-focus-visible={scrollerFocusVisible || undefined}
227
236
  >
228
- {props.items.map((item, i) => (
237
+ {slides.map((slide, i) => (
229
238
  <CarouselSlide
230
- key={item.id}
239
+ key={slideKeys[i]}
231
240
  index={i}
232
241
  store={store}
233
242
  onResize={onItemResize}
234
243
  >
235
- {item.children}
244
+ {slide}
236
245
  </CarouselSlide>
237
246
  ))}
238
247
  </div>
@@ -260,9 +269,9 @@ const Carousel = forwardRef<CarouselHandlerRef, CarouselProps>(function Render(
260
269
  data-visible={showIndicator}
261
270
  aria-hidden={!showIndicator}
262
271
  >
263
- {props.items.map((item, i) => (
272
+ {slides.map((_, i) => (
264
273
  <CarouselIndicatorItem
265
- key={item.id}
274
+ key={slideKeys[i]}
266
275
  index={i}
267
276
  isActive={i === activeIndex}
268
277
  onSelect={scrollToItem}
@@ -1,16 +1,8 @@
1
- import {
2
- useCallback,
3
- useEffect,
4
- useLayoutEffect,
5
- useRef,
6
- type RefObject,
7
- } from 'react'
1
+ import { useCallback, useEffect, useRef, type RefObject } from 'react'
2
+ import { useIsomorphicLayoutEffect } from '../../_lib/useIsomorphicLayoutEffect'
8
3
  import type { CarouselStore } from './carouselStore'
9
4
  import type { ScrollAlign, ScrollStep } from './index'
10
5
 
11
- const useIsomorphicLayoutEffect =
12
- typeof window !== 'undefined' ? useLayoutEffect : useEffect
13
-
14
6
  const INTERACTION_EVENTS = ['pointerdown', 'wheel', 'touchstart'] as const
15
7
 
16
8
  export type CarouselScrollerOptions = Readonly<{
@@ -74,8 +66,12 @@ export function useCarouselScroller(
74
66
  left = maxScroll + offset
75
67
  break
76
68
  }
77
- // eslint-disable-next-line react-compiler/react-compiler
78
- el.scrollLeft = Math.max(0, Math.min(left, maxScroll))
69
+ // scrollLeft 代入は CSS の scroll-behavior: smooth の対象になり
70
+ // 初期位置決めがアニメーションしてしまうため、instant で確定させる。
71
+ el.scrollTo({
72
+ left: Math.max(0, Math.min(left, maxScroll)),
73
+ behavior: 'instant',
74
+ })
79
75
  }, [scrollerRef, align, offset])
80
76
 
81
77
  // canPrev/canNext: scroll で更新。onScroll もここから発火。itemCount 変化で貼り直し。
@@ -3,7 +3,7 @@
3
3
  line-height: 22px;
4
4
  font-weight: bold;
5
5
  display: flow-root;
6
- color: var(--charcoal-color-text-default);
6
+ color: var(--charcoal-color-text-default-text1);
7
7
  }
8
8
 
9
9
  .charcoal-field-label-required-text {
@@ -3,7 +3,7 @@
3
3
  line-height: 22px;
4
4
  font-weight: bold;
5
5
  display: flow-root;
6
- color: var(--charcoal-color-text-default);
6
+ color: var(--charcoal-color-text-default-text1);
7
7
  }
8
8
 
9
9
  .charcoal-field-label-required-text {
@@ -54,7 +54,7 @@
54
54
 
55
55
  .charcoal-icon-button[data-variant='Default'] {
56
56
  color: var(--charcoal-color-icon-tertiary-default);
57
- background-color: var(--charcoal-color-container-default-a);
57
+ background-color: transparent;
58
58
  }
59
59
 
60
60
  .charcoal-icon-button[data-variant='Default'][data-active='true']:not(:disabled):not([aria-disabled]), .charcoal-icon-button[data-variant='Default'][data-active='true'][aria-disabled='false'] {
@@ -56,7 +56,7 @@
56
56
 
57
57
  &[data-variant='Default'] {
58
58
  color: var(--charcoal-color-icon-tertiary-default);
59
- background-color: var(--charcoal-color-container-default-a);
59
+ background-color: transparent;
60
60
 
61
61
  &[data-active='true']:not(:disabled):not([aria-disabled]),
62
62
  &[data-active='true'][aria-disabled='false'] {
@@ -19,6 +19,38 @@
19
19
  }
20
20
  }
21
21
 
22
+ /* easeOutQuart。duration は useTransitionPresence の MODAL_TRANSITION_DURATION_MS と一致させること。
23
+ transition は entering / exiting に限定し、開いたままテーマ切り替え等で
24
+ 色や transform が変わってもアニメーションしないようにする */
25
+
26
+ .charcoal-modal-background[data-animation] {
27
+ overflow: hidden;
28
+ }
29
+
30
+ .charcoal-modal-background[data-animation='entering'], .charcoal-modal-background[data-animation='exiting'] {
31
+ transition: background-color 400ms cubic-bezier(0.25, 1, 0.5, 1);
32
+ }
33
+
34
+ /* 閉じアニメーション中はクリックを下のページへ通す(transitionend が来ない環境で
35
+ 透明なオーバーレイがタップを奪い続けるのも防ぐ) */
36
+
37
+ .charcoal-modal-background[data-animation='exited'], .charcoal-modal-background[data-animation='exiting'] {
38
+ background-color: transparent;
39
+ pointer-events: none;
40
+ }
41
+
42
+ .charcoal-modal-background[data-animation='entered'] {
43
+ overflow: auto;
44
+ }
45
+
46
+ .charcoal-modal-background[data-animation='entering'] > .charcoal-modal-dialog, .charcoal-modal-background[data-animation='exiting'] > .charcoal-modal-dialog {
47
+ transition: transform 400ms cubic-bezier(0.25, 1, 0.5, 1);
48
+ }
49
+
50
+ .charcoal-modal-background[data-animation='exited'] > .charcoal-modal-dialog, .charcoal-modal-background[data-animation='exiting'] > .charcoal-modal-dialog {
51
+ transform: translateY(100%);
52
+ }
53
+
22
54
  .charcoal-modal-close-button {
23
55
  position: absolute;
24
56
  top: 8px;
@@ -32,4 +64,3 @@
32
64
  font-weight: inherit;
33
65
  font-size: inherit;
34
66
  }
35
-
@@ -20,6 +20,40 @@
20
20
  }
21
21
  }
22
22
 
23
+ /* easeOutQuart。duration は useTransitionPresence の MODAL_TRANSITION_DURATION_MS と一致させること。
24
+ transition は entering / exiting に限定し、開いたままテーマ切り替え等で
25
+ 色や transform が変わってもアニメーションしないようにする */
26
+ .charcoal-modal-background[data-animation] {
27
+ overflow: hidden;
28
+ }
29
+
30
+ .charcoal-modal-background[data-animation='entering'],
31
+ .charcoal-modal-background[data-animation='exiting'] {
32
+ transition: background-color 400ms cubic-bezier(0.25, 1, 0.5, 1);
33
+ }
34
+
35
+ /* 閉じアニメーション中はクリックを下のページへ通す(transitionend が来ない環境で
36
+ 透明なオーバーレイがタップを奪い続けるのも防ぐ) */
37
+ .charcoal-modal-background[data-animation='exited'],
38
+ .charcoal-modal-background[data-animation='exiting'] {
39
+ background-color: transparent;
40
+ pointer-events: none;
41
+ }
42
+
43
+ .charcoal-modal-background[data-animation='entered'] {
44
+ overflow: auto;
45
+ }
46
+
47
+ .charcoal-modal-background[data-animation='entering'] > .charcoal-modal-dialog,
48
+ .charcoal-modal-background[data-animation='exiting'] > .charcoal-modal-dialog {
49
+ transition: transform 400ms cubic-bezier(0.25, 1, 0.5, 1);
50
+ }
51
+
52
+ .charcoal-modal-background[data-animation='exited'] > .charcoal-modal-dialog,
53
+ .charcoal-modal-background[data-animation='exiting'] > .charcoal-modal-dialog {
54
+ transform: translateY(100%);
55
+ }
56
+
23
57
  .charcoal-modal-close-button {
24
58
  position: absolute;
25
59
  top: 8px;