From a62c749b6d69bddd897d20e73e2df4b2152c7a99 Mon Sep 17 00:00:00 2001 From: Clauderic Demers Date: Tue, 23 Mar 2021 21:54:22 -0400 Subject: [PATCH] Add stories for removable sortable items --- .../src/components/SortableContext.tsx | 16 ++++++++ packages/sortable/src/hooks/types.ts | 3 ++ packages/sortable/src/hooks/useSortable.ts | 24 +++++++----- .../hooks/utilities/useDerivedTransform.ts | 2 +- .../2 - Presets/Sortable/1-Vertical.story.tsx | 25 +++++++++++- .../Sortable/2-Horizontal.story.tsx | 24 +++++++++++- stories/2 - Presets/Sortable/3-Grid.story.tsx | 25 +++++++++++- stories/2 - Presets/Sortable/Sortable.tsx | 39 ++++++++++++++----- stories/3 - Examples/Advanced/Pages/Page.tsx | 4 +- .../Advanced/Pages/icons/index.ts | 2 +- .../Pages/icons/{trash.tsx => remove.tsx} | 2 +- .../components/Draggable/Draggable.module.css | 2 +- stories/components/Item/Item.module.css | 19 +++++++++ stories/components/Item/Item.tsx | 13 +++++-- .../Action.module.css} | 24 +++++++++--- .../Item/components/Action/Action.tsx | 28 +++++++++++++ .../Item/components/Action/index.ts | 2 + .../Item/components/Handle/Handle.tsx | 17 +++----- .../Item/components/Remove/Remove.tsx | 19 +++++++++ .../Item/components/Remove/index.ts | 1 + stories/components/Item/components/index.ts | 1 + 21 files changed, 240 insertions(+), 52 deletions(-) rename stories/3 - Examples/Advanced/Pages/icons/{trash.tsx => remove.tsx} (96%) rename stories/components/Item/components/{Handle/Handle.module.css => Action/Action.module.css} (60%) create mode 100644 stories/components/Item/components/Action/Action.tsx create mode 100644 stories/components/Item/components/Action/index.ts create mode 100644 stories/components/Item/components/Remove/Remove.tsx create mode 100644 stories/components/Item/components/Remove/index.ts diff --git a/packages/sortable/src/components/SortableContext.tsx b/packages/sortable/src/components/SortableContext.tsx index 874f88b40..02efacbb3 100644 --- a/packages/sortable/src/components/SortableContext.tsx +++ b/packages/sortable/src/components/SortableContext.tsx @@ -24,6 +24,7 @@ interface ContextDescriptor { useDragOverlay: boolean; sortedRects: LayoutRect[]; strategy: SortingStrategy; + wasSorting: boolean; } export const Context = React.createContext({ @@ -35,6 +36,7 @@ export const Context = React.createContext({ useDragOverlay: false, sortedRects: [], strategy: rectSortingStrategy, + wasSorting: false, }); export function SortableContext({ @@ -55,6 +57,8 @@ export function SortableContext({ const useDragOverlay = Boolean(overlayNode.rect !== null); const activeIndex = active ? items.indexOf(active) : -1; const isSorting = activeIndex !== -1; + const prevSorting = useRef(isSorting); + const wasSorting = !isSorting && prevSorting.current === true; const overIndex = over ? items.indexOf(over.id) : -1; const previousItemsRef = useRef(items); const sortedRects = getSortedRects(items, droppableRects); @@ -73,6 +77,16 @@ export function SortableContext({ previousItemsRef.current = items; }, [items]); + useEffect(() => { + if (isSorting) { + prevSorting.current = isSorting; + } else { + requestAnimationFrame(() => { + prevSorting.current = isSorting; + }); + } + }, [isSorting]); + const contextValue = useMemo( (): ContextDescriptor => ({ activeIndex, @@ -83,6 +97,7 @@ export function SortableContext({ useDragOverlay, sortedRects, strategy, + wasSorting, }), [ activeIndex, @@ -93,6 +108,7 @@ export function SortableContext({ sortedRects, useDragOverlay, strategy, + wasSorting, ] ); diff --git a/packages/sortable/src/hooks/types.ts b/packages/sortable/src/hooks/types.ts index e840cb757..a94cd7794 100644 --- a/packages/sortable/src/hooks/types.ts +++ b/packages/sortable/src/hooks/types.ts @@ -4,10 +4,13 @@ import type {Transition} from '@dnd-kit/utilities'; export type SortableTransition = Pick; export type AnimateLayoutChanges = (args: { + active: UniqueIdentifier | null; + isDragging: boolean; isSorting: boolean; id: UniqueIdentifier; index: number; newIndex: number; items: UniqueIdentifier[]; transition: SortableTransition | null; + wasSorting: boolean; }) => boolean; diff --git a/packages/sortable/src/hooks/useSortable.ts b/packages/sortable/src/hooks/useSortable.ts index 3b2245870..bb2675ba4 100644 --- a/packages/sortable/src/hooks/useSortable.ts +++ b/packages/sortable/src/hooks/useSortable.ts @@ -38,6 +38,7 @@ export function useSortable({ overIndex, useDragOverlay, strategy: globalStrategy, + wasSorting, } = useContext(Context); const { active, @@ -94,15 +95,18 @@ export function useSortable({ : index; const prevNewIndex = useRef(newIndex); const shouldAnimateLayoutChanges = animateLayoutChanges({ + active, + isDragging, isSorting, id, index, items, newIndex: prevNewIndex.current, transition, + wasSorting, }); const derivedTransform = useDerivedTransform({ - disabled: transition === null, + disabled: !shouldAnimateLayoutChanges, index, node, rect, @@ -138,17 +142,17 @@ export function useSortable({ return disabledTransition; } - if ( - shouldDisplaceDragSource || - !shouldAnimateLayoutChanges || - !transition - ) { + if (shouldDisplaceDragSource || !transition) { return null; } - return CSS.Transition.toString({ - ...transition, - property: transitionProperty, - }); + if (isSorting || shouldAnimateLayoutChanges) { + return CSS.Transition.toString({ + ...transition, + property: transitionProperty, + }); + } + + return null; } } diff --git a/packages/sortable/src/hooks/utilities/useDerivedTransform.ts b/packages/sortable/src/hooks/utilities/useDerivedTransform.ts index c366708b5..8518cba14 100644 --- a/packages/sortable/src/hooks/utilities/useDerivedTransform.ts +++ b/packages/sortable/src/hooks/utilities/useDerivedTransform.ts @@ -41,7 +41,7 @@ export function useDerivedTransform({rect, disabled, index, node}: Arguments) { if (index !== prevIndex.current) { prevIndex.current = index; } - }, [rect, disabled, index, node]); + }, [disabled, index, node, rect]); useEffect(() => { if (derivedTransform) { diff --git a/stories/2 - Presets/Sortable/1-Vertical.story.tsx b/stories/2 - Presets/Sortable/1-Vertical.story.tsx index 3584f2813..17836da93 100644 --- a/stories/2 - Presets/Sortable/1-Vertical.story.tsx +++ b/stories/2 - Presets/Sortable/1-Vertical.story.tsx @@ -1,7 +1,11 @@ import React from 'react'; - +import {LayoutMeasuringStrategy} from '@dnd-kit/core'; import {restrictToWindowEdges} from '@dnd-kit/modifiers'; -import {verticalListSortingStrategy} from '@dnd-kit/sortable'; +import { + AnimateLayoutChanges, + defaultAnimateLayoutChanges, + verticalListSortingStrategy, +} from '@dnd-kit/sortable'; import { restrictToVerticalAxis, restrictToFirstScrollableAncestor, @@ -142,3 +146,20 @@ export const RerenderBeforeSorting = () => { /> ); }; + +export const RemovableItems = () => { + const animateLayoutChanges: AnimateLayoutChanges = (args) => + args.isSorting || args.wasSorting + ? defaultAnimateLayoutChanges(args) + : true; + + return ( + + ); +}; diff --git a/stories/2 - Presets/Sortable/2-Horizontal.story.tsx b/stories/2 - Presets/Sortable/2-Horizontal.story.tsx index 25408ffaa..184502fc6 100644 --- a/stories/2 - Presets/Sortable/2-Horizontal.story.tsx +++ b/stories/2 - Presets/Sortable/2-Horizontal.story.tsx @@ -1,5 +1,10 @@ import React from 'react'; -import {horizontalListSortingStrategy} from '@dnd-kit/sortable'; +import {LayoutMeasuringStrategy} from '@dnd-kit/core'; +import { + AnimateLayoutChanges, + defaultAnimateLayoutChanges, + horizontalListSortingStrategy, +} from '@dnd-kit/sortable'; import {restrictToHorizontalAxis} from '@dnd-kit/modifiers'; import {createRange} from '../../utilities'; @@ -107,3 +112,20 @@ export const MarginBetweenItems = () => { /> ); }; + +export const RemovableItems = () => { + const animateLayoutChanges: AnimateLayoutChanges = (args) => + args.isSorting || args.wasSorting + ? defaultAnimateLayoutChanges(args) + : true; + + return ( + + ); +}; diff --git a/stories/2 - Presets/Sortable/3-Grid.story.tsx b/stories/2 - Presets/Sortable/3-Grid.story.tsx index 797cc88dc..9a196503d 100644 --- a/stories/2 - Presets/Sortable/3-Grid.story.tsx +++ b/stories/2 - Presets/Sortable/3-Grid.story.tsx @@ -1,7 +1,11 @@ import React from 'react'; - +import {LayoutMeasuringStrategy} from '@dnd-kit/core'; import {restrictToWindowEdges} from '@dnd-kit/modifiers'; -import {rectSortingStrategy} from '@dnd-kit/sortable'; +import { + AnimateLayoutChanges, + defaultAnimateLayoutChanges, + rectSortingStrategy, +} from '@dnd-kit/sortable'; import {Sortable, Props as SortableProps} from './Sortable'; import {GridContainer} from '../../components'; @@ -120,3 +124,20 @@ export const MinimumDistance = () => ( }} /> ); + +export const RemovableItems = () => { + const animateLayoutChanges: AnimateLayoutChanges = (args) => + args.isSorting || args.wasSorting + ? defaultAnimateLayoutChanges(args) + : true; + + return ( + + ); +}; diff --git a/stories/2 - Presets/Sortable/Sortable.tsx b/stories/2 - Presets/Sortable/Sortable.tsx index 47411dc53..b48aa9546 100644 --- a/stories/2 - Presets/Sortable/Sortable.tsx +++ b/stories/2 - Presets/Sortable/Sortable.tsx @@ -10,6 +10,7 @@ import { KeyboardSensor, Modifiers, MouseSensor, + LayoutMeasuring, PointerActivationConstraint, ScreenReaderInstructions, TouchSensor, @@ -24,6 +25,7 @@ import { sortableKeyboardCoordinates, SortingStrategy, rectSortingStrategy, + AnimateLayoutChanges, } from '@dnd-kit/sortable'; import {createRange} from '../../utilities'; @@ -31,14 +33,19 @@ import {Item, List, Wrapper} from '../../components'; export interface Props { activationConstraint?: PointerActivationConstraint; + animateLayoutChanges?: AnimateLayoutChanges; adjustScale?: boolean; collisionDetection?: CollisionDetection; Container?: any; // To-do: Fix me - strategy?: SortingStrategy; itemCount?: number; items?: string[]; - renderItem?: any; handle?: boolean; + layoutMeasuring?: Partial; + modifiers?: Modifiers; + renderItem?: any; + removable?: boolean; + strategy?: SortingStrategy; + useDragOverlay?: boolean; getItemStyles?(args: { id: UniqueIdentifier; index: number; @@ -53,8 +60,6 @@ export interface Props { id: string; }): React.CSSProperties; isDisabled?(id: UniqueIdentifier): boolean; - modifiers?: Modifiers; - useDragOverlay?: boolean; } const screenReaderInstructions: ScreenReaderInstructions = { @@ -67,19 +72,22 @@ const screenReaderInstructions: ScreenReaderInstructions = { export function Sortable({ activationConstraint, + animateLayoutChanges, adjustScale = false, Container = List, collisionDetection = closestCenter, - strategy = rectSortingStrategy, + getItemStyles = () => ({}), + handle = false, itemCount = 16, items: initialItems, - renderItem, - handle = false, - getItemStyles = () => ({}), - wrapperStyle = () => ({}), isDisabled = () => false, + layoutMeasuring, modifiers, + removable, + renderItem, + strategy = rectSortingStrategy, useDragOverlay = true, + wrapperStyle = () => ({}), }: Props) { const [items, setItems] = useState( () => @@ -101,7 +109,9 @@ export function Sortable({ const getIndex = items.indexOf.bind(items); const getPosition = (id: string) => getIndex(id) + 1; const activeIndex = activeId ? getIndex(activeId) : -1; - + const handleRemove = removable + ? (id: string) => setItems((items) => items.filter((item) => item !== id)) + : undefined; const announcements: Announcements = { onDragStart(id) { return `Picked up sortable item ${id}. Sortable item ${id} is in position ${getPosition( @@ -155,6 +165,7 @@ export function Sortable({ } }} onDragCancel={() => setActiveId(null)} + layoutMeasuring={layoutMeasuring} modifiers={modifiers} > @@ -170,6 +181,8 @@ export function Sortable({ wrapperStyle={wrapperStyle} disabled={isDisabled(value)} renderItem={renderItem} + onRemove={handleRemove} + animateLayoutChanges={animateLayoutChanges} useDragOverlay={useDragOverlay} /> ))} @@ -209,11 +222,13 @@ export function Sortable({ } interface SortableItemProps { + animateLayoutChanges?: AnimateLayoutChanges; disabled?: boolean; id: string; index: number; handle: boolean; useDragOverlay?: boolean; + onRemove?(id: string): void; style(values: any): React.CSSProperties; renderItem?(args: any): React.ReactElement; wrapperStyle({ @@ -229,9 +244,11 @@ interface SortableItemProps { export function SortableItem({ disabled, + animateLayoutChanges, id, index, handle, + onRemove, style, renderItem, useDragOverlay, @@ -247,6 +264,7 @@ export function SortableItem({ transform, transition, } = useSortable({ + animateLayoutChanges, id, disabled, }); @@ -268,6 +286,7 @@ export function SortableItem({ isSorting, overIndex, })} + onRemove={onRemove ? () => onRemove(id) : undefined} transform={transform} transition={!useDragOverlay && isDragging ? 'none' : transition} wrapperStyle={wrapperStyle({index, isDragging, id})} diff --git a/stories/3 - Examples/Advanced/Pages/Page.tsx b/stories/3 - Examples/Advanced/Pages/Page.tsx index 837130dc5..180639e9f 100644 --- a/stories/3 - Examples/Advanced/Pages/Page.tsx +++ b/stories/3 - Examples/Advanced/Pages/Page.tsx @@ -1,7 +1,7 @@ import React, {forwardRef, HTMLAttributes} from 'react'; import classNames from 'classnames'; -import {trashIcon} from './icons'; +import {removeIcon} from './icons'; import styles from './Page.module.css'; export enum Position { @@ -44,7 +44,7 @@ export const Page = forwardRef(function Page( ) : null} {index != null ? ( diff --git a/stories/3 - Examples/Advanced/Pages/icons/index.ts b/stories/3 - Examples/Advanced/Pages/icons/index.ts index 0363de2f8..61769c577 100644 --- a/stories/3 - Examples/Advanced/Pages/icons/index.ts +++ b/stories/3 - Examples/Advanced/Pages/icons/index.ts @@ -1 +1 @@ -export {trashIcon} from './trash'; +export {removeIcon} from './remove'; diff --git a/stories/3 - Examples/Advanced/Pages/icons/trash.tsx b/stories/3 - Examples/Advanced/Pages/icons/remove.tsx similarity index 96% rename from stories/3 - Examples/Advanced/Pages/icons/trash.tsx rename to stories/3 - Examples/Advanced/Pages/icons/remove.tsx index 964e3ecb3..6464f3f32 100644 --- a/stories/3 - Examples/Advanced/Pages/icons/trash.tsx +++ b/stories/3 - Examples/Advanced/Pages/icons/remove.tsx @@ -1,6 +1,6 @@ import React from 'react'; -export const trashIcon = ( +export const removeIcon = ( svg { margin-right: 5px; diff --git a/stories/components/Item/Item.module.css b/stories/components/Item/Item.module.css index 6fec1b7aa..5cfac939b 100644 --- a/stories/components/Item/Item.module.css +++ b/stories/components/Item/Item.module.css @@ -125,4 +125,23 @@ $focused-outline-color: #4c9ffe; border-bottom-left-radius: 3px; background-color: var(--color); } + + &:hover { + .Remove { + visibility: visible; + } + } +} + +.Remove { + visibility: hidden; +} + +.Actions { + display: flex; + align-self: flex-start; + margin-top: -12px; + margin-left: auto; + margin-bottom: -15px; + margin-right: -10px; } diff --git a/stories/components/Item/Item.tsx b/stories/components/Item/Item.tsx index a951dbf42..51645a13c 100644 --- a/stories/components/Item/Item.tsx +++ b/stories/components/Item/Item.tsx @@ -3,7 +3,7 @@ import classNames from 'classnames'; import {DraggableSyntheticListeners} from '@dnd-kit/core'; import {Transform} from '@dnd-kit/utilities'; -import {Handle} from './components'; +import {Handle, Remove} from './components'; import styles from './Item.module.css'; @@ -20,9 +20,10 @@ export interface Props { listeners?: DraggableSyntheticListeners; sorting?: boolean; style?: React.CSSProperties; - transition?: string; + transition?: string | null; wrapperStyle?: React.CSSProperties; value: React.ReactNode; + onRemove?(): void; renderItem?(args: { dragOverlay: boolean; dragging: boolean; @@ -51,6 +52,7 @@ export const Item = React.memo( height, index, listeners, + onRemove, renderItem, sorting, style, @@ -134,7 +136,12 @@ export const Item = React.memo( tabIndex={!handle ? 0 : undefined} > {value} - {handle ? : null} + + {onRemove ? ( + + ) : null} + {handle ? : null} + ); diff --git a/stories/components/Item/components/Handle/Handle.module.css b/stories/components/Item/components/Action/Action.module.css similarity index 60% rename from stories/components/Item/components/Handle/Handle.module.css rename to stories/components/Item/components/Action/Action.module.css index af9438080..ac4011ca5 100644 --- a/stories/components/Item/components/Handle/Handle.module.css +++ b/stories/components/Item/components/Action/Action.module.css @@ -1,32 +1,44 @@ $focused-outline-color: #4c9ffe; -.Handle { +.Action { display: flex; width: 12px; - padding: 15px 10px; + padding: 15px; align-items: center; justify-content: center; flex: 0 0 auto; - margin: -15px -10px; - margin-left: auto; touch-action: none; cursor: var(--cursor, grab); border-radius: 5px; + border: none; outline: none; + appearance: none; + background-color: transparent; &:hover { - background-color: var(--handle-background, rgba(0, 0, 0, 0.05)); + background-color: var(--action-background, rgba(0, 0, 0, 0.05)); + + svg { + fill: #6f7b88; + } } svg { flex: 0 0 auto; margin: auto; - width: 100%; height: 100%; overflow: visible; fill: #919eab; } + &:active { + background-color: var(--background, rgba(0, 0, 0, 0.05)); + + svg { + fill: var(--fill, #788491); + } + } + &:focus-visible { outline: none; box-shadow: 0 0 0 2px rgba(255, 255, 255, 0), diff --git a/stories/components/Item/components/Action/Action.tsx b/stories/components/Item/components/Action/Action.tsx new file mode 100644 index 000000000..bfbeec491 --- /dev/null +++ b/stories/components/Item/components/Action/Action.tsx @@ -0,0 +1,28 @@ +import React, {CSSProperties} from 'react'; +import classNames from 'classnames'; + +import styles from './Action.module.css'; + +export interface Props extends React.HTMLAttributes { + active?: { + fill: string; + background: string; + }; +} + +export function Action({active, className, style, ...props}: Props) { + return ( +