@purpurds/drawer 5.15.1

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 (60) hide show
  1. package/dist/LICENSE.txt +478 -0
  2. package/dist/drawer-container.d.ts +11 -0
  3. package/dist/drawer-container.d.ts.map +1 -0
  4. package/dist/drawer-content.d.ts +27 -0
  5. package/dist/drawer-content.d.ts.map +1 -0
  6. package/dist/drawer-frame.d.ts +23 -0
  7. package/dist/drawer-frame.d.ts.map +1 -0
  8. package/dist/drawer-handle.d.ts +13 -0
  9. package/dist/drawer-handle.d.ts.map +1 -0
  10. package/dist/drawer-header.d.ts +14 -0
  11. package/dist/drawer-header.d.ts.map +1 -0
  12. package/dist/drawer-scroll-area.d.ts +9 -0
  13. package/dist/drawer-scroll-area.d.ts.map +1 -0
  14. package/dist/drawer-trigger.d.ts +9 -0
  15. package/dist/drawer-trigger.d.ts.map +1 -0
  16. package/dist/drawer.cjs.js +62 -0
  17. package/dist/drawer.cjs.js.map +1 -0
  18. package/dist/drawer.context.d.ts +4 -0
  19. package/dist/drawer.context.d.ts.map +1 -0
  20. package/dist/drawer.d.ts +17 -0
  21. package/dist/drawer.d.ts.map +1 -0
  22. package/dist/drawer.es.js +2791 -0
  23. package/dist/drawer.es.js.map +1 -0
  24. package/dist/styles.css +1 -0
  25. package/dist/types.d.ts +8 -0
  26. package/dist/types.d.ts.map +1 -0
  27. package/dist/use-swipe-to-dismiss.hook.d.ts +13 -0
  28. package/dist/use-swipe-to-dismiss.hook.d.ts.map +1 -0
  29. package/dist/use-swipe-tracking.hook.d.ts +15 -0
  30. package/dist/use-swipe-tracking.hook.d.ts.map +1 -0
  31. package/package.json +67 -0
  32. package/src/drawer-container.module.scss +24 -0
  33. package/src/drawer-container.test.tsx +74 -0
  34. package/src/drawer-container.tsx +48 -0
  35. package/src/drawer-content.module.scss +101 -0
  36. package/src/drawer-content.test.tsx +80 -0
  37. package/src/drawer-content.tsx +124 -0
  38. package/src/drawer-frame.module.scss +44 -0
  39. package/src/drawer-frame.test.tsx +139 -0
  40. package/src/drawer-frame.tsx +140 -0
  41. package/src/drawer-handle.module.scss +23 -0
  42. package/src/drawer-handle.test.tsx +37 -0
  43. package/src/drawer-handle.tsx +59 -0
  44. package/src/drawer-header.module.scss +29 -0
  45. package/src/drawer-header.test.tsx +173 -0
  46. package/src/drawer-header.tsx +117 -0
  47. package/src/drawer-scroll-area.module.scss +42 -0
  48. package/src/drawer-scroll-area.test.tsx +28 -0
  49. package/src/drawer-scroll-area.tsx +45 -0
  50. package/src/drawer-trigger.test.tsx +33 -0
  51. package/src/drawer-trigger.tsx +34 -0
  52. package/src/drawer.context.ts +5 -0
  53. package/src/drawer.module.scss +3 -0
  54. package/src/drawer.stories.tsx +197 -0
  55. package/src/drawer.test.tsx +210 -0
  56. package/src/drawer.tsx +59 -0
  57. package/src/global.d.ts +4 -0
  58. package/src/types.ts +3 -0
  59. package/src/use-swipe-to-dismiss.hook.ts +60 -0
  60. package/src/use-swipe-tracking.hook.ts +78 -0
