paris 0.21.2 → 0.22.0

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 (32) hide show
  1. package/CHANGELOG.md +39 -0
  2. package/package.json +1 -1
  3. package/src/helpers/OpenChangeEffect.tsx +21 -0
  4. package/src/helpers/useControllableState.test.ts +88 -0
  5. package/src/helpers/useControllableState.ts +59 -0
  6. package/src/stories/accordionselect/AccordionSelect.test.tsx +72 -0
  7. package/src/stories/accordionselect/AccordionSelect.tsx +22 -12
  8. package/src/stories/checkbox/Checkbox.test.tsx +53 -0
  9. package/src/stories/checkbox/Checkbox.tsx +21 -6
  10. package/src/stories/combobox/Combobox.test.tsx +111 -0
  11. package/src/stories/combobox/Combobox.tsx +192 -137
  12. package/src/stories/drawer/Drawer.module.scss +56 -15
  13. package/src/stories/drawer/Drawer.stories.tsx +287 -109
  14. package/src/stories/drawer/Drawer.test.tsx +486 -11
  15. package/src/stories/drawer/Drawer.tsx +366 -240
  16. package/src/stories/drawer/DrawerActions.tsx +28 -0
  17. package/src/stories/drawer/DrawerBottomPanel.tsx +55 -0
  18. package/src/stories/drawer/DrawerContext.tsx +31 -0
  19. package/src/stories/drawer/DrawerPage.tsx +37 -0
  20. package/src/stories/drawer/DrawerPageContext.tsx +35 -0
  21. package/src/stories/drawer/DrawerPaginationContext.tsx +22 -0
  22. package/src/stories/drawer/DrawerProgressBar.tsx +72 -0
  23. package/src/stories/drawer/DrawerSlotContext.tsx +172 -0
  24. package/src/stories/drawer/DrawerTitle.tsx +35 -0
  25. package/src/stories/drawer/index.ts +9 -0
  26. package/src/stories/menu/Menu.test.tsx +43 -0
  27. package/src/stories/menu/Menu.tsx +13 -2
  28. package/src/stories/popover/Popover.tsx +8 -5
  29. package/src/stories/select/Select.module.scss +1 -1
  30. package/src/stories/select/Select.test.tsx +108 -0
  31. package/src/stories/select/Select.tsx +121 -92
  32. package/src/test/render.tsx +2 -2
@@ -3,8 +3,17 @@
3
3
  import { Dialog, DialogPanel, DialogTitle, Transition, TransitionChild } from '@headlessui/react';
4
4
  import type { CSSLength } from '@ssh/csstypes';
5
5
  import { clsx } from 'clsx';
6
- import type { ComponentPropsWithoutRef, ReactNode } from 'react';
7
- import { useMemo, useState } from 'react';
6
+ import { motion } from 'framer-motion';
7
+ import {
8
+ Children,
9
+ type ComponentPropsWithoutRef,
10
+ type ReactNode,
11
+ useCallback,
12
+ useEffect,
13
+ useMemo,
14
+ useRef,
15
+ useState,
16
+ } from 'react';
8
17
  import { Button } from '../button';
9
18
  import { ChevronLeft, ChevronRight, Close, Icon } from '../icon';
10
19
  import type { PaginationState } from '../pagination';
@@ -13,9 +22,24 @@ import { RemoveFromDOM } from '../utility/RemoveFromDOM';
13
22
  import { TextWhenString } from '../utility/TextWhenString';
14
23
  import { VisuallyHidden } from '../utility/VisuallyHidden';
15
24
  import styles from './Drawer.module.scss';
25
+ import { DrawerProvider } from './DrawerContext';
26
+ import { type DrawerPageProps, isDrawerPageElement } from './DrawerPage';
27
+ import { DrawerPageProvider } from './DrawerPageContext';
28
+ import { DrawerPaginationProvider } from './DrawerPaginationContext';
29
+ import { DrawerProgressBar, type DrawerProgressBarStyleProps } from './DrawerProgressBar';
30
+ import { DrawerSlotProvider, useDrawerSlotContext } from './DrawerSlotContext';
16
31
 
