@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.
Files changed (39) 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__/css-variables.test.ts +1 -1
  20. package/src/__tests__/token-v1-compat.test.ts +1 -2
  21. package/src/__tests__/token-v1-remap.test.ts +6 -6
  22. package/src/_lib/useIsomorphicLayoutEffect.ts +7 -0
  23. package/src/components/Carousel/MIGRATION.md +39 -39
  24. package/src/components/Carousel/__snapshots__/index.css.snap +39 -37
  25. package/src/components/Carousel/index.css +45 -44
  26. package/src/components/Carousel/index.story.tsx +87 -68
  27. package/src/components/Carousel/index.test.tsx +68 -47
  28. package/src/components/Carousel/index.tsx +23 -14
  29. package/src/components/Carousel/useCarouselScroller.ts +8 -12
  30. package/src/components/FieldLabel/__snapshots__/index.css.snap +1 -1
  31. package/src/components/FieldLabel/index.css +1 -1
  32. package/src/components/IconButton/__snapshots__/index.css.snap +1 -1
  33. package/src/components/IconButton/index.css +1 -1
  34. package/src/components/Modal/__snapshots__/index.css.snap +32 -1
  35. package/src/components/Modal/index.css +34 -0
  36. package/src/components/Modal/index.test.tsx +200 -0
  37. package/src/components/Modal/index.tsx +48 -78
  38. package/src/components/Modal/useTransitionPresence.ts +95 -0
  39. package/src/index.ts +0 -1