@@ -0,0 +1 @@
1
+ ._purpur-drawer_xzrkg_1{height:100vh}._purpur-drawer-content_1j9c0_1{animation:_drawerSmallScreenAnimation_1j9c0_1 var(--purpur-motion-duration-200) var(--purpur-motion-easing-ease-in-out);height:90%;max-width:100%;position:absolute;right:0;top:10%;transition:transform var(--purpur-motion-duration-200) var(--purpur-motion-easing-ease-in-out);width:100%}@media screen and (min-width: 600px){._purpur-drawer-content_1j9c0_1{animation:_drawerLargeScreenAnimation_1j9c0_1 var(--purpur-motion-duration-200) var(--purpur-motion-easing-ease-in-out);height:100%;max-width:calc(30rem * var(--purpur-rescale));top:0}}._purpur-drawer-content_1j9c0_1:focus{outline:none}._purpur-drawer-content__content-container_1j9c0_22{display:flex;flex-direction:column;gap:var(--purpur-spacing-400)}._purpur-drawer-content__drawer-frame_1j9c0_27[data-swipe=cancel]{transform:translateY(0);transition:transform var(--purpur-motion-duration-200) var(--purpur-motion-easing-ease-in-out)}._purpur-drawer-content__drawer-frame_1j9c0_27[data-swipe=move]{transform:translateY(var(--purpur-drawer-swipe-move-y))}._purpur-drawer-content__drawer-frame_1j9c0_27[data-swipe=end]{animation:_slideDown_1j9c0_1 var(--purpur-motion-duration-200) var(--purpur-motion-easing-ease-in-out) forwards}._purpur-drawer-content__description_1j9c0_37{color:var(--purpur-color-text-default);display:block;font-family:var(--purpur-typography-family-default);font-size:var(--purpur-typography-scale-100);font-weight:var(--purpur-typography-weight-normal);-webkit-hyphens:none;hyphens:none;line-height:var(--purpur-typography-line-height-loose);margin:0}._purpur-drawer-overlay_1j9c0_48{animation:_overlayAnimation_1j9c0_1 var(--purpur-motion-duration-200) var(--purpur-motion-easing-ease-in-out);background:var(--purpur-color-overlay-default);top:0;right:0;bottom:0;left:0;position:fixed;transition:opacity var(--purpur-motion-duration-200) var(--purpur-motion-easing-ease-in-out)}@keyframes _slideDown_1j9c0_1{0%{transform:translateY(var(--purpur-drawer-swipe-end-y))}to{transform:translateY(100%)}}@keyframes _overlayAnimation_1j9c0_1{0%{opacity:0}to{opacity:1}}@keyframes _drawerLargeScreenAnimation_1j9c0_1{0%{transform:translate(100%)}to{transform:translate(0)}}@keyframes _drawerSmallScreenAnimation_1j9c0_1{0%{transform:translateY(100%)}to{transform:translateY(0)}}._purpur-drawer-container--header_1r6tb_1{padding:var(--purpur-spacing-250) var(--purpur-spacing-300) var(--purpur-spacing-200);border-bottom:var(--purpur-border-width-xs) solid var(--purpur-color-border-weak)}._purpur-drawer-container--body_1r6tb_5{padding:var(--purpur-spacing-400) var(--purpur-spacing-300) 0}._purpur-drawer-container--body_1r6tb_5._purpur-drawer-container--sticky_1r6tb_8{padding:var(--purpur-spacing-400) var(--purpur-spacing-300)}._purpur-drawer-container--footer_1r6tb_11{padding:0 var(--purpur-spacing-300) var(--purpur-spacing-200)}._purpur-drawer-container--footer_1r6tb_11._purpur-drawer-container--sticky_1r6tb_8{border-top:var(--purpur-border-width-xs) solid var(--purpur-color-border-weak);padding:var(--purpur-spacing-200) var(--purpur-spacing-300)}._purpur-drawer-frame_jj7dt_1{background-color:var(--purpur-color-background-primary);border-top-left-radius:var(--purpur-border-radius-lg);border-top-right-radius:var(--purpur-border-radius-lg);box-shadow:var(--purpur-shadow-lg);display:flex;flex-direction:column;height:100%;position:relative}@media screen and (min-width: 600px){._purpur-drawer-frame_jj7dt_1{border-bottom-left-radius:var(--purpur-border-radius-lg);border-top-right-radius:0}}._purpur-drawer-frame--sticky-footer_jj7dt_17{gap:0}._purpur-drawer-frame__header_jj7dt_20{flex:0 0 auto}._purpur-drawer-frame__body_jj7dt_23{flex:1 1 auto;overflow:hidden}._purpur-drawer-frame__footer_jj7dt_27{flex:0 0 auto}._purpur-drawer-frame__content-container_jj7dt_30{display:flex;flex-direction:column;gap:var(--purpur-spacing-400)}._purpur-drawer-frame__content-container--no-footer_jj7dt_35{margin-bottom:var(--purpur-spacing-400)}._purpur-drawer-handle_3n0ew_1{align-items:center;display:flex;height:var(--purpur-spacing-250);justify-content:center;position:absolute;top:0;width:100%}@media screen and (min-width: 600px){._purpur-drawer-handle_3n0ew_1{display:none}}._purpur-drawer-handle_3n0ew_1:before{content:"";background:var(--purpur-color-border-weak);border-radius:var(--purpur-border-radius-full);height:var(--purpur-spacing-50);width:var(--purpur-spacing-600)}._purpur-drawer-header__row_1yg5w_1{display:flex;align-items:center;gap:var(--purpur-spacing-100)}._purpur-drawer-header__row--with-back-button_1yg5w_6{margin-bottom:var(--purpur-spacing-100)}._purpur-drawer-header__left_1yg5w_9{flex:1 1 auto}._purpur-drawer-header__right_1yg5w_12{flex:0 0 auto}._purpur-drawer-header__close-button_1yg5w_15{margin-right:calc(-1 * var(--purpur-spacing-100))}._purpur-drawer-header__back-button--only-icon_1yg5w_18{margin-left:calc(-1 * var(--purpur-spacing-100))}._purpur-drawer-scroll-area__root_1p63i_1,._purpur-drawer-scroll-area__viewport_1p63i_4{height:100%}._purpur-drawer-scroll-area__scrollbar_1p63i_7{display:flex;-webkit-user-select:none;user-select:none;touch-action:none;padding:var(--purpur-spacing-25);background:var(--purpur-color-functional-white);transition:background var(--purpur-motion-duration-200) var(--purpur-motion-easing-ease-in-out);width:var(--purpur-spacing-100)}._purpur-drawer-scroll-area__thumb_1p63i_18{background:var(--purpur-color-gray-200);border-radius:var(--purpur-spacing-200);flex:1;position:relative}._purpur-drawer-scroll-area__thumb_1p63i_18:before{content:"";height:100%;left:50%;min-height:var(--purpur-spacing-300);min-width:var(--purpur-spacing-300);position:absolute;top:50%;transform:translate(-50%,-50%);width:100%}
@@ -0,0 +1,8 @@
1
+ export type SwipeEvent = {
2
+ originalEvent: React.PointerEvent;
3
+ delta: {
4
+ y: number;
5
+ };
6
+ };
7
+ export type OpenHandlerFunction = (open: boolean) => void;
8
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,UAAU,GAAG;IAAE,aAAa,EAAE,KAAK,CAAC,YAAY,CAAC;IAAC,KAAK,EAAE;QAAE,CAAC,EAAE,MAAM,CAAA;KAAE,CAAA;CAAE,CAAC;AAErF,MAAM,MAAM,mBAAmB,GAAG,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAC"}
@@ -0,0 +1,13 @@
1
+ import { default as React } from 'react';
2
+ import { SwipeEvent } from './types';
3
+
4
+ type UseSwipeToDismiss = {
5
+ onAnimationEnd(event: React.AnimationEvent<HTMLDivElement>): void;
6
+ onSwipeStart(): void;
7
+ onSwipeMove(event: SwipeEvent): void;
8
+ onSwipeCancel(): void;
9
+ onSwipeEnd(event: SwipeEvent): void;
10
+ };
11
+ export declare const useSwipeToDismiss: <T extends HTMLElement>(containerRef: React.MutableRefObject<T | null>, handleOpenChange: (open: boolean) => void) => UseSwipeToDismiss;
12
+ export {};
13
+ //# sourceMappingURL=use-swipe-to-dismiss.hook.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"use-swipe-to-dismiss.hook.d.ts","sourceRoot":"","sources":["../src/use-swipe-to-dismiss.hook.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAC;AAE1B,OAAO,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AAErC,KAAK,iBAAiB,GAAG;IACvB,cAAc,CAAC,KAAK,EAAE,KAAK,CAAC,cAAc,CAAC,cAAc,CAAC,GAAG,IAAI,CAAC;IAClE,YAAY,IAAI,IAAI,CAAC;IACrB,WAAW,CAAC,KAAK,EAAE,UAAU,GAAG,IAAI,CAAC;IACrC,aAAa,IAAI,IAAI,CAAC;IACtB,UAAU,CAAC,KAAK,EAAE,UAAU,GAAG,IAAI,CAAC;CACrC,CAAC;AAEF,eAAO,MAAM,iBAAiB,GAAI,CAAC,SAAS,WAAW,gBACvC,KAAK,CAAC,gBAAgB,CAAC,CAAC,GAAG,IAAI,CAAC,oBAC5B,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,KACxC,iBA4CF,CAAC"}
@@ -0,0 +1,15 @@
1
+ import { default as React } from 'react';
2
+ import { SwipeEvent } from './types';
3
+
4
+ type UseSwipeTracking = {
5
+ onPointerDown(event: React.PointerEvent): void;
6
+ onPointerMove(event: React.PointerEvent): void;
7
+ onPointerUp(event: React.PointerEvent): void;
8
+ };
9
+ export declare const useSwipeTracking: (pointerStartRef: React.MutableRefObject<{
10
+ y: number;
11
+ } | null>, swipeDeltaRef: React.MutableRefObject<{
12
+ y: number;
13
+ } | null>, onSwipeStart: () => void, onSwipeMove: (event: SwipeEvent) => void, onSwipeCancel: () => void, onSwipeEnd: (event: SwipeEvent) => void) => UseSwipeTracking;
14
+ export {};
15
+ //# sourceMappingURL=use-swipe-tracking.hook.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"use-swipe-tracking.hook.d.ts","sourceRoot":"","sources":["../src/use-swipe-tracking.hook.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAC;AAE1B,OAAO,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AAErC,KAAK,gBAAgB,GAAG;IACtB,aAAa,CAAC,KAAK,EAAE,KAAK,CAAC,YAAY,GAAG,IAAI,CAAC;IAC/C,aAAa,CAAC,KAAK,EAAE,KAAK,CAAC,YAAY,GAAG,IAAI,CAAC;IAC/C,WAAW,CAAC,KAAK,EAAE,KAAK,CAAC,YAAY,GAAG,IAAI,CAAC;CAC9C,CAAC;AAEF,eAAO,MAAM,gBAAgB,oBACV,KAAK,CAAC,gBAAgB,CAAC;IAAE,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAAC,iBAC9C,KAAK,CAAC,gBAAgB,CAAC;IAAE,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAAC,gBAC7C,MAAM,IAAI,eACX,CAAC,KAAK,EAAE,UAAU,KAAK,IAAI,iBACzB,MAAM,IAAI,cACb,CAAC,KAAK,EAAE,UAAU,KAAK,IAAI,KACtC,gBAuDF,CAAC"}
package/package.json ADDED
@@ -0,0 +1,67 @@
1
+ {
2
+ "name": "@purpurds/drawer",
3
+ "version": "5.15.1",
4
+ "license": "AGPL-3.0-only",
5
+ "main": "./dist/drawer.cjs.js",
6
+ "types": "./dist/drawer.d.ts",
7
+ "exports": {
8
+ ".": {
9
+ "require": "./dist/drawer.cjs.js",
10
+ "types": "./dist/drawer.d.ts",
11
+ "default": "./dist/drawer.es.js"
12
+ },
13
+ "./styles": "./dist/styles.css"
14
+ },
15
+ "source": "src/drawer.tsx",
16
+ "dependencies": {
17
+ "@radix-ui/react-dialog": "~1.0.5",
18
+ "@radix-ui/react-scroll-area": "~1.0.5",
19
+ "classnames": "~2.5.0",
20
+ "@purpurds/heading": "5.15.1",
21
+ "@purpurds/tokens": "5.15.1",
22
+ "@purpurds/button": "5.15.1",
23
+ "@purpurds/icon": "5.15.1",
24
+ "@purpurds/paragraph": "5.15.1"
25
+ },
26
+ "devDependencies": {
27
+ "@rushstack/eslint-patch": "~1.10.0",
28
+ "@storybook/blocks": "^8.2.6",
29
+ "@storybook/preview-api": "^8.2.6",
30
+ "@storybook/react": "^8.2.6",
31
+ "@telia/base-rig": "~8.2.0",
32
+ "@telia/react-rig": "~3.2.0",
33
+ "@testing-library/dom": "~9.3.3",
34
+ "@testing-library/jest-dom": "~6.4.0",
35
+ "@testing-library/react": "~14.3.0",
36
+ "@testing-library/user-event": "~14.5.1",
37
+ "@types/node": "20.12.12",
38
+ "@types/react-dom": "^18.3.0",
39
+ "@types/react": "^18.3.3",
40
+ "eslint-plugin-testing-library": "~6.2.0",
41
+ "eslint": "^8.57.0",
42
+ "jsdom": "~22.1.0",
43
+ "lint-staged": "~10.5.3",
44
+ "prettier": "~2.8.8",
45
+ "react-dom": "^18.3.1",
46
+ "react": "^18.3.1",
47
+ "storybook": "^8.2.6",
48
+ "typescript": "^5.5.4",
49
+ "vite": "5.3.4",
50
+ "vitest": "~1.5.0",
51
+ "@purpurds/component-rig": "1.0.0"
52
+ },
53
+ "scripts": {
54
+ "build:dev": "vite",
55
+ "build:watch": "vite build --watch",
56
+ "build": "vite build",
57
+ "ci:build": "rushx build",
58
+ "coverage": "vitest run --coverage",
59
+ "lint:fix": "eslint . --fix",
60
+ "lint": "lint-staged --no-stash 2>&1",
61
+ "sbdev": "rush sbdev",
62
+ "test:unit": "vitest run --passWithNoTests",
63
+ "test:watch": "vitest --watch",
64
+ "test": "rushx test:unit",
65
+ "typecheck": "tsc -p ./tsconfig.json"
66
+ }
67
+ }
@@ -0,0 +1,24 @@
1
+ .purpur-drawer-container {
2
+ $root: &;
3
+ &--header {
4
+ padding: var(--purpur-spacing-250) var(--purpur-spacing-300) var(--purpur-spacing-200);
5
+ border-bottom: var(--purpur-border-width-xs) solid var(--purpur-color-border-weak);
6
+ }
7
+
8
+ &--body {
9
+ padding: var(--purpur-spacing-400) var(--purpur-spacing-300) 0;
10
+
11
+ &#{$root}--sticky {
12
+ padding: var(--purpur-spacing-400) var(--purpur-spacing-300);
13
+ }
14
+ }
15
+
16
+ &--footer {
17
+ padding: 0 var(--purpur-spacing-300) var(--purpur-spacing-200);
18
+
19
+ &#{$root}--sticky {
20
+ border-top: var(--purpur-border-width-xs) solid var(--purpur-color-border-weak);
21
+ padding: var(--purpur-spacing-200) var(--purpur-spacing-300);
22
+ }
23
+ }
24
+ }
@@ -0,0 +1,74 @@
1
+ import React from "react";
2
+ import * as matchers from "@testing-library/jest-dom/matchers";
3
+ import { cleanup, render, screen } from "@testing-library/react";
4
+ import { afterEach, describe, expect, it } from "vitest";
5
+
6
+ import { DrawerContainer } from "./drawer-container";
7
+
8
+ expect.extend(matchers);
9
+
10
+ afterEach(() => {
11
+ cleanup();
12
+ });
13
+
14
+ describe("DrawerContainer", () => {
15
+ it.each([
16
+ { variant: "header" as const, expectedClassModifier: "header" },
17
+ { variant: "body" as const, expectedClassModifier: "body" },
18
+ { variant: "footer" as const, expectedClassModifier: "footer" },
19
+ { variant: undefined, expectedClassModifier: "body" },
20
+ ])(
21
+ "variant $variant should set the correct variant class purpur-drawer-container--$expectedClassModifier",
22
+ (args) => {
23
+ render(
24
+ <DrawerContainer
25
+ data-testid={Selectors.DRAWER_CONTAINER}
26
+ variant={args.variant}
27
+ stickyFooter
28
+ >
29
+ Some drawer container content
30
+ </DrawerContainer>
31
+ );
32
+ expect(screen.getByTestId(Selectors.DRAWER_CONTAINER)).toHaveClass(
33
+ `purpur-drawer-container--${args.expectedClassModifier}`
34
+ );
35
+ }
36
+ );
37
+
38
+ it("should set the sticky class when stickyFooter is true", () => {
39
+ render(
40
+ <DrawerContainer data-testid={Selectors.DRAWER_CONTAINER} stickyFooter>
41
+ Some drawer container content
42
+ </DrawerContainer>
43
+ );
44
+ expect(screen.getByTestId(Selectors.DRAWER_CONTAINER)).toHaveClass(
45
+ "purpur-drawer-container--sticky"
46
+ );
47
+ });
48
+
49
+ it("should not set the sticky class when stickyFooter is false", () => {
50
+ render(
51
+ <DrawerContainer data-testid={Selectors.DRAWER_CONTAINER} stickyFooter={false}>
52
+ Some drawer container content
53
+ </DrawerContainer>
54
+ );
55
+ expect(screen.getByTestId(Selectors.DRAWER_CONTAINER)).not.toHaveClass(
56
+ "purpur-drawer-container--sticky"
57
+ );
58
+ });
59
+
60
+ it("should render children", () => {
61
+ render(
62
+ <DrawerContainer data-testid={Selectors.DRAWER_CONTAINER} stickyFooter={false}>
63
+ Some drawer container content
64
+ </DrawerContainer>
65
+ );
66
+ expect(screen.getByTestId(Selectors.DRAWER_CONTAINER)).toHaveTextContent(
67
+ "Some drawer container content"
68
+ );
69
+ });
70
+ });
71
+
72
+ const Selectors = {
73
+ DRAWER_CONTAINER: "purpur-drawer-container",
74
+ };
@@ -0,0 +1,48 @@
1
+ import React, { ForwardedRef, forwardRef, ReactNode } from "react";
2
+ import c from "classnames/bind";
3
+
4
+ import styles from "./drawer-container.module.scss";
5
+ const cx = c.bind(styles);
6
+
7
+ export type DrawerContainerProps = {
8
+ ["data-testid"]?: string;
9
+ children: ReactNode;
10
+ className?: string;
11
+ stickyFooter: boolean;
12
+ variant?: "header" | "body" | "footer";
13
+ };
14
+
15
+ const rootClassName = "purpur-drawer-container";
16
+
17
+ export const DrawerContainer = forwardRef(
18
+ (
19
+ {
20
+ ["data-testid"]: dataTestId = "purpur-drawer-container",
21
+ children,
22
+ className,
23
+ variant = "body",
24
+ stickyFooter,
25
+ ...props
26
+ }: DrawerContainerProps,
27
+ ref: ForwardedRef<HTMLDivElement>
28
+ ) => {
29
+ const classes = cx([
30
+ className,
31
+ rootClassName,
32
+ {
33
+ [`${rootClassName}--${variant}`]: variant,
34
+ },
35
+ {
36
+ [`${rootClassName}--sticky`]: stickyFooter,
37
+ },
38
+ ]);
39
+
40
+ return (
41
+ <div className={classes} data-testid={dataTestId} ref={ref} {...props}>
42
+ {children}
43
+ </div>
44
+ );
45
+ }
46
+ );
47
+
48
+ DrawerContainer.displayName = "DrawerContainer";
@@ -0,0 +1,101 @@
1
+ @import "@purpurds/tokens/breakpoint/variables";
2
+
3
+ $animation-settings: var(--purpur-motion-duration-200) var(--purpur-motion-easing-ease-in-out);
4
+
5
+ .purpur-drawer-content {
6
+ animation: drawerSmallScreenAnimation $animation-settings;
7
+ height: 90%;
8
+ max-width: 100%;
9
+ position: absolute;
10
+ right: 0;
11
+ top: 10%;
12
+ transition: transform $animation-settings;
13
+ width: 100%;
14
+
15
+ @media screen and (min-width: $purpur-breakpoint-md) {
16
+ animation: drawerLargeScreenAnimation $animation-settings;
17
+ height: 100%;
18
+ max-width: calc(30rem * var(--purpur-rescale));
19
+ top: 0;
20
+ }
21
+
22
+ &:focus {
23
+ outline: none;
24
+ }
25
+
26
+ &__content-container {
27
+ display: flex;
28
+ flex-direction: column;
29
+ gap: var(--purpur-spacing-400);
30
+ }
31
+
32
+ &__drawer-frame {
33
+ &[data-swipe="cancel"] {
34
+ transform: translateY(0);
35
+ transition: transform $animation-settings;
36
+ }
37
+
38
+ &[data-swipe="move"] {
39
+ transform: translateY(var(--purpur-drawer-swipe-move-y));
40
+ }
41
+
42
+ &[data-swipe="end"] {
43
+ animation: slideDown $animation-settings forwards;
44
+ }
45
+ }
46
+
47
+ &__description {
48
+ color: var(--purpur-color-text-default);
49
+ display: block;
50
+ font-family: var(--purpur-typography-family-default);
51
+ font-size: var(--purpur-typography-scale-100);
52
+ font-weight: var(--purpur-typography-weight-normal);
53
+ hyphens: none;
54
+ line-height: var(--purpur-typography-line-height-loose);
55
+ margin: 0;
56
+ }
57
+ }
58
+
59
+ .purpur-drawer-overlay {
60
+ animation: overlayAnimation $animation-settings;
61
+ background: var(--purpur-color-overlay-default);
62
+ inset: 0;
63
+ position: fixed;
64
+ transition: opacity $animation-settings;
65
+ }
66
+
67
+ @keyframes slideDown {
68
+ from {
69
+ transform: translateY(var(--purpur-drawer-swipe-end-y));
70
+ }
71
+ to {
72
+ transform: translateY(100%);
73
+ }
74
+ }
75
+
76
+ @keyframes overlayAnimation {
77
+ from {
78
+ opacity: 0;
79
+ }
80
+ to {
81
+ opacity: 1;
82
+ }
83
+ }
84
+
85
+ @keyframes drawerLargeScreenAnimation {
86
+ from {
87
+ transform: translateX(100%);
88
+ }
89
+ to {
90
+ transform: translateX(0%);
91
+ }
92
+ }
93
+
94
+ @keyframes drawerSmallScreenAnimation {
95
+ from {
96
+ transform: translateY(100%);
97
+ }
98
+ to {
99
+ transform: translateY(0%);
100
+ }
101
+ }
@@ -0,0 +1,80 @@
1
+ import React from "react";
2
+ import * as RadixDialog from "@radix-ui/react-dialog";
3
+ import * as matchers from "@testing-library/jest-dom/matchers";
4
+ import { cleanup, render, screen } from "@testing-library/react";
5
+ import { afterEach, describe, expect, it } from "vitest";
6
+
7
+ import { DrawerContent } from "./drawer-content";
8
+
9
+ expect.extend(matchers);
10
+
11
+ afterEach(() => {
12
+ cleanup();
13
+ });
14
+
15
+ describe("DrawerContent", () => {
16
+ it("should show the body text if it is provided", () => {
17
+ render(
18
+ <RadixDialog.Root open={true}>
19
+ <DrawerContent
20
+ data-testid={Selectors.DRAWER_CONTENT}
21
+ backButton
22
+ backButtonText="Back"
23
+ bodyText="This is the body text"
24
+ closeButtonText="Close"
25
+ onBackButtonClick={() => {}}
26
+ stickyFooter
27
+ title="Title"
28
+ >
29
+ Some drawer-content
30
+ </DrawerContent>
31
+ </RadixDialog.Root>
32
+ );
33
+ expect(screen.getByTestId(Selectors.BODY_TEXT)).toBeInTheDocument();
34
+ expect(screen.getByTestId(Selectors.BODY_TEXT)).toHaveTextContent("This is the body text");
35
+ });
36
+
37
+ it("should not show the body text if it is provided", () => {
38
+ render(
39
+ <RadixDialog.Root open={true}>
40
+ <DrawerContent
41
+ data-testid={Selectors.DRAWER_CONTENT}
42
+ backButton
43
+ backButtonText="Back"
44
+ closeButtonText="Close"
45
+ onBackButtonClick={() => {}}
46
+ stickyFooter
47
+ title="Title"
48
+ >
49
+ Some drawer-content
50
+ </DrawerContent>
51
+ </RadixDialog.Root>
52
+ );
53
+ expect(screen.queryByTestId(Selectors.BODY_TEXT)).not.toBeInTheDocument();
54
+ });
55
+
56
+ it.each(["This is the bodyText", undefined])("should render children", (bodyText) => {
57
+ render(
58
+ <RadixDialog.Root open={true}>
59
+ <DrawerContent
60
+ data-testid={Selectors.DRAWER_CONTENT}
61
+ backButton
62
+ backButtonText="Back"
63
+ bodyText={bodyText}
64
+ closeButtonText="Close"
65
+ onBackButtonClick={() => {}}
66
+ stickyFooter
67
+ title="Title"
68
+ >
69
+ Some drawer-content
70
+ </DrawerContent>
71
+ </RadixDialog.Root>
72
+ );
73
+ expect(screen.getByTestId(Selectors.DRAWER_CONTENT)).toHaveTextContent("Some drawer-content");
74
+ });
75
+ });
76
+
77
+ const Selectors = {
78
+ DRAWER_CONTENT: "purpur-drawer-content",
79
+ BODY_TEXT: "purpur-drawer-content-description",
80
+ };
@@ -0,0 +1,124 @@
1
+ import React, { ForwardedRef, forwardRef, ReactNode, useContext, useRef } from "react";
2
+ import * as RadixDialog from "@radix-ui/react-dialog";
3
+ import c from "classnames/bind";
4
+
5
+ import { DrawerContext } from "./drawer.context";
6
+ import styles from "./drawer-content.module.scss";
7
+ import { DrawerFrame } from "./drawer-frame";
8
+ import { OpenHandlerFunction } from "./types";
9
+ import { useSwipeToDismiss } from "./use-swipe-to-dismiss.hook";
10
+ const cx = c.bind(styles);
11
+
12
+ export type WithBackButton = {
13
+ backButton: boolean;
14
+ backButtonText: string;
15
+ backButtonOnlyIcon?: boolean;
16
+ onBackButtonClick: () => void;
17
+ };
18
+
19
+ export type WithoutBackButton = {
20
+ backButton?: never;
21
+ backButtonText?: never;
22
+ backButtonOnlyIcon?: never;
23
+ onBackButtonClick?: never;
24
+ };
25
+
26
+ export type DrawerContentProps = {
27
+ ["data-testid"]?: string;
28
+ bodyText?: string;
29
+ children: ReactNode;
30
+ className?: string;
31
+ closeButtonText: string;
32
+ disableCloseOnClickOutside?: boolean;
33
+ footerContent?: ReactNode;
34
+ stickyFooter?: boolean;
35
+ title: string;
36
+ } & (WithoutBackButton | WithBackButton);
37
+
38
+ const rootClassName = "purpur-drawer-content";
39
+
40
+ export const DrawerContent = forwardRef(
41
+ (
42
+ {
43
+ ["data-testid"]: dataTestId = "purpur-drawer-content",
44
+ backButton = false,
45
+ backButtonText,
46
+ backButtonOnlyIcon = false,
47
+ bodyText,
48
+ children,
49
+ className,
50
+ closeButtonText,
51
+ disableCloseOnClickOutside = false,
52
+ footerContent,
53
+ onBackButtonClick,
54
+ stickyFooter = false,
55
+ title,
56
+ ...props
57
+ }: DrawerContentProps,
58
+ ref: ForwardedRef<HTMLDivElement>
59
+ ) => {
60
+ const classes = cx([className, rootClassName]);
61
+
62
+ const drawerFrameRef = useRef<HTMLDivElement>(null);
63
+ const onOpenChange = useContext(DrawerContext);
64
+ const { onAnimationEnd, onSwipeStart, onSwipeMove, onSwipeCancel, onSwipeEnd } =
65
+ useSwipeToDismiss(drawerFrameRef, onOpenChange as OpenHandlerFunction);
66
+
67
+ const handlePointerDownOutside = (event: CustomEvent<{ originalEvent: PointerEvent }>) => {
68
+ if (disableCloseOnClickOutside) {
69
+ event.preventDefault();
70
+ }
71
+ };
72
+
73
+ return (
74
+ <RadixDialog.Portal>
75
+ <RadixDialog.Overlay
76
+ className={cx("purpur-drawer-overlay")}
77
+ data-testid={`${dataTestId}-overlay`}
78
+ >
79
+ <RadixDialog.Content
80
+ onPointerDownOutside={handlePointerDownOutside}
81
+ className={classes}
82
+ data-testid={dataTestId}
83
+ ref={ref}
84
+ {...props}
85
+ >
86
+ <DrawerFrame
87
+ backButton={backButton}
88
+ backButtonText={backButtonText}
89
+ backButtonOnlyIcon={backButtonOnlyIcon}
90
+ closeButtonText={closeButtonText}
91
+ className={cx(`${rootClassName}__drawer-frame`)}
92
+ footerContent={footerContent}
93
+ ref={drawerFrameRef}
94
+ onAnimationEnd={onAnimationEnd}
95
+ onBackButtonClick={onBackButtonClick}
96
+ onSwipeStart={onSwipeStart}
97
+ onSwipeMove={onSwipeMove}
98
+ onSwipeCancel={onSwipeCancel}
99
+ onSwipeEnd={onSwipeEnd}
100
+ stickyFooter={stickyFooter}
101
+ title={title}
102
+ >
103
+ {bodyText ? (
104
+ <div className={cx(`${rootClassName}__content-container`)}>
105
+ <RadixDialog.Description
106
+ className={cx(`${rootClassName}__description`)}
107
+ data-testid={`${dataTestId}-description`}
108
+ >
109
+ {bodyText}
110
+ </RadixDialog.Description>
111
+ <div>{children}</div>
112
+ </div>
113
+ ) : (
114
+ children
115
+ )}
116
+ </DrawerFrame>
117
+ </RadixDialog.Content>
118
+ </RadixDialog.Overlay>
119
+ </RadixDialog.Portal>
120
+ );
121
+ }
122
+ );
123
+
124
+ DrawerContent.displayName = "DrawerContent";
@@ -0,0 +1,44 @@
1
+ @import "@purpurds/tokens/breakpoint/variables";
2
+
3
+ .purpur-drawer-frame {
4
+ background-color: var(--purpur-color-background-primary);
5
+ border-top-left-radius: var(--purpur-border-radius-lg);
6
+ border-top-right-radius: var(--purpur-border-radius-lg);
7
+ box-shadow: var(--purpur-shadow-lg);
8
+ display: flex;
9
+ flex-direction: column;
10
+ height: 100%;
11
+ position: relative;
12
+
13
+ @media screen and (min-width: $purpur-breakpoint-md) {
14
+ border-bottom-left-radius: var(--purpur-border-radius-lg);
15
+ border-top-right-radius: 0;
16
+ }
17
+
18
+ &--sticky-footer {
19
+ gap: 0;
20
+ }
21
+
22
+ &__header {
23
+ flex: 0 0 auto;
24
+ }
25
+
26
+ &__body {
27
+ flex: 1 1 auto;
28
+ overflow: hidden;
29
+ }
30
+
31
+ &__footer {
32
+ flex: 0 0 auto;
33
+ }
34
+
35
+ &__content-container {
36
+ display: flex;
37
+ flex-direction: column;
38
+ gap: var(--purpur-spacing-400);
39
+
40
+ &--no-footer {
41
+ margin-bottom: var(--purpur-spacing-400);
42
+ }
43
+ }
44
+ }