17
32
  export const DrawerSizePresets = ['content', 'default', 'full', 'fullWithMargin', 'fullOnMobile'] as const;
18
33
 
34
+ export type DrawerPageTransition = 'none' | 'crossfade' | 'slide';
35
+
36
+ /** Extract the page ID from a child element (DrawerPage or legacy div-with-key). */
37
+ const getChildPageID = (child: ReactNode): string | null => {
38
+ if (isDrawerPageElement(child)) return (child as React.ReactElement<DrawerPageProps>).props.id;
39
+ if (child != null && typeof child === 'object' && 'key' in child) return child.key as string;
40
+ return null;
41
+ };
42
+
19
43
  export type DrawerProps<T extends string[] | readonly string[] = string[]> = {
20
44
  /**
21
45
  * The dialog's open state.
@@ -40,8 +64,6 @@ export type DrawerProps<T extends string[] | readonly string[] = string[]> = {
40
64
  *
41
65
  * If you're hiding the title to add a custom header, you may also want to hide the close button and render your own by using the `hideCloseButton` prop.
42
66
  *
43
- * When pagination is enabled, the title is always hidden regardless of this prop.
44
- *
45
67
  * @default false
46
68
  */
47
69
  hideTitle?: boolean;
@@ -51,17 +73,6 @@ export type DrawerProps<T extends string[] | readonly string[] = string[]> = {
51
73
  * @default false
52
74
  */
53
75
  hideCloseButton?: boolean;
54
- /**
55
- * An optional panel that will be rendered at the bottom of the Drawer. This is useful for adding a footer to the Drawer with actions.
56
- */
57
- bottomPanel?: ReactNode;
58
- /**
59
- * Whether the bottom panel should have default padding. Set to `false` for edge-to-edge content like dividers or multi-section layouts.
60
- *
61
- * @default true
62
- */
63
- bottomPanelPadding?: boolean;
64
-
65
76
  /**
66
77
  * An optional area that will be rendered at the top of the Drawer next to the title. This is useful for adding actions to the Drawer. Recommended to use {@link Menu} for an action menu.
67
78
  */
@@ -91,9 +102,9 @@ export type DrawerProps<T extends string[] | readonly string[] = string[]> = {
91
102
  * const pagination = usePagination<['step1', 'step2', 'step3']>();
92
103
  * // ...
93
104
  * <Drawer pagination={pagination}>
94
- * <div key="step1">Step 1</div>
95
- * <div key="step2">Step 2</div>
96
- * <div key="step3">Step 3</div>
105
+ * <DrawerPage id="step1">Step 1</DrawerPage>
106
+ * <DrawerPage id="step2">Step 2</DrawerPage>
107
+ * <DrawerPage id="step3">Step 3</DrawerPage>
97
108
  * </Drawer>
98
109
  *```
99
110
  *
@@ -102,6 +113,32 @@ export type DrawerProps<T extends string[] | readonly string[] = string[]> = {
102
113
  * @default false
103
114
  */
104
115
  pagination?: PaginationState<T>;
116
+ /**
117
+ * The page transition animation style for paginated drawers.
118
+ *
119
+ * - `'none'` — instant page switch
120
+ * - `'crossfade'` — opacity crossfade between pages (default)
121
+ * - `'slide'` — direction-aware horizontal slide (Framer Motion)
122
+ *
123
+ * @default 'crossfade'
124
+ */
125
+ pageTransition?: DrawerPageTransition;
126
+ /**
127
+ * Show a progress bar at the top of the bottom panel. Requires `pagination` to be set.
128
+ * The bar auto-fills based on the current page position.
129
+ *
130
+ * Pass `true` for defaults, or an object with `fill`, `track`, and `height` overrides.
131
+ *
132
+ * For explicit percentage control, use `<DrawerProgressBar value={75} />` directly instead.
133
+ *
134
+ * @default false
135
+ */
136
+ progressBar?: boolean | DrawerProgressBarStyleProps;
137
+ /**
138
+ * Callback fired after the drawer's close animation completes.
139
+ * Use this to reset forms, clear state, and clean up without visual glitches.
140
+ */
141
+ onAfterClose?: () => void;
105
142
  /**
106
143
  * The overlay style of the Drawer, either 'grey' or 'blur'.
107
144
  *
@@ -122,7 +159,7 @@ export type DrawerProps<T extends string[] | readonly string[] = string[]> = {
122
159
  titleBarButtons?: ComponentPropsWithoutRef<'div'>;
123
160
  content?: ComponentPropsWithoutRef<'div'>;
124
161
  contentChildren?: ComponentPropsWithoutRef<'div'>;
125
- contentChildrenChildren: ComponentPropsWithoutRef<'div'>;
162
+ contentChildrenChildren?: ComponentPropsWithoutRef<'div'>;
126
163
  bottomPanelSpacer?: ComponentPropsWithoutRef<'div'>;
127
164
  bottomPanel?: ComponentPropsWithoutRef<'div'>;
128
165
  bottomPanelContent?: ComponentPropsWithoutRef<'div'>;
@@ -141,268 +178,357 @@ export type DrawerProps<T extends string[] | readonly string[] = string[]> = {
141
178
  * ```
142
179
  * @constructor
143
180
  */
144
- export const Drawer = <T extends string[] | readonly string[] = string[]>({
145
- isOpen = false,
181
+ export const Drawer = <T extends string[] | readonly string[] = string[]>(props: DrawerProps<T>) => {
182
+ const { isOpen = false, onClose = () => {}, onAfterClose } = props;
183
+
184
+ const handleClose = useCallback(() => onClose(false), [onClose]);
185
+
186
+ const hasBeenOpen = useRef(false);
187
+ useEffect(() => {
188
+ if (isOpen) hasBeenOpen.current = true;
189
+ }, [isOpen]);
190
+
191
+ const handleAfterLeave = useCallback(() => {
192
+ if (hasBeenOpen.current) {
193
+ onAfterClose?.();
194
+ }
195
+ }, [onAfterClose]);
196
+
197
+ return (
198
+ <Transition show={isOpen} afterLeave={handleAfterLeave}>
199
+ <Dialog
200
+ as="div"
201
+ className={clsx(styles.root, props.overrides?.dialog?.className)}
202
+ onClose={onClose}
203
+ {...props.overrides?.dialog}
204
+ role="dialog"
205
+ >
206
+ <DrawerProvider close={handleClose} isOpen={isOpen}>
207
+ <DrawerPaginationProvider pagination={props.pagination ?? null}>
208
+ <DrawerSlotProvider>
209
+ <DrawerInner {...props} />
210
+ </DrawerSlotProvider>
211
+ </DrawerPaginationProvider>
212
+ </DrawerProvider>
213
+ </Dialog>
214
+ </Transition>
215
+ );
216
+ };
217
+
218
+ /** Internal component that renders inside all providers so it can consume slot context. */
219
+ const DrawerInner = <T extends string[] | readonly string[] = string[]>({
146
220
  onClose = () => {},
147
221
  title,
148
222
  hideTitle = false,
149
223
  hideCloseButton = false,
150
- bottomPanel,
151
- bottomPanelPadding = true,
152
224
  from = 'right',
153
225
  size = 'default',
154
226
  pagination,
227
+ pageTransition = 'crossfade',
228
+ progressBar,
155
229
  overlayStyle = 'grey',
156
230
  additionalActions,
157
231
  children,
158
232
  overrides,
159
233
  }: DrawerProps<T>) => {
160
- // Check if the drawer is on the x-axis.
161
- const xAxisDrawer = useMemo(() => ['left', 'right'].includes(from), [from]);
234
+ const slotContext = useDrawerSlotContext();
162
235
 
163
- // Check if the size is a preset.
164
- const sizeIsPreset = useMemo(() => (DrawerSizePresets as readonly string[]).includes(size), [size]);
236
+ const xAxisDrawer = from === 'left' || from === 'right';
237
+ const sizeIsPreset = (DrawerSizePresets as readonly string[]).includes(size);
238
+ const isPaginated = Boolean(pagination);
239
+ const hasAdditionalActions = Boolean(additionalActions);
165
240
 
166
- // Check if pagination is enabled.
167
- const isPaginated = useMemo(() => Boolean(pagination), [pagination]);
241
+ const showBottomPanel = (slotContext?.hasAnyBottomPanelSlot ?? false) || (slotContext?.hasProgressBar ?? false);
168
242
 
169
- const hasAdditionalActions = useMemo(() => Boolean(additionalActions), [additionalActions]);
243
+ // Sync spacer height with absolute-positioned bottom panel so content doesn't get clipped
244
+ const bottomPanelElRef = useRef<HTMLDivElement | null>(null);
245
+ const spacerRef = useRef<HTMLDivElement | null>(null);
246
+ useEffect(() => {
247
+ if (!showBottomPanel) return;
248
+ const panel = bottomPanelElRef.current;
249
+ const spacer = spacerRef.current;
250
+ if (!panel || !spacer) return;
251
+ const observer = new ResizeObserver(([entry]) => {
252
+ spacer.style.height = `${entry.contentRect.height}px`;
253
+ });
254
+ observer.observe(panel);
255
+ return () => observer.disconnect();
256
+ }, [showBottomPanel]);
170
257
 
171
- const [loadedPage, setLoadedPage] = useState<string | null>(pagination?.history[0] || null);
258
+ const [loadedPage, setLoadedPage] = useState<string | null>(pagination?.currentPage ?? null);
172
259
 
173
- // const bottomPanelRef = useRef<HTMLDivElement>(null);
174
- // const { width = 0, height = 0 } = useResizeObserver({
175
- // ref: bottomPanelRef,
176
- // box: 'border-box',
177
- // });
178
- //
179
- // useEffect(() => {
180
- // console.log(bottomPanelRef.current);
181
- // }, [bottomPanelRef.current]);
260
+ const pageEntries = useMemo(() => {
261
+ if (!isPaginated || !pagination || !children) return null;
262
+ const childArray = Array.isArray(children) ? children : Children.toArray(children);
263
+ return childArray
264
+ .map((child) => ({ id: getChildPageID(child), child }))
265
+ .filter((entry): entry is { id: string; child: ReactNode } => entry.id !== null);
266
+ }, [isPaginated, pagination, children]);
182
267
 
183
- // Decide what children to render.
184
- const currentChild: ReactNode = useMemo(() => {
185
- // If no children are provided, render nothing.
186
- if (!children) {
187
- return <></>;
188
- }
268
+ const activePageIndex = pageEntries?.findIndex((p) => p.id === pagination?.currentPage) ?? -1;
269
+
270
+ const paginatedContent = useMemo(() => {
271
+ if (!pageEntries || !pagination) return null;
272
+
273
+ switch (pageTransition) {
274
+ case 'none':
275
+ return pageEntries.map((page) => {
276
+ const isActive = pagination.currentPage === page.id;
277
+ return (
278
+ <DrawerPageProvider key={page.id} isActive={isActive} pageID={page.id}>
279
+ <div
280
+ style={{ display: isActive ? undefined : 'none' }}
281
+ className={overrides?.contentChildrenChildren?.className}
282
+ >
283
+ {page.child}
284
+ </div>
285
+ </DrawerPageProvider>
286
+ );
287
+ });
189
288
 
190
- // If pagination is enabled, and multiple children are provided, render the currently active child by matching its key against `pagination.currentPage`.
191
- if (pagination && Array.isArray(children) && children.length > 0) {
192
- const found = children.find((child) => {
193
- if (!(child && typeof child === 'object' && 'key' in child)) {
194
- console.warn(
195
- 'Drawer: Pagination is enabled, but the following child is missing a `key` prop. Pagination will likely not work as expected and this child will never be rendered.',
196
- child,
289
+ case 'crossfade':
290
+ return pageEntries.map((page) => {
291
+ const isActive = pagination.currentPage === page.id && loadedPage === page.id;
292
+ return (
293
+ <DrawerPageProvider key={page.id} isActive={isActive} pageID={page.id}>
294
+ <Transition
295
+ show={isActive}
296
+ as="div"
297
+ enter={styles.paginationEnter}
298
+ enterFrom={styles.enterFromOpacity}
299
+ enterTo={styles.enterToOpacity}
300
+ leave={styles.paginationLeave}
301
+ leaveFrom={styles.leaveFromOpacity}
302
+ leaveTo={styles.leaveToOpacity}
303
+ afterLeave={() => setLoadedPage(pagination.currentPage)}
304
+ className={overrides?.contentChildrenChildren?.className}
305
+ >
306
+ {page.child}
307
+ </Transition>
308
+ </DrawerPageProvider>
197
309
  );
198
- return false;
199
- }
200
- return child.key === pagination.currentPage;
201
- });
202
- if (found) {
203
- return found;
310
+ });
311
+
312
+ case 'slide':
313
+ return (
314
+ <div className={clsx(styles.pageStack, styles.pageStackClip)}>
315
+ {pageEntries.map((page, i) => {
316
+ const isActive = pagination.currentPage === page.id;
317
+ const offset = (i - activePageIndex) * 100;
318
+ return (
319
+ <DrawerPageProvider key={page.id} isActive={isActive} pageID={page.id}>
320
+ <motion.div
321
+ initial={false}
322
+ animate={{ x: `${offset}%` }}
323
+ transition={{
324
+ type: 'tween',
325
+ duration: 0.3,
326
+ ease: [0.32, 0.72, 0, 1],
327
+ }}
328
+ className={clsx(
329
+ styles.pageStackItem,
330
+ overrides?.contentChildrenChildren?.className,
331
+ )}
332
+ data-active={isActive}
333
+ >
334
+ {page.child}
335
+ </motion.div>
336
+ </DrawerPageProvider>
337
+ );
338
+ })}
339
+ </div>
340
+ );
341
+
342
+ default: {
343
+ const _exhaustive: never = pageTransition;
344
+ return _exhaustive;
204
345
  }
205
346
  }
347
+ }, [
348
+ pageEntries,
349
+ pagination,
350
+ pageTransition,
351
+ loadedPage,
352
+ activePageIndex,
353
+ overrides?.contentChildrenChildren?.className,
354
+ ]);
206
355
 
207
- // As a fallback, render all children.
208
- return children;
209
- }, [children, pagination]);
356
+ /** Non-page children (e.g. Drawer-level DrawerBottomPanel) that should render alongside paginated content. */
357
+ const nonPageChildren = useMemo(() => {
358
+ if (!isPaginated || !children) return null;
359
+ const childArray = Array.isArray(children) ? children : Children.toArray(children);
360
+ return childArray.filter((child) => getChildPageID(child) === null);
361
+ }, [isPaginated, children]);
210
362
 
211
363
  return (
212
- <Transition show={isOpen}>
213
- <Dialog
214
- as="div"
215
- className={clsx(styles.root, overrides?.dialog?.className)}
216
- onClose={onClose}
217
- {...overrides?.dialog}
218
- role="dialog"
364
+ <>
365
+ <div
366
+ className={clsx(
367
+ overlayStyle === 'blur' && styles.overlayBlurContainer,
368
+ overlayStyle === 'grey' && styles.overlayGreyContainer,
369
+ overrides?.overlay?.className,
370
+ )}
219
371
  >
220
- <div
221
- className={clsx(
222
- overlayStyle === 'blur' && styles.overlayBlurContainer,
223
- overlayStyle === 'grey' && styles.overlayGreyContainer,
224
- overrides?.overlay?.className,
225
- )}
372
+ <TransitionChild
373
+ enter={styles.enter}
374
+ enterFrom={styles.enterFrom}
375
+ enterTo={styles.enterTo}
376
+ leave={styles.leave}
377
+ leaveFrom={styles.leaveFrom}
378
+ leaveTo={styles.leaveTo}
226
379
  >
227
- <TransitionChild
228
- enter={styles.enter}
229
- enterFrom={styles.enterFrom}
230
- enterTo={styles.enterTo}
231
- leave={styles.leave}
232
- leaveFrom={styles.leaveFrom}
233
- leaveTo={styles.leaveTo}
234
- >
235
- <div
236
- className={clsx(
237
- styles.overlay,
238
- overlayStyle === 'blur' && styles.overlayBlur,
239
- overlayStyle === 'grey' && styles.overlayGrey,
240
- )}
241
- />
242
- </TransitionChild>
243
- </div>
380
+ <div
381
+ className={clsx(
382
+ styles.overlay,
383
+ overlayStyle === 'blur' && styles.overlayBlur,
384
+ overlayStyle === 'grey' && styles.overlayGrey,
385
+ )}
386
+ />
387
+ </TransitionChild>
388
+ </div>
244
389
 
245
- <div
246
- className={clsx(
247
- styles.panelContainer,
248
- styles[`from-${from}`],
249
- { [styles[`size-${size}`]]: sizeIsPreset },
250
- overrides?.panelContainer?.className,
251
- )}
252
- style={
253
- !sizeIsPreset
254
- ? {
255
- [xAxisDrawer ? 'width' : 'height']: size,
256
- ...overrides?.panelContainer?.style,
257
- }
258
- : overrides?.panelContainer?.style
259
- }
260
- {...overrides?.panelContainer}
390
+ <div
391
+ className={clsx(
392
+ styles.panelContainer,
393
+ styles[`from-${from}`],
394
+ { [styles[`size-${size}`]]: sizeIsPreset },
395
+ overrides?.panelContainer?.className,
396
+ )}
397
+ style={
398
+ !sizeIsPreset
399
+ ? {
400
+ [xAxisDrawer ? 'width' : 'height']: size,
401
+ ...overrides?.panelContainer?.style,
402
+ }
403
+ : overrides?.panelContainer?.style
404
+ }
405
+ {...overrides?.panelContainer}
406
+ >
407
+ <TransitionChild
408
+ enter={styles.enter}
409
+ enterFrom={styles.enterFrom}
410
+ enterTo={styles.enterTo}
411
+ leave={styles.leave}
412
+ leaveFrom={styles.leaveFrom}
413
+ leaveTo={styles.leaveTo}
261
414
  >
262
- <TransitionChild
263
- enter={styles.enter}
264
- enterFrom={styles.enterFrom}
265
- enterTo={styles.enterTo}
266
- leave={styles.leave}
267
- leaveFrom={styles.leaveFrom}
268
- leaveTo={styles.leaveTo}
269
- >
270
- <DialogPanel
271
- className={clsx(styles.panel, styles[`from-${from}`], overrides?.panel?.className)}
272
- >
273
- {/* Dialog title bar */}
274
- <div className={clsx(styles.titleBar, overrides?.titleBar?.className)}>
275
- <div className={clsx(styles.titleArea, overrides?.titleArea?.className)}>
276
- <RemoveFromDOM
277
- // Hide when pagination is not enabled.
278
- when={!isPaginated}
279
- >
280
- <div className={clsx(styles.paginationButtons)}>
281
- <Button
282
- className={clsx(styles.navButton)}
283
- size="medium"
284
- kind="tertiary"
285
- shape="circle"
286
- onClick={() => pagination?.back()}
287
- disabled={!pagination?.canGoBack()}
288
- startEnhancer={<Icon icon={ChevronLeft} size={16} />}
289
- >
290
- Go to previous page in this modal
291
- </Button>
292
- <Button
293
- className={clsx(styles.navButton)}
294
- size="medium"
295
- kind="tertiary"
296
- shape="circle"
297
- onClick={() => pagination?.forward()}
298
- disabled={!pagination?.canGoForward()}
299
- startEnhancer={<Icon icon={ChevronRight} size={16} />}
300
- >
301
- Go to next page in this modal
302
- </Button>
303
- </div>
304
- </RemoveFromDOM>
305
- <VisuallyHidden
306
- // Hide when requested, or when pagination is enabled (the title isn't relevant to any specific page).
307
- when={hideTitle}
308
- >
309
- <DialogTitle as="h2" className={styles.titleTextContainer}>
310
- <TextWhenString kind="paragraphSmall" weight="medium">
311
- {title}
312
- </TextWhenString>
313
- </DialogTitle>
314
- </VisuallyHidden>
315
- </div>
316
- <div className={clsx(styles.titleBarButtons, overrides?.titleBarButtons?.className)}>
317
- {/* Action Menu */}
318
- <RemoveFromDOM when={!hasAdditionalActions}>{additionalActions}</RemoveFromDOM>
319
-
320
- {/* Close button */}
321
- <RemoveFromDOM
322
- // Hide when requested, or when pagination is enabled (the page navigation bar will render its own close button).
323
- when={hideCloseButton}
324
- >
415
+ <DialogPanel className={clsx(styles.panel, styles[`from-${from}`], overrides?.panel?.className)}>
416
+ {/* Dialog title bar */}
417
+ <div className={clsx(styles.titleBar, overrides?.titleBar?.className)}>
418
+ <div className={clsx(styles.titleArea, overrides?.titleArea?.className)}>
419
+ <RemoveFromDOM when={!isPaginated}>
420
+ <div className={clsx(styles.paginationButtons)}>
325
421
  <Button
422
+ className={clsx(styles.navButton)}
423
+ size="medium"
326
424
  kind="tertiary"
327
425
  shape="circle"
328
- onClick={() => onClose(false)}
329
- startEnhancer={<Icon icon={Close} size={20} />}
330
- data-title-hidden={hideTitle}
331
- className={clsx(styles.closeButton)}
426
+ onClick={() => pagination?.back()}
427
+ disabled={!pagination?.canGoBack()}
428
+ startEnhancer={<Icon icon={ChevronLeft} size={16} />}
332
429
  >
333
- Close dialog
430
+ Go to previous page in this drawer
334
431
  </Button>
335
- </RemoveFromDOM>
336
- </div>
432
+ <Button
433
+ className={clsx(styles.navButton)}
434
+ size="medium"
435
+ kind="tertiary"
436
+ shape="circle"
437
+ onClick={() => pagination?.forward()}
438
+ disabled={!pagination?.canGoForward()}
439
+ startEnhancer={<Icon icon={ChevronRight} size={16} />}
440
+ >
441
+ Go to next page in this drawer
442
+ </Button>
443
+ </div>
444
+ </RemoveFromDOM>
445
+ {slotContext && <div ref={slotContext.titleRef} />}
446
+ <VisuallyHidden when={hideTitle || (slotContext?.hasTitleSlot ?? false)}>
447
+ <DialogTitle as="h2" className={styles.titleTextContainer}>
448
+ <TextWhenString kind="paragraphSmall" weight="medium">
449
+ {title}
450
+ </TextWhenString>
451
+ </DialogTitle>
452
+ </VisuallyHidden>
453
+ </div>
454
+ <div className={clsx(styles.titleBarButtons, overrides?.titleBarButtons?.className)}>
455
+ {slotContext && <div ref={slotContext.actionsRef} />}
456
+ {!slotContext?.hasActionsSlot && (
457
+ <RemoveFromDOM when={!hasAdditionalActions}>{additionalActions}</RemoveFromDOM>
458
+ )}
459
+
460
+ {/* Close button */}
461
+ <RemoveFromDOM when={hideCloseButton}>
462
+ <Button
463
+ kind="tertiary"
464
+ shape="circle"
465
+ onClick={() => onClose(false)}
466
+ startEnhancer={<Icon icon={Close} size={20} />}
467
+ data-title-hidden={hideTitle}
468
+ className={clsx(styles.closeButton)}
469
+ >
470
+ Close drawer
471
+ </Button>
472
+ </RemoveFromDOM>
337
473
  </div>
474
+ </div>
338
475
 
339
- <div className={clsx(styles.content, overrides?.content?.className)}>
340
- <div className={clsx(styles.contentChildren, overrides?.contentChildren?.className)}>
341
- {isPaginated && Array.isArray(children)
342
- ? children.map(
343
- (child) =>
344
- child &&
345
- typeof child === 'object' &&
346
- 'key' in child && (
347
- <Transition
348
- show={
349
- child.key === pagination?.currentPage &&
350
- loadedPage === child.key
351
- }
352
- key={`transition_${child.key}`}
353
- as="div"
354
- enter={styles.paginationEnter}
355
- enterFrom={styles.enterFromOpacity}
356
- enterTo={styles.enterToOpacity}
357
- leave={styles.paginationLeave}
358
- leaveFrom={styles.leaveFromOpacity}
359
- leaveTo={styles.leaveToOpacity}
360
- afterLeave={() => {
361
- setLoadedPage(pagination?.currentPage || null);
362
- }}
363
- className={clsx(
364
- overrides?.contentChildrenChildren?.className,
365
- )}
366
- >
367
- {child}
368
- </Transition>
369
- ),
370
- )
371
- : children}
372
- </div>
373
- {bottomPanel && (
476
+ <div className={clsx(styles.content, overrides?.content?.className)}>
477
+ <div className={clsx(styles.contentChildren, overrides?.contentChildren?.className)}>
478
+ {isPaginated ? (
374
479
  <>
480
+ {paginatedContent}
481
+ {nonPageChildren}
482
+ {progressBar && pageEntries && (
483
+ <DrawerProgressBar
484
+ steps={pageEntries.map((p) => p.id)}
485
+ {...(typeof progressBar === 'object' ? progressBar : undefined)}
486
+ />
487
+ )}
488
+ </>
489
+ ) : (
490
+ children
491
+ )}
492
+ </div>
493
+ {showBottomPanel && (
494
+ <>
495
+ <div
496
+ ref={spacerRef}
497
+ tabIndex={-1}
498
+ aria-hidden="true"
499
+ className={clsx(
500
+ styles.bottomPanelSpacer,
501
+ overrides?.bottomPanelSpacer?.className,
502
+ )}
503
+ />
504
+ <div
505
+ ref={bottomPanelElRef}
506
+ className={clsx(styles.bottomPanel, overrides?.bottomPanel?.className)}
507
+ >
508
+ {slotContext && <div ref={slotContext.progressBarRef} />}
509
+ <div className={styles.glassOpacity} />
510
+ <div className={styles.glassBlend} />
375
511
  <div
376
- tabIndex={-1}
377
- aria-hidden="true"
378
512
  className={clsx(
379
- styles.bottomPanelSpacer,
380
- { [styles.noPadding]: !bottomPanelPadding },
381
- overrides?.bottomPanelSpacer?.className,
513
+ styles.bottomPanelContent,
514
+ styles.noPadding,
515
+ overrides?.bottomPanelContent?.className,
382
516
  )}
383
517
  >
384
- {bottomPanel}
385
- </div>
386
- <div className={clsx(styles.bottomPanel, overrides?.bottomPanel?.className)}>
387
- <div className={styles.glassOpacity} />
388
- <div className={styles.glassBlend} />
389
- <div
390
- className={clsx(
391
- styles.bottomPanelContent,
392
- { [styles.noPadding]: !bottomPanelPadding },
393
- overrides?.bottomPanelContent?.className,
394
- )}
395
- >
396
- {bottomPanel}
397
- </div>
518
+ {slotContext && (
519
+ <div
520
+ ref={slotContext.bottomPanelCallbackRef}
521
+ className={styles.bottomPanelSlots}
522
+ />
523
+ )}
398
524
  </div>
399
- </>
400
- )}
401
- </div>
402
- </DialogPanel>
403
- </TransitionChild>
404
- </div>
405
- </Dialog>
406
- </Transition>
525
+ </div>
526
+ </>
527
+ )}
528
+ </div>
529
+ </DialogPanel>
530
+ </TransitionChild>
531
+ </div>
532
+ </>
407
533
  );
408
534
  };