diff --git a/app/client/packages/design-system/widgets/package.json b/app/client/packages/design-system/widgets/package.json index 799d1e31679e..126c104ff3d7 100644 --- a/app/client/packages/design-system/widgets/package.json +++ b/app/client/packages/design-system/widgets/package.json @@ -29,11 +29,13 @@ "react-aria-components": "^1.2.1", "react-markdown": "^9.0.1", "react-syntax-highlighter": "^15.5.0", + "react-transition-group": "^4.4.5", "remark-gfm": "^4.0.0" }, "devDependencies": { "@types/fs-extra": "^11.0.4", "@types/react-syntax-highlighter": "^15.5.13", + "@types/react-transition-group": "^4.4.11", "eslint-plugin-storybook": "^0.6.10" }, "peerDependencies": { diff --git a/app/client/packages/design-system/widgets/src/components/Sheet/index.ts b/app/client/packages/design-system/widgets/src/components/Sheet/index.ts new file mode 100644 index 000000000000..3bd16e178a03 --- /dev/null +++ b/app/client/packages/design-system/widgets/src/components/Sheet/index.ts @@ -0,0 +1 @@ +export * from "./src"; diff --git a/app/client/packages/design-system/widgets/src/components/Sheet/src/Sheet.tsx b/app/client/packages/design-system/widgets/src/components/Sheet/src/Sheet.tsx new file mode 100644 index 000000000000..3b5261f223a8 --- /dev/null +++ b/app/client/packages/design-system/widgets/src/components/Sheet/src/Sheet.tsx @@ -0,0 +1,62 @@ +import clsx from "clsx"; +import React, { forwardRef, type Ref, useRef } from "react"; +import { + Modal as HeadlessModal, + Dialog as HeadlessDialog, + ModalOverlay as HeadlessModalOverlay, +} from "react-aria-components"; +import { CSSTransition } from "react-transition-group"; + +import styles from "./styles.module.css"; +import type { SheetProps } from "./types"; + +export function _Sheet(props: SheetProps, ref: Ref) { + const { + children, + className, + isOpen, + onEntered, + onExited, + onOpenChange, + position = "start", + ...rest + } = props; + const root = document.body.querySelector( + "[data-theme-provider]", + ) as HTMLButtonElement; + + const overlayRef = useRef(); + + return ( + + + + + {children} + + + + + ); +} + +export const Sheet = forwardRef(_Sheet); diff --git a/app/client/packages/design-system/widgets/src/components/Sheet/src/index.ts b/app/client/packages/design-system/widgets/src/components/Sheet/src/index.ts new file mode 100644 index 000000000000..c415d8b0d501 --- /dev/null +++ b/app/client/packages/design-system/widgets/src/components/Sheet/src/index.ts @@ -0,0 +1,2 @@ +export { Sheet } from "./Sheet"; +export { DialogTrigger as SheetTrigger } from "react-aria-components"; diff --git a/app/client/packages/design-system/widgets/src/components/Sheet/src/styles.module.css b/app/client/packages/design-system/widgets/src/components/Sheet/src/styles.module.css new file mode 100644 index 000000000000..27dfe571139d --- /dev/null +++ b/app/client/packages/design-system/widgets/src/components/Sheet/src/styles.module.css @@ -0,0 +1,117 @@ +.sheet { + position: fixed; + background: var(--color-bg-elevation-3); + width: 80%; +} + +.overlay { + display: flex; + align-items: center; + justify-content: center; + background: var(--color-bg-neutral-opacity); + z-index: var(--z-index-99); + contain: strict; + position: fixed; + top: 0; + left: 0; + height: 100%; + width: 100%; +} + +.dialog { + height: 100%; +} + +.overlay[data-entering] { + animation: fade-in 0.3s ease-in-out; +} + +.overlay[data-exiting] { + animation: fade-out 0.3s ease-in-out; +} + +@keyframes fade-in { + from { + opacity: 0; + } + to { + opacity: 1; + } +} + +@keyframes fade-out { + from { + opacity: 1; + } + to { + opacity: 0; + } +} + +.start { + top: 0; + left: 0; + bottom: 0; + max-width: 90%; + height: 100%; +} + +.start[data-entering] { + animation: slide-in-start 0.3s cubic-bezier(0.4, 0, 0.2, 1); +} + +.start[data-exiting] { + animation: slide-out-start 0.3s cubic-bezier(0.4, 0, 0.2, 1); +} + +.end { + top: 0; + right: 0; + bottom: 0; + max-width: 90%; + height: 100%; +} + +.end[data-entering] { + animation: slide-in-end 0.3s cubic-bezier(0.4, 0, 0.2, 1); +} + +.end[data-exiting] { + animation: slide-out-end 0.3s cubic-bezier(0.4, 0, 0.2, 1); +} + +@keyframes slide-in-start { + from { + transform: translateX(-100%); + } + to { + transform: translateX(0); + } +} + +@keyframes slide-out-start { + from { + transform: translateX(0); + } + to { + transform: translateX(-100%); + } +} + +@keyframes slide-in-end { + from { + transform: translateX(100%); + } + to { + transform: translateX(0); + } +} + +@keyframes slide-out-end { + from { + transform: translateX(0); + } + to { + transform: translateX(100%); + } +} diff --git a/app/client/packages/design-system/widgets/src/components/Sheet/src/types.ts b/app/client/packages/design-system/widgets/src/components/Sheet/src/types.ts new file mode 100644 index 000000000000..d66e9d5a8906 --- /dev/null +++ b/app/client/packages/design-system/widgets/src/components/Sheet/src/types.ts @@ -0,0 +1,12 @@ +import type { ModalOverlayProps as HeadlessModalOverlayProps } from "react-aria-components"; + +export interface SheetProps + extends Omit { + /** + * The position from which the sheet slides in + * @default 'start' + */ + position?: "start" | "end"; + onEntered?: () => void; + onExited?: () => void; +} diff --git a/app/client/packages/design-system/widgets/src/components/Sheet/stories/Sheet.stories.tsx b/app/client/packages/design-system/widgets/src/components/Sheet/stories/Sheet.stories.tsx new file mode 100644 index 000000000000..ab336257e155 --- /dev/null +++ b/app/client/packages/design-system/widgets/src/components/Sheet/stories/Sheet.stories.tsx @@ -0,0 +1,27 @@ +import React from "react"; +import type { Meta, StoryObj } from "@storybook/react"; +import { Sheet } from "../index"; +import { SimpleSheet } from "./SimpleSheet"; + +const meta: Meta = { + title: "WDS/Widgets/Sheet", + component: Sheet, + render: (args) => , +}; + +export default meta; +type Story = StoryObj; + +// Default story with left position (start) +export const Default: Story = { + args: { + position: "start", + }, +}; + +// Right position (end) +export const RightPositioned: Story = { + args: { + position: "end", + }, +}; diff --git a/app/client/packages/design-system/widgets/src/components/Sheet/stories/SimpleSheet.tsx b/app/client/packages/design-system/widgets/src/components/Sheet/stories/SimpleSheet.tsx new file mode 100644 index 000000000000..200e37d00841 --- /dev/null +++ b/app/client/packages/design-system/widgets/src/components/Sheet/stories/SimpleSheet.tsx @@ -0,0 +1,20 @@ +import React, { useState } from "react"; +import { Button } from "../../Button"; +import { Sheet } from "../index"; +import type { SheetProps } from "../src/types"; + +export const SimpleSheet = (props: Omit) => { + const [isOpen, setIsOpen] = useState(props.isOpen); + + return ( + <> + + +
+

Sheet Content

+

This is an example of sheet content.

+
+
+ + ); +}; diff --git a/app/client/packages/design-system/widgets/src/components/Sidebar/index.ts b/app/client/packages/design-system/widgets/src/components/Sidebar/index.ts new file mode 100644 index 000000000000..3bd16e178a03 --- /dev/null +++ b/app/client/packages/design-system/widgets/src/components/Sidebar/index.ts @@ -0,0 +1 @@ +export * from "./src"; diff --git a/app/client/packages/design-system/widgets/src/components/Sidebar/src/Sidebar.tsx b/app/client/packages/design-system/widgets/src/components/Sidebar/src/Sidebar.tsx new file mode 100644 index 000000000000..c8b231cef3cb --- /dev/null +++ b/app/client/packages/design-system/widgets/src/components/Sidebar/src/Sidebar.tsx @@ -0,0 +1,72 @@ +import clsx from "clsx"; +import * as React from "react"; +import { type Ref, useRef } from "react"; +import { Sheet } from "../../Sheet"; +import { useSidebar } from "./use-sidebar"; +import styles from "./styles.module.css"; +import type { SidebarProps } from "./types"; +import { CSSTransition } from "react-transition-group"; + +const _Sidebar = (props: SidebarProps, ref: Ref) => { + const { + children, + className, + collapsible = "offcanvas", + onEntered, + onExited, + side = "start", + variant = "sidebar", + ...rest + } = props; + const { isMobile, setOpen, state } = useSidebar(); + const sidebarRef = useRef(); + + if (collapsible === "none") { + return ( +
+ {children} +
+ ); + } + + if (Boolean(isMobile)) { + return ( + + {children} + + ); + } + + return ( + +
+
+
+
{children}
+
+
+ + ); +}; + +export const Sidebar = React.forwardRef(_Sidebar); diff --git a/app/client/packages/design-system/widgets/src/components/Sidebar/src/SidebarContent.tsx b/app/client/packages/design-system/widgets/src/components/Sidebar/src/SidebarContent.tsx new file mode 100644 index 000000000000..bb90417a91b1 --- /dev/null +++ b/app/client/packages/design-system/widgets/src/components/Sidebar/src/SidebarContent.tsx @@ -0,0 +1,22 @@ +import clsx from "clsx"; +import React, { type ComponentProps, type Ref } from "react"; + +import styles from "./styles.module.css"; + +const _SidebarContent = ( + props: ComponentProps<"div">, + ref: Ref, +) => { + const { className, ...rest } = props; + + return ( +
+ ); +}; + +export const SidebarContent = React.forwardRef(_SidebarContent); diff --git a/app/client/packages/design-system/widgets/src/components/Sidebar/src/SidebarInset.tsx b/app/client/packages/design-system/widgets/src/components/Sidebar/src/SidebarInset.tsx new file mode 100644 index 000000000000..f6c3a1d0bc57 --- /dev/null +++ b/app/client/packages/design-system/widgets/src/components/Sidebar/src/SidebarInset.tsx @@ -0,0 +1,22 @@ +import clsx from "clsx"; +import * as React from "react"; +import type { ComponentProps, Ref } from "react"; + +import styles from "./styles.module.css"; + +export const _SidebarInset = ( + props: ComponentProps<"main">, + ref: Ref, +) => { + const { className, ...rest } = props; + + return ( +
+ ); +}; + +export const SidebarInset = React.forwardRef(_SidebarInset); diff --git a/app/client/packages/design-system/widgets/src/components/Sidebar/src/SidebarProvider.tsx b/app/client/packages/design-system/widgets/src/components/Sidebar/src/SidebarProvider.tsx new file mode 100644 index 000000000000..35d688f23837 --- /dev/null +++ b/app/client/packages/design-system/widgets/src/components/Sidebar/src/SidebarProvider.tsx @@ -0,0 +1,96 @@ +import clsx from "clsx"; +import React, { type Ref, useCallback, useState } from "react"; + +import styles from "./styles.module.css"; +import { SidebarContext } from "./context"; +import { useIsMobile } from "./use-mobile"; +import { SIDEBAR_CONSTANTS } from "./constants"; +import type { SidebarContextType, SidebarProviderProps } from "./types"; + +export const _SidebarProvider = ( + props: SidebarProviderProps, + ref: Ref, +) => { + const { + children, + className, + defaultOpen = true, + isOpen: openProp, + onOpen: setOpenProp, + style, + ...rest + } = props; + const isMobile = useIsMobile(); + + const [_open, _setOpen] = useState(defaultOpen); + const open = openProp ?? _open; + const setOpen = useCallback( + (value: boolean | ((value: boolean) => boolean)) => { + const openState = typeof value === "function" ? value(open) : value; + + if (setOpenProp) { + setOpenProp(openState); + } else { + _setOpen(openState); + } + }, + [setOpenProp, open], + ); + + const toggleSidebar = React.useCallback(() => { + return isMobile ? setOpen((open) => !open) : setOpen((open) => !open); + }, [isMobile, setOpen]); + + React.useEffect( + function handleKeyboardShortcuts() { + const handleKeyDown = (event: KeyboardEvent) => { + if ( + event.key === SIDEBAR_CONSTANTS.KEYBOARD_SHORTCUT && + (event.metaKey || event.ctrlKey) + ) { + event.preventDefault(); + toggleSidebar(); + } + }; + + window.addEventListener("keydown", handleKeyDown); + + return () => window.removeEventListener("keydown", handleKeyDown); + }, + [toggleSidebar, isMobile], + ); + + const state = open ? "expanded" : "collapsed"; + + const contextValue = React.useMemo( + () => ({ + state, + open, + setOpen, + isMobile, + toggleSidebar, + }), + [state, open, setOpen, isMobile, toggleSidebar], + ); + + return ( + +
+ {children} +
+
+ ); +}; + +export const SidebarProvider = React.forwardRef(_SidebarProvider); diff --git a/app/client/packages/design-system/widgets/src/components/Sidebar/src/SidebarTrigger.tsx b/app/client/packages/design-system/widgets/src/components/Sidebar/src/SidebarTrigger.tsx new file mode 100644 index 000000000000..4649d3ae877c --- /dev/null +++ b/app/client/packages/design-system/widgets/src/components/Sidebar/src/SidebarTrigger.tsx @@ -0,0 +1,29 @@ +import * as React from "react"; +import { Button } from "@appsmith/wds"; + +import { useSidebar } from "./use-sidebar"; + +interface SidebarTriggerProps { + onPress?: () => void; +} + +export const SidebarTrigger = (props: SidebarTriggerProps) => { + const { onPress } = props; + const { state, toggleSidebar } = useSidebar(); + + return ( +