@@ -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;
@@ -0,0 +1,200 @@
1
+ import { useState } from 'react'
2
+ import { render, fireEvent, act, cleanup } from '@testing-library/react'
3
+ import { OverlayProvider } from 'react-aria'
4
+ import { vi, describe, it, expect, afterEach } from 'vitest'
5
+ import Modal from '.'
6
+ import { MODAL_TRANSITION_DURATION_MS } from './useTransitionPresence'
7
+
8
+ const MOBILE_WIDTH = 500
9
+ const DESKTOP_WIDTH = 1024
10
+
11
+ function setWindowWidth(width: number) {
12
+ Object.defineProperty(window, 'innerWidth', {
13
+ writable: true,
14
+ configurable: true,
15
+ value: width,
16
+ })
17
+ fireEvent(window, new Event('resize'))
18
+ }
19
+
20
+ // jsdom は CSS transition を実行しないので transitionend を手動で発火させる
21
+ function fireTransitionEnd(el: Element, propertyName: string) {
22
+ fireEvent(
23
+ el,
24
+ Object.assign(new Event('transitionend', { bubbles: true }), {
25
+ propertyName,
26
+ }),
27
+ )
28
+ }
29
+
30
+ function getBackground() {
31
+ return document.querySelector('.charcoal-modal-background')
32
+ }
33
+
34
+ function getDialog() {
35
+ return document.querySelector('.charcoal-modal-dialog')
36
+ }
37
+
38
+ function TestApp({ defaultOpen = false }: { defaultOpen?: boolean }) {
39
+ const [isOpen, setIsOpen] = useState(defaultOpen)
40
+ return (
41
+ <OverlayProvider>
42
+ <button data-testid="open" onClick={() => setIsOpen(true)}>
43
+ open
44
+ </button>
45
+ <button data-testid="close" onClick={() => setIsOpen(false)}>
46
+ close
47
+ </button>
48
+ <Modal
49
+ title="test modal"
50
+ isOpen={isOpen}
51
+ onClose={() => setIsOpen(false)}
52
+ bottomSheet
53
+ >
54
+ <div>modal content</div>
55
+ </Modal>
56
+ </OverlayProvider>
57
+ )
58
+ }
59
+
60
+ function renderModal({ defaultOpen = false } = {}) {
61
+ const result = render(<TestApp defaultOpen={defaultOpen} />)
62
+ const open = () =>
63
+ fireEvent.click(result.getByTestId('open', { exact: true }))
64
+ const close = () =>
65
+ fireEvent.click(result.getByTestId('close', { exact: true }))
66
+ return { open, close }
67
+ }
68
+
69
+ afterEach(() => {
70
+ cleanup()
71
+ vi.useRealTimers()
72
+ setWindowWidth(DESKTOP_WIDTH)
73
+ })
74
+
75
+ describe('Modal (bottom sheet transition)', () => {
76
+ it('desktop: mounts and unmounts immediately without data-animation', () => {
77
+ setWindowWidth(DESKTOP_WIDTH)
78
+ const { open, close } = renderModal()
79
+
80
+ expect(getDialog()).not.toBeInTheDocument()
81
+
82
+ open()
83
+ expect(getDialog()).toBeInTheDocument()
84
+ expect(getBackground()).not.toHaveAttribute('data-animation')
85
+
86
+ close()
87
+ expect(getDialog()).not.toBeInTheDocument()
88
+ })
89
+
90
+ it('mobile: enters via transition and settles on the dialog transform transitionend only', () => {
91
+ setWindowWidth(MOBILE_WIDTH)
92
+ const { open } = renderModal()
93
+
94
+ open()
95
+ expect(getDialog()).toBeInTheDocument()
96
+ expect(getBackground()).toHaveAttribute('data-animation', 'entering')
97
+
98
+ // 背景の background-color の transitionend では entered にならない
99
+ fireTransitionEnd(getBackground() as Element, 'background-color')
100
+ expect(getBackground()).toHaveAttribute('data-animation', 'entering')
101
+
102
+ fireTransitionEnd(getDialog() as Element, 'transform')
103
+ expect(getBackground()).toHaveAttribute('data-animation', 'entered')
104
+ })
105
+
106
+ it('mobile: keeps the DOM mounted while exiting, unmounts after transitionend', () => {
107
+ setWindowWidth(MOBILE_WIDTH)
108
+ const { open, close } = renderModal()
109
+
110
+ open()
111
+ fireTransitionEnd(getDialog() as Element, 'transform')
112
+
113
+ close()
114
+ expect(getBackground()).toHaveAttribute('data-animation', 'exiting')
115
+ expect(getDialog()).toBeInTheDocument()
116
+
117
+ fireTransitionEnd(getDialog() as Element, 'transform')
118
+ expect(getDialog()).not.toBeInTheDocument()
119
+ })
120
+
121
+ it('mobile: closing mid-enter goes straight to exiting without unmounting', () => {
122
+ setWindowWidth(MOBILE_WIDTH)
123
+ const { open, close } = renderModal()
124
+
125
+ open()
126
+ expect(getBackground()).toHaveAttribute('data-animation', 'entering')
127
+
128
+ // enter の transitionend を待たずに閉じる
129
+ close()
130
+ expect(getBackground()).toHaveAttribute('data-animation', 'exiting')
131
+ expect(getDialog()).toBeInTheDocument()
132
+ })
133
+
134
+ it('mobile: reopening mid-exit goes straight to entering, the element never leaves the document', () => {
135
+ setWindowWidth(MOBILE_WIDTH)
136
+ const { open, close } = renderModal()
137
+
138
+ open()
139
+ fireTransitionEnd(getDialog() as Element, 'transform')
140
+ const dialog = getDialog()
141
+
142
+ close()
143
+ expect(getBackground()).toHaveAttribute('data-animation', 'exiting')
144
+
145
+ // exit の transitionend を待たずに開き直す
146
+ open()
147
+ expect(getBackground()).toHaveAttribute('data-animation', 'entering')
148
+ // 同じ要素のまま(アンマウント → 再マウントが起きていない)
149
+ expect(getDialog()).toBe(dialog)
150
+ })
151
+
152
+ it('mobile: a stale transitionend after reopening settles to entered instead of unmounting', () => {
153
+ setWindowWidth(MOBILE_WIDTH)
154
+ const { open, close } = renderModal()
155
+
156
+ open()
157
+ fireTransitionEnd(getDialog() as Element, 'transform')
158
+ close()
159
+ open()
160
+ expect(getBackground()).toHaveAttribute('data-animation', 'entering')
161
+
162
+ // 開き直しの直後に、キャンセルされた exit 側の transitionend が届く
163
+ fireTransitionEnd(getDialog() as Element, 'transform')
164
+ expect(getDialog()).toBeInTheDocument()
165
+ expect(getBackground()).toHaveAttribute('data-animation', 'entered')
166
+ })
167
+
168
+ it('mobile: unmounts via the timeout fallback when transitionend never fires', () => {
169
+ vi.useFakeTimers()
170
+ setWindowWidth(MOBILE_WIDTH)
171
+ const { open, close } = renderModal()
172
+
173
+ open()
174
+ fireTransitionEnd(getDialog() as Element, 'transform')
175
+
176
+ close()
177
+ expect(getDialog()).toBeInTheDocument()
178
+
179
+ act(() => {
180
+ vi.advanceTimersByTime(MODAL_TRANSITION_DURATION_MS + 100)
181
+ })
182
+ expect(getDialog()).not.toBeInTheDocument()
183
+ })
184
+
185
+ it('resizing to desktop while open drops the animation, then closes immediately', () => {
186
+ setWindowWidth(MOBILE_WIDTH)
187
+ const { open, close } = renderModal()
188
+
189
+ open()
190
+ fireTransitionEnd(getDialog() as Element, 'transform')
191
+ expect(getBackground()).toHaveAttribute('data-animation', 'entered')
192
+
193
+ setWindowWidth(DESKTOP_WIDTH)
194
+ expect(getDialog()).toBeInTheDocument()
195
+ expect(getBackground()).not.toHaveAttribute('data-animation')
196
+
197
+ close()
198
+ expect(getDialog()).not.toBeInTheDocument()
199
+ })
200
+ })
@@ -1,7 +1,6 @@
1
1
  import { useContext, forwardRef, memo } from 'react'
