diff --git a/.storybook/pages/CoursePlannerEdit/CoursePlannerEdit.tsx b/.storybook/pages/CoursePlannerEdit/CoursePlannerEdit.tsx index 6194822e5..fbce552bd 100644 --- a/.storybook/pages/CoursePlannerEdit/CoursePlannerEdit.tsx +++ b/.storybook/pages/CoursePlannerEdit/CoursePlannerEdit.tsx @@ -10,7 +10,6 @@ import { Card, DataBar, DragDrop, - DragDropContainerHeader, Grid, GridItem, Heading, @@ -421,7 +420,7 @@ export const CoursePlannerEdit = () => { itemIds: ['item-1', 'item-2', 'item-3', 'item-4', 'item-5'], emptyContent: container1EmptyContent(), header: ( - +
Available projects @@ -433,14 +432,14 @@ export const CoursePlannerEdit = () => {
-
+ ), }, 'container-2': { itemIds: [], emptyContent: container2EmptyContent(), header: ( - +
Planned projects @@ -452,7 +451,7 @@ export const CoursePlannerEdit = () => {
-
+ ), }, }); diff --git a/src/components/DragDrop/DragDrop.stories.tsx b/src/components/DragDrop/DragDrop.stories.tsx index 666036a94..a6b2d1113 100644 --- a/src/components/DragDrop/DragDrop.stories.tsx +++ b/src/components/DragDrop/DragDrop.stories.tsx @@ -1,16 +1,8 @@ import type { StoryObj, Meta } from '@storybook/react'; import type { ComponentProps } from 'react'; import React, { useState } from 'react'; -import type { NewState } from './DragDrop'; -import { DragDrop } from './DragDrop'; -import { - Button, - Card, - DragDropContainerHeader, - Heading, - Icon, - Text, -} from '../..'; +import { DragDrop, type NewState } from './DragDrop'; +import { Button, Card, Heading, Icon, Text } from '../..'; import styles from './DragDrop.stories.module.css'; export default { @@ -89,13 +81,13 @@ export const Default: StoryObj = { 'container-1': { itemIds: ['item-1', 'item-2', 'item-3', 'item-4', 'item-5'], header: ( - +
Available Projects
-
+ ), }, 'container-2': { @@ -296,13 +288,13 @@ export const Default: StoryObj = { ), header: ( - +
Selected Projects
-
+ ), }, }, @@ -363,13 +355,13 @@ export const HoveredHandle: StoryObj = { 'container-1': { itemIds: ['item-1', 'item-2', 'item-3', 'item-4', 'item-5'], header: ( - +
Available Projects
-
+ ), }, 'container-2': { @@ -490,13 +482,13 @@ export const HoveredHandle: StoryObj = { ), header: ( - +
Selected Projects
-
+ ), }, }, @@ -512,13 +504,13 @@ const InteractiveDragDrop = () => { ); const [indexState, setIndexState] = useState(2); - const [containers, setContainers] = useState({ + const [containers, setContainers] = useState({ 'container-1': { columnClassName: styles['bg-yellow'], emptyContent: emptyContent(), itemIds: ['item-1', 'item-2', 'item-3', 'item-4', 'item-5'], header: ( - +
Available projects @@ -530,7 +522,7 @@ const InteractiveDragDrop = () => {
-
+ ), }, 'container-2': { @@ -539,7 +531,7 @@ const InteractiveDragDrop = () => { emptyContent: emptyContent(), itemIds: [], header: ( - +
Planned projects @@ -551,7 +543,7 @@ const InteractiveDragDrop = () => {
-
+ ), }, 'container-3': { @@ -560,7 +552,7 @@ const InteractiveDragDrop = () => { emptyContent: emptyContent(), itemIds: [], header: ( - +
Planned projects @@ -572,7 +564,7 @@ const InteractiveDragDrop = () => {
-
+ ), }, 'container-4': { @@ -581,7 +573,7 @@ const InteractiveDragDrop = () => { emptyContent: emptyContent(), itemIds: [], header: ( - +
Planned projects @@ -593,11 +585,11 @@ const InteractiveDragDrop = () => {
-
+ ), }, }); - const [items, setItems] = useState({ + const [items, setItems] = useState({ 'item-1': { title: 'Project #1', children: ( @@ -639,7 +631,7 @@ const InteractiveDragDrop = () => { ), }, }); - const returnUpdatedItems = (updatedItems: any) => { + const returnUpdatedItems = (updatedItems: NewState) => { setContainers(updatedItems.containers); setItems(updatedItems.items); updatedItems.containers['container-2'].itemIds.map( @@ -655,7 +647,7 @@ const InteractiveDragDrop = () => { returnUpdatedItems(updatedItems)} + getNewState={(updatedItems) => returnUpdatedItems(updatedItems)} items={items} multipleContainers={false} unstyledItems diff --git a/src/components/DragDrop/DragDrop.tsx b/src/components/DragDrop/DragDrop.tsx index f3a56641a..42544e178 100644 --- a/src/components/DragDrop/DragDrop.tsx +++ b/src/components/DragDrop/DragDrop.tsx @@ -1,18 +1,49 @@ import clsx from 'clsx'; import React, { - type ReactNode, - useState, useEffect, useRef, + useState, + type ReactNode, type UIEvent, } from 'react'; -import type { DropResult, DroppableProvided } from 'react-beautiful-dnd'; -import { DragDropContext, Droppable } from 'react-beautiful-dnd'; -import type { Items, Containers } from './DragDropTypes'; -import DragDropContainer from '../DragDropContainer'; +import { + DragDropContext, + Draggable, + Droppable, + type DraggableProvided, + type DropResult, + type DroppableProvided, +} from 'react-beautiful-dnd'; +import { oneByType } from 'react-children-by-type'; +import Icon from '../Icon'; import styles from './DragDrop.module.css'; -export interface Props { +type ItemContents = { + id?: string; + title?: string; + behavior?: 'hover'; + children?: React.ReactNode; + handle?: React.ReactNode; +}; + +type Items = { + [key: string]: ItemContents; +}; + +type ContainerContents = { + className?: string; + columnClassName?: string; + emptyContent?: React.ReactNode; + header?: React.ReactNode; + id?: string; + itemIds: string[]; +}; + +type Containers = { + [key: string]: ContainerContents; +}; + +type DragDropProps = { /** * CSS class names that can be appended to the component. */ @@ -45,13 +76,179 @@ export interface Props { * Prop that allows parent components to get the updated drag and drop object data from the outside */ getNewState?: (newState: NewState) => void; -} +}; -export interface NewState { +export type NewState = { containerOrder: string[]; containers: Containers; items: Items; -} +}; + +type DragDropItemProps = { + /** + * CSS class names that can be appended to the component. + */ + className?: string; + /** + * The contents of an item; includes id, title (optional) and children (optional) + */ + item: ItemContents; + /** + * Item's original indexed position + */ + index: number; +}; + +type DragDropContainerHeaderProps = { + /** + * Child node(s) to be rendered as the header. + */ + children?: ReactNode; + /** + * CSS class names that can be appended to the component. + */ + className?: string; +}; + +type DragDropContainerProps = { + /** + * Prop that will contain an id for each container and an array of itemIds that will be used on initial render + */ + container: ContainerContents; + /** + * Empty state contents + */ + emptyContent?: ReactNode; + /** + * Prop that will be an array of items + */ + items: ItemContents[]; +}; + +/** + * Container for draggable components to be dropped within the container. + */ +const DragDropContainer = ({ + container, + items, + emptyContent, +}: DragDropContainerProps) => { + const componentClassName = clsx( + styles['drag-drop__container'], + items.length < 1 && styles['drag-drop__container--empty'], + container.className, + ); + + const containerInnerClassName = clsx( + styles['drag-drop__container-inner'], + container.columnClassName, + ); + + const dragDropContainerHeader = oneByType( + container.header, + DragDropContainerHeader, + ); + + const header = React.Children.map(dragDropContainerHeader, (child) => { + return React.cloneElement(child); + }); + + return container.id ? ( +
+ {header} + + {(provided: DroppableProvided) => + items.length > 0 ? ( +
    + {items.map((item: ItemContents, index: number) => ( + + ))} + {provided.placeholder} +
+ ) : ( +
+ {emptyContent} + {provided.placeholder} +
+ ) + } +
+
+ ) : null; +}; + +/** + * Component that contains header section for the container which consists of drag and drop components. + */ + +const DragDropContainerHeader = ({ + className, + children, + ...other +}: DragDropContainerHeaderProps) => { + return ( +
+ {children} +
+ ); +}; + +/** + * Item to be dragged and dropped in containers. + */ +const DragDropItem = ({ className, item, index }: DragDropItemProps) => { + const componentClassName = clsx( + styles['drag-drop__item'], + item.behavior === 'hover' && styles['drag-drop__item--hover'], + className, + ); + + // `id` is injected in + return item.id ? ( + + {(provided: DraggableProvided, snapshot) => { + const childrenWithProps = React.Children.map( + item.children, + (child: ReactNode) => { + // Checking isValidElement is the safe way and avoids a typescript + // error too. + if (React.isValidElement(child)) { + // @ts-expect-error "No overload matches this call" error due to type mismatch + return React.cloneElement(child, { + isDragging: snapshot.isDragging, + number: index + 1, + }); + } + }, + ); + return ( +
  • +
    + +
    + {childrenWithProps} +
  • + ); + }} +
    + ) : null; +}; /** * `import {DragDrop} from "@chanzuckerberg/eds"` @@ -66,7 +263,7 @@ export const DragDrop = ({ getNewState, multipleContainers = false, unstyledItems = false, -}: Props) => { +}: DragDropProps) => { /** * Set states and refs * @@ -339,3 +536,5 @@ export const DragDrop = ({ ); }; + +DragDrop.ContainerHeader = DragDropContainerHeader; diff --git a/src/components/DragDrop/DragDropTypes.ts b/src/components/DragDrop/DragDropTypes.ts deleted file mode 100644 index e38589ba8..000000000 --- a/src/components/DragDrop/DragDropTypes.ts +++ /dev/null @@ -1,26 +0,0 @@ -import type React from 'react'; - -export interface Items { - [key: string]: ItemType; -} - -export interface ItemType { - id?: string; - title?: string; - behavior?: 'hover'; - children?: React.ReactNode; - handle?: React.ReactNode; -} - -export interface Containers { - [key: string]: ContainerType; -} - -export interface ContainerType { - className?: string; - columnClassName?: string; - emptyContent?: React.ReactNode; - header?: React.ReactNode; - id?: string; - itemIds: string[]; -} diff --git a/src/components/DragDropContainer/DragDropContainer.tsx b/src/components/DragDropContainer/DragDropContainer.tsx deleted file mode 100644 index 16e8c3e11..000000000 --- a/src/components/DragDropContainer/DragDropContainer.tsx +++ /dev/null @@ -1,85 +0,0 @@ -import clsx from 'clsx'; -import type { ReactNode } from 'react'; -import React from 'react'; -import type { DroppableProvided } from 'react-beautiful-dnd'; -import { Droppable } from 'react-beautiful-dnd'; -import { oneByType } from 'react-children-by-type'; -import type { ContainerType, ItemType } from '../DragDrop/DragDropTypes'; -import DragDropContainerHeader from '../DragDropContainerHeader'; -import DragDropItem from '../DragDropItem'; -import styles from '../DragDrop/DragDrop.module.css'; - -export interface Props { - /** - * Prop that will contain an id for each container and an array of itemIds that will be used on initial render - */ - container: ContainerType; - /** - * Empty state contents - */ - emptyContent?: ReactNode; - /** - * Prop that will be an array of items - */ - items: ItemType[]; -} - -/** - * Container for draggable components to be dropped within the container. - */ -export const DragDropContainer = ({ - container, - items, - emptyContent, -}: Props) => { - const componentClassName = clsx( - styles['drag-drop__container'], - items.length < 1 && styles['drag-drop__container--empty'], - container.className, - ); - - const containerInnerClassName = clsx( - styles['drag-drop__container-inner'], - container.columnClassName, - ); - - const dragDropContainerHeader = oneByType( - container.header, - DragDropContainerHeader, - ); - - const header = React.Children.map(dragDropContainerHeader, (child) => { - return React.cloneElement(child); - }); - - return container.id ? ( -
    - {header} - - {(provided: DroppableProvided) => - items.length > 0 ? ( -
      - {items.map((item: ItemType, index: number) => ( - - ))} - {provided.placeholder} -
    - ) : ( -
    - {emptyContent} - {provided.placeholder} -
    - ) - } -
    -
    - ) : null; -}; diff --git a/src/components/DragDropContainer/index.ts b/src/components/DragDropContainer/index.ts deleted file mode 100644 index 6284aa56f..000000000 --- a/src/components/DragDropContainer/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { DragDropContainer as default } from './DragDropContainer'; diff --git a/src/components/DragDropContainerHeader/DragDropContainerHeader.tsx b/src/components/DragDropContainerHeader/DragDropContainerHeader.tsx deleted file mode 100644 index 3b92f3512..000000000 --- a/src/components/DragDropContainerHeader/DragDropContainerHeader.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import type { ReactNode } from 'react'; -import React from 'react'; - -export interface Props { - /** - * Child node(s) that can be nested inside component. `ModalHeader`, `ModalBody`, and `ModelFooter` are the only permissible children of the Modal - */ - children?: ReactNode; - /** - * CSS class names that can be appended to the component. - */ - className?: string; -} - -/** - * Component that contains header section for the container which consists of drag and drop components. - */ - -export const DragDropContainerHeader = ({ - className, - children, - ...other -}: Props) => { - return ( -
    - {children} -
    - ); -}; diff --git a/src/components/DragDropContainerHeader/index.ts b/src/components/DragDropContainerHeader/index.ts deleted file mode 100644 index cdbfe1985..000000000 --- a/src/components/DragDropContainerHeader/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { DragDropContainerHeader as default } from './DragDropContainerHeader'; diff --git a/src/components/DragDropItem/DragDropItem.tsx b/src/components/DragDropItem/DragDropItem.tsx deleted file mode 100644 index d1b4541e0..000000000 --- a/src/components/DragDropItem/DragDropItem.tsx +++ /dev/null @@ -1,77 +0,0 @@ -import clsx from 'clsx'; -import type { ReactNode } from 'react'; -import React from 'react'; -import type { DraggableProvided } from 'react-beautiful-dnd'; -import { Draggable } from 'react-beautiful-dnd'; -import type { ItemType } from '../DragDrop/DragDropTypes'; -import Icon from '../Icon'; -import styles from '../DragDrop/DragDrop.module.css'; - -export interface Props { - /** - * Behavior variants - * - **hover** renders a drag handle that only shows up on hover - */ - behavior?: 'hover'; - /** - * CSS class names that can be appended to the component. - */ - className?: string; - /** - * The contents of an item; includes id, title (optional) and children (optional) - */ - item: ItemType; - /** - * Item's original indexed position - */ - index: number; -} - -/** - * Item to be dragged and dropped in containers. - */ -export const DragDropItem = ({ behavior, className, item, index }: Props) => { - const componentClassName = clsx( - styles['drag-drop__item'], - item.behavior === 'hover' && styles['drag-drop__item--hover'], - className, - ); - - // `id` is injected in - return item.id ? ( - - {(provided: DraggableProvided, snapshot) => { - const childrenWithProps = React.Children.map( - item.children, - (child: ReactNode) => { - // Checking isValidElement is the safe way and avoids a typescript - // error too. - if (React.isValidElement(child)) { - // @ts-expect-error "No overload matches this call" error due to type mismatch - return React.cloneElement(child, { - isDragging: snapshot.isDragging, - number: index + 1, - }); - } - }, - ); - return ( -
  • -
    - -
    - {childrenWithProps} -
  • - ); - }} -
    - ) : null; -}; diff --git a/src/components/DragDropItem/index.ts b/src/components/DragDropItem/index.ts deleted file mode 100644 index bcd2a0d32..000000000 --- a/src/components/DragDropItem/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { DragDropItem as default } from './DragDropItem'; diff --git a/src/index.ts b/src/index.ts index b8eb0936c..a7e124692 100644 --- a/src/index.ts +++ b/src/index.ts @@ -14,9 +14,6 @@ export { default as Checkbox } from './components/Checkbox'; export { default as ClickableStyle } from './components/ClickableStyle'; export { default as DataBar } from './components/DataBar'; export { default as DragDrop } from './components/DragDrop'; -export { default as DragDropContainer } from './components/DragDropContainer'; -export { default as DragDropContainerHeader } from './components/DragDropContainerHeader'; -export { default as DragDropItem } from './components/DragDropItem'; export { default as Drawer } from './components/Drawer'; export { default as Dropdown } from './components/Dropdown'; export { default as DropdownButton } from './components/DropdownButton';