From 81f360d7c1917fdd20210fbd8bd0aaea370e25e5 Mon Sep 17 00:00:00 2001 From: Mark Rosenberg <38965626+markdoeswork@users.noreply.github.com> Date: Fri, 27 Sep 2024 12:11:05 -0400 Subject: [PATCH] [PLAY-1528] Drawer Beta Kit (#3686) **What does this PR do?** A clear and concise description with your runway ticket url. Runway https://runway.powerhrg.com/backlog_items/PLAY-1528 This is the first implementation of the draw kit. Its like the dialog except it only goes on the side on the page **Screenshots:** Screenshots to visualize your addition/change ![screenshot-pr3686_playbook_beta_px_powerapp_cloud-2024_09_18-09_33_10](https://github.com/user-attachments/assets/d554bbc9-93d5-46d4-813d-491f2fb5c981) **How to test?** Steps to confirm the desired behavior: 1. Go to https://pr3686.playbook.beta.px.powerapp.cloud/kits/drawer/react 2. Click buttons #### Checklist: - [x] **LABELS** Add a label: `enhancement`, `bug`, `improvement`, `new kit`, `deprecated`, or `breaking`. See [Changelog & Labels](https://github.com/powerhome/playbook/wiki/Changelog-&-Labels) for details. - [x] **DEPLOY** I have added the `milano` label to show I'm ready for a review. - [x] **TESTS** I have added test coverage to my code. --------- Co-authored-by: Nida Ghuman --- playbook-website/config/menu.yml | 4 +- playbook/app/entrypoints/playbook-doc.js | 2 + playbook/app/entrypoints/playbook.scss | 2 + playbook/app/javascript/kits.js | 1 + .../pb_advanced_table/advanced_table.test.jsx | 2 +- .../playbook/pb_drawer/_close_icon.tsx | 25 + .../pb_kits/playbook/pb_drawer/_drawer.scss | 465 ++++++++++++++++++ .../pb_kits/playbook/pb_drawer/_drawer.tsx | 195 ++++++++ .../playbook/pb_drawer/_drawer_context.tsx | 3 + .../pb_drawer/docs/_drawer_borders.jsx | 117 +++++ .../pb_drawer/docs/_drawer_breakpoints.jsx | 43 ++ .../pb_drawer/docs/_drawer_default.html.erb | 1 + .../pb_drawer/docs/_drawer_default.jsx | 63 +++ .../pb_drawer/docs/_drawer_overlay.jsx | 55 +++ .../playbook/pb_drawer/docs/_drawer_sizes.jsx | 113 +++++ .../playbook/pb_drawer/docs/example.yml | 12 + .../pb_kits/playbook/pb_drawer/docs/index.js | 5 + .../playbook/pb_drawer/drawer.html.erb | 12 + .../app/pb_kits/playbook/pb_drawer/drawer.rb | 8 + .../playbook/pb_drawer/drawer.test.jsx | 77 +++ 20 files changed, 1203 insertions(+), 2 deletions(-) create mode 100644 playbook/app/pb_kits/playbook/pb_drawer/_close_icon.tsx create mode 100644 playbook/app/pb_kits/playbook/pb_drawer/_drawer.scss create mode 100644 playbook/app/pb_kits/playbook/pb_drawer/_drawer.tsx create mode 100644 playbook/app/pb_kits/playbook/pb_drawer/_drawer_context.tsx create mode 100644 playbook/app/pb_kits/playbook/pb_drawer/docs/_drawer_borders.jsx create mode 100644 playbook/app/pb_kits/playbook/pb_drawer/docs/_drawer_breakpoints.jsx create mode 100644 playbook/app/pb_kits/playbook/pb_drawer/docs/_drawer_default.html.erb create mode 100644 playbook/app/pb_kits/playbook/pb_drawer/docs/_drawer_default.jsx create mode 100644 playbook/app/pb_kits/playbook/pb_drawer/docs/_drawer_overlay.jsx create mode 100644 playbook/app/pb_kits/playbook/pb_drawer/docs/_drawer_sizes.jsx create mode 100644 playbook/app/pb_kits/playbook/pb_drawer/docs/example.yml create mode 100644 playbook/app/pb_kits/playbook/pb_drawer/docs/index.js create mode 100644 playbook/app/pb_kits/playbook/pb_drawer/drawer.html.erb create mode 100644 playbook/app/pb_kits/playbook/pb_drawer/drawer.rb create mode 100644 playbook/app/pb_kits/playbook/pb_drawer/drawer.test.jsx diff --git a/playbook-website/config/menu.yml b/playbook-website/config/menu.yml index b731001d6d..882f663f75 100644 --- a/playbook-website/config/menu.yml +++ b/playbook-website/config/menu.yml @@ -43,6 +43,9 @@ kits: platforms: *1 description: status: stable + - name: drawer + platforms: *1 + status: beta - category: buttons description: Buttons are used primarily for actions, such as “Save” and “Cancel”. Link Buttons are used for less important or less commonly used actions, such as @@ -569,4 +572,3 @@ kits: with avatar, titles, name and territory. This is a versatile kit with features than can be added to display more info. status: stable - diff --git a/playbook/app/entrypoints/playbook-doc.js b/playbook/app/entrypoints/playbook-doc.js index 6099d27999..91fc773807 100755 --- a/playbook/app/entrypoints/playbook-doc.js +++ b/playbook/app/entrypoints/playbook-doc.js @@ -35,6 +35,7 @@ import * as Detail from 'kits/pb_detail/docs' import * as Dialog from 'kits/pb_dialog/docs' import * as DistributionBarDocs from 'kits/pb_distribution_bar/docs' import * as Draggable from 'kits/pb_draggable/docs' +import * as Drawer from 'kits/pb_drawer/docs' import * as Dropdown from 'kits/pb_dropdown/docs' import * as FileUpload from 'kits/pb_file_upload/docs' import * as Filter from 'kits/pb_filter/docs' @@ -142,6 +143,7 @@ WebpackerReact.registerComponents({ ...Dialog, ...DistributionBarDocs, ...Draggable, + ...Drawer, ...Dropdown, ...FileUpload, ...Filter, diff --git a/playbook/app/entrypoints/playbook.scss b/playbook/app/entrypoints/playbook.scss index de6e5f3e9d..eed254252d 100755 --- a/playbook/app/entrypoints/playbook.scss +++ b/playbook/app/entrypoints/playbook.scss @@ -1,3 +1,4 @@ + @import 'kits/pb_advanced_table/advanced_table'; @import 'kits/pb_avatar/avatar'; @import 'kits/pb_avatar_action_button/avatar_action_button'; @@ -105,6 +106,7 @@ @import 'kits/pb_user_badge/user_badge'; @import 'kits/pb_walkthrough/walkthrough'; @import 'kits/pb_weekday_stacked/weekday_stacked'; +@import 'kits/pb_drawer/drawer'; @import 'utilities/mixins'; @import 'utilities/spacing'; @import 'utilities/cursor'; diff --git a/playbook/app/javascript/kits.js b/playbook/app/javascript/kits.js index b3e1abad72..9651ffa73b 100644 --- a/playbook/app/javascript/kits.js +++ b/playbook/app/javascript/kits.js @@ -34,6 +34,7 @@ export { default as Detail} from '../pb_kits/playbook/pb_detail/_detail' export { default as Dialog } from '../pb_kits/playbook/pb_dialog/_dialog' export { default as DistributionBar } from '../pb_kits/playbook/pb_distribution_bar/_distribution_bar' export { default as Draggable} from '../pb_kits/playbook/pb_draggable/_draggable' +export { default as Drawer} from '../pb_kits/playbook/pb_drawer/_drawer' export { default as Dropdown} from '../pb_kits/playbook/pb_dropdown/_dropdown' export { default as FileUpload } from '../pb_kits/playbook/pb_file_upload/_file_upload' export { default as Filter } from '../pb_kits/playbook/pb_filter/_filter' diff --git a/playbook/app/pb_kits/playbook/pb_advanced_table/advanced_table.test.jsx b/playbook/app/pb_kits/playbook/pb_advanced_table/advanced_table.test.jsx index 16177faa41..2cb8a51674 100644 --- a/playbook/app/pb_kits/playbook/pb_advanced_table/advanced_table.test.jsx +++ b/playbook/app/pb_kits/playbook/pb_advanced_table/advanced_table.test.jsx @@ -462,4 +462,4 @@ test("responsive none prop functions as expected", () => { const kit = screen.getByTestId(testId) expect(kit).toHaveClass("pb_advanced_table table-responsive-none") -}) \ No newline at end of file +}) diff --git a/playbook/app/pb_kits/playbook/pb_drawer/_close_icon.tsx b/playbook/app/pb_kits/playbook/pb_drawer/_close_icon.tsx new file mode 100644 index 0000000000..41da821bc8 --- /dev/null +++ b/playbook/app/pb_kits/playbook/pb_drawer/_close_icon.tsx @@ -0,0 +1,25 @@ +import React from 'react' +import Icon from '../pb_icon/_icon' + +import { getAllIcons } from "../utilities/icons/allicons" + +type CloseIconProps = { + onClose: () => void, +} + +export const CloseIcon = (props: CloseIconProps): React.ReactElement => { + const { onClose } = props + const timesIcon = getAllIcons()["times"] + return ( +
+ +
+ ) +} diff --git a/playbook/app/pb_kits/playbook/pb_drawer/_drawer.scss b/playbook/app/pb_kits/playbook/pb_drawer/_drawer.scss new file mode 100644 index 0000000000..6b54045b0f --- /dev/null +++ b/playbook/app/pb_kits/playbook/pb_drawer/_drawer.scss @@ -0,0 +1,465 @@ +@import "../tokens/positioning"; +@import "../tokens/colors"; +@import "../pb_card/card_mixin"; +@import "../tokens/shadows"; +@import "../tokens/border_radius"; +@import "../tokens/spacing"; +@import "../tokens/animation-curves"; +@import "../tokens/positioning"; + +// Drawer animations +// Drawer animations for fading in and out from the center +@keyframes modalFadeIn { + from { + transform: translate3d(0, -100%, 0); + opacity: 0; + } + to { + transform: translate3d(0, 0, 0); + opacity: 1; + } +} + +@keyframes modalFadeOut { + from { + transform: translate3d(0, 0, 0); + opacity: 1; + } + to { + transform: translate3d(0, -50%, 0); + opacity: 0; + } +} + +// Drawer animations for fading in and out from the right side + +@keyframes overlayFade { + from { + opacity: 0; + transform: translateY(0); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +@keyframes overlayFadeOut { + from { + opacity: 1; + } + to { + opacity: 0; + } +} + +body.ReactModal__Body--open { + transition: margin-left 0.3s ease-in-out, margin-right 0.3s ease-in-out; +} + +.pb_drawer_lg_left.pb_drawer { + transform: translateX(-100%); +} + +.pb_drawer_lg_right.pb_drawer { + transform: translateX(100%); +} + +.pb_drawer.pb_drawer_after_open { + transform: translateX(0); /* Slide in */ +} +// Drawer Styles +.pb_drawer { + + // Local Variables + $gutter: $space_lg; + $xsmall: 64px; + $small: 200px; + $medium: 250px; + $large: 300px; + $xlarge: 365px; + $animation-duration: .2s; + $z-index: 100; + $opacity_visible: 1; + $opacity_hidden: 0; + + .drawer { + position: sticky; + top: 0; + background-color: $white; + z-index: $z_8; + } + + // @include pb_card; + background-color: $white; + border: 0; + box-shadow: $shadow_deepest; // border class here + max-height: calc(100vh - #{$gutter * 2}); + max-width: calc(100vw - #{$gutter * 2}); + overflow: auto; + animation-name: modalFadeIn; + animation-duration: $animation-duration; + outline: none; + animation-timing-function: $easeInOutQuint; + transition: transform 0.3s ease-in-out; + + &.drawer_border_full { + box-shadow: none; + border: 2px solid #f3f7fb; + } + + &.drawer_border_right { + border-right: 2px solid #f3f7fb; + } + + &.drawer_border_left { + border-left: 2px solid #f3f7fb; + } + + &[class*="_left"] { + animation-name: modalFadeInLeft; + &[class*="_before_close"] { + animation-name: modalFadeOutLeft; + animation-duration: $animation-duration; + opacity: $opacity_hidden; + } + } + + &[class*="_right"] { + animation-name: modalFadeInRight; + &[class*="_before_close"] { + animation-name: modalFadeOutRight; + animation-duration: $animation-duration; + opacity: $opacity_hidden; + } + } + + &[class*="_xs_"] { + width: $xsmall; + max-width: $xsmall; + } + + &[class*="_sm_"] { + width: $small; + max-width: $small; + } + + &[class*="_md_"] { + width: $medium; + max-width: $medium; + } + + &[class*="_lg_"] { + width: $large; + max-width: $large; + } + + &[class*="_xl_"] { + width: $xlarge; + max-width: $xlarge; + } + + &_body_open { + overflow: hidden; + } + + &_after_open { + opacity: $opacity_visible; + } + + &.no-background { + background-color: transparent; + } + + &[class*="_before_close"] { + animation-name: modalFadeOut; + animation-duration: $animation-duration; + opacity: $opacity_hidden; + } + + &_close_icon { + cursor: pointer; + } + + &_overlay { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + display: flex; + align-items: center; + justify-content: center; + background-color: rgba($bg_dark, $opacity_4); + z-index: $z-index; + animation-name: overlayFade; + animation-duration: $animation-duration; + + &_after_open { + opacity: $opacity_visible; + } + &_before_close { + animation-name: overlayFadeOut; + animation-duration: $animation-duration; + opacity: $opacity_hidden; + } + &[class*="full_height"] { + &[class*="_left"]{ + justify-content: flex-start; + } + + &[class*="_center"]{ + justify-content: center; + } + + &[class*="_right"]{ + justify-content: flex-end; + } + + .pb_drawer { + height: 100%; + max-height: 100%; + max-width: none; + // This empty div only has height when drawer is full height + // Fix for drawer body content disappearing behind sticky footer + .drawer-pseudo-footer { + height: $space_xl * 2; + } + .drawer_footer { + position: fixed; + bottom: 0; + background-color: $white; + max-width: 100%; + } + &[class*="_xs_"] { + width: $xsmall; + .dialog_footer { + width: $xsmall; + } + } + &[class*="_sm_"] { + width: $small; + .dialog_footer { + width: $small; + } + } + &[class*="_md_"] { + width: $medium; + .dialog_footer { + width: $medium; + } + } + &[class*="_lg_"] { + width: $large; + .dialog_footer { + width: $large; + } + } + &[class*="_xl_"] { + width: $xlarge; + .dialog_footer { + width: $xlarge; + } + } + } + } +} + + &_no_overlay { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + display: flex; + align-items: center; + justify-content: center; + z-index: $z-index; + animation-name: overlayFade; + animation-duration: $animation-duration; + overflow: none; /* Ensure body remains scrollable */ + pointer-events: none; /* Allow interaction inside the drawer itself */ + + body.ReactModal__Body--open { + overflow: none; /* Ensure body remains scrollable */ + pointer-events: none; /* Allow interaction inside the drawer itself */ + } + + &_after_open { + opacity: $opacity_visible; + overflow: none; /* Ensure body remains scrollable */ + pointer-events: none; /* Allow interaction inside the drawer itself */ + } + &_before_close { + animation-name: overlayFadeOut; + animation-duration: $animation-duration; + opacity: $opacity_hidden; + } + &[class*="full_height"] { + &[class*="_left"]{ + justify-content: flex-start; + } + + &[class*="_center"]{ + justify-content: center; + } + + &[class*="_right"]{ + justify-content: flex-end; + } + + .pb_drawer { + height: 100%; + max-height: 100%; + max-width: none; + // This empty div only has height when drawer is full height + // Fix for drawer body content disappearing behind sticky footer + .drawer-pseudo-footer { + height: $space_xl * 2; + } + .drawer_footer { + position: fixed; + bottom: 0; + background-color: $white; + max-width: 100%; + } + &[class*="_xs_"] { + width: $xsmall; + .dialog_footer { + width: $xsmall; + } + } + &[class*="_sm_"] { + width: $small; + .dialog_footer { + width: $small; + } + } + &[class*="_md_"] { + width: $medium; + .dialog_footer { + width: $medium; + } + } + &[class*="_lg_"] { + width: $large; + .dialog_footer { + width: $large; + } + } + &[class*="_xl_"] { + width: $xlarge; + .dialog_footer { + width: $xlarge; + } + } + } + } +} + +[class*="drawer_body"] { + padding: $space_sm; +} + +[class*="drawer_header"] { + padding: $space_sm; +} + +[class*="drawer_footer"] { + padding: $space_sm; +} + +//styles specific to rails version of kit +// rails version has own wrapper because of the way the overlay functions for the HTML drawer used to create this +.pb_drawer_wrapper_rails { + $medium: 500px; + $large: 800px; + $xlarge: 1150px; + + &[class*="full_height"] { + &[class*="_left"]{ + .pb_drawer_rails { + margin: unset !important; + margin-right: auto !important; + } + } + + &[class*="_center"]{ + justify-content: center; + } + + &[class*="_right"]{ + .pb_drawer_rails { + margin: unset !important; + margin-left: auto !important; + } + } + + .pb_drawer { + height: 100% !important; + max-height: 100% !important; + max-width: 100%; + // This empty div only has height when drawer is full height. + // Fix for drawer body content disappearing behind sticky footer + .drawer-pseudo-footer { + height: $space_xl * 2; + } + .drawer_footer { + position:fixed; + bottom: 0; + background-color: $white; + max-width: 100%; + } + &[class*="_sm"] { + width: $medium; + .drawer_footer { + width: $medium; + } + } + &[class*="_md"] { + width: $large; + .drawer_footer { + width: $large; + } + } + &[class*="_lg"] { + width: $xlarge; + .drawer_footer { + width: $xlarge; + } + } + } + } + + // Fixes for stylesheets in nitro that were conflicting with our kit. DO NOT REMOVE. + // Conflicts were only apparent in nitro, not in playbook local env + .pb_drawer_rails { + position: fixed !important; + top: 0 !important; + padding: unset !important; + margin: auto; + + } + + // Overlay for rails kit + drawer::backdrop { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + display: flex; + align-items: center; + justify-content: center; + background-color: rgba($bg_dark, $opacity_4); + animation-name: overlayFade; + animation-duration: 0.2s; + } + + .drawer-button-class { + background-color: unset; + border: none; + cursor: pointer; + } +} +} diff --git a/playbook/app/pb_kits/playbook/pb_drawer/_drawer.tsx b/playbook/app/pb_kits/playbook/pb_drawer/_drawer.tsx new file mode 100644 index 0000000000..60d72a6bdc --- /dev/null +++ b/playbook/app/pb_kits/playbook/pb_drawer/_drawer.tsx @@ -0,0 +1,195 @@ +import React, { useState, useEffect } from "react"; +import classnames from "classnames"; +import Modal from "react-modal"; + +import { buildAriaProps, buildCss, buildDataProps, buildHtmlProps } from "../utilities/props"; +import { globalProps } from "../utilities/globalProps"; + +import { DialogContext } from "../pb_dialog/_dialog_context"; + +type DrawerProps = { + aria?: { [key: string]: string }; + behavior?: "floating" | "push"; + border?: "full" | "none" | "right" | "left"; + breakpoint?: "none" | "xs" | "sm" | "md" | "lg" | "xl"; + children: React.ReactNode | React.ReactNode[] | string; + className?: string; + data?: { [key: string]: string }; + htmlOptions?: { [key: string]: string | number | boolean | (() => void) }; + id?: string; + fullHeight?: boolean; + onClose?: () => void; + opened: boolean; + overlay: boolean; + placement?: "left" | "right"; + size?: "xs" | "sm" | "md" | "lg" | "xl"; + text?: string; + trigger?: string; +}; + +const Drawer = (props: DrawerProps): React.ReactElement => { + const { + aria = {}, + behavior = "floating", + border = "none", + breakpoint = "none", + className, + data = {}, + htmlOptions = {}, + id, + size = "md", + children, + fullHeight = false, + opened, + onClose, + overlay = true, + placement = "left", + trigger, + } = props; + const ariaProps = buildAriaProps(aria); + const dataProps = buildDataProps(data); + const htmlProps = buildHtmlProps(htmlOptions); + + let globalPropsString: string = globalProps(props); + + // Check if the string contains any of the prefixes + const containsPrefix = ['p_', 'pb_', 'pt_', 'pl_', 'pr_', 'px_', 'py_'].some((prefix) => + globalPropsString.includes(prefix) + ); + + // If none of the prefixes are found, append 'p_sm' to the string + if (!containsPrefix) { + globalPropsString += ' p_sm'; + } + + const drawerClassNames = { + base: `${classnames( + "pb_drawer", + buildCss("pb_drawer", size, placement), + { + "drawer_border_full": border === "full", + "drawer_border_right": border === "right", + "drawer_border_left": border === "left", + } + )} ${globalPropsString}`, + afterOpen: "pb_drawer_after_open", + beforeClose: "pb_drawer_before_close", + }; + + const fullHeightClassNames = () => { + if (!fullHeight) return null; + return `full_height_${placement}`; + }; + + const overlayClassNames = { + base: `pb_drawer${overlay ? '_overlay' : '_no_overlay'} ${fullHeight !== null && fullHeightClassNames()} ${!overlay ? 'no-background' : ''}`, + afterOpen: "pb_drawer_overlay_after_open", + beforeClose: "pb_drawer_overlay_before_close", + }; + + const classes = classnames( + buildCss("pb_drawer_wrapper"), + className + ); + + const [triggerOpened, setTriggerOpened] = useState(false); + + const breakpointWidths: Record = { + none: 0, + xs: 575, + sm: 768, + md: 992, + lg: 1200, + xl: 1400, + }; + + // State to manage opening the drawer based on breakpoint + const [isBreakpointOpen, setIsBreakpointOpen] = useState(false); + + useEffect(() => { + if (breakpoint === 'none') return; + + const handleResize = () => { + const width = window.innerWidth; + const breakpointWidth = breakpointWidths[breakpoint]; + + if (width <= breakpointWidth) { + setIsBreakpointOpen(true); + } else { + setIsBreakpointOpen(false); + } + }; + + window.addEventListener('resize', handleResize); + + // Call handler once on mount to set initial state + handleResize(); + + return () => { + window.removeEventListener('resize', handleResize); + }; + }, [breakpoint]); + + const modalIsOpened = trigger ? triggerOpened : (opened || isBreakpointOpen); + + useEffect(() => { + const sizeMap: Record = { + xl: '365px', + lg: '300px', + md: '250px', + sm: '200px', + xs: '64px', + }; + const body = document.querySelector('body'); + + if (modalIsOpened && behavior === 'push' && body) { + if (placement === 'left') { + body.style.cssText = `margin-left: ${sizeMap[size]} !important; margin-right: '' !important;`; + } else if (placement === 'right') { + body.style.cssText = `margin-right: ${sizeMap[size]} !important; margin-left: '' !important;`; + } + + body.classList.add('ReactModal__Body--open'); + } else if (body) { + body.style.cssText = ''; // Clear the styles when modal is closed or behavior is not 'push' + body.classList.remove('ReactModal__Body--open'); + } + }, [modalIsOpened, behavior, placement, size]); + + const api = { + onClose: trigger + ? function () { + setTriggerOpened(false); + } + : onClose, + }; + + return ( + +
+ + <> + {children} + + +
+
+ ); +}; + +export default Drawer; diff --git a/playbook/app/pb_kits/playbook/pb_drawer/_drawer_context.tsx b/playbook/app/pb_kits/playbook/pb_drawer/_drawer_context.tsx new file mode 100644 index 0000000000..2f55333840 --- /dev/null +++ b/playbook/app/pb_kits/playbook/pb_drawer/_drawer_context.tsx @@ -0,0 +1,3 @@ +import React from "react"; + +export const DrawerContext = React.createContext(null); diff --git a/playbook/app/pb_kits/playbook/pb_drawer/docs/_drawer_borders.jsx b/playbook/app/pb_kits/playbook/pb_drawer/docs/_drawer_borders.jsx new file mode 100644 index 0000000000..6b55833604 --- /dev/null +++ b/playbook/app/pb_kits/playbook/pb_drawer/docs/_drawer_borders.jsx @@ -0,0 +1,117 @@ +import React, { useState } from "react"; +import { Button, Drawer, Flex } from "playbook-ui"; + +const DrawerBorders = () => { + // Individual state variables for each drawer size + const [openedBRightDrawer, setOpenedBRightDrawer] = useState(false); + const [openedBLeftDrawer, setOpenedBLeftDrawer] = useState(false); + const [openedBFullDrawer, setOpenedBFullDrawer] = useState(false); + const [openedBDefaultDrawer, setOpenedBDefaultDrawer] = useState(false); + const [openedBRoundedDrawer, setOpenedBRoundedDrawer] = useState(false); + + // Toggle functions for each drawer + const toggleBRightDrawer = () => setOpenedBRightDrawer(!openedBRightDrawer); + const toggleBLeftDrawer = () => setOpenedBLeftDrawer(!openedBLeftDrawer); + const toggleBFullDrawer = () => setOpenedBFullDrawer(!openedBFullDrawer); + const toggleBDefaultDrawer = () => setOpenedBDefaultDrawer(!openedBDefaultDrawer); + const toggleBRoundedDrawer = () => setOpenedBRoundedDrawer(!openedBRoundedDrawer); + + return ( + <> + + + + + + + + + {/* Drawers for each size */} + + This is a Drawer with border right + + + This is a Drawer with border left + + + This is a Drawer with border full + + + This is a Default Drawer + + +
+ This is a Rounded Drawer +
+
+ + ); +}; + +export default DrawerBorders; diff --git a/playbook/app/pb_kits/playbook/pb_drawer/docs/_drawer_breakpoints.jsx b/playbook/app/pb_kits/playbook/pb_drawer/docs/_drawer_breakpoints.jsx new file mode 100644 index 0000000000..a69c147cff --- /dev/null +++ b/playbook/app/pb_kits/playbook/pb_drawer/docs/_drawer_breakpoints.jsx @@ -0,0 +1,43 @@ +import React, { useState } from "react"; +import { Button, Drawer, Flex } from "playbook-ui"; + +const useDrawer = (visible = false) => { + const [opened, setOpened] = useState(visible); + const toggle = () => setOpened(!opened); + + return [opened, toggle]; +}; + +const DrawerBreakpoints = () => { + const [smallDrawerOpened, toggleSmallDrawer] = useDrawer(); + + return ( + <> + + + + + + Open because small breakpoint + + + + ); +}; + +export default DrawerBreakpoints; diff --git a/playbook/app/pb_kits/playbook/pb_drawer/docs/_drawer_default.html.erb b/playbook/app/pb_kits/playbook/pb_drawer/docs/_drawer_default.html.erb new file mode 100644 index 0000000000..a9426c01e6 --- /dev/null +++ b/playbook/app/pb_kits/playbook/pb_drawer/docs/_drawer_default.html.erb @@ -0,0 +1 @@ +<%= pb_rails("drawer") %> diff --git a/playbook/app/pb_kits/playbook/pb_drawer/docs/_drawer_default.jsx b/playbook/app/pb_kits/playbook/pb_drawer/docs/_drawer_default.jsx new file mode 100644 index 0000000000..be8669b3f8 --- /dev/null +++ b/playbook/app/pb_kits/playbook/pb_drawer/docs/_drawer_default.jsx @@ -0,0 +1,63 @@ +import React, { useState } from "react"; +import { Button, Drawer, Flex } from "playbook-ui"; + +const useDrawer = (visible = false) => { + const [opened, setOpened] = useState(visible); + const toggle = () => setOpened(!opened); + + return [opened, toggle]; +}; + +const DrawerDefault = () => { + const [headerSeparatorDrawerOpened, toggleHeaderSeparatorDrawer] = useDrawer(); + const [bothSeparatorsDrawerOpened, toggleBothSeparatorsDrawer] = useDrawer(); + + return ( + <> + + + + + + {/* Left Drawer */} + + Test me (Left Drawer) + + + {/* Right Drawer */} + + Test me (Right Drawer) + + + + ); +}; + +export default DrawerDefault; diff --git a/playbook/app/pb_kits/playbook/pb_drawer/docs/_drawer_overlay.jsx b/playbook/app/pb_kits/playbook/pb_drawer/docs/_drawer_overlay.jsx new file mode 100644 index 0000000000..56c38dfbc3 --- /dev/null +++ b/playbook/app/pb_kits/playbook/pb_drawer/docs/_drawer_overlay.jsx @@ -0,0 +1,55 @@ +import React, { useState } from "react"; +import { Button, Drawer, Flex } from "playbook-ui"; + +const DrawerSizes = () => { + // Individual state variables for each drawer size + const [openedNoOverlayDrawer, setOpenedNoOverlayDrawer] = useState(false); + const [openedOverlayDrawer, setOpenedOverlayDrawer] = useState(false); + + // Toggle functions for each drawer + const toggleNoOverlayDrawer = () => setOpenedNoOverlayDrawer(!openedNoOverlayDrawer); + const toggleOverlayDrawer = () => setOpenedOverlayDrawer(!openedOverlayDrawer); + + return ( + <> + + + + + + {/* Drawers for each size */} + + This is a Drawer with no overlay + + + This is a Drawer with an overlay + + + ); +}; + +export default DrawerSizes; diff --git a/playbook/app/pb_kits/playbook/pb_drawer/docs/_drawer_sizes.jsx b/playbook/app/pb_kits/playbook/pb_drawer/docs/_drawer_sizes.jsx new file mode 100644 index 0000000000..02c7891c9b --- /dev/null +++ b/playbook/app/pb_kits/playbook/pb_drawer/docs/_drawer_sizes.jsx @@ -0,0 +1,113 @@ +import React, { useState } from "react"; +import { Button, Drawer, Flex } from "playbook-ui"; + +const DrawerSizes = () => { + // Individual state variables for each drawer size + const [openedXsDrawer, setOpenedXsDrawer] = useState(false); + const [openedSmDrawer, setOpenedSmDrawer] = useState(false); + const [openedMdDrawer, setOpenedMdDrawer] = useState(false); + const [openedLgDrawer, setOpenedLgDrawer] = useState(false); + const [openedXlDrawer, setOpenedXlDrawer] = useState(false); + + // Toggle functions for each drawer + const toggleXsDrawer = () => setOpenedXsDrawer(!openedXsDrawer); + const toggleSmDrawer = () => setOpenedSmDrawer(!openedSmDrawer); + const toggleMdDrawer = () => setOpenedMdDrawer(!openedMdDrawer); + const toggleLgDrawer = () => setOpenedLgDrawer(!openedLgDrawer); + const toggleXlDrawer = () => setOpenedXlDrawer(!openedXlDrawer); + + return ( + <> + + + + + + + + + {/* Drawers for each size */} + + XS + + + + This is an SM Drawer + + + + This is an MD Drawer + + + + This is an LG Drawer + + + + This is an XL Drawer + + + ); +}; + +export default DrawerSizes; diff --git a/playbook/app/pb_kits/playbook/pb_drawer/docs/example.yml b/playbook/app/pb_kits/playbook/pb_drawer/docs/example.yml new file mode 100644 index 0000000000..32d166df0a --- /dev/null +++ b/playbook/app/pb_kits/playbook/pb_drawer/docs/example.yml @@ -0,0 +1,12 @@ +examples: + + rails: + - drawer_default: Default + + + react: + - drawer_default: Default + - drawer_sizes: Sizes + - drawer_overlay: Overlay + - drawer_borders: Borders + - drawer_breakpoints: Open on Breakpoints diff --git a/playbook/app/pb_kits/playbook/pb_drawer/docs/index.js b/playbook/app/pb_kits/playbook/pb_drawer/docs/index.js new file mode 100644 index 0000000000..acb54022b4 --- /dev/null +++ b/playbook/app/pb_kits/playbook/pb_drawer/docs/index.js @@ -0,0 +1,5 @@ +export { default as DrawerDefault } from './_drawer_default.jsx' +export { default as DrawerSizes } from './_drawer_sizes.jsx' +export { default as DrawerOverlay } from './_drawer_overlay.jsx' +export { default as DrawerBorders } from './_drawer_borders.jsx' +export { default as DrawerBreakpoints } from './_drawer_breakpoints.jsx' diff --git a/playbook/app/pb_kits/playbook/pb_drawer/drawer.html.erb b/playbook/app/pb_kits/playbook/pb_drawer/drawer.html.erb new file mode 100644 index 0000000000..402677df08 --- /dev/null +++ b/playbook/app/pb_kits/playbook/pb_drawer/drawer.html.erb @@ -0,0 +1,12 @@ + + +<%= pb_content_tag( + # :div, + # aria: object.aria, + # class: object.classname, + # data: object.data, + # id: object.id, + # **combined_html_options +) do %> + DRAWER CONTENT +<% end %> \ No newline at end of file diff --git a/playbook/app/pb_kits/playbook/pb_drawer/drawer.rb b/playbook/app/pb_kits/playbook/pb_drawer/drawer.rb new file mode 100644 index 0000000000..962557a3a4 --- /dev/null +++ b/playbook/app/pb_kits/playbook/pb_drawer/drawer.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +module Playbook + module PbDrawer + class Drawer < ::Playbook::KitBase + end + end +end diff --git a/playbook/app/pb_kits/playbook/pb_drawer/drawer.test.jsx b/playbook/app/pb_kits/playbook/pb_drawer/drawer.test.jsx new file mode 100644 index 0000000000..7231458518 --- /dev/null +++ b/playbook/app/pb_kits/playbook/pb_drawer/drawer.test.jsx @@ -0,0 +1,77 @@ +import React, { useState } from 'react'; +import { render, cleanup, fireEvent, screen } from '../utilities/test-utils'; +import { Drawer, Button } from 'playbook-ui'; + +const size = 'sm'; + +function DrawerTest({ props }) { + const [isOpen, setIsOpen] = useState(false); + const close = () => setIsOpen(false); + const open = () => setIsOpen(true); + + return ( + <> + + + {props && props.children} + + + ); +} + +afterEach(cleanup); + +test('renders with the right border class when border prop is right', async () => { + render(); + + fireEvent.click(screen.getByText('Open Drawer')); + + const drawer = await screen.findByRole('dialog'); + expect(drawer).toHaveClass('drawer_border_right'); +}); + +test('renders with the left border class when border prop is left', async () => { + render(); + + fireEvent.click(screen.getByText('Open Drawer')); + + const drawer = await screen.findByRole('dialog'); + expect(drawer).toHaveClass('drawer_border_left'); +}); + +test('renders with the full border class when border prop is full', async () => { + render(); + + fireEvent.click(screen.getByText('Open Drawer')); + + const drawer = await screen.findByRole('dialog'); + expect(drawer).toHaveClass('drawer_border_full'); +}); + +test('does not have a border class when border prop is none', async () => { + render(); + + fireEvent.click(screen.getByText('Open Drawer')); + + const drawer = await screen.findByRole('dialog'); + expect(drawer).not.toHaveClass('drawer_border_right'); + expect(drawer).not.toHaveClass('drawer_border_left'); + expect(drawer).not.toHaveClass('drawer_border_full'); +}); + +test('renders the correct size class for a large drawer', async () => { + render(); + + fireEvent.click(screen.getByText('Open Drawer')); + + const drawer = await screen.findByRole('dialog'); + expect(drawer).toHaveClass('pb_drawer pb_drawer_lg_left'); +});