2
2
  import * as React from 'react'
3
3
  import type { AriaDialogProps } from 'react-aria/useDialog'
4
- import { animated, useTransition, easings } from '@react-spring/web'
5
4
  import Button, { ButtonProps } from '../Button'
6
5
  import IconButton, { IconButtonProps } from '../IconButton'
7
6
  import { Dialog } from './Dialog'
@@ -11,6 +10,7 @@ import {
11
10
  useCharcoalModalOverlay,
12
11
  useWindowWidth,
13
12
  } from './useCustomModalOverlay'
13
+ import { useTransitionPresence } from './useTransitionPresence'
14
14
 
15
15
  import './index.css'
16
16
 
@@ -101,31 +101,11 @@ const Modal = forwardRef<HTMLDivElement, ModalProps>(function ModalInner(
101
101
  const transitionEnabled = isMobile && bottomSheet !== false
102
102
  const showDismiss = !isMobile || bottomSheet !== true
103
103
 
104
- const transition = useTransition(isOpen, {
105
- from: {
106
- transform: 'translateY(100%)',
107
- backgroundColor: 'rgba(0, 0, 0, 0)',
108
- overflow: 'hidden',
109
- },
110
- enter: {
111
- transform: 'translateY(0%)',
112
- backgroundColor: 'rgba(0, 0, 0, 0.4)',
113
- },
114
- update: {
115
- overflow: 'auto',
116
- },
117
- leave: {
118
- transform: 'translateY(100%)',
119
- backgroundColor: 'rgba(0, 0, 0, 0)',
120
- overflow: 'hidden',
121
- },
122
- config: transitionEnabled
123
- ? { duration: 400, easing: easings.easeOutQuart }
124
- : { duration: 0 },
125
- })
126
-
127
104
  const bgRef = React.useRef<HTMLDivElement>(null)
128
105
 
106
+ const { isPresent, animationState, handleTransitionEnd } =
107
+ useTransitionPresence(isOpen, transitionEnabled, bgRef, ref)
108
+
129
109
  const handleClick = React.useCallback(
130
110
  (e: React.MouseEvent<HTMLDivElement, MouseEvent>) => {
131
111
  if (e.currentTarget === e.target) {
@@ -135,64 +115,54 @@ const Modal = forwardRef<HTMLDivElement, ModalProps>(function ModalInner(
135
115
  [onClose],
136
116
  )
137
117
 
138
- return transition(
139
- ({ backgroundColor, overflow, transform }, item) =>
140
- item && (
141
- <Overlay portalContainer={portalContainer}>
142
- {/* https://github.com/pmndrs/react-spring/issues/1515 */}
143
- {/* eslint-disable-next-line @typescript-eslint/ban-ts-comment */}
144
- {/* @ts-ignore */}
145
- <animated.div
146
- className="charcoal-modal-background"
147
- ref={bgRef}
148
- {...underlayProps}
149
- style={
150
- transitionEnabled
151
- ? { backgroundColor, overflow, zIndex }
152
- : { zIndex }
153
- }
154
- data-bottom-sheet={bottomSheet}
155
- onClick={handleClick}
118
+ if (!isPresent) {
119
+ return null
120
+ }
121
+
122
+ return (
123
+ <Overlay portalContainer={portalContainer}>
124
+ <div
125
+ className="charcoal-modal-background"
126
+ ref={bgRef}
127
+ {...underlayProps}
128
+ style={{ zIndex }}
129
+ data-bottom-sheet={bottomSheet}
130
+ data-animation={transitionEnabled ? animationState : undefined}
131
+ onClick={handleClick}
132
+ onTransitionEnd={handleTransitionEnd}
133
+ >
134
+ <ModalBackgroundContext.Provider value={bgRef.current}>
135
+ <Dialog
136
+ ref={ref}
137
+ {...modalProps}
138
+ size={size}
139
+ bottomSheet={bottomSheet}
140
+ className={className}
156
141
  >
157
- <ModalBackgroundContext.Provider value={bgRef.current}>
158
- {/* https://github.com/pmndrs/react-spring/issues/1515 */}
159
- {/* eslint-disable-next-line @typescript-eslint/ban-ts-comment */}
160
- {/* @ts-ignore */}
161
- <AnimatedDialog
162
- ref={ref}
163
- {...modalProps}
164
- style={transitionEnabled ? { transform } : {}}
165
- size={size}
166
- bottomSheet={bottomSheet}
167
- className={className}
168
- >
169
- <ModalContext.Provider
170
- value={{
171
- titleProps: {},
172
- title,
173
- close: onClose,
174
- showDismiss,
175
- bottomSheet,
176
- }}
177
- >
178
- {children}
179
- {isDismissable === true && (
180
- <ModalCloseButton
181
- aria-label={closeButtonAriaLabel}
182
- onClick={onClose}
183
- />
184
- )}
185
- </ModalContext.Provider>
186
- </AnimatedDialog>
187
- </ModalBackgroundContext.Provider>
188
- </animated.div>
189
- </Overlay>
190
- ),
142
+ <ModalContext.Provider
143
+ value={{
144
+ titleProps: {},
145
+ title,
146
+ close: onClose,
147
+ showDismiss,
148
+ bottomSheet,
149
+ }}
150
+ >
151
+ {children}
152
+ {isDismissable === true && (
153
+ <ModalCloseButton
154
+ aria-label={closeButtonAriaLabel}
155
+ onClick={onClose}
156
+ />
157
+ )}
158
+ </ModalContext.Provider>
159
+ </Dialog>
160
+ </ModalBackgroundContext.Provider>
161
+ </div>
162
+ </Overlay>
191
163
  )
192
164
  })
193
165
 
194
- const AnimatedDialog = animated(Dialog)
195
-
196
166
  export default memo(Modal)
197
167
 
198
168
  export const ModalContext = React.createContext<{