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 (
+ <>
+
+
+ Drawer with border right
+
+
+ Drawer with border left
+
+
+ Drawer with border full
+
+
+ Default Drawer
+
+
+ Rounded Drawer
+
+
+
+ {/* 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 (
+ <>
+
+
+ {"Will open at small breakpoint"}
+
+
+
+
+ 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"}
+
+
+ {"Right Drawer"}
+
+
+
+ {/* 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 (
+ <>
+
+
+ No Overlay Drawer
+
+
+ Overlay Drawer
+
+
+
+ {/* 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 (
+ <>
+
+
+ XS Drawer
+
+
+ SM Drawer
+
+
+ MD Drawer
+
+
+ LG Drawer
+
+
+ XL Drawer
+
+
+
+ {/* 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 (
+ <>
+ {'Open Drawer'}
+
+ {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');
+});