From b866832e30cb0cc291233e4f801050564a59ea02 Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Thu, 14 Mar 2019 17:15:06 +1100 Subject: [PATCH 001/117] placeholder --- src/view/placeholder/placeholder.jsx | 205 +++++++++++++-------------- 1 file changed, 99 insertions(+), 106 deletions(-) diff --git a/src/view/placeholder/placeholder.jsx b/src/view/placeholder/placeholder.jsx index 7f0ca6cc80..48a45c74d9 100644 --- a/src/view/placeholder/placeholder.jsx +++ b/src/view/placeholder/placeholder.jsx @@ -1,5 +1,5 @@ // @flow -import React, { PureComponent } from 'react'; +import React, { useState, useRef, useEffect } from 'react'; import type { Spacing } from 'css-box-model'; import type { Placeholder as PlaceholderType, @@ -37,9 +37,10 @@ type Size = {| margin: Spacing, |}; -type State = {| +type HelperArgs = {| isAnimatingOpenOnMount: boolean, - // useEmpty: boolean, + placeholder: PlaceholderType, + animate: InOutAnimationMode, |}; const empty: Size = { @@ -48,54 +49,95 @@ const empty: Size = { margin: noSpacing, }; -export default class Placeholder extends PureComponent { - mountTimerId: ?TimeoutID = null; +const getSize = ({ + isAnimatingOpenOnMount, + placeholder, + animate, +}: HelperArgs): Size => { + if (isAnimatingOpenOnMount) { + return empty; + } + + if (animate === 'close') { + return empty; + } - state: State = { - isAnimatingOpenOnMount: this.props.animate === 'open', + return { + height: placeholder.client.borderBox.height, + width: placeholder.client.borderBox.width, + margin: placeholder.client.margin, }; +}; - // called before render() on initial mount and updates - static getDerivedStateFromProps(props: Props, state: State): State { - // An animated open is no longer relevant. - if (state.isAnimatingOpenOnMount && props.animate !== 'open') { - return { - isAnimatingOpenOnMount: false, - }; - } +const getStyle = ({ + isAnimatingOpenOnMount, + placeholder, + animate, +}: HelperArgs): PlaceholderStyle => { + const size: Size = getSize({ isAnimatingOpenOnMount, placeholder, animate }); + + return { + display: placeholder.display, + // ## Recreating the box model + // We created the borderBox and then apply the margins directly + // this is to maintain any margin collapsing behaviour + + // creating borderBox + // background: 'green', + boxSizing: 'border-box', + width: size.width, + height: size.height, + // creating marginBox + marginTop: size.margin.top, + marginRight: size.margin.right, + marginBottom: size.margin.bottom, + marginLeft: size.margin.left, + + // ## Avoiding collapsing + // Avoiding the collapsing or growing of this element when pushed by flex child siblings. + // We have already taken a snapshot the current dimensions we do not want this element + // to recalculate its dimensions + // It is okay for these properties to be applied on elements that are not flex children + flexShrink: '0', + flexGrow: '0', + // Just a little performance optimisation: avoiding the browser needing + // to worry about pointer events for this element + pointerEvents: 'none', + + // Animate the placeholder size and margin + transition: transitions.placeholder, + }; +}; - return state; - } +const Placeholder = function Placeholder(props: Props) { + const mountTimer = useRef(null); - componentDidMount() { - if (!this.state.isAnimatingOpenOnMount) { - return; - } + const [isAnimatingOpenOnMount, setIsAnimatingOpenOnMount] = useState( + props.animate === 'open', + ); - // Ensuring there is one browser update with an empty size - // .setState in componentDidMount will cause two react renders - // but only a single browser update - // https://reactjs.org/docs/react-component.html#componentdidmount - this.mountTimerId = setTimeout(() => { - this.mountTimerId = null; - - if (this.state.isAnimatingOpenOnMount) { - this.setState({ - isAnimatingOpenOnMount: false, - }); + useEffect(() => { + const clear = () => { + if (mountTimer.current) { + clearTimeout(mountTimer.current); + mountTimer.current = null; } - }); - } + }; - componentWillUnmount() { - if (!this.mountTimerId) { - return; + if (!isAnimatingOpenOnMount) { + return clear; } - clearTimeout(this.mountTimerId); - this.mountTimerId = null; - } - onTransitionEnd = (event: TransitionEvent) => { + mountTimer.current = setTimeout(() => { + mountTimer.current = null; + + setIsAnimatingOpenOnMount(false); + }); + + return clear; + }, [isAnimatingOpenOnMount]); + + const onTransitionEnd = (event: TransitionEvent) => { // We transition height, width and margin // each of those transitions will independently call this callback // Because they all have the same duration we can just respond to one of them @@ -104,73 +146,24 @@ export default class Placeholder extends PureComponent { return; } - this.props.onTransitionEnd(); + props.onTransitionEnd(); - if (this.props.animate === 'close') { - this.props.onClose(); + if (props.animate === 'close') { + props.onClose(); } }; - getSize(): Size { - if (this.state.isAnimatingOpenOnMount) { - return empty; - } - - if (this.props.animate === 'close') { - return empty; - } - - const placeholder: PlaceholderType = this.props.placeholder; - return { - height: placeholder.client.borderBox.height, - width: placeholder.client.borderBox.width, - margin: placeholder.client.margin, - }; - } - - render() { - const placeholder: PlaceholderType = this.props.placeholder; - const size: Size = this.getSize(); - const { display, tagName } = placeholder; - - // The goal of the placeholder is to take up the same amount of space - // as the original draggable - const style: PlaceholderStyle = { - display, - // ## Recreating the box model - // We created the borderBox and then apply the margins directly - // this is to maintain any margin collapsing behaviour - - // creating borderBox - // background: 'green', - boxSizing: 'border-box', - width: size.width, - height: size.height, - // creating marginBox - marginTop: size.margin.top, - marginRight: size.margin.right, - marginBottom: size.margin.bottom, - marginLeft: size.margin.left, - - // ## Avoiding collapsing - // Avoiding the collapsing or growing of this element when pushed by flex child siblings. - // We have already taken a snapshot the current dimensions we do not want this element - // to recalculate its dimensions - // It is okay for these properties to be applied on elements that are not flex children - flexShrink: '0', - flexGrow: '0', - // Just a little performance optimisation: avoiding the browser needing - // to worry about pointer events for this element - pointerEvents: 'none', - - // Animate the placeholder size and margin - transition: transitions.placeholder, - }; + const style: PlaceholderStyle = getStyle({ + isAnimatingOpenOnMount, + animate: props.animate, + placeholder: props.placeholder, + }); + + return React.createElement(props.placeholder.tagName, { + style, + onTransitionEnd, + ref: props.innerRef, + }); +}; - return React.createElement(tagName, { - style, - onTransitionEnd: this.onTransitionEnd, - ref: this.props.innerRef, - }); - } -} +export default Placeholder; From f47daa56509988e730346324f9522f98cd0dd8bd Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Thu, 14 Mar 2019 17:16:14 +1100 Subject: [PATCH 002/117] using memo --- src/view/placeholder/placeholder.jsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/view/placeholder/placeholder.jsx b/src/view/placeholder/placeholder.jsx index 48a45c74d9..971aafc502 100644 --- a/src/view/placeholder/placeholder.jsx +++ b/src/view/placeholder/placeholder.jsx @@ -109,8 +109,8 @@ const getStyle = ({ }; }; -const Placeholder = function Placeholder(props: Props) { - const mountTimer = useRef(null); +const Placeholder = React.memo(function Placeholder(props: Props) { + const mountTimer = useRef(null); const [isAnimatingOpenOnMount, setIsAnimatingOpenOnMount] = useState( props.animate === 'open', @@ -164,6 +164,6 @@ const Placeholder = function Placeholder(props: Props) { onTransitionEnd, ref: props.innerRef, }); -}; +}); export default Placeholder; From bd627675545532ef5e7ce4a1308010322794cc00 Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Fri, 15 Mar 2019 08:40:47 +1100 Subject: [PATCH 003/117] animate in out --- src/view/animate-in-out/animate-in-out.jsx | 70 ++++++++++++++++++++-- 1 file changed, 66 insertions(+), 4 deletions(-) diff --git a/src/view/animate-in-out/animate-in-out.jsx b/src/view/animate-in-out/animate-in-out.jsx index 8c60a4a218..b8ba9148bc 100644 --- a/src/view/animate-in-out/animate-in-out.jsx +++ b/src/view/animate-in-out/animate-in-out.jsx @@ -1,5 +1,5 @@ // @flow -import React, { type Node } from 'react'; +import React, { useMemo, useCallback, useEffect, useState } from 'react'; import type { InOutAnimationMode } from '../../types'; export type AnimateProvided = {| @@ -8,10 +8,9 @@ export type AnimateProvided = {| data: mixed, |}; -type Props = {| +type Args = {| on: mixed, shouldAnimate: boolean, - children: (provided: AnimateProvided) => Node, |}; type State = {| @@ -20,7 +19,70 @@ type State = {| animate: InOutAnimationMode, |}; -export default class AnimateInOut extends React.PureComponent { +function useAnimateInOut(args: Args): ?AnimateProvided { + const [isVisible, setIsVisible] = useState(Boolean(args.on)); + const [data, setData] = useState(args.on); + const [animate, setAnimate] = useState( + args.shouldAnimate && args.on ? 'open' : 'none', + ); + + useEffect(() => { + if (!args.shouldAnimate) { + setIsVisible(Boolean(args.on)); + setData(args.on); + setAnimate('none'); + return; + } + + // need to animate in + if (args.on) { + setIsVisible(true); + setData(args.on); + setAnimate('open'); + return; + } + + // need to animate out if there was data + + if (isVisible) { + setAnimate('close'); + return; + } + + // close animation no longer visible + + setIsVisible(false); + setAnimate('close'); + setData(null); + }); + + const onClose = useCallback(() => { + if (animate !== 'close') { + return; + } + + setIsVisible(false); + }, [animate]); + + const provided: AnimateProvided = useMemo( + () => ({ + onClose, + data, + animate, + }), + [animate, data, onClose], + ); + + if (!isVisible) { + return null; + } + + return provided; +} + +export default useAnimateInOut; + +export class AnimateInOut extends React.PureComponent { state: State = { isVisible: Boolean(this.props.on), data: this.props.on, From 9958d323bd354b1269cd365e99b7311286b0501c Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Fri, 15 Mar 2019 10:06:50 +1100 Subject: [PATCH 004/117] wip --- src/view/animate-in-out/index.js | 2 - .../droppable-dimension-publisher.jsx | 111 +++++++++++++++++- src/view/droppable/droppable.jsx | 12 +- src/view/use-animate-in-out/index.js | 2 + .../use-animate-in-out.jsx} | 0 5 files changed, 121 insertions(+), 6 deletions(-) delete mode 100644 src/view/animate-in-out/index.js create mode 100644 src/view/use-animate-in-out/index.js rename src/view/{animate-in-out/animate-in-out.jsx => use-animate-in-out/use-animate-in-out.jsx} (100%) diff --git a/src/view/animate-in-out/index.js b/src/view/animate-in-out/index.js deleted file mode 100644 index 5869168b0f..0000000000 --- a/src/view/animate-in-out/index.js +++ /dev/null @@ -1,2 +0,0 @@ -// @flow -export { default } from './animate-in-out'; diff --git a/src/view/droppable-dimension-publisher/droppable-dimension-publisher.jsx b/src/view/droppable-dimension-publisher/droppable-dimension-publisher.jsx index 36b61f2b2d..cbd3d1ff6b 100644 --- a/src/view/droppable-dimension-publisher/droppable-dimension-publisher.jsx +++ b/src/view/droppable-dimension-publisher/droppable-dimension-publisher.jsx @@ -1,5 +1,11 @@ // @flow -import React, { type Node } from 'react'; +import React, { + useCallback, + useMemo, + useEffect, + useContext, + useRef, +} from 'react'; import PropTypes from 'prop-types'; import memoizeOne from 'memoize-one'; import invariant from 'tiny-invariant'; @@ -74,7 +80,108 @@ const withoutPlaceholder = ( return result; }; -export default class DroppableDimensionPublisher extends React.Component { +export default function useDroppableDimensionPublisher(args: Props) { + const marshal: DimensionMarshal = useContext(dimensionMarshalContext); + const whileDraggingRef = useRef(null); + + const descriptor: DroppableDescriptor = useMemo( + (): DroppableDescriptor => ({ + id: args.droppableId, + type: args.type, + }), + [args.droppableId, args.type], + ); + + const onClosestScroll = useCallback(() => { + const whileDragging: ?WhileDragging = whileDraggingRef.current;. + invariant( + whileDragging, + 'Cannot handle scroll if there is no drag', + ); + const closest: ?Element = getClosestScrollable(whileDragging); + invariant(closest); + + const options: ScrollOptions = whileDragging.scrollOptions; + + if (options.shouldPublishImmediately) { + this.updateScroll(); + return; + } + this.scheduleScrollUpdate(); + }, []); + + const getDimensionAndWatchScroll = useCallback( + (windowScroll: Position, options: ScrollOptions) => { + invariant( + !whileDraggingRef.current, + 'Cannot collect a droppable while a drag is occurring', + ); + const ref: ?HTMLElement = args.getDroppableRef(); + invariant(ref, 'Cannot collect without a droppable ref'); + const env: Env = getEnv(ref); + + const dragging: WhileDragging = { + ref, + descriptor, + env, + scrollOptions: options, + }; + // side effect + whileDraggingRef.current = dragging; + + const dimension: DroppableDimension = getDimension({ + ref, + descriptor, + env, + windowScroll, + direction: args.direction, + isDropDisabled: this.props.isDropDisabled, + isCombineEnabled: this.props.isCombineEnabled, + shouldClipSubject: !this.props.ignoreContainerClipping, + }); + + if (env.closestScrollable) { + // bind scroll listener + + env.closestScrollable.addEventListener( + 'scroll', + onClosestScroll, + getListenerOptions(dragging.scrollOptions), + ); + // print a debug warning if using an unsupported nested scroll container setup + if (process.env.NODE_ENV !== 'production') { + checkForNestedScrollContainers(env.closestScrollable); + } + } + + return dimension; + }, + ); + const recollect = useCallback(() => {}, []); + const dragStopped = useCallback(() => {}, []); + const scroll = useCallback(() => {}); + + const callbacks: DroppableCallbacks = useMemo( + () => ({ + getDimensionAndWatchScroll, + recollect, + dragStopped, + scroll, + }), + [dragStopped, getDimensionAndWatchScroll, recollect, scroll], + ); + + // Register with the marshal and let it know of: + // - any descriptor changes + // - when it unmounts + useEffect(() => { + marshal.registerDroppable(descriptor, callbacks); + + return () => marshal.unregisterDroppable(descriptor); + }, [callbacks, descriptor, marshal]); +} + +export class DroppableDimensionPublisher extends React.Component { /* eslint-disable react/sort-comp */ dragging: ?WhileDragging; callbacks: DroppableCallbacks; diff --git a/src/view/droppable/droppable.jsx b/src/view/droppable/droppable.jsx index 2c84004e09..ea5e0819cb 100644 --- a/src/view/droppable/droppable.jsx +++ b/src/view/droppable/droppable.jsx @@ -14,15 +14,23 @@ import { } from '../context-keys'; import { warning } from '../../dev-warning'; import checkOwnProps from './check-own-props'; -import AnimateInOut, { +import useAnimateInOut, { type AnimateProvided, -} from '../animate-in-out/animate-in-out'; +} from '../use-animate-in-out/use-animate-in-out'; import getMaxWindowScroll from '../window/get-max-window-scroll'; type Context = { [string]: DroppableId | TypeId, }; +function useDroppable(props: Props) { + const styleContext: string = useContext(styleContext); + const isMovementAllowed: string = useContext(isMovementAllowed); + + // eslint-disable-next-line react-hooks/exhaustive-deps + useEffect(() => checkOwnProps(props), []); +} + export default class Droppable extends React.Component { /* eslint-disable react/sort-comp */ styleContext: string; diff --git a/src/view/use-animate-in-out/index.js b/src/view/use-animate-in-out/index.js new file mode 100644 index 0000000000..122e867330 --- /dev/null +++ b/src/view/use-animate-in-out/index.js @@ -0,0 +1,2 @@ +// @flow +export { default } from './use-animate-in-out'; diff --git a/src/view/animate-in-out/animate-in-out.jsx b/src/view/use-animate-in-out/use-animate-in-out.jsx similarity index 100% rename from src/view/animate-in-out/animate-in-out.jsx rename to src/view/use-animate-in-out/use-animate-in-out.jsx From bb7795a5ed925a560f33bc91c46a4c5722c20a16 Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Fri, 15 Mar 2019 13:25:45 +1100 Subject: [PATCH 005/117] Droppable --- src/view/context/app-context.js | 13 ++ src/view/context/droppable-context.js | 10 + src/view/draggable/draggable.jsx | 11 +- src/view/droppable/check-own-props.js | 19 -- src/view/droppable/check.js | 55 +++++ src/view/droppable/droppable.jsx | 300 ++++++++++---------------- 6 files changed, 206 insertions(+), 202 deletions(-) create mode 100644 src/view/context/app-context.js create mode 100644 src/view/context/droppable-context.js delete mode 100644 src/view/droppable/check-own-props.js create mode 100644 src/view/droppable/check.js diff --git a/src/view/context/app-context.js b/src/view/context/app-context.js new file mode 100644 index 0000000000..45dcf6dc7a --- /dev/null +++ b/src/view/context/app-context.js @@ -0,0 +1,13 @@ +// @flow +import React from 'react'; +import type { DraggableId } from '../../types'; +import type { DimensionMarshal } from '../../state/dimension-marshal/dimension-marshal-types'; + +export type AppContextValue = {| + marshal: DimensionMarshal, + style: string, + canLift: (id: DraggableId) => boolean, + isMovementAllowed: () => boolean, +|}; + +export default React.createContext(null); diff --git a/src/view/context/droppable-context.js b/src/view/context/droppable-context.js new file mode 100644 index 0000000000..a8e27227af --- /dev/null +++ b/src/view/context/droppable-context.js @@ -0,0 +1,10 @@ +// @flow +import React from 'react'; +import type { DroppableId, TypeId } from '../../types'; + +export type DroppableContextValue = {| + droppableId: DroppableId, + type: TypeId, +|}; + +export default React.createContext(null); diff --git a/src/view/draggable/draggable.jsx b/src/view/draggable/draggable.jsx index 031228b00d..0bbc353c44 100644 --- a/src/view/draggable/draggable.jsx +++ b/src/view/draggable/draggable.jsx @@ -1,5 +1,5 @@ // @flow -import React, { type Node } from 'react'; +import React, { useEffect } from 'react'; import { type Position, type BoxModel } from 'css-box-model'; import PropTypes from 'prop-types'; import memoizeOne from 'memoize-one'; @@ -72,6 +72,15 @@ const getShouldDraggingAnimate = (dragging: DraggingMapProps): boolean => { return dragging.mode === 'SNAP'; }; +// function Draggable(props: Props) { +// const styleContext: string = useContext(styleContext); +// const ref = useRef(null); + +// // A one time check for the validity of hooks +// // eslint-disable-next-line react-hooks/exhaustive-deps +// useEffect(() => checkOwnProps(props), []); +// } + export default class Draggable extends React.Component { /* eslint-disable react/sort-comp */ callbacks: DragHandleCallbacks; diff --git a/src/view/droppable/check-own-props.js b/src/view/droppable/check-own-props.js deleted file mode 100644 index 16d0830e2c..0000000000 --- a/src/view/droppable/check-own-props.js +++ /dev/null @@ -1,19 +0,0 @@ -// @flow -import invariant from 'tiny-invariant'; -import type { Props } from './droppable-types'; - -export default (props: Props) => { - invariant(props.droppableId, 'A Droppable requires a droppableId prop'); - invariant( - typeof props.isDropDisabled === 'boolean', - 'isDropDisabled must be a boolean', - ); - invariant( - typeof props.isCombineEnabled === 'boolean', - 'isCombineEnabled must be a boolean', - ); - invariant( - typeof props.ignoreContainerClipping === 'boolean', - 'ignoreContainerClipping must be a boolean', - ); -}; diff --git a/src/view/droppable/check.js b/src/view/droppable/check.js new file mode 100644 index 0000000000..f89290c854 --- /dev/null +++ b/src/view/droppable/check.js @@ -0,0 +1,55 @@ +// @flow +import invariant from 'tiny-invariant'; +import type { Props } from './droppable-types'; +import isHtmlElement from '../is-type-of-element/is-html-element'; +import { warning } from '../../dev-warning'; + +export function checkOwnProps(props: Props) { + invariant(props.droppableId, 'A Droppable requires a droppableId prop'); + invariant( + typeof props.isDropDisabled === 'boolean', + 'isDropDisabled must be a boolean', + ); + invariant( + typeof props.isCombineEnabled === 'boolean', + 'isCombineEnabled must be a boolean', + ); + invariant( + typeof props.ignoreContainerClipping === 'boolean', + 'ignoreContainerClipping must be a boolean', + ); +} + +export function checkPlaceholder(props: Props, placeholderEl: ?HTMLElement) { + if (process.env.NODE_ENV === 'production') { + return; + } + + if (!props.placeholder) { + return; + } + + if (placeholderEl) { + return; + } + + warning(` + Droppable setup issue [droppableId: "${props.droppableId}"]: + DroppableProvided > placeholder could not be found. + + Please be sure to add the {provided.placeholder} React Node as a child of your Droppable. + More information: https://github.com/atlassian/react-beautiful-dnd/blob/master/docs/api/droppable.md + `); +} + +export function checkProvidedRef(ref: ?mixed) { + invariant( + ref && isHtmlElement(ref), + ` + provided.innerRef has not been provided with a HTMLElement. + + You can find a guide on using the innerRef callback functions at: + https://github.com/atlassian/react-beautiful-dnd/blob/master/docs/guides/using-inner-ref.md + `, + ); +} diff --git a/src/view/droppable/droppable.jsx b/src/view/droppable/droppable.jsx index ea5e0819cb..51f1e626fc 100644 --- a/src/view/droppable/droppable.jsx +++ b/src/view/droppable/droppable.jsx @@ -1,206 +1,142 @@ // @flow -import React, { type Node } from 'react'; -import PropTypes from 'prop-types'; -import DroppableDimensionPublisher from '../droppable-dimension-publisher'; +import invariant from 'tiny-invariant'; +import React, { + useEffect, + useMemo, + useRef, + useCallback, + useContext, + type Node, +} from 'react'; import type { Props, Provided, StateSnapshot } from './droppable-types'; import type { DroppableId, TypeId } from '../../types'; import Placeholder from '../placeholder'; import throwIfRefIsInvalid from '../throw-if-invalid-inner-ref'; -import { - droppableIdKey, - droppableTypeKey, - styleKey, - isMovementAllowedKey, -} from '../context-keys'; +import AppContext, { type AppContextValue } from '../context/app-context'; import { warning } from '../../dev-warning'; -import checkOwnProps from './check-own-props'; +import DroppableContext, { + type DroppableContextValue, +} from '../context/droppable-context'; import useAnimateInOut, { type AnimateProvided, } from '../use-animate-in-out/use-animate-in-out'; import getMaxWindowScroll from '../window/get-max-window-scroll'; +import { checkOwnProps, checkPlaceholder, checkProvidedRef } from './check'; type Context = { [string]: DroppableId | TypeId, }; -function useDroppable(props: Props) { - const styleContext: string = useContext(styleContext); - const isMovementAllowed: string = useContext(isMovementAllowed); - - // eslint-disable-next-line react-hooks/exhaustive-deps - useEffect(() => checkOwnProps(props), []); -} - -export default class Droppable extends React.Component { - /* eslint-disable react/sort-comp */ - styleContext: string; - ref: ?HTMLElement = null; - placeholderRef: ?HTMLElement = null; - - // Need to declare childContextTypes without flow - static contextTypes = { - [styleKey]: PropTypes.string.isRequired, - [isMovementAllowedKey]: PropTypes.func.isRequired, - }; - - constructor(props: Props, context: Object) { - super(props, context); - - this.styleContext = context[styleKey]; - - // a little run time check to avoid an easy to catch setup issues - if (process.env.NODE_ENV !== 'production') { - checkOwnProps(props); - } - } - - // Need to declare childContextTypes without flow - // https://github.com/brigand/babel-plugin-flow-react-proptypes/issues/22 - static childContextTypes = { - [droppableIdKey]: PropTypes.string.isRequired, - [droppableTypeKey]: PropTypes.string.isRequired, - }; - - getChildContext(): Context { - const value: Context = { - [droppableIdKey]: this.props.droppableId, - [droppableTypeKey]: this.props.type, - }; - return value; - } - - componentDidMount() { - throwIfRefIsInvalid(this.ref); - this.warnIfPlaceholderNotMounted(); - } - - componentDidUpdate() { - this.warnIfPlaceholderNotMounted(); - } - - componentWillUnmount() { - // allowing garbage collection - this.ref = null; - this.placeholderRef = null; - } - - warnIfPlaceholderNotMounted() { - if (process.env.NODE_ENV === 'production') { - return; - } - - if (!this.props.placeholder) { - return; - } - - if (this.placeholderRef) { - return; - } - - warning(` - Droppable setup issue [droppableId: "${this.props.droppableId}"]: - DroppableProvided > placeholder could not be found. - - Please be sure to add the {provided.placeholder} React Node as a child of your Droppable. - More information: https://github.com/atlassian/react-beautiful-dnd/blob/master/docs/api/droppable.md - `); - } - - /* eslint-enable */ - - setPlaceholderRef = (ref: ?HTMLElement) => { - this.placeholderRef = ref; - }; - - getPlaceholderRef = () => this.placeholderRef; - - // React calls ref callback twice for every render - // https://github.com/facebook/react/pull/8333/files - setRef = (ref: ?HTMLElement) => { - if (ref === null) { - return; - } - - if (ref === this.ref) { - return; - } - - this.ref = ref; - throwIfRefIsInvalid(ref); - }; - - getDroppableRef = (): ?HTMLElement => this.ref; - - onPlaceholderTransitionEnd = () => { - const isMovementAllowed: boolean = this.context[isMovementAllowedKey](); +export default function Droppable(props: Props) { + const appContext: ?AppContextValue = useContext(AppContext); + invariant(appContext, 'Could not find app context'); + const { style: styleContext, isMovementAllowed } = appContext; + const droppableRef = useRef(null); + const placeholderRef = useRef(null); + + // validating setup + useEffect(() => { + checkOwnProps(props); + checkPlaceholder(props, placeholderRef.current); + checkProvidedRef(droppableRef.current); + }); + + const { + // own props + children, + droppableId, + type, + direction, + ignoreContainerClipping, + isDropDisabled, + isCombineEnabled, + // map props + isDraggingOver, + draggingOverWith, + draggingFromThisWith, + // dispatch props + updateViewportMaxScroll, + } = props; + + const getDroppableRef = useCallback( + (): ?HTMLElement => droppableRef.current, + [], + ); + const getPlaceholderRef = useCallback( + (): ?HTMLElement => placeholderRef.current, + [], + ); + const setDroppableRef = useCallback((value: ?HTMLElement) => { + droppableRef.current = value; + }, []); + const setPlaceholderRef = useCallback((value: ?HTMLElement) => { + placeholderRef.current = value; + }, []); + + const onPlaceholderTransitionEnd = useCallback(() => { // A placeholder change can impact the window's max scroll - if (isMovementAllowed) { - this.props.updateViewportMaxScroll({ maxScroll: getMaxWindowScroll() }); + if (isMovementAllowed()) { + updateViewportMaxScroll({ maxScroll: getMaxWindowScroll() }); } - }; - - getPlaceholder(): Node { - // Placeholder > onClose / onTransitionEnd - // might not fire in the case of very fast toggling - return ( - - {({ onClose, data, animate }: AnimateProvided) => ( - - )} - - ); - } - - render() { - const { - // ownProps - children, - direction, - type, - droppableId, - isDropDisabled, - isCombineEnabled, - // mapProps - ignoreContainerClipping, - isDraggingOver, - draggingOverWith, - draggingFromThisWith, - } = this.props; - const provided: Provided = { - innerRef: this.setRef, - placeholder: this.getPlaceholder(), + }, [isMovementAllowed, updateViewportMaxScroll]); + + useDroppableDimensionPublisher({ + droppableId, + type, + direction, + ignoreContainerClipping, + isDropDisabled, + isCombineEnabled, + getDroppableRef, + getPlaceholderRef, + }); + + const instruction: ?AnimateProvided = useAnimateInOut({ + on: props.placeholder, + shouldAnimate: props.shouldAnimatePlaceholder, + }); + + const placeholder: Node | null = instruction ? ( + + ) : null; + + const provided: Provided = useMemo( + (): Provided => ({ + innerRef: setDroppableRef, + placeholder, droppableProps: { - 'data-react-beautiful-dnd-droppable': this.styleContext, + 'data-react-beautiful-dnd-droppable': styleContext, }, - }; - const snapshot: StateSnapshot = { + }), + [placeholder, setDroppableRef, styleContext], + ); + + const snapshot: StateSnapshot = useMemo( + () => ({ isDraggingOver, draggingOverWith, draggingFromThisWith, - }; + }), + [draggingFromThisWith, draggingOverWith, isDraggingOver], + ); - return ( - - {children(provided, snapshot)} - - ); - } + const droppableContext: ?DroppableContextValue = useMemo( + () => ({ + droppableId, + type, + }), + [droppableId, type], + ); + + return ( + + {children(provided, snapshot)} + + ); } From 3b09df541c85834d6a9523d9731fa7ac9102f7f3 Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Fri, 15 Mar 2019 14:36:53 +1100 Subject: [PATCH 006/117] droppable dimension publisher --- package.json | 1 - .../droppable-dimension-publisher/index.js | 2 - src/view/droppable/droppable.jsx | 10 +- .../check-for-nested-scroll-container.js | 0 .../droppable-dimension-publisher.jsx | 103 +------ .../get-closest-scrollable.js | 0 .../get-dimension.js | 0 .../get-env.js | 0 .../get-listener-options.js | 12 + .../get-scroll.js | 0 .../index.js | 2 + .../is-in-fixed-container.js | 0 .../use-droppable-dimension-publisher.jsx | 259 ++++++++++++++++++ .../without-placeholder.js | 18 ++ 14 files changed, 294 insertions(+), 113 deletions(-) delete mode 100644 src/view/droppable-dimension-publisher/index.js rename src/view/{droppable-dimension-publisher => use-droppable-dimension-publisher}/check-for-nested-scroll-container.js (100%) rename src/view/{droppable-dimension-publisher => use-droppable-dimension-publisher}/droppable-dimension-publisher.jsx (76%) rename src/view/{droppable-dimension-publisher => use-droppable-dimension-publisher}/get-closest-scrollable.js (100%) rename src/view/{droppable-dimension-publisher => use-droppable-dimension-publisher}/get-dimension.js (100%) rename src/view/{droppable-dimension-publisher => use-droppable-dimension-publisher}/get-env.js (100%) create mode 100644 src/view/use-droppable-dimension-publisher/get-listener-options.js rename src/view/{droppable-dimension-publisher => use-droppable-dimension-publisher}/get-scroll.js (100%) create mode 100644 src/view/use-droppable-dimension-publisher/index.js rename src/view/{droppable-dimension-publisher => use-droppable-dimension-publisher}/is-in-fixed-container.js (100%) create mode 100644 src/view/use-droppable-dimension-publisher/use-droppable-dimension-publisher.jsx create mode 100644 src/view/use-droppable-dimension-publisher/without-placeholder.js diff --git a/package.json b/package.json index 165b4bbf7c..48caa8d438 100644 --- a/package.json +++ b/package.json @@ -57,7 +57,6 @@ "@babel/runtime-corejs2": "^7.3.4", "css-box-model": "^1.1.1", "memoize-one": "^5.0.0", - "prop-types": "^15.6.1", "raf-schd": "^4.0.0", "react-redux": "^5.0.7", "redux": "^4.0.1", diff --git a/src/view/droppable-dimension-publisher/index.js b/src/view/droppable-dimension-publisher/index.js deleted file mode 100644 index 6195e3a83a..0000000000 --- a/src/view/droppable-dimension-publisher/index.js +++ /dev/null @@ -1,2 +0,0 @@ -// @flow -export { default } from './droppable-dimension-publisher'; diff --git a/src/view/droppable/droppable.jsx b/src/view/droppable/droppable.jsx index 51f1e626fc..03f58df72f 100644 --- a/src/view/droppable/droppable.jsx +++ b/src/view/droppable/droppable.jsx @@ -9,11 +9,9 @@ import React, { type Node, } from 'react'; import type { Props, Provided, StateSnapshot } from './droppable-types'; -import type { DroppableId, TypeId } from '../../types'; +import useDroppableDimensionPublisher from '../use-droppable-dimension-publisher'; import Placeholder from '../placeholder'; -import throwIfRefIsInvalid from '../throw-if-invalid-inner-ref'; import AppContext, { type AppContextValue } from '../context/app-context'; -import { warning } from '../../dev-warning'; import DroppableContext, { type DroppableContextValue, } from '../context/droppable-context'; @@ -23,10 +21,6 @@ import useAnimateInOut, { import getMaxWindowScroll from '../window/get-max-window-scroll'; import { checkOwnProps, checkPlaceholder, checkProvidedRef } from './check'; -type Context = { - [string]: DroppableId | TypeId, -}; - export default function Droppable(props: Props) { const appContext: ?AppContextValue = useContext(AppContext); invariant(appContext, 'Could not find app context'); @@ -84,9 +78,9 @@ export default function Droppable(props: Props) { droppableId, type, direction, - ignoreContainerClipping, isDropDisabled, isCombineEnabled, + ignoreContainerClipping, getDroppableRef, getPlaceholderRef, }); diff --git a/src/view/droppable-dimension-publisher/check-for-nested-scroll-container.js b/src/view/use-droppable-dimension-publisher/check-for-nested-scroll-container.js similarity index 100% rename from src/view/droppable-dimension-publisher/check-for-nested-scroll-container.js rename to src/view/use-droppable-dimension-publisher/check-for-nested-scroll-container.js diff --git a/src/view/droppable-dimension-publisher/droppable-dimension-publisher.jsx b/src/view/use-droppable-dimension-publisher/droppable-dimension-publisher.jsx similarity index 76% rename from src/view/droppable-dimension-publisher/droppable-dimension-publisher.jsx rename to src/view/use-droppable-dimension-publisher/droppable-dimension-publisher.jsx index cbd3d1ff6b..c4f7c4dfc9 100644 --- a/src/view/droppable-dimension-publisher/droppable-dimension-publisher.jsx +++ b/src/view/use-droppable-dimension-publisher/droppable-dimension-publisher.jsx @@ -80,108 +80,7 @@ const withoutPlaceholder = ( return result; }; -export default function useDroppableDimensionPublisher(args: Props) { - const marshal: DimensionMarshal = useContext(dimensionMarshalContext); - const whileDraggingRef = useRef(null); - - const descriptor: DroppableDescriptor = useMemo( - (): DroppableDescriptor => ({ - id: args.droppableId, - type: args.type, - }), - [args.droppableId, args.type], - ); - - const onClosestScroll = useCallback(() => { - const whileDragging: ?WhileDragging = whileDraggingRef.current;. - invariant( - whileDragging, - 'Cannot handle scroll if there is no drag', - ); - const closest: ?Element = getClosestScrollable(whileDragging); - invariant(closest); - - const options: ScrollOptions = whileDragging.scrollOptions; - - if (options.shouldPublishImmediately) { - this.updateScroll(); - return; - } - this.scheduleScrollUpdate(); - }, []); - - const getDimensionAndWatchScroll = useCallback( - (windowScroll: Position, options: ScrollOptions) => { - invariant( - !whileDraggingRef.current, - 'Cannot collect a droppable while a drag is occurring', - ); - const ref: ?HTMLElement = args.getDroppableRef(); - invariant(ref, 'Cannot collect without a droppable ref'); - const env: Env = getEnv(ref); - - const dragging: WhileDragging = { - ref, - descriptor, - env, - scrollOptions: options, - }; - // side effect - whileDraggingRef.current = dragging; - - const dimension: DroppableDimension = getDimension({ - ref, - descriptor, - env, - windowScroll, - direction: args.direction, - isDropDisabled: this.props.isDropDisabled, - isCombineEnabled: this.props.isCombineEnabled, - shouldClipSubject: !this.props.ignoreContainerClipping, - }); - - if (env.closestScrollable) { - // bind scroll listener - - env.closestScrollable.addEventListener( - 'scroll', - onClosestScroll, - getListenerOptions(dragging.scrollOptions), - ); - // print a debug warning if using an unsupported nested scroll container setup - if (process.env.NODE_ENV !== 'production') { - checkForNestedScrollContainers(env.closestScrollable); - } - } - - return dimension; - }, - ); - const recollect = useCallback(() => {}, []); - const dragStopped = useCallback(() => {}, []); - const scroll = useCallback(() => {}); - - const callbacks: DroppableCallbacks = useMemo( - () => ({ - getDimensionAndWatchScroll, - recollect, - dragStopped, - scroll, - }), - [dragStopped, getDimensionAndWatchScroll, recollect, scroll], - ); - - // Register with the marshal and let it know of: - // - any descriptor changes - // - when it unmounts - useEffect(() => { - marshal.registerDroppable(descriptor, callbacks); - - return () => marshal.unregisterDroppable(descriptor); - }, [callbacks, descriptor, marshal]); -} - -export class DroppableDimensionPublisher extends React.Component { +export default class DroppableDimensionPublisher extends React.Component { /* eslint-disable react/sort-comp */ dragging: ?WhileDragging; callbacks: DroppableCallbacks; diff --git a/src/view/droppable-dimension-publisher/get-closest-scrollable.js b/src/view/use-droppable-dimension-publisher/get-closest-scrollable.js similarity index 100% rename from src/view/droppable-dimension-publisher/get-closest-scrollable.js rename to src/view/use-droppable-dimension-publisher/get-closest-scrollable.js diff --git a/src/view/droppable-dimension-publisher/get-dimension.js b/src/view/use-droppable-dimension-publisher/get-dimension.js similarity index 100% rename from src/view/droppable-dimension-publisher/get-dimension.js rename to src/view/use-droppable-dimension-publisher/get-dimension.js diff --git a/src/view/droppable-dimension-publisher/get-env.js b/src/view/use-droppable-dimension-publisher/get-env.js similarity index 100% rename from src/view/droppable-dimension-publisher/get-env.js rename to src/view/use-droppable-dimension-publisher/get-env.js diff --git a/src/view/use-droppable-dimension-publisher/get-listener-options.js b/src/view/use-droppable-dimension-publisher/get-listener-options.js new file mode 100644 index 0000000000..dcaadf66bc --- /dev/null +++ b/src/view/use-droppable-dimension-publisher/get-listener-options.js @@ -0,0 +1,12 @@ +// @flow +import type { ScrollOptions } from '../../types'; + +const immediate = { + passive: false, +}; +const delayed = { + passive: true, +}; + +export default (options: ScrollOptions) => + options.shouldPublishImmediately ? immediate : delayed; diff --git a/src/view/droppable-dimension-publisher/get-scroll.js b/src/view/use-droppable-dimension-publisher/get-scroll.js similarity index 100% rename from src/view/droppable-dimension-publisher/get-scroll.js rename to src/view/use-droppable-dimension-publisher/get-scroll.js diff --git a/src/view/use-droppable-dimension-publisher/index.js b/src/view/use-droppable-dimension-publisher/index.js new file mode 100644 index 0000000000..21a4c1fa25 --- /dev/null +++ b/src/view/use-droppable-dimension-publisher/index.js @@ -0,0 +1,2 @@ +// @flow +export { default } from './use-droppable-dimension-publisher'; diff --git a/src/view/droppable-dimension-publisher/is-in-fixed-container.js b/src/view/use-droppable-dimension-publisher/is-in-fixed-container.js similarity index 100% rename from src/view/droppable-dimension-publisher/is-in-fixed-container.js rename to src/view/use-droppable-dimension-publisher/is-in-fixed-container.js diff --git a/src/view/use-droppable-dimension-publisher/use-droppable-dimension-publisher.jsx b/src/view/use-droppable-dimension-publisher/use-droppable-dimension-publisher.jsx new file mode 100644 index 0000000000..21ed532231 --- /dev/null +++ b/src/view/use-droppable-dimension-publisher/use-droppable-dimension-publisher.jsx @@ -0,0 +1,259 @@ +// @flow +import { useCallback, useMemo, useEffect, useContext, useRef } from 'react'; +import invariant from 'tiny-invariant'; +import { type Position } from 'css-box-model'; +import rafSchedule from 'raf-schd'; +import checkForNestedScrollContainers from './check-for-nested-scroll-container'; +import { origin } from '../../state/position'; +import getScroll from './get-scroll'; +import type { + DimensionMarshal, + DroppableCallbacks, + RecollectDroppableOptions, +} from '../../state/dimension-marshal/dimension-marshal-types'; +import getEnv, { type Env } from './get-env'; +import type { + DroppableId, + TypeId, + DroppableDimension, + DroppableDescriptor, + Direction, + ScrollOptions, +} from '../../types'; +import getDimension from './get-dimension'; +import AppContext, { type AppContextValue } from '../context/app-context'; +import withoutPlaceholder from './without-placeholder'; +import { warning } from '../../dev-warning'; +import getListenerOptions from './get-listener-options'; + +type Props = {| + droppableId: DroppableId, + type: TypeId, + direction: Direction, + isDropDisabled: boolean, + isCombineEnabled: boolean, + ignoreContainerClipping: boolean, + getPlaceholderRef: () => ?HTMLElement, + getDroppableRef: () => ?HTMLElement, +|}; + +type WhileDragging = {| + ref: HTMLElement, + descriptor: DroppableDescriptor, + env: Env, + scrollOptions: ScrollOptions, +|}; + +const getClosestScrollableFromDrag = (dragging: ?WhileDragging): ?Element => + (dragging && dragging.env.closestScrollable) || null; + +export default function useDroppableDimensionPublisher(args: Props) { + const whileDraggingRef = useRef(null); + const appContext: ?AppContextValue = useContext(AppContext); + invariant(appContext, 'Could not find app content'); + const marshal: DimensionMarshal = appContext.marshal; + + const { + direction, + droppableId, + type, + isDropDisabled, + isCombineEnabled, + ignoreContainerClipping, + getDroppableRef, + getPlaceholderRef, + } = args; + + const descriptor: DroppableDescriptor = useMemo( + (): DroppableDescriptor => ({ + id: droppableId, + type, + }), + [droppableId, type], + ); + + const memoizedUpdateScroll = useCallback( + (x: number, y: number) => { + invariant( + whileDraggingRef.current, + 'Can only update scroll when dragging', + ); + const scroll: Position = { x, y }; + marshal.updateDroppableScroll(descriptor.id, scroll); + }, + [descriptor.id, marshal], + ); + + const getClosestScroll = useCallback((): Position => { + const dragging: ?WhileDragging = whileDraggingRef.current; + if (!dragging || !dragging.env.closestScrollable) { + return origin; + } + + return getScroll(dragging.env.closestScrollable); + }, []); + + const updateScroll = useCallback(() => { + const scroll: Position = getClosestScroll(); + memoizedUpdateScroll(scroll.x, scroll.y); + }, [getClosestScroll, memoizedUpdateScroll]); + + const scheduleScrollUpdate = useCallback(() => { + rafSchedule(updateScroll); + }, [updateScroll]); + + const onClosestScroll = useCallback(() => { + const dragging: ?WhileDragging = whileDraggingRef.current; + const closest: ?Element = getClosestScrollableFromDrag(dragging); + + invariant( + dragging && closest, + 'Could not find scroll options while scrolling', + ); + const options: ScrollOptions = dragging.scrollOptions; + if (options.shouldPublishImmediately) { + updateScroll(); + return; + } + scheduleScrollUpdate(); + }, [scheduleScrollUpdate, updateScroll]); + + const getDimensionAndWatchScroll = useCallback( + (windowScroll: Position, options: ScrollOptions) => { + invariant( + !whileDraggingRef.current, + 'Cannot collect a droppable while a drag is occurring', + ); + const ref: ?HTMLElement = getDroppableRef(); + invariant(ref, 'Cannot collect without a droppable ref'); + const env: Env = getEnv(ref); + + const dragging: WhileDragging = { + ref, + descriptor, + env, + scrollOptions: options, + }; + // side effect + whileDraggingRef.current = dragging; + + const dimension: DroppableDimension = getDimension({ + ref, + descriptor, + env, + windowScroll, + direction, + isDropDisabled, + isCombineEnabled, + shouldClipSubject: !ignoreContainerClipping, + }); + + if (env.closestScrollable) { + // bind scroll listener + + env.closestScrollable.addEventListener( + 'scroll', + onClosestScroll, + getListenerOptions(dragging.scrollOptions), + ); + // print a debug warning if using an unsupported nested scroll container setup + if (process.env.NODE_ENV !== 'production') { + checkForNestedScrollContainers(env.closestScrollable); + } + } + + return dimension; + }, + [ + descriptor, + direction, + getDroppableRef, + ignoreContainerClipping, + isCombineEnabled, + isDropDisabled, + onClosestScroll, + ], + ); + const recollect = useCallback( + (options: RecollectDroppableOptions): DroppableDimension => { + const dragging: ?WhileDragging = whileDraggingRef.current; + const closest: ?Element = getClosestScrollableFromDrag(dragging); + invariant( + dragging && closest, + 'Can only recollect Droppable client for Droppables that have a scroll container', + ); + + const execute = (): DroppableDimension => + getDimension({ + ref: dragging.ref, + descriptor: dragging.descriptor, + env: dragging.env, + windowScroll: origin, + direction, + isDropDisabled, + isCombineEnabled, + shouldClipSubject: !ignoreContainerClipping, + }); + + if (!options.withoutPlaceholder) { + return execute(); + } + + return withoutPlaceholder(getPlaceholderRef(), execute); + }, + [ + direction, + getPlaceholderRef, + ignoreContainerClipping, + isCombineEnabled, + isDropDisabled, + ], + ); + const dragStopped = useCallback(() => { + const dragging: ?WhileDragging = whileDraggingRef.current; + invariant(dragging, 'Cannot stop drag when no active drag'); + const closest: ?Element = getClosestScrollableFromDrag(dragging); + + // goodbye old friend + whileDraggingRef.current = null; + + if (!closest) { + return; + } + + // unwatch scroll + scheduleScrollUpdate.cancel(); + closest.removeEventListener( + 'scroll', + onClosestScroll, + getListenerOptions(dragging.scrollOptions), + ); + }, [onClosestScroll, scheduleScrollUpdate]); + const scroll = useCallback(() => {}); + + const callbacks: DroppableCallbacks = useMemo( + () => ({ + getDimensionAndWatchScroll, + recollect, + dragStopped, + scroll, + }), + [dragStopped, getDimensionAndWatchScroll, recollect, scroll], + ); + + // Register with the marshal and let it know of: + // - any descriptor changes + // - when it unmounts + useEffect(() => { + marshal.registerDroppable(descriptor, callbacks); + + return () => { + if (whileDraggingRef.current) { + warning('Unmounting Droppable while a drag is occurring'); + dragStopped(); + } + + marshal.unregisterDroppable(descriptor); + }; + }, [callbacks, descriptor, dragStopped, marshal]); +} diff --git a/src/view/use-droppable-dimension-publisher/without-placeholder.js b/src/view/use-droppable-dimension-publisher/without-placeholder.js new file mode 100644 index 0000000000..40d4fe8abe --- /dev/null +++ b/src/view/use-droppable-dimension-publisher/without-placeholder.js @@ -0,0 +1,18 @@ +// @flow +import type { DroppableDimension } from '../../types'; + +export default function withoutPlaceholder( + placeholder: ?HTMLElement, + fn: () => DroppableDimension, +): DroppableDimension { + if (!placeholder) { + return fn(); + } + + const last: string = placeholder.style.display; + placeholder.style.display = 'none'; + const result: DroppableDimension = fn(); + placeholder.style.display = last; + + return result; +} From cb1f7174be45b1b404acda5a45f19f9146c9991c Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Fri, 15 Mar 2019 15:35:54 +1100 Subject: [PATCH 007/117] working on drag drop context --- package.json | 2 +- src/state/create-store.js | 14 +-- src/state/middleware/auto-scroll.js | 14 +-- .../middleware/dimension-marshal-stopper.js | 7 +- src/state/middleware/lift.js | 3 +- src/view/announcer/announcer-types.js | 9 -- .../drag-drop-context/drag-drop-context.jsx | 89 ++++++++++++++++--- src/view/use-announcer/index.js | 2 + .../use-announcer.js} | 79 ++++++++-------- yarn.lock | 29 +++--- 10 files changed, 153 insertions(+), 95 deletions(-) delete mode 100644 src/view/announcer/announcer-types.js create mode 100644 src/view/use-announcer/index.js rename src/view/{announcer/announcer.js => use-announcer/use-announcer.js} (63%) diff --git a/package.json b/package.json index 48caa8d438..cba1ff9ffb 100644 --- a/package.json +++ b/package.json @@ -58,7 +58,7 @@ "css-box-model": "^1.1.1", "memoize-one": "^5.0.0", "raf-schd": "^4.0.0", - "react-redux": "^5.0.7", + "react-redux": "^6.0.1", "redux": "^4.0.1", "tiny-invariant": "^1.0.3" }, diff --git a/src/state/create-store.js b/src/state/create-store.js index 6555251da6..1bca665478 100644 --- a/src/state/create-store.js +++ b/src/state/create-store.js @@ -27,19 +27,19 @@ const composeEnhancers = : compose; type Args = {| - getDimensionMarshal: () => DimensionMarshal, + dimensionMarshal: DimensionMarshal, styleMarshal: StyleMarshal, getResponders: () => Responders, announce: Announce, - getScroller: () => AutoScroller, + autoScroller: AutoScroller, |}; export default ({ - getDimensionMarshal, + dimensionMarshal, styleMarshal, getResponders, announce, - getScroller, + autoScroller, }: Args): Store => createStore( reducer, @@ -71,14 +71,14 @@ export default ({ // when moving into a phase where collection is no longer needed. // We need to stop the marshal before responders fire as responders can cause // dimension registration changes in response to reordering - dimensionMarshalStopper(getDimensionMarshal), + dimensionMarshalStopper(dimensionMarshal), // Fire application responders in response to drag changes - lift(getDimensionMarshal), + lift(dimensionMarshal), drop, // When a drop animation finishes - fire a drop complete dropAnimationFinish, pendingDrop, - autoScroll(getScroller), + autoScroll(autoScroller), // Fire responders for consumers (after update to store) responders(getResponders, announce), ), diff --git a/src/state/middleware/auto-scroll.js b/src/state/middleware/auto-scroll.js index 048f2848b4..57123c1cc4 100644 --- a/src/state/middleware/auto-scroll.js +++ b/src/state/middleware/auto-scroll.js @@ -12,17 +12,17 @@ const shouldEnd = (action: Action): boolean => const shouldCancelPending = (action: Action): boolean => action.type === 'COLLECTION_STARTING'; -export default (getScroller: () => AutoScroller) => ( - store: MiddlewareStore, -) => (next: Dispatch) => (action: Action): any => { +export default (autoScroller: AutoScroller) => (store: MiddlewareStore) => ( + next: Dispatch, +) => (action: Action): any => { if (shouldEnd(action)) { - getScroller().stop(); + autoScroller.stop(); next(action); return; } if (shouldCancelPending(action)) { - getScroller().cancelPending(); + autoScroller.cancelPending(); next(action); return; } @@ -35,12 +35,12 @@ export default (getScroller: () => AutoScroller) => ( state.phase === 'DRAGGING', 'Expected phase to be DRAGGING after INITIAL_PUBLISH', ); - getScroller().start(state); + autoScroller.start(state); return; } // auto scroll happens in response to state changes // releasing all actions to the reducer first next(action); - getScroller().scroll(store.getState()); + autoScroller.scroll(store.getState()); }; diff --git a/src/state/middleware/dimension-marshal-stopper.js b/src/state/middleware/dimension-marshal-stopper.js index d2a39e9f87..1f6bc0a4f8 100644 --- a/src/state/middleware/dimension-marshal-stopper.js +++ b/src/state/middleware/dimension-marshal-stopper.js @@ -2,9 +2,9 @@ import type { Action, Dispatch } from '../store-types'; import type { DimensionMarshal } from '../dimension-marshal/dimension-marshal-types'; -export default (getMarshal: () => DimensionMarshal) => () => ( - next: Dispatch, -) => (action: Action): any => { +export default (marshal: DimensionMarshal) => () => (next: Dispatch) => ( + action: Action, +): any => { // Not stopping a collection on a 'DROP' as we want a collection to continue if ( // drag is finished @@ -13,7 +13,6 @@ export default (getMarshal: () => DimensionMarshal) => () => ( // no longer accepting changes once the drop has started action.type === 'DROP_ANIMATE' ) { - const marshal: DimensionMarshal = getMarshal(); marshal.stopPublishing(); } diff --git a/src/state/middleware/lift.js b/src/state/middleware/lift.js index 8db6baff3f..129f579810 100644 --- a/src/state/middleware/lift.js +++ b/src/state/middleware/lift.js @@ -5,7 +5,7 @@ import type { DimensionMarshal } from '../dimension-marshal/dimension-marshal-ty import type { State, ScrollOptions, LiftRequest } from '../../types'; import type { MiddlewareStore, Action, Dispatch } from '../store-types'; -export default (getMarshal: () => DimensionMarshal) => ({ +export default (marshal: DimensionMarshal) => ({ getState, dispatch, }: MiddlewareStore) => (next: Dispatch) => (action: Action): any => { @@ -14,7 +14,6 @@ export default (getMarshal: () => DimensionMarshal) => ({ return; } - const marshal: DimensionMarshal = getMarshal(); const { id, clientSelection, movementMode } = action.payload; const initial: State = getState(); diff --git a/src/view/announcer/announcer-types.js b/src/view/announcer/announcer-types.js deleted file mode 100644 index 5f23f05208..0000000000 --- a/src/view/announcer/announcer-types.js +++ /dev/null @@ -1,9 +0,0 @@ -// @flow -import type { Announce } from '../../types'; - -export type Announcer = {| - announce: Announce, - id: string, - mount: () => void, - unmount: () => void, -|}; diff --git a/src/view/drag-drop-context/drag-drop-context.jsx b/src/view/drag-drop-context/drag-drop-context.jsx index 651d4489db..7fc9be2ad9 100644 --- a/src/view/drag-drop-context/drag-drop-context.jsx +++ b/src/view/drag-drop-context/drag-drop-context.jsx @@ -1,8 +1,14 @@ // @flow -import React, { type Node } from 'react'; +import React, { + useCallback, + useMemo, + useEffect, + useRef, + type Node, +} from 'react'; import { bindActionCreators } from 'redux'; +import { Provider } from 'react-redux'; import invariant from 'tiny-invariant'; -import PropTypes from 'prop-types'; import createStore from '../../state/create-store'; import createDimensionMarshal from '../../state/dimension-marshal/dimension-marshal'; import createStyleMarshal, { @@ -10,16 +16,14 @@ import createStyleMarshal, { } from '../style-marshal/style-marshal'; import canStartDrag from '../../state/can-start-drag'; import scrollWindow from '../window/scroll-window'; -import createAnnouncer from '../announcer/announcer'; import createAutoScroller from '../../state/auto-scroller'; -import type { Announcer } from '../announcer/announcer-types'; import type { AutoScroller } from '../../state/auto-scroller/auto-scroller-types'; import type { StyleMarshal } from '../style-marshal/style-marshal-types'; import type { DimensionMarshal, Callbacks as DimensionMarshalCallbacks, } from '../../state/dimension-marshal/dimension-marshal-types'; -import type { DraggableId, State, Responders } from '../../types'; +import type { DraggableId, State, Responders, Announce } from '../../types'; import type { Store } from '../../state/store-types'; import { storeKey, @@ -42,6 +46,8 @@ import { peerDependencies } from '../../../package.json'; import checkReactVersion from './check-react-version'; import checkDoctype from './check-doctype'; import isMovementAllowed from '../../state/is-movement-allowed'; +import useAnnouncer from '../use-announcer'; +import AppContext, { type AppContextValue } from '../context/app-context'; type Props = {| ...Responders, @@ -49,10 +55,6 @@ type Props = {| children: Node | null, |}; -type Context = { - [string]: Store, -}; - // Reset any context that gets persisted across server side renders export const resetServerContext = () => { resetStyleContext(); @@ -77,7 +79,74 @@ const printFatalDevError = (error: Error) => { console.error('raw', error); }; -export default class DragDropContext extends React.Component { +const createResponders = (props: Props): Responders => ({ + onBeforeDragStart: props.onBeforeDragStart, + onDragStart: props.onDragStart, + onDragEnd: props.onDragEnd, + onDragUpdate: props.onDragUpdate, +}); + +export default function DragDropContext(props: Props) { + const announce: Announce = useAnnouncer(); + const styleMarshal: StyleMarshal = useStyleMarshal(); + const dimensionMarshal: DimensionMarshal = useDimensionMarshal(); + const autoScroller: AutoScroller = useAutoScroller(); + + // lazy collection of responders using a ref + const respondersRef = useRef(null); + useEffect(() => { + respondersRef.current = createResponders(props); + }, [props]); + + const getResponders = useCallback((): Responders => { + const responders: ?Responders = respondersRef.current; + invariant(responders, 'Responders not created yet'); + return responders; + }, [respondersRef]); + + const storeRef = useRef( + createStore({ + dimensionMarshal, + styleMarshal, + announce, + autoScroller, + getResponders, + }), + ); + + const getCanLift = useCallback( + (id: DraggableId) => canStartDrag(storeRef.current.getState(), id), + [storeRef], + ); + + const getIsMovementAllowed = useCallback( + () => isMovementAllowed(storeRef.current.getState()), + [storeRef], + ); + + const appContext: AppContextValue = useMemo( + () => ({ + marshal: dimensionMarshal, + style: styleMarshal.styleContext, + canLift: getCanLift, + isMovementAllowed: getIsMovementAllowed, + }), + [ + dimensionMarshal, + getCanLift, + getIsMovementAllowed, + styleMarshal.styleContext, + ], + ); + + return ( + + {props.children} + + ); +} + +export class DragDropContext extends React.Component { /* eslint-disable react/sort-comp */ store: Store; dimensionMarshal: DimensionMarshal; diff --git a/src/view/use-announcer/index.js b/src/view/use-announcer/index.js new file mode 100644 index 0000000000..ecb54f8a50 --- /dev/null +++ b/src/view/use-announcer/index.js @@ -0,0 +1,2 @@ +// @flow +export { default } from './use-announcer'; diff --git a/src/view/announcer/announcer.js b/src/view/use-announcer/use-announcer.js similarity index 63% rename from src/view/announcer/announcer.js rename to src/view/use-announcer/use-announcer.js index 73079a4963..cb27722d0e 100644 --- a/src/view/announcer/announcer.js +++ b/src/view/use-announcer/use-announcer.js @@ -1,7 +1,7 @@ // @flow +import { useRef, useMemo, useEffect, useCallback } from 'react'; import invariant from 'tiny-invariant'; import type { Announce } from '../../types'; -import type { Announcer } from './announcer-types'; import { warning } from '../../dev-warning'; import getBodyElement from '../get-body-element'; @@ -22,32 +22,19 @@ const visuallyHidden: Object = { 'clip-path': 'inset(100%)', }; -export default (): Announcer => { - const id: string = `react-beautiful-dnd-announcement-${count++}`; - let el: ?HTMLElement = null; +export default function useAnnouncer(): Announce { + const id: string = useMemo( + () => `react-beautiful-dnd-announcement-${count++}`, + [], + ); + const ref = useRef(null); - const announce: Announce = (message: string): void => { - if (el) { - el.textContent = message; - return; - } - - warning(` - A screen reader message was trying to be announced but it was unable to do so. - This can occur if you unmount your in your onDragEnd. - Consider calling provided.announce() before the unmount so that the instruction will - not be lost for users relying on a screen reader. - - Message not passed to screen reader: + useEffect(() => { + invariant(!ref.current, 'Announcement node already mounted'); - "${message}" - `); - }; + const el: HTMLElement = document.createElement('div'); + ref.current = el; - const mount = () => { - invariant(!el, 'Announcer already mounted'); - - el = document.createElement('div'); // identifier el.id = id; @@ -64,23 +51,35 @@ export default (): Announcer => { // Add to body getBodyElement().appendChild(el); - }; - const unmount = () => { - invariant(el, 'Will not unmount announcer as it is already unmounted'); + return () => { + const toBeRemoved: ?HTMLElement = ref.current; + invariant(toBeRemoved, 'Cannot unmount announcement node'); - // Remove from body - getBodyElement().removeChild(el); - // Unset - el = null; - }; + // Remove from body + getBodyElement().removeChild(toBeRemoved); + ref.current = null; + }; + }, [id]); - const announcer: Announcer = { - announce, - id, - mount, - unmount, - }; + const announce: Announce = useCallback((message: string): void => { + const el: ?HTMLElement = ref.current; + if (el) { + el.textContent = message; + return; + } - return announcer; -}; + warning(` + A screen reader message was trying to be announced but it was unable to do so. + This can occur if you unmount your in your onDragEnd. + Consider calling provided.announce() before the unmount so that the instruction will + not be lost for users relying on a screen reader. + + Message not passed to screen reader: + + "${message}" + `); + }, []); + + return announce; +} diff --git a/yarn.lock b/yarn.lock index 03580643b1..94e6f0796a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -874,7 +874,7 @@ dependencies: regenerator-runtime "^0.12.0" -"@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.2.0", "@babel/runtime@^7.3.4": +"@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.2.0", "@babel/runtime@^7.3.1", "@babel/runtime@^7.3.4": version "7.3.4" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.3.4.tgz#73d12ba819e365fcf7fd152aed56d6df97d21c83" integrity sha512-IvfvnMdSaLBateu0jfsYIpZTxAc2cKEXEMiezGGN75QcBcecDUKd3PgLAncT0oOgxKy8dd8hrJKj9MfzgfZd6g== @@ -5884,7 +5884,7 @@ hoist-non-react-statics@^2.3.1, hoist-non-react-statics@^2.5.0: resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-2.5.5.tgz#c5903cf409c0dfd908f388e619d86b9c1174cb47" integrity sha512-rqcy4pJo55FTTLWt+bU8ukscqHeE/e9KWvsOW2b/a3afxQZhwkQdT1rPPCJ0rYXdj4vNcasY8zHTH+jF/qStxw== -hoist-non-react-statics@^3.1.0: +hoist-non-react-statics@^3.3.0: version "3.3.0" resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.0.tgz#b09178f0122184fb95acf525daaecb4d8f45958b" integrity sha512-0XsbTXxgiaCDYDIWFcwkmerZPSwywfUqYmwT4jzewKTQSWoE6FCMoUVOeBJWK3E/CrWbxRG3m5GzY4lnIwGRBA== @@ -9074,7 +9074,7 @@ prompts@^2.0.1: kleur "^3.0.2" sisteransi "^1.0.0" -prop-types@^15.5.10, prop-types@^15.5.4, prop-types@^15.6.0, prop-types@^15.6.1, prop-types@^15.6.2: +prop-types@^15.5.10, prop-types@^15.5.4, prop-types@^15.6.0, prop-types@^15.6.1, prop-types@^15.6.2, prop-types@^15.7.2: version "15.7.2" resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.7.2.tgz#52c41e75b8c87e72b9d9360e0206b99dcbffa6c5" integrity sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ== @@ -9410,12 +9410,12 @@ react-inspector@^2.3.0, react-inspector@^2.3.1: is-dom "^1.0.9" prop-types "^15.6.1" -react-is@^16.3.1, react-is@^16.6.0, react-is@^16.7.0, react-is@^16.8.1, react-is@^16.8.3: +react-is@^16.3.1, react-is@^16.7.0, react-is@^16.8.1, react-is@^16.8.3: version "16.8.3" resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.8.3.tgz#4ad8b029c2a718fc0cfc746c8d4e1b7221e5387d" integrity sha512-Y4rC1ZJmsxxkkPuMLwvKvlL1Zfpbcu+Bf4ZigkHup3v9EfdYhAlWAaVyA19olXq2o2mGn0w+dFKvk3pVVlYcIA== -react-is@^16.8.4: +react-is@^16.8.2, react-is@^16.8.4: version "16.8.4" resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.8.4.tgz#90f336a68c3a29a096a3d648ab80e87ec61482a2" integrity sha512-PVadd+WaUDOAciICm/J1waJaSvgq+4rHE/K70j0PFqKhkTBsPv/82UGQJNXAngz1fOQLLxI6z1sEDmJDQhCTAA== @@ -9455,18 +9455,17 @@ react-popper@^1.3.3: typed-styles "^0.0.7" warning "^4.0.2" -react-redux@^5.0.7: - version "5.1.1" - resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-5.1.1.tgz#88e368682c7fa80e34e055cd7ac56f5936b0f52f" - integrity sha512-LE7Ned+cv5qe7tMV5BPYkGQ5Lpg8gzgItK07c67yHvJ8t0iaD9kPFPAli/mYkiyJYrs2pJgExR2ZgsGqlrOApg== +react-redux@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-6.0.1.tgz#0d423e2c1cb10ada87293d47e7de7c329623ba4d" + integrity sha512-T52I52Kxhbqy/6TEfBv85rQSDz6+Y28V/pf52vDWs1YRXG19mcFOGfHnY2HsNFHyhP+ST34Aih98fvt6tqwVcQ== dependencies: - "@babel/runtime" "^7.1.2" - hoist-non-react-statics "^3.1.0" + "@babel/runtime" "^7.3.1" + hoist-non-react-statics "^3.3.0" invariant "^2.2.4" - loose-envify "^1.1.0" - prop-types "^15.6.1" - react-is "^16.6.0" - react-lifecycles-compat "^3.0.0" + loose-envify "^1.4.0" + prop-types "^15.7.2" + react-is "^16.8.2" react-resize-detector@^3.2.1: version "3.4.0" From 0feeee29d105e7029affc925a93db428f107b7b9 Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Fri, 15 Mar 2019 15:59:26 +1100 Subject: [PATCH 008/117] style marshal --- .../drag-drop-context/drag-drop-context.jsx | 41 ++++-- src/view/style-marshal/style-marshal.js | 97 -------------- src/view/use-announcer/use-announcer.js | 8 +- .../get-styles.js | 4 +- .../style-marshal-types.js | 2 - .../use-style-marshal/use-style-marshal.js | 118 ++++++++++++++++++ 6 files changed, 151 insertions(+), 119 deletions(-) delete mode 100644 src/view/style-marshal/style-marshal.js rename src/view/{style-marshal => use-style-marshal}/get-styles.js (97%) rename src/view/{style-marshal => use-style-marshal}/style-marshal-types.js (82%) create mode 100644 src/view/use-style-marshal/use-style-marshal.js diff --git a/src/view/drag-drop-context/drag-drop-context.jsx b/src/view/drag-drop-context/drag-drop-context.jsx index 7fc9be2ad9..fd73346674 100644 --- a/src/view/drag-drop-context/drag-drop-context.jsx +++ b/src/view/drag-drop-context/drag-drop-context.jsx @@ -11,14 +11,13 @@ import { Provider } from 'react-redux'; import invariant from 'tiny-invariant'; import createStore from '../../state/create-store'; import createDimensionMarshal from '../../state/dimension-marshal/dimension-marshal'; -import createStyleMarshal, { - resetStyleContext, -} from '../style-marshal/style-marshal'; +import createStyleMarshal from '../use-style-marshal/use-style-marshal'; import canStartDrag from '../../state/can-start-drag'; import scrollWindow from '../window/scroll-window'; import createAutoScroller from '../../state/auto-scroller'; +import useStyleMarshal from '../use-style-marshal/use-style-marshal'; import type { AutoScroller } from '../../state/auto-scroller/auto-scroller-types'; -import type { StyleMarshal } from '../style-marshal/style-marshal-types'; +import type { StyleMarshal } from '../use-style-marshal/style-marshal-types'; import type { DimensionMarshal, Callbacks as DimensionMarshalCallbacks, @@ -55,11 +54,6 @@ type Props = {| children: Node | null, |}; -// Reset any context that gets persisted across server side renders -export const resetServerContext = () => { - resetStyleContext(); -}; - const printFatalDevError = (error: Error) => { if (process.env.NODE_ENV === 'production') { return; @@ -86,12 +80,33 @@ const createResponders = (props: Props): Responders => ({ onDragUpdate: props.onDragUpdate, }); +let count: number = 0; + +// Reset any context that gets persisted across server side renders +export function resetServerContext() { + count = 0; +} + export default function DragDropContext(props: Props) { - const announce: Announce = useAnnouncer(); - const styleMarshal: StyleMarshal = useStyleMarshal(); + const uniqueId: number = useMemo( + (): number => count++, + // this must stay constant for each component + // eslint-disable-next-line react-hooks/exhaustive-deps + [], + ); + const announce: Announce = useAnnouncer(uniqueId); + const styleMarshal: StyleMarshal = useStyleMarshal(uniqueId); const dimensionMarshal: DimensionMarshal = useDimensionMarshal(); const autoScroller: AutoScroller = useAutoScroller(); + // some validation when mounting + useEffect(() => { + if (process.env.NODE_ENV !== 'production') { + checkReactVersion(peerDependencies.react, React.version); + checkDoctype(document); + } + }, []); + // lazy collection of responders using a ref const respondersRef = useRef(null); useEffect(() => { @@ -116,12 +131,12 @@ export default function DragDropContext(props: Props) { const getCanLift = useCallback( (id: DraggableId) => canStartDrag(storeRef.current.getState(), id), - [storeRef], + [], ); const getIsMovementAllowed = useCallback( () => isMovementAllowed(storeRef.current.getState()), - [storeRef], + [], ); const appContext: AppContextValue = useMemo( diff --git a/src/view/style-marshal/style-marshal.js b/src/view/style-marshal/style-marshal.js deleted file mode 100644 index 7c521280a9..0000000000 --- a/src/view/style-marshal/style-marshal.js +++ /dev/null @@ -1,97 +0,0 @@ -// @flow -import memoizeOne from 'memoize-one'; -import invariant from 'tiny-invariant'; -import getStyles, { type Styles } from './get-styles'; -import { prefix } from '../data-attributes'; -import type { StyleMarshal } from './style-marshal-types'; -import type { DropReason } from '../../types'; - -let count: number = 0; - -// Required for server side rendering as count is persisted across requests -export const resetStyleContext = () => { - count = 0; -}; - -const getHead = (): HTMLHeadElement => { - const head: ?HTMLHeadElement = document.querySelector('head'); - invariant(head, 'Cannot find the head to append a style to'); - return head; -}; - -const createStyleEl = (): HTMLStyleElement => { - const el: HTMLStyleElement = document.createElement('style'); - el.type = 'text/css'; - return el; -}; - -export default () => { - const context: string = `${count++}`; - const styles: Styles = getStyles(context); - let always: ?HTMLStyleElement = null; - let dynamic: ?HTMLStyleElement = null; - - // using memoizeOne as a way of not updating the innerHTML - // unless there is a new value required - const setStyle = memoizeOne((el: ?HTMLStyleElement, proposed: string) => { - invariant(el, 'Cannot set style of style tag if not mounted'); - // This technique works with ie11+ so no need for a nasty fallback as seen here: - // https://stackoverflow.com/a/22050778/1374236 - el.innerHTML = proposed; - }); - - // exposing this as a seperate step so that it works nicely with - // server side rendering - const mount = () => { - invariant(!always && !dynamic, 'Style marshal already mounted'); - - always = createStyleEl(); - dynamic = createStyleEl(); - // for easy identification - always.setAttribute(`${prefix}-always`, context); - dynamic.setAttribute(`${prefix}-dynamic`, context); - - // add style tags to head - getHead().appendChild(always); - getHead().appendChild(dynamic); - - // set initial style - setStyle(always, styles.always); - setStyle(dynamic, styles.resting); - }; - - const dragging = () => setStyle(dynamic, styles.dragging); - const dropping = (reason: DropReason) => { - if (reason === 'DROP') { - setStyle(dynamic, styles.dropAnimating); - return; - } - setStyle(dynamic, styles.userCancel); - }; - const resting = () => setStyle(dynamic, styles.resting); - - const unmount = (): void => { - invariant( - always && dynamic, - 'Cannot unmount style marshal as it is already unmounted', - ); - - // Remove from head - getHead().removeChild(always); - getHead().removeChild(dynamic); - // Unset - always = null; - dynamic = null; - }; - - const marshal: StyleMarshal = { - dragging, - dropping, - resting, - styleContext: context, - mount, - unmount, - }; - - return marshal; -}; diff --git a/src/view/use-announcer/use-announcer.js b/src/view/use-announcer/use-announcer.js index cb27722d0e..e10ec326c2 100644 --- a/src/view/use-announcer/use-announcer.js +++ b/src/view/use-announcer/use-announcer.js @@ -5,8 +5,6 @@ import type { Announce } from '../../types'; import { warning } from '../../dev-warning'; import getBodyElement from '../get-body-element'; -let count: number = 0; - // https://allyjs.io/tutorials/hiding-elements.html // Element is visually hidden but is readable by screen readers const visuallyHidden: Object = { @@ -22,10 +20,10 @@ const visuallyHidden: Object = { 'clip-path': 'inset(100%)', }; -export default function useAnnouncer(): Announce { +export default function useAnnouncer(uniqueId: number): Announce { const id: string = useMemo( - () => `react-beautiful-dnd-announcement-${count++}`, - [], + () => `react-beautiful-dnd-announcement-${uniqueId}`, + [uniqueId], ); const ref = useRef(null); diff --git a/src/view/style-marshal/get-styles.js b/src/view/use-style-marshal/get-styles.js similarity index 97% rename from src/view/style-marshal/get-styles.js rename to src/view/use-style-marshal/get-styles.js index a61b12697a..c7157125d2 100644 --- a/src/view/style-marshal/get-styles.js +++ b/src/view/use-style-marshal/get-styles.js @@ -40,8 +40,8 @@ const getStyles = (rules: Rule[], property: string): string => const noPointerEvents: string = 'pointer-events: none;'; -export default (styleContext: string): Styles => { - const getSelector = makeGetSelector(styleContext); +export default (uniqueContext: string): Styles => { + const getSelector = makeGetSelector(uniqueContext); // ## Drag handle styles diff --git a/src/view/style-marshal/style-marshal-types.js b/src/view/use-style-marshal/style-marshal-types.js similarity index 82% rename from src/view/style-marshal/style-marshal-types.js rename to src/view/use-style-marshal/style-marshal-types.js index 18d255d3e0..16e95c0372 100644 --- a/src/view/style-marshal/style-marshal-types.js +++ b/src/view/use-style-marshal/style-marshal-types.js @@ -6,6 +6,4 @@ export type StyleMarshal = {| dropping: (reason: DropReason) => void, resting: () => void, styleContext: string, - unmount: () => void, - mount: () => void, |}; diff --git a/src/view/use-style-marshal/use-style-marshal.js b/src/view/use-style-marshal/use-style-marshal.js new file mode 100644 index 0000000000..748852b928 --- /dev/null +++ b/src/view/use-style-marshal/use-style-marshal.js @@ -0,0 +1,118 @@ +// @flow +import { useRef, useCallback, useEffect } from 'react'; +import invariant from 'tiny-invariant'; +import memoizeOne from 'memoize-one'; +import getStyles, { type Styles } from './get-styles'; +import { prefix } from '../data-attributes'; +import type { StyleMarshal } from './style-marshal-types'; +import type { DropReason } from '../../types'; + +const getHead = (): HTMLHeadElement => { + const head: ?HTMLHeadElement = document.querySelector('head'); + invariant(head, 'Cannot find the head to append a style to'); + return head; +}; + +const createStyleEl = (): HTMLStyleElement => { + const el: HTMLStyleElement = document.createElement('style'); + el.type = 'text/css'; + return el; +}; + +export default function useStyleMarshal(uniqueId: number) { + const uniqueContext: string = `${uniqueId}`; + const styles: Styles = getStyles(uniqueContext); + const alwaysRef = useRef(null); + const dynamicRef = useRef(null); + + const setDynamicStyle = useCallback( + // Using memoizeOne to prevent frequent updates to textContext + memoizeOne((proposed: string) => { + const el: ?HTMLStyleElement = dynamicRef.current; + invariant(el, 'Cannot set dynamic style element if it is not set'); + el.textContent = proposed; + }), + [], + ); + + const setAlwaysStyle = useCallback((proposed: string) => { + const el: ?HTMLStyleElement = alwaysRef.current; + invariant(el, 'Cannot set dynamic style element if it is not set'); + el.textContent = proposed; + }); + + useEffect(() => { + invariant( + alwaysRef.current || dynamicRef.current, + 'style elements already mounted', + ); + + const always: HTMLStyleElement = createStyleEl(); + const dynamic: HTMLStyleElement = createStyleEl(); + + // store their refs + alwaysRef.current = always; + dynamicRef.current = dynamic; + + // for easy identification + always.setAttribute(`${prefix}-always`, uniqueContext); + dynamic.setAttribute(`${prefix}-dynamic`, uniqueContext); + + // add style tags to head + getHead().appendChild(always); + getHead().appendChild(dynamic); + + // set initial style + setAlwaysStyle(styles.always); + setDynamicStyle(styles.resting); + + return () => { + const remove = ref => { + const current: ?HTMLStyleElement = ref.current; + invariant(current, 'Cannot unmount ref as it is not set'); + getHead().removeChild(current); + ref.current = null; + }; + + remove(alwaysRef); + remove(dynamicRef); + }; + }, [ + setAlwaysStyle, + setDynamicStyle, + styles.always, + styles.resting, + uniqueContext, + ]); + + const dragging = useCallback( + () => setDynamicStyle(styles.dragging), + // we can never invalidate this reference + // eslint-disable-next-line react-hooks/exhaustive-deps + [], + ); + const dropping = useCallback((reason: DropReason) => { + if (reason === 'DROP') { + setDynamicStyle(styles.dropAnimating); + return; + } + setDynamicStyle(styles.userCancel); + // we can never invalidate this reference + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + const resting = useCallback( + () => setDynamicStyle(styles.resting), + // we can never invalidate this reference + // eslint-disable-next-line react-hooks/exhaustive-deps + [], + ); + + const marshal: StyleMarshal = { + dragging, + dropping, + resting, + styleContext: uniqueContext, + }; + + return marshal; +} From a004b6e1dea167daa56eb19eaeb30371825c51c2 Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Fri, 15 Mar 2019 16:01:17 +1100 Subject: [PATCH 009/117] adding style marshal --- src/view/use-style-marshal/use-style-marshal.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/view/use-style-marshal/use-style-marshal.js b/src/view/use-style-marshal/use-style-marshal.js index 748852b928..98147f043b 100644 --- a/src/view/use-style-marshal/use-style-marshal.js +++ b/src/view/use-style-marshal/use-style-marshal.js @@ -1,5 +1,5 @@ // @flow -import { useRef, useCallback, useEffect } from 'react'; +import { useRef, useCallback, useEffect, useMemo } from 'react'; import invariant from 'tiny-invariant'; import memoizeOne from 'memoize-one'; import getStyles, { type Styles } from './get-styles'; @@ -21,7 +21,9 @@ const createStyleEl = (): HTMLStyleElement => { export default function useStyleMarshal(uniqueId: number) { const uniqueContext: string = `${uniqueId}`; - const styles: Styles = getStyles(uniqueContext); + const styles: Styles = useMemo(() => getStyles(uniqueContext), [ + uniqueContext, + ]); const alwaysRef = useRef(null); const dynamicRef = useRef(null); @@ -39,7 +41,7 @@ export default function useStyleMarshal(uniqueId: number) { const el: ?HTMLStyleElement = alwaysRef.current; invariant(el, 'Cannot set dynamic style element if it is not set'); el.textContent = proposed; - }); + }, []); useEffect(() => { invariant( From c7b09dcc22ecef79669dd5e733d5cd8c67cc8bfa Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Fri, 15 Mar 2019 16:07:35 +1100 Subject: [PATCH 010/117] more drag drop context --- src/view/drag-drop-context/drag-drop-context.jsx | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/src/view/drag-drop-context/drag-drop-context.jsx b/src/view/drag-drop-context/drag-drop-context.jsx index fd73346674..a535c5efab 100644 --- a/src/view/drag-drop-context/drag-drop-context.jsx +++ b/src/view/drag-drop-context/drag-drop-context.jsx @@ -11,7 +11,6 @@ import { Provider } from 'react-redux'; import invariant from 'tiny-invariant'; import createStore from '../../state/create-store'; import createDimensionMarshal from '../../state/dimension-marshal/dimension-marshal'; -import createStyleMarshal from '../use-style-marshal/use-style-marshal'; import canStartDrag from '../../state/can-start-drag'; import scrollWindow from '../window/scroll-window'; import createAutoScroller from '../../state/auto-scroller'; @@ -107,17 +106,15 @@ export default function DragDropContext(props: Props) { } }, []); - // lazy collection of responders using a ref - const respondersRef = useRef(null); + // lazy collection of responders using a ref - update on ever render + const lastPropsRef = useRef(props); useEffect(() => { - respondersRef.current = createResponders(props); - }, [props]); + lastPropsRef.current = props; + }); const getResponders = useCallback((): Responders => { - const responders: ?Responders = respondersRef.current; - invariant(responders, 'Responders not created yet'); - return responders; - }, [respondersRef]); + return createResponders(lastPropsRef.current); + }, []); const storeRef = useRef( createStore({ From e6af834d144cc9abd35a6560281a4c258e01ed51 Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Fri, 15 Mar 2019 17:11:50 +1100 Subject: [PATCH 011/117] wip --- src/view/context/store-context.js | 5 + .../drag-drop-context/drag-drop-context.jsx | 368 ++++++++++-------- src/view/draggable/connected-draggable.js | 7 +- src/view/droppable/connected-droppable.js | 7 +- 4 files changed, 213 insertions(+), 174 deletions(-) create mode 100644 src/view/context/store-context.js diff --git a/src/view/context/store-context.js b/src/view/context/store-context.js new file mode 100644 index 0000000000..fc63ab137e --- /dev/null +++ b/src/view/context/store-context.js @@ -0,0 +1,5 @@ +// @flow +import React from 'react'; +import type { Store } from '../../state/store-types'; + +export default React.createContext(null); diff --git a/src/view/drag-drop-context/drag-drop-context.jsx b/src/view/drag-drop-context/drag-drop-context.jsx index a535c5efab..9e243baa21 100644 --- a/src/view/drag-drop-context/drag-drop-context.jsx +++ b/src/view/drag-drop-context/drag-drop-context.jsx @@ -5,10 +5,10 @@ import React, { useEffect, useRef, type Node, + type MutableRefObject, } from 'react'; import { bindActionCreators } from 'redux'; import { Provider } from 'react-redux'; -import invariant from 'tiny-invariant'; import createStore from '../../state/create-store'; import createDimensionMarshal from '../../state/dimension-marshal/dimension-marshal'; import canStartDrag from '../../state/can-start-drag'; @@ -22,14 +22,8 @@ import type { Callbacks as DimensionMarshalCallbacks, } from '../../state/dimension-marshal/dimension-marshal-types'; import type { DraggableId, State, Responders, Announce } from '../../types'; -import type { Store } from '../../state/store-types'; -import { - storeKey, - dimensionMarshalKey, - styleKey, - canLiftKey, - isMovementAllowedKey, -} from '../context-keys'; +import type { Store, Action } from '../../state/store-types'; +import StoreContext from '../context/store-context'; import { clean, move, @@ -53,6 +47,7 @@ type Props = {| children: Node | null, |}; +// TODO: handle errors const printFatalDevError = (error: Error) => { if (process.env.NODE_ENV === 'production') { return; @@ -93,10 +88,8 @@ export default function DragDropContext(props: Props) { // eslint-disable-next-line react-hooks/exhaustive-deps [], ); - const announce: Announce = useAnnouncer(uniqueId); - const styleMarshal: StyleMarshal = useStyleMarshal(uniqueId); - const dimensionMarshal: DimensionMarshal = useDimensionMarshal(); - const autoScroller: AutoScroller = useAutoScroller(); + + let storeRef: MutableRefObject; // some validation when mounting useEffect(() => { @@ -116,7 +109,50 @@ export default function DragDropContext(props: Props) { return createResponders(lastPropsRef.current); }, []); - const storeRef = useRef( + const announce: Announce = useAnnouncer(uniqueId); + const styleMarshal: StyleMarshal = useStyleMarshal(uniqueId); + + const lazyDispatch = useCallback((action: Action) => { + storeRef.current.dispatch(action); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + const callbacks: DimensionMarshalCallbacks = bindActionCreators( + { + publishWhileDragging, + updateDroppableScroll, + updateDroppableIsEnabled, + updateDroppableIsCombineEnabled, + collectionStarting, + }, + lazyDispatch, + ); + const dimensionMarshal: DimensionMarshal = useMemo( + () => createDimensionMarshal(callbacks), + // dimension marshal cannot change + // eslint-disable-next-line react-hooks/exhaustive-deps + [], + ); + + const autoScroller: AutoScroller = useMemo( + () => + createAutoScroller({ + scrollWindow, + scrollDroppable: dimensionMarshal.scrollDroppable, + ...bindActionCreators( + { + move, + }, + lazyDispatch, + ), + }), + // forcing only one time creation. + // eslint-disable-next-line react-hooks/exhaustive-deps + [], + ); + + // TODO: will this create a new store every render? + storeRef = useRef( createStore({ dimensionMarshal, styleMarshal, @@ -128,12 +164,12 @@ export default function DragDropContext(props: Props) { const getCanLift = useCallback( (id: DraggableId) => canStartDrag(storeRef.current.getState(), id), - [], + [storeRef], ); const getIsMovementAllowed = useCallback( () => isMovementAllowed(storeRef.current.getState()), - [], + [storeRef], ); const appContext: AppContextValue = useMemo( @@ -153,156 +189,158 @@ export default function DragDropContext(props: Props) { return ( - {props.children} + + {props.children} + ); } -export class DragDropContext extends React.Component { - /* eslint-disable react/sort-comp */ - store: Store; - dimensionMarshal: DimensionMarshal; - styleMarshal: StyleMarshal; - autoScroller: AutoScroller; - announcer: Announcer; - unsubscribe: Function; - - constructor(props: Props, context: mixed) { - super(props, context); - - // A little setup check for dev - if (process.env.NODE_ENV !== 'production') { - invariant( - typeof props.onDragEnd === 'function', - 'A DragDropContext requires an onDragEnd function to perform reordering logic', - ); - } - - this.announcer = createAnnouncer(); - - // create the style marshal - this.styleMarshal = createStyleMarshal(); - - this.store = createStore({ - // Lazy reference to dimension marshal get around circular dependency - getDimensionMarshal: (): DimensionMarshal => this.dimensionMarshal, - styleMarshal: this.styleMarshal, - // This is a function as users are allowed to change their responder functions - // at any time - getResponders: (): Responders => ({ - onBeforeDragStart: this.props.onBeforeDragStart, - onDragStart: this.props.onDragStart, - onDragEnd: this.props.onDragEnd, - onDragUpdate: this.props.onDragUpdate, - }), - announce: this.announcer.announce, - getScroller: () => this.autoScroller, - }); - const callbacks: DimensionMarshalCallbacks = bindActionCreators( - { - publishWhileDragging, - updateDroppableScroll, - updateDroppableIsEnabled, - updateDroppableIsCombineEnabled, - collectionStarting, - }, - this.store.dispatch, - ); - this.dimensionMarshal = createDimensionMarshal(callbacks); - - this.autoScroller = createAutoScroller({ - scrollWindow, - scrollDroppable: this.dimensionMarshal.scrollDroppable, - ...bindActionCreators( - { - move, - }, - this.store.dispatch, - ), - }); - } - // Need to declare childContextTypes without flow - // https://github.com/brigand/babel-plugin-flow-react-proptypes/issues/22 - static childContextTypes = { - [storeKey]: PropTypes.shape({ - dispatch: PropTypes.func.isRequired, - subscribe: PropTypes.func.isRequired, - getState: PropTypes.func.isRequired, - }).isRequired, - [dimensionMarshalKey]: PropTypes.object.isRequired, - [styleKey]: PropTypes.string.isRequired, - [canLiftKey]: PropTypes.func.isRequired, - [isMovementAllowedKey]: PropTypes.func.isRequired, - }; - - getChildContext(): Context { - return { - [storeKey]: this.store, - [dimensionMarshalKey]: this.dimensionMarshal, - [styleKey]: this.styleMarshal.styleContext, - [canLiftKey]: this.canLift, - [isMovementAllowedKey]: this.getIsMovementAllowed, - }; - } - - // Providing function on the context for drag handles to use to - // let them know if they can start a drag or not. This is done - // rather than mapping a prop onto the drag handle so that we - // do not need to re-render a connected drag handle in order to - // pull this state off. It would cause a re-render of all items - // on drag start which is too expensive. - // This is useful when the user - canLift = (id: DraggableId) => canStartDrag(this.store.getState(), id); - getIsMovementAllowed = () => isMovementAllowed(this.store.getState()); - - componentDidMount() { - window.addEventListener('error', this.onWindowError); - this.styleMarshal.mount(); - this.announcer.mount(); - - if (process.env.NODE_ENV !== 'production') { - checkReactVersion(peerDependencies.react, React.version); - checkDoctype(document); - } - } - - componentDidCatch(error: Error) { - this.onFatalError(error); - - // If the failure was due to an invariant failure - then we handle the error - if (error.message.indexOf('Invariant failed') !== -1) { - this.setState({}); - return; - } - - // Error is more serious and we throw it - throw error; - } - - componentWillUnmount() { - window.removeEventListener('error', this.onWindowError); - - const state: State = this.store.getState(); - if (state.phase !== 'IDLE') { - this.store.dispatch(clean()); - } - - this.styleMarshal.unmount(); - this.announcer.unmount(); - } - - onFatalError = (error: Error) => { - printFatalDevError(error); - - const state: State = this.store.getState(); - if (state.phase !== 'IDLE') { - this.store.dispatch(clean()); - } - }; - - onWindowError = (error: Error) => this.onFatalError(error); - - render() { - return this.props.children; - } -} +// export class DragDropContext extends React.Component { +// /* eslint-disable react/sort-comp */ +// store: Store; +// dimensionMarshal: DimensionMarshal; +// styleMarshal: StyleMarshal; +// autoScroller: AutoScroller; +// announcer: Announcer; +// unsubscribe: Function; + +// constructor(props: Props, context: mixed) { +// super(props, context); + +// // A little setup check for dev +// if (process.env.NODE_ENV !== 'production') { +// invariant( +// typeof props.onDragEnd === 'function', +// 'A DragDropContext requires an onDragEnd function to perform reordering logic', +// ); +// } + +// this.announcer = createAnnouncer(); + +// // create the style marshal +// this.styleMarshal = createStyleMarshal(); + +// this.store = createStore({ +// // Lazy reference to dimension marshal get around circular dependency +// getDimensionMarshal: (): DimensionMarshal => this.dimensionMarshal, +// styleMarshal: this.styleMarshal, +// // This is a function as users are allowed to change their responder functions +// // at any time +// getResponders: (): Responders => ({ +// onBeforeDragStart: this.props.onBeforeDragStart, +// onDragStart: this.props.onDragStart, +// onDragEnd: this.props.onDragEnd, +// onDragUpdate: this.props.onDragUpdate, +// }), +// announce: this.announcer.announce, +// getScroller: () => this.autoScroller, +// }); +// const callbacks: DimensionMarshalCallbacks = bindActionCreators( +// { +// publishWhileDragging, +// updateDroppableScroll, +// updateDroppableIsEnabled, +// updateDroppableIsCombineEnabled, +// collectionStarting, +// }, +// this.store.dispatch, +// ); +// this.dimensionMarshal = createDimensionMarshal(callbacks); + +// this.autoScroller = createAutoScroller({ +// scrollWindow, +// scrollDroppable: this.dimensionMarshal.scrollDroppable, +// ...bindActionCreators( +// { +// move, +// }, +// this.store.dispatch, +// ), +// }); +// } +// // Need to declare childContextTypes without flow +// // https://github.com/brigand/babel-plugin-flow-react-proptypes/issues/22 +// static childContextTypes = { +// [storeKey]: PropTypes.shape({ +// dispatch: PropTypes.func.isRequired, +// subscribe: PropTypes.func.isRequired, +// getState: PropTypes.func.isRequired, +// }).isRequired, +// [dimensionMarshalKey]: PropTypes.object.isRequired, +// [styleKey]: PropTypes.string.isRequired, +// [canLiftKey]: PropTypes.func.isRequired, +// [isMovementAllowedKey]: PropTypes.func.isRequired, +// }; + +// getChildContext(): Context { +// return { +// [storeKey]: this.store, +// [dimensionMarshalKey]: this.dimensionMarshal, +// [styleKey]: this.styleMarshal.styleContext, +// [canLiftKey]: this.canLift, +// [isMovementAllowedKey]: this.getIsMovementAllowed, +// }; +// } + +// // Providing function on the context for drag handles to use to +// // let them know if they can start a drag or not. This is done +// // rather than mapping a prop onto the drag handle so that we +// // do not need to re-render a connected drag handle in order to +// // pull this state off. It would cause a re-render of all items +// // on drag start which is too expensive. +// // This is useful when the user +// canLift = (id: DraggableId) => canStartDrag(this.store.getState(), id); +// getIsMovementAllowed = () => isMovementAllowed(this.store.getState()); + +// componentDidMount() { +// window.addEventListener('error', this.onWindowError); +// this.styleMarshal.mount(); +// this.announcer.mount(); + +// if (process.env.NODE_ENV !== 'production') { +// checkReactVersion(peerDependencies.react, React.version); +// checkDoctype(document); +// } +// } + +// componentDidCatch(error: Error) { +// this.onFatalError(error); + +// // If the failure was due to an invariant failure - then we handle the error +// if (error.message.indexOf('Invariant failed') !== -1) { +// this.setState({}); +// return; +// } + +// // Error is more serious and we throw it +// throw error; +// } + +// componentWillUnmount() { +// window.removeEventListener('error', this.onWindowError); + +// const state: State = this.store.getState(); +// if (state.phase !== 'IDLE') { +// this.store.dispatch(clean()); +// } + +// this.styleMarshal.unmount(); +// this.announcer.unmount(); +// } + +// onFatalError = (error: Error) => { +// printFatalDevError(error); + +// const state: State = this.store.getState(); +// if (state.phase !== 'IDLE') { +// this.store.dispatch(clean()); +// } +// }; + +// onWindowError = (error: Error) => this.onFatalError(error); + +// render() { +// return this.props.children; +// } +// } diff --git a/src/view/draggable/connected-draggable.js b/src/view/draggable/connected-draggable.js index 5e936d90a6..6e7ad36535 100644 --- a/src/view/draggable/connected-draggable.js +++ b/src/view/draggable/connected-draggable.js @@ -41,6 +41,7 @@ import type { Selector, } from './draggable-types'; import whatIsDraggedOver from '../../state/droppable/what-is-dragged-over'; +import StoreContext from '../context/store-context'; const getCombineWith = (impact: DragImpact): ?DraggableId => { if (!impact.merge) { @@ -288,10 +289,8 @@ const ConnectedDraggable: typeof DraggableType = (connect( null, // options { - // Using our own store key. - // This allows consumers to also use redux - // Note: the default store key is 'store' - storeKey, + // Using our own context for the store to avoid clashing with consumers + context: StoreContext, // Default value, but being really clear pure: true, // When pure, compares the result of mapStateToProps to its previous value. diff --git a/src/view/droppable/connected-droppable.js b/src/view/droppable/connected-droppable.js index 7b3531ee2c..f56d82b2aa 100644 --- a/src/view/droppable/connected-droppable.js +++ b/src/view/droppable/connected-droppable.js @@ -22,7 +22,6 @@ import type { Selector, DispatchProps, } from './droppable-types'; -import { storeKey } from '../context-keys'; import Droppable from './droppable'; import isStrictEqual from '../is-strict-equal'; import whatIsDraggedOver from '../../state/droppable/what-is-dragged-over'; @@ -220,10 +219,8 @@ const ConnectedDroppable: typeof DroppableType = (connect( // mergeProps - using default null, { - // Using our own store key. - // This allows consumers to also use redux - // Note: the default store key is 'store' - storeKey, + // Ensuring our context does not clash with consumers + context: StoreContext, // pure: true is default value, but being really clear pure: true, // When pure, compares the result of mapStateToProps to its previous value. From 283f55acbb73963120d8e66e1a02f2319e4d383b Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Fri, 15 Mar 2019 17:32:15 +1100 Subject: [PATCH 012/117] draggable wip --- src/view/draggable/connected-draggable.js | 1 - src/view/draggable/draggable.jsx | 142 ++++++++++++++-------- src/view/droppable/connected-droppable.js | 1 + 3 files changed, 94 insertions(+), 50 deletions(-) diff --git a/src/view/draggable/connected-draggable.js b/src/view/draggable/connected-draggable.js index 6e7ad36535..2aceb98ffa 100644 --- a/src/view/draggable/connected-draggable.js +++ b/src/view/draggable/connected-draggable.js @@ -5,7 +5,6 @@ import { Component } from 'react'; import memoizeOne from 'memoize-one'; import { connect } from 'react-redux'; import Draggable from './draggable'; -import { storeKey } from '../context-keys'; import { origin } from '../../state/position'; import isStrictEqual from '../is-strict-equal'; import { curves, combine } from '../../animation'; diff --git a/src/view/draggable/draggable.jsx b/src/view/draggable/draggable.jsx index 0bbc353c44..8f24b5491d 100644 --- a/src/view/draggable/draggable.jsx +++ b/src/view/draggable/draggable.jsx @@ -1,5 +1,5 @@ // @flow -import React, { useEffect } from 'react'; +import React, { useEffect, useContext } from 'react'; import { type Position, type BoxModel } from 'css-box-model'; import PropTypes from 'prop-types'; import memoizeOne from 'memoize-one'; @@ -34,6 +34,10 @@ import type { import getWindowScroll from '../window/get-window-scroll'; import throwIfRefIsInvalid from '../throw-if-invalid-inner-ref'; import checkOwnProps from './check-own-props'; +import AppContext, { type AppContextValue } from '../context/app-context'; +import DroppableContext, { + type DroppableContextValue, +} from '../context/droppable-context'; export const zIndexOptions: ZIndexOptions = { dragging: 5000, @@ -72,14 +76,94 @@ const getShouldDraggingAnimate = (dragging: DraggingMapProps): boolean => { return dragging.mode === 'SNAP'; }; -// function Draggable(props: Props) { -// const styleContext: string = useContext(styleContext); -// const ref = useRef(null); +function getDraggingStyle(dragging: DraggingMapProps): DraggingStyle { + const dimension: DraggableDimension = dragging.dimension; + const box: BoxModel = dimension.client; + const { offset, combineWith, dropping } = dragging; + + const isCombining: boolean = Boolean(combineWith); + + const shouldAnimate: boolean = getShouldDraggingAnimate(dragging); + const isDropAnimating: boolean = Boolean(dropping); + + const transform: ?string = isDropAnimating + ? transforms.drop(offset, isCombining) + : transforms.moveTo(offset); + + const style: DraggingStyle = { + // ## Placement + position: 'fixed', + // As we are applying the margins we need to align to the start of the marginBox + top: box.marginBox.top, + left: box.marginBox.left, + + // ## Sizing + // Locking these down as pulling the node out of the DOM could cause it to change size + boxSizing: 'border-box', + width: box.borderBox.width, + height: box.borderBox.height, + + // ## Movement + // Opting out of the standard css transition for the dragging item + transition: getDraggingTransition(shouldAnimate, dropping), + transform, + opacity: getDraggingOpacity(isCombining, isDropAnimating), + // ## Layering + zIndex: isDropAnimating + ? zIndexOptions.dropAnimating + : zIndexOptions.dragging, + + // ## Blocking any pointer events on the dragging or dropping item + // global styles on cover while dragging + pointerEvents: 'none', + }; + return style; +} + +function getSecondaryStyle(secondary: SecondaryMapProps): NotDraggingStyle { + return { + transform: transforms.moveTo(secondary.offset), + // transition style is applied in the head + transition: secondary.shouldAnimateDisplacement ? null : 'none', + }; +} + +function Draggable(props: Props) { + const appContext: ?AppContextValue = useContext(AppContext); + invariant( + appContext, + 'Draggable expected a AppContext to be populated. Have you forgot a DragDropContext?', + ); + const droppableContext: ?DroppableContextValue = useContext(DroppableContext); + invariant( + appContext, + 'Draggable expected a DroppableContext to be populated. Have you forgot a Droppable?', + ); + + const callbacks: DragHandleCallbacks = {}; + const dragHandleProps: DragHandleProvided = useDragHandle(callbacks); + + const { dragging, secondary } = props; + + const provided: Provided = useMemo(() => { + const style: DraggableStyle = dragging + ? getDraggingStyle(dragging) + : getSecondaryStyle(secondary); + const onTransitionEnd = dragging && dragging.dropping ? onMoveEnd : null; + + const result: Provided = { + innerRef: setRef, + draggableProps: { + 'data-react-beautiful-dnd-draggable': appContext.style, + style, + onTransitionEnd, + }, + dragHandleProps, + }; -// // A one time check for the validity of hooks -// // eslint-disable-next-line react-hooks/exhaustive-deps -// useEffect(() => checkOwnProps(props), []); -// } + return result; + }, [appContext.style, dragHandleProps, dragging, secondary]); +} export default class Draggable extends React.Component { /* eslint-disable react/sort-comp */ @@ -193,47 +277,7 @@ export default class Draggable extends React.Component { getDraggingStyle = memoizeOne( (dragging: DraggingMapProps): DraggingStyle => { - const dimension: DraggableDimension = dragging.dimension; - const box: BoxModel = dimension.client; - const { offset, combineWith, dropping } = dragging; - - const isCombining: boolean = Boolean(combineWith); - - const shouldAnimate: boolean = getShouldDraggingAnimate(dragging); - const isDropAnimating: boolean = Boolean(dropping); - - const transform: ?string = isDropAnimating - ? transforms.drop(offset, isCombining) - : transforms.moveTo(offset); - - const style: DraggingStyle = { - // ## Placement - position: 'fixed', - // As we are applying the margins we need to align to the start of the marginBox - top: box.marginBox.top, - left: box.marginBox.left, - - // ## Sizing - // Locking these down as pulling the node out of the DOM could cause it to change size - boxSizing: 'border-box', - width: box.borderBox.width, - height: box.borderBox.height, - - // ## Movement - // Opting out of the standard css transition for the dragging item - transition: getDraggingTransition(shouldAnimate, dropping), - transform, - opacity: getDraggingOpacity(isCombining, isDropAnimating), - // ## Layering - zIndex: isDropAnimating - ? zIndexOptions.dropAnimating - : zIndexOptions.dragging, - - // ## Blocking any pointer events on the dragging or dropping item - // global styles on cover while dragging - pointerEvents: 'none', - }; - return style; + co; }, ); diff --git a/src/view/droppable/connected-droppable.js b/src/view/droppable/connected-droppable.js index f56d82b2aa..e0d4e2d356 100644 --- a/src/view/droppable/connected-droppable.js +++ b/src/view/droppable/connected-droppable.js @@ -26,6 +26,7 @@ import Droppable from './droppable'; import isStrictEqual from '../is-strict-equal'; import whatIsDraggedOver from '../../state/droppable/what-is-dragged-over'; import { updateViewportMaxScroll as updateViewportMaxScrollAction } from '../../state/action-creators'; +import StoreContext from '../context/store-context'; const idle: MapProps = { isDraggingOver: false, From b9b4358703d32fdc3e3784a52cf7a106a6a5b78f Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Mon, 18 Mar 2019 08:06:09 +1100 Subject: [PATCH 013/117] useConstant --- .../drag-drop-context/drag-drop-context.jsx | 9 ++++++ test/unit/view/hooks/use-constant.js | 29 +++++++++++++++++++ 2 files changed, 38 insertions(+) create mode 100644 test/unit/view/hooks/use-constant.js diff --git a/src/view/drag-drop-context/drag-drop-context.jsx b/src/view/drag-drop-context/drag-drop-context.jsx index 9e243baa21..dc33ea9fe9 100644 --- a/src/view/drag-drop-context/drag-drop-context.jsx +++ b/src/view/drag-drop-context/drag-drop-context.jsx @@ -82,6 +82,7 @@ export function resetServerContext() { } export default function DragDropContext(props: Props) { + // TODO: cannot useMemo as it can be thrown away const uniqueId: number = useMemo( (): number => count++, // this must stay constant for each component @@ -127,6 +128,14 @@ export default function DragDropContext(props: Props) { }, lazyDispatch, ); + // TODO: needs to be a ref + const dimensionMarshalRef = useRef(); + function getDimensionMarshal(): DimensionMarshal { + if (dimensionMarshalRef.current) { + return dimensionMarshalRef.current; + } + dimension; + } const dimensionMarshal: DimensionMarshal = useMemo( () => createDimensionMarshal(callbacks), // dimension marshal cannot change diff --git a/test/unit/view/hooks/use-constant.js b/test/unit/view/hooks/use-constant.js new file mode 100644 index 0000000000..11590c9145 --- /dev/null +++ b/test/unit/view/hooks/use-constant.js @@ -0,0 +1,29 @@ +// @flow +import { useRef } from 'react'; +import { warning } from '../../../../src/dev-warning'; + +export function useConstantValue(fn: () => T): T { + const bucket = useRef(null); + + if (bucket.current) { + return bucket.current; + } + + bucket.current = fn(); + + if (!bucket.current) { + warning('Expected constantFn to be truthy'); + } + return bucket.current; +} + +export function useConstantFn(getFn: () => () => T): () => T { + const bucket = useRef T>(null); + + if (bucket.current) { + return bucket.current; + } + + bucket.current = getFn(); + return bucket.current; +} From fa5ff4c4d2acab26074d7eb5c1d5d39463f59c06 Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Mon, 18 Mar 2019 08:48:57 +1100 Subject: [PATCH 014/117] use constant in the house --- .../drag-drop-context/drag-drop-context.jsx | 129 +++++++----------- src/view/use-constant.js | 25 ++++ test/unit/view/hooks/use-constant.js | 29 ---- 3 files changed, 74 insertions(+), 109 deletions(-) create mode 100644 src/view/use-constant.js delete mode 100644 test/unit/view/hooks/use-constant.js diff --git a/src/view/drag-drop-context/drag-drop-context.jsx b/src/view/drag-drop-context/drag-drop-context.jsx index dc33ea9fe9..9e5ff94115 100644 --- a/src/view/drag-drop-context/drag-drop-context.jsx +++ b/src/view/drag-drop-context/drag-drop-context.jsx @@ -1,12 +1,5 @@ // @flow -import React, { - useCallback, - useMemo, - useEffect, - useRef, - type Node, - type MutableRefObject, -} from 'react'; +import React, { useEffect, useRef, type Node } from 'react'; import { bindActionCreators } from 'redux'; import { Provider } from 'react-redux'; import createStore from '../../state/create-store'; @@ -24,6 +17,7 @@ import type { import type { DraggableId, State, Responders, Announce } from '../../types'; import type { Store, Action } from '../../state/store-types'; import StoreContext from '../context/store-context'; +import { useConstant, useConstantFn } from '../use-constant'; import { clean, move, @@ -82,13 +76,7 @@ export function resetServerContext() { } export default function DragDropContext(props: Props) { - // TODO: cannot useMemo as it can be thrown away - const uniqueId: number = useMemo( - (): number => count++, - // this must stay constant for each component - // eslint-disable-next-line react-hooks/exhaustive-deps - [], - ); + const uniqueId: number = useConstant((): number => count++); let storeRef: MutableRefObject; @@ -106,62 +94,51 @@ export default function DragDropContext(props: Props) { lastPropsRef.current = props; }); - const getResponders = useCallback((): Responders => { + const getResponders: () => Responders = useConstantFn(() => { return createResponders(lastPropsRef.current); - }, []); + }); const announce: Announce = useAnnouncer(uniqueId); const styleMarshal: StyleMarshal = useStyleMarshal(uniqueId); - const lazyDispatch = useCallback((action: Action) => { - storeRef.current.dispatch(action); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - - const callbacks: DimensionMarshalCallbacks = bindActionCreators( - { - publishWhileDragging, - updateDroppableScroll, - updateDroppableIsEnabled, - updateDroppableIsCombineEnabled, - collectionStarting, + const lazyDispatch: Action => void = useConstantFn( + (action: Action): void => { + storeRef.current.dispatch(action); }, - lazyDispatch, ); - // TODO: needs to be a ref - const dimensionMarshalRef = useRef(); - function getDimensionMarshal(): DimensionMarshal { - if (dimensionMarshalRef.current) { - return dimensionMarshalRef.current; - } - dimension; - } - const dimensionMarshal: DimensionMarshal = useMemo( - () => createDimensionMarshal(callbacks), - // dimension marshal cannot change - // eslint-disable-next-line react-hooks/exhaustive-deps - [], + + const callbacks: DimensionMarshalCallbacks = useConstant(() => + bindActionCreators( + { + publishWhileDragging, + updateDroppableScroll, + updateDroppableIsEnabled, + updateDroppableIsCombineEnabled, + collectionStarting, + }, + // $FlowFixMe - not sure why this is wrong + lazyDispatch, + ), + ); + const dimensionMarshal: DimensionMarshal = useConstant(() => + createDimensionMarshal(callbacks), ); - const autoScroller: AutoScroller = useMemo( - () => - createAutoScroller({ - scrollWindow, - scrollDroppable: dimensionMarshal.scrollDroppable, - ...bindActionCreators( - { - move, - }, - lazyDispatch, - ), - }), - // forcing only one time creation. - // eslint-disable-next-line react-hooks/exhaustive-deps - [], + const autoScroller: AutoScroller = useConstant(() => + createAutoScroller({ + scrollWindow, + scrollDroppable: dimensionMarshal.scrollDroppable, + ...bindActionCreators( + { + move, + }, + // $FlowFixMe - not sure why this is wrong + lazyDispatch, + ), + }), ); - // TODO: will this create a new store every render? - storeRef = useRef( + const store: Store = useConstant(() => createStore({ dimensionMarshal, styleMarshal, @@ -171,31 +148,23 @@ export default function DragDropContext(props: Props) { }), ); - const getCanLift = useCallback( - (id: DraggableId) => canStartDrag(storeRef.current.getState(), id), - [storeRef], - ); + storeRef = useRef(store); - const getIsMovementAllowed = useCallback( - () => isMovementAllowed(storeRef.current.getState()), - [storeRef], + const getCanLift = useConstantFn((id: DraggableId) => + canStartDrag(storeRef.current.getState(), id), ); - const appContext: AppContextValue = useMemo( - () => ({ - marshal: dimensionMarshal, - style: styleMarshal.styleContext, - canLift: getCanLift, - isMovementAllowed: getIsMovementAllowed, - }), - [ - dimensionMarshal, - getCanLift, - getIsMovementAllowed, - styleMarshal.styleContext, - ], + const getIsMovementAllowed = useConstantFn(() => + isMovementAllowed(storeRef.current.getState()), ); + const appContext: AppContextValue = useConstant(() => ({ + marshal: dimensionMarshal, + style: styleMarshal.styleContext, + canLift: getCanLift, + isMovementAllowed: getIsMovementAllowed, + })); + return ( diff --git a/src/view/use-constant.js b/src/view/use-constant.js new file mode 100644 index 0000000000..246ff48a6d --- /dev/null +++ b/src/view/use-constant.js @@ -0,0 +1,25 @@ +// @flow +import { useRef } from 'react'; +import { warning } from '../dev-warning'; + +export function useConstant(fn: () => T): T { + const bucket = useRef(null); + + if (bucket.current) { + return bucket.current; + } + + bucket.current = fn(); + + if (!bucket.current) { + warning('Expected constantFn to be truthy'); + } + return bucket.current; +} + +export function useConstantFn(getter: GetterFn): GetterFn { + // set bucket with original getter + const bucket = useRef(getter); + // never override the original, just keep returning it + return bucket.current; +} diff --git a/test/unit/view/hooks/use-constant.js b/test/unit/view/hooks/use-constant.js deleted file mode 100644 index 11590c9145..0000000000 --- a/test/unit/view/hooks/use-constant.js +++ /dev/null @@ -1,29 +0,0 @@ -// @flow -import { useRef } from 'react'; -import { warning } from '../../../../src/dev-warning'; - -export function useConstantValue(fn: () => T): T { - const bucket = useRef(null); - - if (bucket.current) { - return bucket.current; - } - - bucket.current = fn(); - - if (!bucket.current) { - warning('Expected constantFn to be truthy'); - } - return bucket.current; -} - -export function useConstantFn(getFn: () => () => T): () => T { - const bucket = useRef T>(null); - - if (bucket.current) { - return bucket.current; - } - - bucket.current = getFn(); - return bucket.current; -} From b8d9627ecf4373b400abd5c6831047ad6a819e8f Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Mon, 18 Mar 2019 09:00:16 +1100 Subject: [PATCH 015/117] cleanup. moving to newer react-redux --- package.json | 2 +- .../drag-drop-context/drag-drop-context.jsx | 7 +- src/view/droppable/droppable.jsx | 15 +- .../use-animate-in-out/use-animate-in-out.js | 75 +++++++++ .../use-animate-in-out/use-animate-in-out.jsx | 153 ------------------ ...x => use-droppable-dimension-publisher.js} | 0 .../use-style-marshal/use-style-marshal.js | 55 +++---- yarn.lock | 24 +-- 8 files changed, 129 insertions(+), 202 deletions(-) create mode 100644 src/view/use-animate-in-out/use-animate-in-out.js delete mode 100644 src/view/use-animate-in-out/use-animate-in-out.jsx rename src/view/use-droppable-dimension-publisher/{use-droppable-dimension-publisher.jsx => use-droppable-dimension-publisher.js} (100%) diff --git a/package.json b/package.json index cba1ff9ffb..48e2ff292c 100644 --- a/package.json +++ b/package.json @@ -58,7 +58,7 @@ "css-box-model": "^1.1.1", "memoize-one": "^5.0.0", "raf-schd": "^4.0.0", - "react-redux": "^6.0.1", + "react-redux": "npm:@acemarke/react-redux@next", "redux": "^4.0.1", "tiny-invariant": "^1.0.3" }, diff --git a/src/view/drag-drop-context/drag-drop-context.jsx b/src/view/drag-drop-context/drag-drop-context.jsx index 9e5ff94115..dc813f0b2c 100644 --- a/src/view/drag-drop-context/drag-drop-context.jsx +++ b/src/view/drag-drop-context/drag-drop-context.jsx @@ -1,5 +1,10 @@ // @flow -import React, { useEffect, useRef, type Node } from 'react'; +import React, { + useEffect, + useRef, + type Node, + type MutableRefObject, +} from 'react'; import { bindActionCreators } from 'redux'; import { Provider } from 'react-redux'; import createStore from '../../state/create-store'; diff --git a/src/view/droppable/droppable.jsx b/src/view/droppable/droppable.jsx index 03f58df72f..e4af7b513e 100644 --- a/src/view/droppable/droppable.jsx +++ b/src/view/droppable/droppable.jsx @@ -19,6 +19,7 @@ import useAnimateInOut, { type AnimateProvided, } from '../use-animate-in-out/use-animate-in-out'; import getMaxWindowScroll from '../window/get-max-window-scroll'; +import { useConstantFn } from '../use-constant'; import { checkOwnProps, checkPlaceholder, checkProvidedRef } from './check'; export default function Droppable(props: Props) { @@ -52,20 +53,18 @@ export default function Droppable(props: Props) { updateViewportMaxScroll, } = props; - const getDroppableRef = useCallback( + const getDroppableRef = useConstantFn( (): ?HTMLElement => droppableRef.current, - [], ); - const getPlaceholderRef = useCallback( + const getPlaceholderRef = useConstantFn( (): ?HTMLElement => placeholderRef.current, - [], ); - const setDroppableRef = useCallback((value: ?HTMLElement) => { + const setDroppableRef = useConstantFn((value: ?HTMLElement) => { droppableRef.current = value; - }, []); - const setPlaceholderRef = useCallback((value: ?HTMLElement) => { + }); + const setPlaceholderRef = useConstantFn((value: ?HTMLElement) => { placeholderRef.current = value; - }, []); + }); const onPlaceholderTransitionEnd = useCallback(() => { // A placeholder change can impact the window's max scroll diff --git a/src/view/use-animate-in-out/use-animate-in-out.js b/src/view/use-animate-in-out/use-animate-in-out.js new file mode 100644 index 0000000000..dd481cb72f --- /dev/null +++ b/src/view/use-animate-in-out/use-animate-in-out.js @@ -0,0 +1,75 @@ +// @flow +import { useMemo, useCallback, useEffect, useState } from 'react'; +import type { InOutAnimationMode } from '../../types'; + +export type AnimateProvided = {| + onClose: () => void, + animate: InOutAnimationMode, + data: mixed, +|}; + +type Args = {| + on: mixed, + shouldAnimate: boolean, +|}; + +export default function useAnimateInOut(args: Args): ?AnimateProvided { + const [isVisible, setIsVisible] = useState(Boolean(args.on)); + const [data, setData] = useState(args.on); + const [animate, setAnimate] = useState( + args.shouldAnimate && args.on ? 'open' : 'none', + ); + + useEffect(() => { + if (!args.shouldAnimate) { + setIsVisible(Boolean(args.on)); + setData(args.on); + setAnimate('none'); + return; + } + + // need to animate in + if (args.on) { + setIsVisible(true); + setData(args.on); + setAnimate('open'); + return; + } + + // need to animate out if there was data + + if (isVisible) { + setAnimate('close'); + return; + } + + // close animation no longer visible + + setIsVisible(false); + setAnimate('close'); + setData(null); + }); + + const onClose = useCallback(() => { + if (animate !== 'close') { + return; + } + + setIsVisible(false); + }, [animate]); + + const provided: AnimateProvided = useMemo( + () => ({ + onClose, + data, + animate, + }), + [animate, data, onClose], + ); + + if (!isVisible) { + return null; + } + + return provided; +} diff --git a/src/view/use-animate-in-out/use-animate-in-out.jsx b/src/view/use-animate-in-out/use-animate-in-out.jsx deleted file mode 100644 index b8ba9148bc..0000000000 --- a/src/view/use-animate-in-out/use-animate-in-out.jsx +++ /dev/null @@ -1,153 +0,0 @@ -// @flow -import React, { useMemo, useCallback, useEffect, useState } from 'react'; -import type { InOutAnimationMode } from '../../types'; - -export type AnimateProvided = {| - onClose: () => void, - animate: InOutAnimationMode, - data: mixed, -|}; - -type Args = {| - on: mixed, - shouldAnimate: boolean, -|}; - -type State = {| - data: mixed, - isVisible: boolean, - animate: InOutAnimationMode, -|}; - -function useAnimateInOut(args: Args): ?AnimateProvided { - const [isVisible, setIsVisible] = useState(Boolean(args.on)); - const [data, setData] = useState(args.on); - const [animate, setAnimate] = useState( - args.shouldAnimate && args.on ? 'open' : 'none', - ); - - useEffect(() => { - if (!args.shouldAnimate) { - setIsVisible(Boolean(args.on)); - setData(args.on); - setAnimate('none'); - return; - } - - // need to animate in - if (args.on) { - setIsVisible(true); - setData(args.on); - setAnimate('open'); - return; - } - - // need to animate out if there was data - - if (isVisible) { - setAnimate('close'); - return; - } - - // close animation no longer visible - - setIsVisible(false); - setAnimate('close'); - setData(null); - }); - - const onClose = useCallback(() => { - if (animate !== 'close') { - return; - } - - setIsVisible(false); - }, [animate]); - - const provided: AnimateProvided = useMemo( - () => ({ - onClose, - data, - animate, - }), - [animate, data, onClose], - ); - - if (!isVisible) { - return null; - } - - return provided; -} - -export default useAnimateInOut; - -export class AnimateInOut extends React.PureComponent { - state: State = { - isVisible: Boolean(this.props.on), - data: this.props.on, - // not allowing to animate close on mount - animate: this.props.shouldAnimate && this.props.on ? 'open' : 'none', - }; - - static getDerivedStateFromProps(props: Props, state: State): State { - if (!props.shouldAnimate) { - return { - isVisible: Boolean(props.on), - data: props.on, - animate: 'none', - }; - } - - // need to animate in - if (props.on) { - return { - isVisible: true, - // have new data to animate in with - data: props.on, - animate: 'open', - }; - } - - // need to animate out if there was data - - if (state.isVisible) { - return { - isVisible: true, - // use old data for animating out - data: state.data, - animate: 'close', - }; - } - - // close animation no longer visible - return { - isVisible: false, - animate: 'close', - data: null, - }; - } - - onClose = () => { - if (this.state.animate !== 'close') { - return; - } - - this.setState({ - isVisible: false, - }); - }; - - render() { - if (!this.state.isVisible) { - return null; - } - - const provided: AnimateProvided = { - onClose: this.onClose, - data: this.state.data, - animate: this.state.animate, - }; - return this.props.children(provided); - } -} diff --git a/src/view/use-droppable-dimension-publisher/use-droppable-dimension-publisher.jsx b/src/view/use-droppable-dimension-publisher/use-droppable-dimension-publisher.js similarity index 100% rename from src/view/use-droppable-dimension-publisher/use-droppable-dimension-publisher.jsx rename to src/view/use-droppable-dimension-publisher/use-droppable-dimension-publisher.js diff --git a/src/view/use-style-marshal/use-style-marshal.js b/src/view/use-style-marshal/use-style-marshal.js index 98147f043b..ef745d3e8c 100644 --- a/src/view/use-style-marshal/use-style-marshal.js +++ b/src/view/use-style-marshal/use-style-marshal.js @@ -20,13 +20,14 @@ const createStyleEl = (): HTMLStyleElement => { }; export default function useStyleMarshal(uniqueId: number) { - const uniqueContext: string = `${uniqueId}`; + const uniqueContext: string = useMemo(() => `${uniqueId}`, [uniqueId]); const styles: Styles = useMemo(() => getStyles(uniqueContext), [ uniqueContext, ]); const alwaysRef = useRef(null); const dynamicRef = useRef(null); + // TODO: need to check if the memoize one is working const setDynamicStyle = useCallback( // Using memoizeOne to prevent frequent updates to textContext memoizeOne((proposed: string) => { @@ -87,34 +88,34 @@ export default function useStyleMarshal(uniqueId: number) { uniqueContext, ]); - const dragging = useCallback( - () => setDynamicStyle(styles.dragging), - // we can never invalidate this reference - // eslint-disable-next-line react-hooks/exhaustive-deps - [], - ); - const dropping = useCallback((reason: DropReason) => { - if (reason === 'DROP') { - setDynamicStyle(styles.dropAnimating); - return; - } - setDynamicStyle(styles.userCancel); - // we can never invalidate this reference - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - const resting = useCallback( - () => setDynamicStyle(styles.resting), - // we can never invalidate this reference - // eslint-disable-next-line react-hooks/exhaustive-deps - [], + const dragging = useCallback(() => setDynamicStyle(styles.dragging), [ + setDynamicStyle, + styles.dragging, + ]); + const dropping = useCallback( + (reason: DropReason) => { + if (reason === 'DROP') { + setDynamicStyle(styles.dropAnimating); + return; + } + setDynamicStyle(styles.userCancel); + }, + [setDynamicStyle, styles.dropAnimating, styles.userCancel], ); + const resting = useCallback(() => setDynamicStyle(styles.resting), [ + setDynamicStyle, + styles.resting, + ]); - const marshal: StyleMarshal = { - dragging, - dropping, - resting, - styleContext: uniqueContext, - }; + const marshal: StyleMarshal = useMemo( + () => ({ + dragging, + dropping, + resting, + styleContext: uniqueContext, + }), + [dragging, dropping, resting, uniqueContext], + ); return marshal; } diff --git a/yarn.lock b/yarn.lock index 94e6f0796a..bf0262f8de 100644 --- a/yarn.lock +++ b/yarn.lock @@ -874,7 +874,7 @@ dependencies: regenerator-runtime "^0.12.0" -"@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.2.0", "@babel/runtime@^7.3.1", "@babel/runtime@^7.3.4": +"@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.2.0", "@babel/runtime@^7.3.4": version "7.3.4" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.3.4.tgz#73d12ba819e365fcf7fd152aed56d6df97d21c83" integrity sha512-IvfvnMdSaLBateu0jfsYIpZTxAc2cKEXEMiezGGN75QcBcecDUKd3PgLAncT0oOgxKy8dd8hrJKj9MfzgfZd6g== @@ -5884,7 +5884,7 @@ hoist-non-react-statics@^2.3.1, hoist-non-react-statics@^2.5.0: resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-2.5.5.tgz#c5903cf409c0dfd908f388e619d86b9c1174cb47" integrity sha512-rqcy4pJo55FTTLWt+bU8ukscqHeE/e9KWvsOW2b/a3afxQZhwkQdT1rPPCJ0rYXdj4vNcasY8zHTH+jF/qStxw== -hoist-non-react-statics@^3.3.0: +hoist-non-react-statics@^3.2.1: version "3.3.0" resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.0.tgz#b09178f0122184fb95acf525daaecb4d8f45958b" integrity sha512-0XsbTXxgiaCDYDIWFcwkmerZPSwywfUqYmwT4jzewKTQSWoE6FCMoUVOeBJWK3E/CrWbxRG3m5GzY4lnIwGRBA== @@ -9074,7 +9074,7 @@ prompts@^2.0.1: kleur "^3.0.2" sisteransi "^1.0.0" -prop-types@^15.5.10, prop-types@^15.5.4, prop-types@^15.6.0, prop-types@^15.6.1, prop-types@^15.6.2, prop-types@^15.7.2: +prop-types@^15.5.10, prop-types@^15.5.4, prop-types@^15.6.0, prop-types@^15.6.1, prop-types@^15.6.2: version "15.7.2" resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.7.2.tgz#52c41e75b8c87e72b9d9360e0206b99dcbffa6c5" integrity sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ== @@ -9415,7 +9415,7 @@ react-is@^16.3.1, react-is@^16.7.0, react-is@^16.8.1, react-is@^16.8.3: resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.8.3.tgz#4ad8b029c2a718fc0cfc746c8d4e1b7221e5387d" integrity sha512-Y4rC1ZJmsxxkkPuMLwvKvlL1Zfpbcu+Bf4ZigkHup3v9EfdYhAlWAaVyA19olXq2o2mGn0w+dFKvk3pVVlYcIA== -react-is@^16.8.2, react-is@^16.8.4: +react-is@^16.8.4: version "16.8.4" resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.8.4.tgz#90f336a68c3a29a096a3d648ab80e87ec61482a2" integrity sha512-PVadd+WaUDOAciICm/J1waJaSvgq+4rHE/K70j0PFqKhkTBsPv/82UGQJNXAngz1fOQLLxI6z1sEDmJDQhCTAA== @@ -9455,17 +9455,17 @@ react-popper@^1.3.3: typed-styles "^0.0.7" warning "^4.0.2" -react-redux@^6.0.1: - version "6.0.1" - resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-6.0.1.tgz#0d423e2c1cb10ada87293d47e7de7c329623ba4d" - integrity sha512-T52I52Kxhbqy/6TEfBv85rQSDz6+Y28V/pf52vDWs1YRXG19mcFOGfHnY2HsNFHyhP+ST34Aih98fvt6tqwVcQ== +"react-redux@npm:@acemarke/react-redux@next": + version "7.0.0-alpha.5" + resolved "https://registry.yarnpkg.com/@acemarke/react-redux/-/react-redux-7.0.0-alpha.5.tgz#af25d17b11f06eb23dc68462b9af868a64635d43" + integrity sha512-iKuJ9yaIPD3LQXnCIuUjOweMiWm5j0xBOUqSi6F6ip7gbmy1lK6Febik2KgWfYvX7/ZOTgEXUQ8upbuX392/xw== dependencies: - "@babel/runtime" "^7.3.1" - hoist-non-react-statics "^3.3.0" + "@babel/runtime" "^7.2.0" + hoist-non-react-statics "^3.2.1" invariant "^2.2.4" loose-envify "^1.4.0" - prop-types "^15.7.2" - react-is "^16.8.2" + prop-types "^15.6.2" + react-is "^16.7.0" react-resize-detector@^3.2.1: version "3.4.0" From b4748d5fca991ccb8a90acd3c4c0713607f61294 Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Mon, 18 Mar 2019 09:38:21 +1100 Subject: [PATCH 016/117] draggable --- src/view/drag-handle/drag-handle-types.js | 2 +- src/view/draggable/draggable-types.js | 4 + src/view/draggable/draggable.jsx | 252 +++++++++++++--------- src/view/draggable/get-style.js | 113 ++++++++++ src/view/use-required-context.js | 9 + 5 files changed, 272 insertions(+), 108 deletions(-) create mode 100644 src/view/draggable/get-style.js create mode 100644 src/view/use-required-context.js diff --git a/src/view/drag-handle/drag-handle-types.js b/src/view/drag-handle/drag-handle-types.js index b769ec9d31..5dab4efabb 100644 --- a/src/view/drag-handle/drag-handle-types.js +++ b/src/view/drag-handle/drag-handle-types.js @@ -7,7 +7,7 @@ export type Callbacks = {| onLift: ({ clientSelection: Position, movementMode: MovementMode, - }) => void, + }) => mixed, onMove: (point: Position) => mixed, onWindowScroll: () => mixed, onMoveUp: () => mixed, diff --git a/src/view/draggable/draggable-types.js b/src/view/draggable/draggable-types.js index e45d75a4a3..4c0b0b369d 100644 --- a/src/view/draggable/draggable-types.js +++ b/src/view/draggable/draggable-types.js @@ -114,12 +114,16 @@ export type DraggingMapProps = {| draggingOver: ?DroppableId, combineWith: ?DraggableId, forceShouldAnimate: ?boolean, + // TODO: snapshot + // snapshot: StateSnapshot, |}; export type SecondaryMapProps = {| offset: Position, combineTargetFor: ?DraggableId, shouldAnimateDisplacement: boolean, + // TODO: snapshot + // snapshot: StateSnapshot, |}; export type MapProps = {| diff --git a/src/view/draggable/draggable.jsx b/src/view/draggable/draggable.jsx index 8f24b5491d..5633071af3 100644 --- a/src/view/draggable/draggable.jsx +++ b/src/view/draggable/draggable.jsx @@ -1,10 +1,16 @@ // @flow -import React, { useEffect, useContext } from 'react'; +import React, { + useMemo, + useRef, + useEffect, + useContext, + useCallback, +} from 'react'; import { type Position, type BoxModel } from 'css-box-model'; import PropTypes from 'prop-types'; import memoizeOne from 'memoize-one'; import invariant from 'tiny-invariant'; -import { transitions, transforms, combine } from '../../animation'; +import getStyle from './get-style'; import type { DraggableDimension, DroppableId, @@ -30,6 +36,7 @@ import type { SecondaryMapProps, DraggingMapProps, ChildrenFn, + DraggableStyle, } from './draggable-types'; import getWindowScroll from '../window/get-window-scroll'; import throwIfRefIsInvalid from '../throw-if-invalid-inner-ref'; @@ -38,117 +45,115 @@ import AppContext, { type AppContextValue } from '../context/app-context'; import DroppableContext, { type DroppableContextValue, } from '../context/droppable-context'; +import useRequiredContext from '../use-required-context'; + +export default function Draggable(props: Props) { + // instance members + const ref = useRef(null); + const setRef = useCallback((el: ?HTMLElement) => { + ref.current = el; + }, []); + + // context + const appContext: AppContextValue = useRequiredContext(AppContext); + const droppableContext: DroppableContextValue = useRequiredContext( + DroppableContext, + ); -export const zIndexOptions: ZIndexOptions = { - dragging: 5000, - dropAnimating: 4500, -}; - -const getDraggingTransition = ( - shouldAnimateDragMovement: boolean, - dropping: ?DropAnimation, -): string => { - if (dropping) { - return transitions.drop(dropping.duration); - } - if (shouldAnimateDragMovement) { - return transitions.snap; - } - return transitions.fluid; -}; - -const getDraggingOpacity = ( - isCombining: boolean, - isDropAnimating: boolean, -): ?number => { - // if not combining: no not impact opacity - if (!isCombining) { - return null; - } - - return isDropAnimating ? combine.opacity.drop : combine.opacity.combining; -}; + // props + const { + // ownProps + children, + draggableId, + isDragDisabled, + + // mapProps + dragging, + secondary, + + // dispatchProps + moveUp: moveUpAction, + move: moveAction, + drop: dropAction, + moveDown: moveDownAction, + moveRight: moveRightAction, + moveLeft: moveLeftAction, + moveByWindowScroll: moveByWindowScrollAction, + lift: liftAction, + dropAnimationFinished: dropAnimationFinishedAction, + } = props; + + const onLift = useCallback( + () => (options: { + clientSelection: Position, + movementMode: MovementMode, + }) => { + timings.start('LIFT'); + const el: ?HTMLElement = ref.current; + invariant(el); + invariant(!isDragDisabled, 'Cannot lift a Draggable when it is disabled'); + const { clientSelection, movementMode } = options; + + liftAction({ + id: draggableId, + clientSelection, + movementMode, + }); + timings.finish('LIFT'); + }, + [draggableId, isDragDisabled, liftAction], + ); -const getShouldDraggingAnimate = (dragging: DraggingMapProps): boolean => { - if (dragging.forceShouldAnimate != null) { - return dragging.forceShouldAnimate; - } - return dragging.mode === 'SNAP'; -}; - -function getDraggingStyle(dragging: DraggingMapProps): DraggingStyle { - const dimension: DraggableDimension = dragging.dimension; - const box: BoxModel = dimension.client; - const { offset, combineWith, dropping } = dragging; - - const isCombining: boolean = Boolean(combineWith); - - const shouldAnimate: boolean = getShouldDraggingAnimate(dragging); - const isDropAnimating: boolean = Boolean(dropping); - - const transform: ?string = isDropAnimating - ? transforms.drop(offset, isCombining) - : transforms.moveTo(offset); - - const style: DraggingStyle = { - // ## Placement - position: 'fixed', - // As we are applying the margins we need to align to the start of the marginBox - top: box.marginBox.top, - left: box.marginBox.left, - - // ## Sizing - // Locking these down as pulling the node out of the DOM could cause it to change size - boxSizing: 'border-box', - width: box.borderBox.width, - height: box.borderBox.height, - - // ## Movement - // Opting out of the standard css transition for the dragging item - transition: getDraggingTransition(shouldAnimate, dropping), - transform, - opacity: getDraggingOpacity(isCombining, isDropAnimating), - // ## Layering - zIndex: isDropAnimating - ? zIndexOptions.dropAnimating - : zIndexOptions.dragging, - - // ## Blocking any pointer events on the dragging or dropping item - // global styles on cover while dragging - pointerEvents: 'none', - }; - return style; -} + const callbacks: DragHandleCallbacks = useMemo( + () => ({ + onLift, + onMove: (clientSelection: Position) => + moveAction({ client: clientSelection }), + onDrop: () => dropAction({ reason: 'DROP' }), + onCancel: () => dropAction({ reason: 'CANCEL' }), + onMoveUp: moveUpAction, + onMoveDown: moveDownAction, + onMoveRight: moveRightAction, + onMoveLeft: moveLeftAction, + onWindowScroll: () => + moveByWindowScrollAction({ + newScroll: getWindowScroll(), + }), + }), + [ + dropAction, + moveAction, + moveByWindowScrollAction, + moveDownAction, + moveLeftAction, + moveRightAction, + moveUpAction, + onLift, + ], + ); + const dragHandleProps: DragHandleProps = useDragHandle(callbacks); -function getSecondaryStyle(secondary: SecondaryMapProps): NotDraggingStyle { - return { - transform: transforms.moveTo(secondary.offset), - // transition style is applied in the head - transition: secondary.shouldAnimateDisplacement ? null : 'none', - }; -} + const onMoveEnd = useCallback( + (event: TransitionEvent) => { + const isDropping: boolean = Boolean(dragging && dragging.dropping); -function Draggable(props: Props) { - const appContext: ?AppContextValue = useContext(AppContext); - invariant( - appContext, - 'Draggable expected a AppContext to be populated. Have you forgot a DragDropContext?', - ); - const droppableContext: ?DroppableContextValue = useContext(DroppableContext); - invariant( - appContext, - 'Draggable expected a DroppableContext to be populated. Have you forgot a Droppable?', - ); + if (!isDropping) { + return; + } - const callbacks: DragHandleCallbacks = {}; - const dragHandleProps: DragHandleProvided = useDragHandle(callbacks); + // There might be other properties on the element that are + // being transitioned. We do not want those to end a drop animation! + if (event.propertyName !== 'transform') { + return; + } - const { dragging, secondary } = props; + dropAnimationFinishedAction(); + }, + [dragging, dropAnimationFinishedAction], + ); const provided: Provided = useMemo(() => { - const style: DraggableStyle = dragging - ? getDraggingStyle(dragging) - : getSecondaryStyle(secondary); + const style: DraggableStyle = getStyle(dragging, secondary); const onTransitionEnd = dragging && dragging.dropping ? onMoveEnd : null; const result: Provided = { @@ -162,10 +167,43 @@ function Draggable(props: Props) { }; return result; - }, [appContext.style, dragHandleProps, dragging, secondary]); + }, [ + appContext.style, + dragHandleProps, + dragging, + onMoveEnd, + secondary, + setRef, + ]); + + const snapshot: StateSnapshot = useMemo(() => { + if (dragging) { + return { + isDragging: true, + isDropAnimating: Boolean(dragging.dropping), + dropAnimation: dragging.dropping, + mode: dragging.mode, + draggingOver: dragging.draggingOver, + combineWith: dragging.combineWith, + combineTargetFor: null, + }; + } + invariant(secondary, 'Expected dragging or secondary snapshot'); + return { + isDragging: false, + isDropAnimating: false, + dropAnimation: null, + mode: null, + draggingOver: null, + combineTargetFor: secondary.combineTargetFor, + combineWith: null, + }; + }, [dragging, secondary]); + + return children(provided, snapshot); } -export default class Draggable extends React.Component { +export class Draggable extends React.Component { /* eslint-disable react/sort-comp */ callbacks: DragHandleCallbacks; styleContext: string; diff --git a/src/view/draggable/get-style.js b/src/view/draggable/get-style.js new file mode 100644 index 0000000000..349caebfa2 --- /dev/null +++ b/src/view/draggable/get-style.js @@ -0,0 +1,113 @@ +// @flow +import type { BoxModel } from 'css-box-model'; +import invariant from 'tiny-invariant'; +import { combine, transforms, transitions } from '../../animation'; +import type { DraggableDimension } from '../../types'; +import type { + DraggingStyle, + NotDraggingStyle, + ZIndexOptions, + DropAnimation, + SecondaryMapProps, + DraggingMapProps, +} from './draggable-types'; + +export const zIndexOptions: ZIndexOptions = { + dragging: 5000, + dropAnimating: 4500, +}; + +const getDraggingTransition = ( + shouldAnimateDragMovement: boolean, + dropping: ?DropAnimation, +): string => { + if (dropping) { + return transitions.drop(dropping.duration); + } + if (shouldAnimateDragMovement) { + return transitions.snap; + } + return transitions.fluid; +}; + +const getDraggingOpacity = ( + isCombining: boolean, + isDropAnimating: boolean, +): ?number => { + // if not combining: no not impact opacity + if (!isCombining) { + return null; + } + + return isDropAnimating ? combine.opacity.drop : combine.opacity.combining; +}; + +const getShouldDraggingAnimate = (dragging: DraggingMapProps): boolean => { + if (dragging.forceShouldAnimate != null) { + return dragging.forceShouldAnimate; + } + return dragging.mode === 'SNAP'; +}; + +function getDraggingStyle(dragging: DraggingMapProps): DraggingStyle { + const dimension: DraggableDimension = dragging.dimension; + const box: BoxModel = dimension.client; + const { offset, combineWith, dropping } = dragging; + + const isCombining: boolean = Boolean(combineWith); + + const shouldAnimate: boolean = getShouldDraggingAnimate(dragging); + const isDropAnimating: boolean = Boolean(dropping); + + const transform: ?string = isDropAnimating + ? transforms.drop(offset, isCombining) + : transforms.moveTo(offset); + + const style: DraggingStyle = { + // ## Placement + position: 'fixed', + // As we are applying the margins we need to align to the start of the marginBox + top: box.marginBox.top, + left: box.marginBox.left, + + // ## Sizing + // Locking these down as pulling the node out of the DOM could cause it to change size + boxSizing: 'border-box', + width: box.borderBox.width, + height: box.borderBox.height, + + // ## Movement + // Opting out of the standard css transition for the dragging item + transition: getDraggingTransition(shouldAnimate, dropping), + transform, + opacity: getDraggingOpacity(isCombining, isDropAnimating), + // ## Layering + zIndex: isDropAnimating + ? zIndexOptions.dropAnimating + : zIndexOptions.dragging, + + // ## Blocking any pointer events on the dragging or dropping item + // global styles on cover while dragging + pointerEvents: 'none', + }; + return style; +} + +function getSecondaryStyle(secondary: SecondaryMapProps): NotDraggingStyle { + return { + transform: transforms.moveTo(secondary.offset), + // transition style is applied in the head + transition: secondary.shouldAnimateDisplacement ? null : 'none', + }; +} + +export default function getStyle( + dragging: ?DraggingMapProps, + secondary: ?SecondaryMapProps, +) { + if (dragging) { + return getDraggingStyle(dragging); + } + invariant(secondary, 'expect either dragging or secondary to exist'); + return getSecondaryStyle(secondary); +} diff --git a/src/view/use-required-context.js b/src/view/use-required-context.js new file mode 100644 index 0000000000..989ee24c85 --- /dev/null +++ b/src/view/use-required-context.js @@ -0,0 +1,9 @@ +// @flow +import { useContext, type Context as ContextType } from 'react'; +import invariant from 'tiny-invariant'; + +export default function useRequiredContext(Context: ContextType): T { + const result: ?T = useContext(Context); + invariant(result, 'Could not find required context'); + return result; +} From da6fa5e3c632d8874fa9b73b9176c70f86019d23 Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Mon, 18 Mar 2019 10:03:31 +1100 Subject: [PATCH 017/117] useConstant cleanup --- src/view/drag-handle/drag-handle-types.js | 3 +-- src/view/drag-handle/use-drag-handle.js | 4 ++++ src/view/draggable/draggable.jsx | 1 + src/view/use-constant.js | 20 ++++++++++++-------- 4 files changed, 18 insertions(+), 10 deletions(-) create mode 100644 src/view/drag-handle/use-drag-handle.js diff --git a/src/view/drag-handle/drag-handle-types.js b/src/view/drag-handle/drag-handle-types.js index 5dab4efabb..0cb29fc18d 100644 --- a/src/view/drag-handle/drag-handle-types.js +++ b/src/view/drag-handle/drag-handle-types.js @@ -44,7 +44,7 @@ export type DragHandleProps = {| onDragStart: (event: DragEvent) => void, |}; -export type Props = {| +export type Args = {| draggableId: DraggableId, // callbacks provided by the draggable callbacks: Callbacks, @@ -59,5 +59,4 @@ export type Props = {| canDragInteractiveElements: boolean, // whether force touch interactions should be respected getShouldRespectForceTouch: () => boolean, - children: (?DragHandleProps) => Node, |}; diff --git a/src/view/drag-handle/use-drag-handle.js b/src/view/drag-handle/use-drag-handle.js new file mode 100644 index 0000000000..9ded0a097b --- /dev/null +++ b/src/view/drag-handle/use-drag-handle.js @@ -0,0 +1,4 @@ +// @flow +import type { Args, DragHandleProps } from './drag-handle-types'; + +export default function useDragHandle(args: Args): DragHandleProps {} diff --git a/src/view/draggable/draggable.jsx b/src/view/draggable/draggable.jsx index 5633071af3..afe902eb26 100644 --- a/src/view/draggable/draggable.jsx +++ b/src/view/draggable/draggable.jsx @@ -11,6 +11,7 @@ import PropTypes from 'prop-types'; import memoizeOne from 'memoize-one'; import invariant from 'tiny-invariant'; import getStyle from './get-style'; +import useDragHandle from './use-drag-handle'; import type { DraggableDimension, DroppableId, diff --git a/src/view/use-constant.js b/src/view/use-constant.js index 246ff48a6d..54cf4bee8f 100644 --- a/src/view/use-constant.js +++ b/src/view/use-constant.js @@ -1,20 +1,24 @@ // @flow import { useRef } from 'react'; -import { warning } from '../dev-warning'; +// Using an object to hold the result incase the result is falsy +type Result = { + value: T, +}; + +// Similiar to useMemo(() => T, []), but not subject to memoization purging export function useConstant(fn: () => T): T { - const bucket = useRef(null); + const bucket = useRef>(null); if (bucket.current) { - return bucket.current; + return bucket.current.value; } - bucket.current = fn(); + bucket.current = { + value: fn(), + }; - if (!bucket.current) { - warning('Expected constantFn to be truthy'); - } - return bucket.current; + return bucket.current.value; } export function useConstantFn(getter: GetterFn): GetterFn { From ef76da613cd0ed5d6e90c2a330dc31f251107864 Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Mon, 18 Mar 2019 10:17:48 +1100 Subject: [PATCH 018/117] drag handle starting --- src/view/drag-handle/index.js | 2 - src/view/draggable/draggable.jsx | 72 +++++++++++++++---- .../drag-handle-types.js | 2 +- .../drag-handle.jsx | 0 src/view/use-drag-handle/index.js | 2 + .../sensor/create-keyboard-sensor.js | 0 .../sensor/create-mouse-sensor.js | 0 .../sensor/create-touch-sensor.js | 0 .../sensor/sensor-types.js | 0 .../use-drag-handle.js | 0 .../util/bind-events.js | 0 .../util/create-event-marshal.js | 0 .../util/create-post-drag-event-preventer.js | 0 .../util/create-scheduler.js | 0 .../util/event-types.js | 0 .../util/focus-retainer.js | 0 .../util/get-drag-handle-ref.js | 0 .../is-sloppy-click-threshold-exceeded.js | 0 .../util/prevent-standard-key-events.js | 0 .../util/should-allow-dragging-from-target.js | 0 .../supported-page-visibility-event-name.js | 0 21 files changed, 63 insertions(+), 15 deletions(-) delete mode 100644 src/view/drag-handle/index.js rename src/view/{drag-handle => use-drag-handle}/drag-handle-types.js (97%) rename src/view/{drag-handle => use-drag-handle}/drag-handle.jsx (100%) create mode 100644 src/view/use-drag-handle/index.js rename src/view/{drag-handle => use-drag-handle}/sensor/create-keyboard-sensor.js (100%) rename src/view/{drag-handle => use-drag-handle}/sensor/create-mouse-sensor.js (100%) rename src/view/{drag-handle => use-drag-handle}/sensor/create-touch-sensor.js (100%) rename src/view/{drag-handle => use-drag-handle}/sensor/sensor-types.js (100%) rename src/view/{drag-handle => use-drag-handle}/use-drag-handle.js (100%) rename src/view/{drag-handle => use-drag-handle}/util/bind-events.js (100%) rename src/view/{drag-handle => use-drag-handle}/util/create-event-marshal.js (100%) rename src/view/{drag-handle => use-drag-handle}/util/create-post-drag-event-preventer.js (100%) rename src/view/{drag-handle => use-drag-handle}/util/create-scheduler.js (100%) rename src/view/{drag-handle => use-drag-handle}/util/event-types.js (100%) rename src/view/{drag-handle => use-drag-handle}/util/focus-retainer.js (100%) rename src/view/{drag-handle => use-drag-handle}/util/get-drag-handle-ref.js (100%) rename src/view/{drag-handle => use-drag-handle}/util/is-sloppy-click-threshold-exceeded.js (100%) rename src/view/{drag-handle => use-drag-handle}/util/prevent-standard-key-events.js (100%) rename src/view/{drag-handle => use-drag-handle}/util/should-allow-dragging-from-target.js (100%) rename src/view/{drag-handle => use-drag-handle}/util/supported-page-visibility-event-name.js (100%) diff --git a/src/view/drag-handle/index.js b/src/view/drag-handle/index.js deleted file mode 100644 index 9fd0d90bc8..0000000000 --- a/src/view/drag-handle/index.js +++ /dev/null @@ -1,2 +0,0 @@ -// @flow -export { default } from './drag-handle'; diff --git a/src/view/draggable/draggable.jsx b/src/view/draggable/draggable.jsx index afe902eb26..c62c19dc25 100644 --- a/src/view/draggable/draggable.jsx +++ b/src/view/draggable/draggable.jsx @@ -11,19 +11,14 @@ import PropTypes from 'prop-types'; import memoizeOne from 'memoize-one'; import invariant from 'tiny-invariant'; import getStyle from './get-style'; -import useDragHandle from './use-drag-handle'; -import type { - DraggableDimension, - DroppableId, - MovementMode, - TypeId, -} from '../../types'; -import DraggableDimensionPublisher from '../draggable-dimension-publisher'; -import DragHandle from '../drag-handle'; +import useDragHandle from '../use-drag-handle/use-drag-handle'; import type { + Args as DragHandleArgs, DragHandleProps, - Callbacks as DragHandleCallbacks, -} from '../drag-handle/drag-handle-types'; +} from '../use-drag-handle/drag-handle-types'; +import type { DroppableId, MovementMode, TypeId } from '../../types'; +import DraggableDimensionPublisher from '../draggable-dimension-publisher'; + import { droppableIdKey, styleKey, droppableTypeKey } from '../context-keys'; import * as timings from '../../debug/timings'; import type { @@ -54,6 +49,7 @@ export default function Draggable(props: Props) { const setRef = useCallback((el: ?HTMLElement) => { ref.current = el; }, []); + const getRef = useCallback((): ?HTMLElement => ref.current, []); // context const appContext: AppContextValue = useRequiredContext(AppContext); @@ -67,6 +63,9 @@ export default function Draggable(props: Props) { children, draggableId, isDragDisabled, + shouldRespectForceTouch, + disableInteractiveElementBlocking: canDragInteractiveElements, + index, // mapProps dragging, @@ -84,6 +83,28 @@ export default function Draggable(props: Props) { dropAnimationFinished: dropAnimationFinishedAction, } = props; + // The dimension publisher + const publisherArgs: DimensionPublisherArgs = useMemo( + () => ({ + draggableId, + droppableId: droppableContext.droppableId, + type: droppableContext.type, + index, + getDraggableRef: getRef, + }), + [ + draggableId, + droppableContext.droppableId, + droppableContext.type, + getRef, + index, + ], + ); + + useDraggableDimensionPublisher(publisherArgs); + + // The Drag handle + const onLift = useCallback( () => (options: { clientSelection: Position, @@ -132,7 +153,34 @@ export default function Draggable(props: Props) { onLift, ], ); - const dragHandleProps: DragHandleProps = useDragHandle(callbacks); + + const isDragging: boolean = Boolean(dragging); + const isDropAnimating: boolean = Boolean(dragging && dragging.dropping); + + const dragHandleArgs: DragHandleArgs = useMemo( + () => ({ + draggableId, + isDragging, + isDropAnimating, + isEnabled: !isDragDisabled, + callbacks, + getDraggableRef: getRef, + shouldRespectForceTouch, + canDragInteractiveElements, + }), + [ + callbacks, + canDragInteractiveElements, + draggableId, + getRef, + isDragDisabled, + isDragging, + isDropAnimating, + shouldRespectForceTouch, + ], + ); + + const dragHandleProps: DragHandleProps = useDragHandle(dragHandleArgs); const onMoveEnd = useCallback( (event: TransitionEvent) => { diff --git a/src/view/drag-handle/drag-handle-types.js b/src/view/use-drag-handle/drag-handle-types.js similarity index 97% rename from src/view/drag-handle/drag-handle-types.js rename to src/view/use-drag-handle/drag-handle-types.js index 0cb29fc18d..8d86c9ebf8 100644 --- a/src/view/drag-handle/drag-handle-types.js +++ b/src/view/use-drag-handle/drag-handle-types.js @@ -58,5 +58,5 @@ export type Args = {| // whether interactive elements should be permitted to start a drag canDragInteractiveElements: boolean, // whether force touch interactions should be respected - getShouldRespectForceTouch: () => boolean, + shouldRespectForceTouch: boolean, |}; diff --git a/src/view/drag-handle/drag-handle.jsx b/src/view/use-drag-handle/drag-handle.jsx similarity index 100% rename from src/view/drag-handle/drag-handle.jsx rename to src/view/use-drag-handle/drag-handle.jsx diff --git a/src/view/use-drag-handle/index.js b/src/view/use-drag-handle/index.js new file mode 100644 index 0000000000..56db94fce8 --- /dev/null +++ b/src/view/use-drag-handle/index.js @@ -0,0 +1,2 @@ +// @flow +export { default } from './use-drag-handle'; diff --git a/src/view/drag-handle/sensor/create-keyboard-sensor.js b/src/view/use-drag-handle/sensor/create-keyboard-sensor.js similarity index 100% rename from src/view/drag-handle/sensor/create-keyboard-sensor.js rename to src/view/use-drag-handle/sensor/create-keyboard-sensor.js diff --git a/src/view/drag-handle/sensor/create-mouse-sensor.js b/src/view/use-drag-handle/sensor/create-mouse-sensor.js similarity index 100% rename from src/view/drag-handle/sensor/create-mouse-sensor.js rename to src/view/use-drag-handle/sensor/create-mouse-sensor.js diff --git a/src/view/drag-handle/sensor/create-touch-sensor.js b/src/view/use-drag-handle/sensor/create-touch-sensor.js similarity index 100% rename from src/view/drag-handle/sensor/create-touch-sensor.js rename to src/view/use-drag-handle/sensor/create-touch-sensor.js diff --git a/src/view/drag-handle/sensor/sensor-types.js b/src/view/use-drag-handle/sensor/sensor-types.js similarity index 100% rename from src/view/drag-handle/sensor/sensor-types.js rename to src/view/use-drag-handle/sensor/sensor-types.js diff --git a/src/view/drag-handle/use-drag-handle.js b/src/view/use-drag-handle/use-drag-handle.js similarity index 100% rename from src/view/drag-handle/use-drag-handle.js rename to src/view/use-drag-handle/use-drag-handle.js diff --git a/src/view/drag-handle/util/bind-events.js b/src/view/use-drag-handle/util/bind-events.js similarity index 100% rename from src/view/drag-handle/util/bind-events.js rename to src/view/use-drag-handle/util/bind-events.js diff --git a/src/view/drag-handle/util/create-event-marshal.js b/src/view/use-drag-handle/util/create-event-marshal.js similarity index 100% rename from src/view/drag-handle/util/create-event-marshal.js rename to src/view/use-drag-handle/util/create-event-marshal.js diff --git a/src/view/drag-handle/util/create-post-drag-event-preventer.js b/src/view/use-drag-handle/util/create-post-drag-event-preventer.js similarity index 100% rename from src/view/drag-handle/util/create-post-drag-event-preventer.js rename to src/view/use-drag-handle/util/create-post-drag-event-preventer.js diff --git a/src/view/drag-handle/util/create-scheduler.js b/src/view/use-drag-handle/util/create-scheduler.js similarity index 100% rename from src/view/drag-handle/util/create-scheduler.js rename to src/view/use-drag-handle/util/create-scheduler.js diff --git a/src/view/drag-handle/util/event-types.js b/src/view/use-drag-handle/util/event-types.js similarity index 100% rename from src/view/drag-handle/util/event-types.js rename to src/view/use-drag-handle/util/event-types.js diff --git a/src/view/drag-handle/util/focus-retainer.js b/src/view/use-drag-handle/util/focus-retainer.js similarity index 100% rename from src/view/drag-handle/util/focus-retainer.js rename to src/view/use-drag-handle/util/focus-retainer.js diff --git a/src/view/drag-handle/util/get-drag-handle-ref.js b/src/view/use-drag-handle/util/get-drag-handle-ref.js similarity index 100% rename from src/view/drag-handle/util/get-drag-handle-ref.js rename to src/view/use-drag-handle/util/get-drag-handle-ref.js diff --git a/src/view/drag-handle/util/is-sloppy-click-threshold-exceeded.js b/src/view/use-drag-handle/util/is-sloppy-click-threshold-exceeded.js similarity index 100% rename from src/view/drag-handle/util/is-sloppy-click-threshold-exceeded.js rename to src/view/use-drag-handle/util/is-sloppy-click-threshold-exceeded.js diff --git a/src/view/drag-handle/util/prevent-standard-key-events.js b/src/view/use-drag-handle/util/prevent-standard-key-events.js similarity index 100% rename from src/view/drag-handle/util/prevent-standard-key-events.js rename to src/view/use-drag-handle/util/prevent-standard-key-events.js diff --git a/src/view/drag-handle/util/should-allow-dragging-from-target.js b/src/view/use-drag-handle/util/should-allow-dragging-from-target.js similarity index 100% rename from src/view/drag-handle/util/should-allow-dragging-from-target.js rename to src/view/use-drag-handle/util/should-allow-dragging-from-target.js diff --git a/src/view/drag-handle/util/supported-page-visibility-event-name.js b/src/view/use-drag-handle/util/supported-page-visibility-event-name.js similarity index 100% rename from src/view/drag-handle/util/supported-page-visibility-event-name.js rename to src/view/use-drag-handle/util/supported-page-visibility-event-name.js From 83ca8a10c76aac04ca1caa1080e3565cc886d991 Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Mon, 18 Mar 2019 11:12:14 +1100 Subject: [PATCH 019/117] figuring out drag handles --- .../drag-drop-context/drag-drop-context.jsx | 2 +- src/view/use-constant.js | 2 +- .../sensor/use-mouse-sensor.js | 0 src/view/use-drag-handle/use-drag-handle.js | 41 ++++++++++++++++++- 4 files changed, 42 insertions(+), 3 deletions(-) create mode 100644 src/view/use-drag-handle/sensor/use-mouse-sensor.js diff --git a/src/view/drag-drop-context/drag-drop-context.jsx b/src/view/drag-drop-context/drag-drop-context.jsx index dc813f0b2c..e6045b478d 100644 --- a/src/view/drag-drop-context/drag-drop-context.jsx +++ b/src/view/drag-drop-context/drag-drop-context.jsx @@ -143,7 +143,7 @@ export default function DragDropContext(props: Props) { }), ); - const store: Store = useConstant(() => + const store: Store = useConstant(() => createStore({ dimensionMarshal, styleMarshal, diff --git a/src/view/use-constant.js b/src/view/use-constant.js index 54cf4bee8f..7f22eed25d 100644 --- a/src/view/use-constant.js +++ b/src/view/use-constant.js @@ -6,7 +6,7 @@ type Result = { value: T, }; -// Similiar to useMemo(() => T, []), but not subject to memoization purging +// Similar to useMemo(() => T, []), but not subject to memoization purging export function useConstant(fn: () => T): T { const bucket = useRef>(null); diff --git a/src/view/use-drag-handle/sensor/use-mouse-sensor.js b/src/view/use-drag-handle/sensor/use-mouse-sensor.js new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/view/use-drag-handle/use-drag-handle.js b/src/view/use-drag-handle/use-drag-handle.js index 9ded0a097b..3b74a1c84e 100644 --- a/src/view/use-drag-handle/use-drag-handle.js +++ b/src/view/use-drag-handle/use-drag-handle.js @@ -1,4 +1,43 @@ // @flow +import { useState, useRef, useEffect, useLayoutEffect } from 'react'; +import { useConstantFn } from '../use-constant'; import type { Args, DragHandleProps } from './drag-handle-types'; +import createKeyboardSensor from './sensor/create-keyboard-sensor'; +import createTouchSensor from './sensor/create-touch-sensor'; -export default function useDragHandle(args: Args): DragHandleProps {} +export default function useDragHandle(args: Args): DragHandleProps { + const isFocusedRef = useRef(false); + const setFocus = useConstantFn((value: boolean) => { + isFocusedRef.current = value; + }); + + const createArgs = { + setFocus, + }; + + const mouse: MouseSensor = useConstant(() => createMouseSensor(createArgs)); + const keyboard: KeyboardSensor = useConstant(() => createKeyboardSensor()); + const touch: TouchSensor = useConstant(() => createTouchSensor(createArgs)); + const sensors: Sensor[] = useConstant(() => [mouse, keyboard, touch]); + + // TODO: focus retention + useLayoutEffect(() => {}); + + // Cleanup any sensors + useLayoutEffect(() => { + () => { + sensors.forEach((sensor: Sensor) => { + // kill the current drag and fire a cancel event if + const wasDragging: boolean = sensor.isDragging(); + + sensor.unmount(); + // cancel if drag was occurring + if (wasDragging) { + callbacks.onCancel(); + } + }); + }; + // sensors is constant + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); +} From 1313ff33de956389e03ae9395793899aac57022f Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Mon, 18 Mar 2019 14:13:42 +1100 Subject: [PATCH 020/117] wip --- .../draggable-dimension-publisher/index.js | 2 - src/view/draggable/use-draggable.js | 244 ++++++++++++++++++ src/view/use-drag-handle/drag-handle-types.js | 3 +- .../use-drag-handle/sensor/sensor-types.js | 2 + .../sensor/use-mouse-sensor.js | 0 src/view/use-drag-handle/use-drag-handle.js | 164 +++++++++++- .../draggable-dimension-publisher.jsx | 0 .../index.js | 2 + .../use-draggable-dimension-publisher.js | 90 +++++++ src/view/use-previous.js | 14 + 10 files changed, 505 insertions(+), 16 deletions(-) delete mode 100644 src/view/draggable-dimension-publisher/index.js create mode 100644 src/view/draggable/use-draggable.js delete mode 100644 src/view/use-drag-handle/sensor/use-mouse-sensor.js rename src/view/{draggable-dimension-publisher => use-draggable-dimension-publisher}/draggable-dimension-publisher.jsx (100%) create mode 100644 src/view/use-draggable-dimension-publisher/index.js create mode 100644 src/view/use-draggable-dimension-publisher/use-draggable-dimension-publisher.js create mode 100644 src/view/use-previous.js diff --git a/src/view/draggable-dimension-publisher/index.js b/src/view/draggable-dimension-publisher/index.js deleted file mode 100644 index 0f07a791a4..0000000000 --- a/src/view/draggable-dimension-publisher/index.js +++ /dev/null @@ -1,2 +0,0 @@ -// @flow -export { default } from './draggable-dimension-publisher'; diff --git a/src/view/draggable/use-draggable.js b/src/view/draggable/use-draggable.js new file mode 100644 index 0000000000..e5bc6a2709 --- /dev/null +++ b/src/view/draggable/use-draggable.js @@ -0,0 +1,244 @@ +// @flow +import { useMemo, useRef, useCallback } from 'react'; +import { type Position } from 'css-box-model'; +import invariant from 'tiny-invariant'; +import getStyle from './get-style'; +import useDragHandle from '../use-drag-handle/use-drag-handle'; +import type { + Args as DragHandleArgs, + Callbacks as DragHandleCallbacks, + DragHandleProps, +} from '../use-drag-handle/drag-handle-types'; +import type { MovementMode } from '../../types'; +import useDraggableDimensionPublisher, { + type Args as DimensionPublisherArgs, +} from '../use-draggable-dimension-publisher/use-draggable-dimension-publisher'; +import * as timings from '../../debug/timings'; +import type { + Props, + Provided, + StateSnapshot, + DraggableStyle, +} from './draggable-types'; +import getWindowScroll from '../window/get-window-scroll'; +// import throwIfRefIsInvalid from '../throw-if-invalid-inner-ref'; +// import checkOwnProps from './check-own-props'; +import AppContext, { type AppContextValue } from '../context/app-context'; +import DroppableContext, { + type DroppableContextValue, +} from '../context/droppable-context'; +import useRequiredContext from '../use-required-context'; + +export default function Draggable(props: Props) { + // instance members + const ref = useRef(null); + const setRef = useCallback((el: ?HTMLElement) => { + ref.current = el; + }, []); + const getRef = useCallback((): ?HTMLElement => ref.current, []); + + // context + const appContext: AppContextValue = useRequiredContext(AppContext); + const droppableContext: DroppableContextValue = useRequiredContext( + DroppableContext, + ); + + // props + const { + // ownProps + children, + draggableId, + isDragDisabled, + shouldRespectForceTouch, + disableInteractiveElementBlocking: canDragInteractiveElements, + index, + + // mapProps + dragging, + secondary, + + // dispatchProps + moveUp: moveUpAction, + move: moveAction, + drop: dropAction, + moveDown: moveDownAction, + moveRight: moveRightAction, + moveLeft: moveLeftAction, + moveByWindowScroll: moveByWindowScrollAction, + lift: liftAction, + dropAnimationFinished: dropAnimationFinishedAction, + } = props; + + // The dimension publisher + const publisherArgs: DimensionPublisherArgs = useMemo( + () => ({ + draggableId, + droppableId: droppableContext.droppableId, + type: droppableContext.type, + index, + getDraggableRef: getRef, + }), + [ + draggableId, + droppableContext.droppableId, + droppableContext.type, + getRef, + index, + ], + ); + + useDraggableDimensionPublisher(publisherArgs); + + // The Drag handle + + const onLift = useCallback( + () => (options: { + clientSelection: Position, + movementMode: MovementMode, + }) => { + timings.start('LIFT'); + const el: ?HTMLElement = ref.current; + invariant(el); + invariant(!isDragDisabled, 'Cannot lift a Draggable when it is disabled'); + const { clientSelection, movementMode } = options; + + liftAction({ + id: draggableId, + clientSelection, + movementMode, + }); + timings.finish('LIFT'); + }, + [draggableId, isDragDisabled, liftAction], + ); + + const getShouldRespectForceTouch = useCallback( + () => shouldRespectForceTouch, + [shouldRespectForceTouch], + ); + + const callbacks: DragHandleCallbacks = useMemo( + () => ({ + onLift, + onMove: (clientSelection: Position) => + moveAction({ client: clientSelection }), + onDrop: () => dropAction({ reason: 'DROP' }), + onCancel: () => dropAction({ reason: 'CANCEL' }), + onMoveUp: moveUpAction, + onMoveDown: moveDownAction, + onMoveRight: moveRightAction, + onMoveLeft: moveLeftAction, + onWindowScroll: () => + moveByWindowScrollAction({ + newScroll: getWindowScroll(), + }), + }), + [ + dropAction, + moveAction, + moveByWindowScrollAction, + moveDownAction, + moveLeftAction, + moveRightAction, + moveUpAction, + onLift, + ], + ); + + const isDragging: boolean = Boolean(dragging); + const isDropAnimating: boolean = Boolean(dragging && dragging.dropping); + + const dragHandleArgs: DragHandleArgs = useMemo( + () => ({ + draggableId, + isDragging, + isDropAnimating, + isEnabled: !isDragDisabled, + callbacks, + getDraggableRef: getRef, + canDragInteractiveElements, + getShouldRespectForceTouch, + }), + [ + callbacks, + canDragInteractiveElements, + draggableId, + getRef, + getShouldRespectForceTouch, + isDragDisabled, + isDragging, + isDropAnimating, + ], + ); + + const dragHandleProps: DragHandleProps = useDragHandle(dragHandleArgs); + + const onMoveEnd = useCallback( + (event: TransitionEvent) => { + const isDropping: boolean = Boolean(dragging && dragging.dropping); + + if (!isDropping) { + return; + } + + // There might be other properties on the element that are + // being transitioned. We do not want those to end a drop animation! + if (event.propertyName !== 'transform') { + return; + } + + dropAnimationFinishedAction(); + }, + [dragging, dropAnimationFinishedAction], + ); + + const provided: Provided = useMemo(() => { + const style: DraggableStyle = getStyle(dragging, secondary); + const onTransitionEnd = dragging && dragging.dropping ? onMoveEnd : null; + + const result: Provided = { + innerRef: setRef, + draggableProps: { + 'data-react-beautiful-dnd-draggable': appContext.style, + style, + onTransitionEnd, + }, + dragHandleProps, + }; + + return result; + }, [ + appContext.style, + dragHandleProps, + dragging, + onMoveEnd, + secondary, + setRef, + ]); + + const snapshot: StateSnapshot = useMemo(() => { + if (dragging) { + return { + isDragging: true, + isDropAnimating: Boolean(dragging.dropping), + dropAnimation: dragging.dropping, + mode: dragging.mode, + draggingOver: dragging.draggingOver, + combineWith: dragging.combineWith, + combineTargetFor: null, + }; + } + invariant(secondary, 'Expected dragging or secondary snapshot'); + return { + isDragging: false, + isDropAnimating: false, + dropAnimation: null, + mode: null, + draggingOver: null, + combineTargetFor: secondary.combineTargetFor, + combineWith: null, + }; + }, [dragging, secondary]); + + return children(provided, snapshot); +} diff --git a/src/view/use-drag-handle/drag-handle-types.js b/src/view/use-drag-handle/drag-handle-types.js index 8d86c9ebf8..8bf62be46c 100644 --- a/src/view/use-drag-handle/drag-handle-types.js +++ b/src/view/use-drag-handle/drag-handle-types.js @@ -1,6 +1,5 @@ // @flow import { type Position } from 'css-box-model'; -import { type Node } from 'react'; import type { MovementMode, DraggableId } from '../../types'; export type Callbacks = {| @@ -58,5 +57,5 @@ export type Args = {| // whether interactive elements should be permitted to start a drag canDragInteractiveElements: boolean, // whether force touch interactions should be respected - shouldRespectForceTouch: boolean, + getShouldRespectForceTouch: () => boolean, |}; diff --git a/src/view/use-drag-handle/sensor/sensor-types.js b/src/view/use-drag-handle/sensor/sensor-types.js index a66a6a6e63..1fb635c141 100644 --- a/src/view/use-drag-handle/sensor/sensor-types.js +++ b/src/view/use-drag-handle/sensor/sensor-types.js @@ -35,3 +35,5 @@ export type TouchSensor = {| ...SensorBase, onTouchStart: (event: TouchEvent) => void, |}; + +export type Sensor = MouseSensor | KeyboardSensor | TouchSensor; diff --git a/src/view/use-drag-handle/sensor/use-mouse-sensor.js b/src/view/use-drag-handle/sensor/use-mouse-sensor.js deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/src/view/use-drag-handle/use-drag-handle.js b/src/view/use-drag-handle/use-drag-handle.js index 3b74a1c84e..f0a5fc1da5 100644 --- a/src/view/use-drag-handle/use-drag-handle.js +++ b/src/view/use-drag-handle/use-drag-handle.js @@ -1,37 +1,129 @@ // @flow -import { useState, useRef, useEffect, useLayoutEffect } from 'react'; -import { useConstantFn } from '../use-constant'; +import { useLayoutEffect, useRef } from 'react'; +import type { + Sensor, + CreateSensorArgs, + MouseSensor, + KeyboardSensor, + TouchSensor, +} from './sensor/sensor-types'; import type { Args, DragHandleProps } from './drag-handle-types'; +import { useConstant, useConstantFn } from '../use-constant'; import createKeyboardSensor from './sensor/create-keyboard-sensor'; import createTouchSensor from './sensor/create-touch-sensor'; +import { warning } from '../../dev-warning'; +import shouldAllowDraggingFromTarget from './util/should-allow-dragging-from-target'; +import getWindowFromEl from '../window/get-window-from-el'; +import useRequiredContext from '../use-required-context'; +import AppContext, { type AppContextValue } from '../context/app-context'; +import createMouseSensor from './sensor/create-mouse-sensor'; + +function preventHtml5Dnd(event: DragEvent) { + event.preventDefault(); +} export default function useDragHandle(args: Args): DragHandleProps { + const { canLift, style }: AppContextValue = useRequiredContext(AppContext); + const { + draggableId, + callbacks, + isEnabled, + isDragging, + canDragInteractiveElements, + getShouldRespectForceTouch, + getDraggableRef, + } = args; + + // TODO: things will go bad if getDraggableRef changes + const getWindow = useConstantFn( + (): HTMLElement => getWindowFromEl(getDraggableRef()), + ); + const isFocusedRef = useRef(false); - const setFocus = useConstantFn((value: boolean) => { - isFocusedRef.current = value; + const onFocus = useConstantFn(() => { + isFocusedRef.current = true; + }); + const onBlur = useConstantFn(() => { + isFocusedRef.current = false; + }); + + let sensors: Sensor[]; + + const isAnySensorCapturing = useConstantFn(() => + sensors.some((sensor: Sensor): boolean => sensor.isCapturing()), + ); + + // TODO: this will break if draggableId changes + const canStartCapturing = useConstantFn((event: Event) => { + // this might be before a drag has started - isolated to this element + if (isAnySensorCapturing()) { + return false; + } + + // this will check if anything else in the system is dragging + if (!canLift(draggableId)) { + return false; + } + + // check if we are dragging an interactive element + return shouldAllowDraggingFromTarget(event, canDragInteractiveElements); }); - const createArgs = { - setFocus, - }; + const createArgs: CreateSensorArgs = useConstant(() => ({ + callbacks, + getDraggableRef, + canStartCapturing, + getWindow, + getShouldRespectForceTouch, + })); const mouse: MouseSensor = useConstant(() => createMouseSensor(createArgs)); - const keyboard: KeyboardSensor = useConstant(() => createKeyboardSensor()); + const keyboard: KeyboardSensor = useConstant(() => + createKeyboardSensor(createArgs), + ); const touch: TouchSensor = useConstant(() => createTouchSensor(createArgs)); - const sensors: Sensor[] = useConstant(() => [mouse, keyboard, touch]); + sensors = useConstant(() => [mouse, keyboard, touch]); + + const onKeyDown = useConstantFn((event: KeyboardEvent) => { + // let the other sensors deal with it + if (mouse.isCapturing() || touch.isCapturing()) { + return; + } + + keyboard.onKeyDown(event); + }); + + const onMouseDown = useConstantFn((event: MouseEvent) => { + // let the other sensors deal with it + if (keyboard.isCapturing() || touch.isCapturing()) { + return; + } + + mouse.onMouseDown(event); + }); + + const onTouchStart = useConstantFn((event: TouchEvent) => { + // let the keyboard sensor deal with it + if (mouse.isCapturing() || keyboard.isCapturing()) { + return; + } + + touch.onTouchStart(event); + }); // TODO: focus retention useLayoutEffect(() => {}); - // Cleanup any sensors + // Cleanup any capturing sensors when unmounting useLayoutEffect(() => { - () => { + // Just a cleanup function + return () => { sensors.forEach((sensor: Sensor) => { // kill the current drag and fire a cancel event if const wasDragging: boolean = sensor.isDragging(); sensor.unmount(); - // cancel if drag was occurring + // Cancel if drag was occurring if (wasDragging) { callbacks.onCancel(); } @@ -40,4 +132,52 @@ export default function useDragHandle(args: Args): DragHandleProps { // sensors is constant // eslint-disable-next-line react-hooks/exhaustive-deps }, []); + + // Checking for disabled changes during a drag + useLayoutEffect(() => { + sensors.forEach((sensor: Sensor) => { + if (!sensor.isCapturing()) { + return; + } + const wasDragging: boolean = sensor.isDragging(); + sensor.kill(); + + // It is fine for a draggable to be disabled while a drag is pending + if (wasDragging) { + warning( + 'You have disabled dragging on a Draggable while it was dragging. The drag has been cancelled', + ); + callbacks.onCancel(); + } + }); + }, [callbacks, isEnabled, sensors]); + + // Drag aborted elsewhere in application + useLayoutEffect(() => { + if (isDragging) { + return; + } + + sensors.forEach((sensor: Sensor) => { + if (sensor.isCapturing()) { + sensor.kill(); + } + }); + }, [isDragging, sensors]); + + const props: DragHandleProps = useConstant(() => ({ + onMouseDown, + onKeyDown, + onTouchStart, + onFocus, + onBlur, + tabIndex: 0, + 'data-react-beautiful-dnd-drag-handle': style, + // English default. Consumers are welcome to add their own start instruction + 'aria-roledescription': 'Draggable item. Press space bar to lift', + draggable: false, + onDragStart: preventHtml5Dnd, + })); + + return props; } diff --git a/src/view/draggable-dimension-publisher/draggable-dimension-publisher.jsx b/src/view/use-draggable-dimension-publisher/draggable-dimension-publisher.jsx similarity index 100% rename from src/view/draggable-dimension-publisher/draggable-dimension-publisher.jsx rename to src/view/use-draggable-dimension-publisher/draggable-dimension-publisher.jsx diff --git a/src/view/use-draggable-dimension-publisher/index.js b/src/view/use-draggable-dimension-publisher/index.js new file mode 100644 index 0000000000..04c9114c8b --- /dev/null +++ b/src/view/use-draggable-dimension-publisher/index.js @@ -0,0 +1,2 @@ +// @flow +export { default } from './use-draggable-dimension-publisher'; diff --git a/src/view/use-draggable-dimension-publisher/use-draggable-dimension-publisher.js b/src/view/use-draggable-dimension-publisher/use-draggable-dimension-publisher.js new file mode 100644 index 0000000000..d47a2215e2 --- /dev/null +++ b/src/view/use-draggable-dimension-publisher/use-draggable-dimension-publisher.js @@ -0,0 +1,90 @@ +// @flow +import { useMemo, useCallback, useLayoutEffect } from 'react'; +import { + calculateBox, + withScroll, + type BoxModel, + type Position, +} from 'css-box-model'; +import invariant from 'tiny-invariant'; +import type { + DraggableDescriptor, + DraggableDimension, + Placeholder, + DraggableId, + DroppableId, + TypeId, +} from '../../types'; +import type { DimensionMarshal } from '../../state/dimension-marshal/dimension-marshal-types'; +import { origin } from '../../state/position'; +import useRequiredContext from '../use-required-context'; +import AppContext, { type AppContextValue } from '../context/app-context'; + +export type Args = {| + draggableId: DraggableId, + droppableId: DroppableId, + type: TypeId, + index: number, + getDraggableRef: () => ?HTMLElement, +|}; + +function getDimension( + descriptor: DraggableDescriptor, + el: HTMLElement, + windowScroll?: Position = origin, +): DraggableDimension { + const computedStyles: CSSStyleDeclaration = window.getComputedStyle(el); + const borderBox: ClientRect = el.getBoundingClientRect(); + const client: BoxModel = calculateBox(borderBox, computedStyles); + const page: BoxModel = withScroll(client, windowScroll); + + const placeholder: Placeholder = { + client, + tagName: el.tagName.toLowerCase(), + display: computedStyles.display, + }; + const displaceBy: Position = { + x: client.marginBox.width, + y: client.marginBox.height, + }; + + const dimension: DraggableDimension = { + descriptor, + placeholder, + displaceBy, + client, + page, + }; + + return dimension; +} + +export default function useDraggableDimensionPublisher(args: Args) { + const { draggableId, droppableId, type, index, getDraggableRef } = args; + const appContext: AppContextValue = useRequiredContext(AppContext); + const marshal: DimensionMarshal = appContext.marshal; + + const descriptor: DraggableDescriptor = useMemo( + () => ({ + id: draggableId, + droppableId, + type, + index, + }), + [draggableId, droppableId, index, type], + ); + + const makeDimension = useCallback( + (windowScroll?: Position): DraggableDimension => { + const el: ?HTMLElement = getDraggableRef(); + invariant(el, 'Cannot get dimension when no ref is set'); + return getDimension(descriptor, el, windowScroll); + }, + [descriptor, getDraggableRef], + ); + + useLayoutEffect(() => { + marshal.registerDraggable(descriptor, makeDimension); + return () => marshal.unregisterDraggable(descriptor); + }, [descriptor, makeDimension, marshal]); +} diff --git a/src/view/use-previous.js b/src/view/use-previous.js new file mode 100644 index 0000000000..6eb0dfc43a --- /dev/null +++ b/src/view/use-previous.js @@ -0,0 +1,14 @@ +// @flow +import { useRef, useEffect } from 'react'; + +export default function usePrevious(current: T): T { + const ref = useRef(current); + + // will be updated on the next render + useEffect(() => { + ref.current = current; + }); + + // return the existing current (pre render) + return ref.current; +} From 00656f0be66b03d15f2120154684f224818547e4 Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Mon, 18 Mar 2019 14:37:36 +1100 Subject: [PATCH 021/117] early signal for size --- .size-snapshot.json | 24 +- package.json | 2 +- rollup.config.js | 13 +- .../drag-drop-context/drag-drop-context.jsx | 149 --------- src/view/draggable/draggable.jsx | 292 +----------------- src/view/draggable/use-draggable.js | 244 --------------- src/view/use-drag-handle/drag-handle.jsx | 268 ---------------- .../use-style-marshal/use-style-marshal.js | 2 +- 8 files changed, 39 insertions(+), 955 deletions(-) delete mode 100644 src/view/draggable/use-draggable.js delete mode 100644 src/view/use-drag-handle/drag-handle.jsx diff --git a/.size-snapshot.json b/.size-snapshot.json index ec67a8043a..dd9ff666a6 100644 --- a/.size-snapshot.json +++ b/.size-snapshot.json @@ -1,25 +1,25 @@ { "dist/react-beautiful-dnd.js": { - "bundled": 355996, - "minified": 138322, - "gzipped": 40545 + "bundled": 340609, + "minified": 129409, + "gzipped": 38525 }, "dist/react-beautiful-dnd.min.js": { - "bundled": 302438, - "minified": 113232, - "gzipped": 32713 + "bundled": 289643, + "minified": 106335, + "gzipped": 31391 }, "dist/react-beautiful-dnd.esm.js": { - "bundled": 236773, - "minified": 125151, - "gzipped": 31183, + "bundled": 223370, + "minified": 114918, + "gzipped": 29519, "treeshaked": { "rollup": { - "code": 85436, - "import_statements": 832 + "code": 27004, + "import_statements": 646 }, "webpack": { - "code": 88124 + "code": 30020 } } } diff --git a/package.json b/package.json index 48e2ff292c..aac8d5ae9c 100644 --- a/package.json +++ b/package.json @@ -132,7 +132,7 @@ "webpack": "^4.29.6" }, "peerDependencies": { - "react": "^16.3.1" + "react": "^16.8.0" }, "license": "apache-2.0", "jest-junit": { diff --git a/rollup.config.js b/rollup.config.js index d7b59f0a1a..b86d918c75 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -36,7 +36,10 @@ const commonjsArgs = { // needed for react-is via react-redux v5.1 // https://stackoverflow.com/questions/50080893/rollup-error-isvalidelementtype-is-not-exported-by-node-modules-react-is-inde/50098540 namedExports: { - 'node_modules/react-is/index.js': ['isValidElementType'], + 'node_modules/react-is/index.js': [ + 'isValidElementType', + 'isContextConsumer', + ], }, }; @@ -50,10 +53,10 @@ export default [ file: 'dist/react-beautiful-dnd.js', format: 'umd', name: 'ReactBeautifulDnd', - globals: { react: 'React' }, + globals: { react: 'React', 'react-dom': 'ReactDOM' }, }, // Only deep dependency required is React - external: ['react'], + external: ['react', 'react-dom'], plugins: [ json(), babel(getBabelOptions({ useESModules: true })), @@ -71,10 +74,10 @@ export default [ file: 'dist/react-beautiful-dnd.min.js', format: 'umd', name: 'ReactBeautifulDnd', - globals: { react: 'React' }, + globals: { react: 'React', 'react-dom': 'ReactDOM' }, }, // Only deep dependency required is React - external: ['react'], + external: ['react', 'react-dom'], plugins: [ json(), babel(getBabelOptions({ useESModules: true })), diff --git a/src/view/drag-drop-context/drag-drop-context.jsx b/src/view/drag-drop-context/drag-drop-context.jsx index e6045b478d..e4b32bf10d 100644 --- a/src/view/drag-drop-context/drag-drop-context.jsx +++ b/src/view/drag-drop-context/drag-drop-context.jsx @@ -178,152 +178,3 @@ export default function DragDropContext(props: Props) { ); } - -// export class DragDropContext extends React.Component { -// /* eslint-disable react/sort-comp */ -// store: Store; -// dimensionMarshal: DimensionMarshal; -// styleMarshal: StyleMarshal; -// autoScroller: AutoScroller; -// announcer: Announcer; -// unsubscribe: Function; - -// constructor(props: Props, context: mixed) { -// super(props, context); - -// // A little setup check for dev -// if (process.env.NODE_ENV !== 'production') { -// invariant( -// typeof props.onDragEnd === 'function', -// 'A DragDropContext requires an onDragEnd function to perform reordering logic', -// ); -// } - -// this.announcer = createAnnouncer(); - -// // create the style marshal -// this.styleMarshal = createStyleMarshal(); - -// this.store = createStore({ -// // Lazy reference to dimension marshal get around circular dependency -// getDimensionMarshal: (): DimensionMarshal => this.dimensionMarshal, -// styleMarshal: this.styleMarshal, -// // This is a function as users are allowed to change their responder functions -// // at any time -// getResponders: (): Responders => ({ -// onBeforeDragStart: this.props.onBeforeDragStart, -// onDragStart: this.props.onDragStart, -// onDragEnd: this.props.onDragEnd, -// onDragUpdate: this.props.onDragUpdate, -// }), -// announce: this.announcer.announce, -// getScroller: () => this.autoScroller, -// }); -// const callbacks: DimensionMarshalCallbacks = bindActionCreators( -// { -// publishWhileDragging, -// updateDroppableScroll, -// updateDroppableIsEnabled, -// updateDroppableIsCombineEnabled, -// collectionStarting, -// }, -// this.store.dispatch, -// ); -// this.dimensionMarshal = createDimensionMarshal(callbacks); - -// this.autoScroller = createAutoScroller({ -// scrollWindow, -// scrollDroppable: this.dimensionMarshal.scrollDroppable, -// ...bindActionCreators( -// { -// move, -// }, -// this.store.dispatch, -// ), -// }); -// } -// // Need to declare childContextTypes without flow -// // https://github.com/brigand/babel-plugin-flow-react-proptypes/issues/22 -// static childContextTypes = { -// [storeKey]: PropTypes.shape({ -// dispatch: PropTypes.func.isRequired, -// subscribe: PropTypes.func.isRequired, -// getState: PropTypes.func.isRequired, -// }).isRequired, -// [dimensionMarshalKey]: PropTypes.object.isRequired, -// [styleKey]: PropTypes.string.isRequired, -// [canLiftKey]: PropTypes.func.isRequired, -// [isMovementAllowedKey]: PropTypes.func.isRequired, -// }; - -// getChildContext(): Context { -// return { -// [storeKey]: this.store, -// [dimensionMarshalKey]: this.dimensionMarshal, -// [styleKey]: this.styleMarshal.styleContext, -// [canLiftKey]: this.canLift, -// [isMovementAllowedKey]: this.getIsMovementAllowed, -// }; -// } - -// // Providing function on the context for drag handles to use to -// // let them know if they can start a drag or not. This is done -// // rather than mapping a prop onto the drag handle so that we -// // do not need to re-render a connected drag handle in order to -// // pull this state off. It would cause a re-render of all items -// // on drag start which is too expensive. -// // This is useful when the user -// canLift = (id: DraggableId) => canStartDrag(this.store.getState(), id); -// getIsMovementAllowed = () => isMovementAllowed(this.store.getState()); - -// componentDidMount() { -// window.addEventListener('error', this.onWindowError); -// this.styleMarshal.mount(); -// this.announcer.mount(); - -// if (process.env.NODE_ENV !== 'production') { -// checkReactVersion(peerDependencies.react, React.version); -// checkDoctype(document); -// } -// } - -// componentDidCatch(error: Error) { -// this.onFatalError(error); - -// // If the failure was due to an invariant failure - then we handle the error -// if (error.message.indexOf('Invariant failed') !== -1) { -// this.setState({}); -// return; -// } - -// // Error is more serious and we throw it -// throw error; -// } - -// componentWillUnmount() { -// window.removeEventListener('error', this.onWindowError); - -// const state: State = this.store.getState(); -// if (state.phase !== 'IDLE') { -// this.store.dispatch(clean()); -// } - -// this.styleMarshal.unmount(); -// this.announcer.unmount(); -// } - -// onFatalError = (error: Error) => { -// printFatalDevError(error); - -// const state: State = this.store.getState(); -// if (state.phase !== 'IDLE') { -// this.store.dispatch(clean()); -// } -// }; - -// onWindowError = (error: Error) => this.onFatalError(error); - -// render() { -// return this.props.children; -// } -// } diff --git a/src/view/draggable/draggable.jsx b/src/view/draggable/draggable.jsx index c62c19dc25..3c9a67568a 100644 --- a/src/view/draggable/draggable.jsx +++ b/src/view/draggable/draggable.jsx @@ -1,42 +1,28 @@ // @flow -import React, { - useMemo, - useRef, - useEffect, - useContext, - useCallback, -} from 'react'; -import { type Position, type BoxModel } from 'css-box-model'; -import PropTypes from 'prop-types'; -import memoizeOne from 'memoize-one'; +import { useMemo, useRef, useCallback } from 'react'; +import { type Position } from 'css-box-model'; import invariant from 'tiny-invariant'; import getStyle from './get-style'; import useDragHandle from '../use-drag-handle/use-drag-handle'; import type { Args as DragHandleArgs, + Callbacks as DragHandleCallbacks, DragHandleProps, } from '../use-drag-handle/drag-handle-types'; -import type { DroppableId, MovementMode, TypeId } from '../../types'; -import DraggableDimensionPublisher from '../draggable-dimension-publisher'; - -import { droppableIdKey, styleKey, droppableTypeKey } from '../context-keys'; +import type { MovementMode } from '../../types'; +import useDraggableDimensionPublisher, { + type Args as DimensionPublisherArgs, +} from '../use-draggable-dimension-publisher/use-draggable-dimension-publisher'; import * as timings from '../../debug/timings'; import type { Props, Provided, StateSnapshot, - DraggingStyle, - NotDraggingStyle, - ZIndexOptions, - DropAnimation, - SecondaryMapProps, - DraggingMapProps, - ChildrenFn, DraggableStyle, } from './draggable-types'; import getWindowScroll from '../window/get-window-scroll'; -import throwIfRefIsInvalid from '../throw-if-invalid-inner-ref'; -import checkOwnProps from './check-own-props'; +// import throwIfRefIsInvalid from '../throw-if-invalid-inner-ref'; +// import checkOwnProps from './check-own-props'; import AppContext, { type AppContextValue } from '../context/app-context'; import DroppableContext, { type DroppableContextValue, @@ -126,6 +112,11 @@ export default function Draggable(props: Props) { [draggableId, isDragDisabled, liftAction], ); + const getShouldRespectForceTouch = useCallback( + () => shouldRespectForceTouch, + [shouldRespectForceTouch], + ); + const callbacks: DragHandleCallbacks = useMemo( () => ({ onLift, @@ -165,18 +156,18 @@ export default function Draggable(props: Props) { isEnabled: !isDragDisabled, callbacks, getDraggableRef: getRef, - shouldRespectForceTouch, canDragInteractiveElements, + getShouldRespectForceTouch, }), [ callbacks, canDragInteractiveElements, draggableId, getRef, + getShouldRespectForceTouch, isDragDisabled, isDragging, isDropAnimating, - shouldRespectForceTouch, ], ); @@ -225,6 +216,7 @@ export default function Draggable(props: Props) { setRef, ]); + // TODO: this could be done in the connected component const snapshot: StateSnapshot = useMemo(() => { if (dragging) { return { @@ -251,253 +243,3 @@ export default function Draggable(props: Props) { return children(provided, snapshot); } - -export class Draggable extends React.Component { - /* eslint-disable react/sort-comp */ - callbacks: DragHandleCallbacks; - styleContext: string; - ref: ?HTMLElement = null; - - // Need to declare contextTypes without flow - // https://github.com/brigand/babel-plugin-flow-react-proptypes/issues/22 - static contextTypes = { - [droppableIdKey]: PropTypes.string.isRequired, - [droppableTypeKey]: PropTypes.string.isRequired, - [styleKey]: PropTypes.string.isRequired, - }; - - constructor(props: Props, context: Object) { - super(props, context); - - const callbacks: DragHandleCallbacks = { - onLift: this.onLift, - onMove: (clientSelection: Position) => - props.move({ client: clientSelection }), - onDrop: () => props.drop({ reason: 'DROP' }), - onCancel: () => props.drop({ reason: 'CANCEL' }), - onMoveUp: props.moveUp, - onMoveDown: props.moveDown, - onMoveRight: props.moveRight, - onMoveLeft: props.moveLeft, - onWindowScroll: () => - props.moveByWindowScroll({ - newScroll: getWindowScroll(), - }), - }; - - this.callbacks = callbacks; - this.styleContext = context[styleKey]; - - // Only running this check on creation. - // Could run it on updates, but I don't think that would be needed - // as it is designed to prevent setup issues - if (process.env.NODE_ENV !== 'production') { - checkOwnProps(props); - } - } - - componentWillUnmount() { - // releasing reference to ref for cleanup - this.ref = null; - } - - onMoveEnd = (event: TransitionEvent) => { - const isDropping: boolean = Boolean( - this.props.dragging && this.props.dragging.dropping, - ); - - if (!isDropping) { - return; - } - - // There might be other properties on the element that are - // being transitioned. We do not want those to end a drop animation! - if (event.propertyName !== 'transform') { - return; - } - - this.props.dropAnimationFinished(); - }; - - onLift = (options: { - clientSelection: Position, - movementMode: MovementMode, - }) => { - timings.start('LIFT'); - const ref: ?HTMLElement = this.ref; - invariant(ref); - invariant( - !this.props.isDragDisabled, - 'Cannot lift a Draggable when it is disabled', - ); - const { clientSelection, movementMode } = options; - const { lift, draggableId } = this.props; - - lift({ - id: draggableId, - clientSelection, - movementMode, - }); - timings.finish('LIFT'); - }; - - // React can call ref callback twice for every render - // if using an arrow function - setRef = (ref: ?HTMLElement) => { - if (ref === null) { - return; - } - - if (ref === this.ref) { - return; - } - - // At this point the ref has been changed or initially populated - - this.ref = ref; - throwIfRefIsInvalid(ref); - }; - - getDraggableRef = (): ?HTMLElement => this.ref; - getShouldRespectForceTouch = (): boolean => - this.props.shouldRespectForceTouch; - - getDraggingStyle = memoizeOne( - (dragging: DraggingMapProps): DraggingStyle => { - co; - }, - ); - - getSecondaryStyle = memoizeOne( - (secondary: SecondaryMapProps): NotDraggingStyle => ({ - transform: transforms.moveTo(secondary.offset), - // transition style is applied in the head - transition: secondary.shouldAnimateDisplacement ? null : 'none', - }), - ); - - getDraggingProvided = memoizeOne( - ( - dragging: DraggingMapProps, - dragHandleProps: ?DragHandleProps, - ): Provided => { - const style: DraggingStyle = this.getDraggingStyle(dragging); - const isDropping: boolean = Boolean(dragging.dropping); - const provided: Provided = { - innerRef: this.setRef, - draggableProps: { - 'data-react-beautiful-dnd-draggable': this.styleContext, - style, - onTransitionEnd: isDropping ? this.onMoveEnd : null, - }, - dragHandleProps, - }; - return provided; - }, - ); - - getSecondaryProvided = memoizeOne( - ( - secondary: SecondaryMapProps, - dragHandleProps: ?DragHandleProps, - ): Provided => { - const style: NotDraggingStyle = this.getSecondaryStyle(secondary); - const provided: Provided = { - innerRef: this.setRef, - draggableProps: { - 'data-react-beautiful-dnd-draggable': this.styleContext, - style, - onTransitionEnd: null, - }, - dragHandleProps, - }; - return provided; - }, - ); - - getDraggingSnapshot = memoizeOne( - (dragging: DraggingMapProps): StateSnapshot => ({ - isDragging: true, - isDropAnimating: Boolean(dragging.dropping), - dropAnimation: dragging.dropping, - mode: dragging.mode, - draggingOver: dragging.draggingOver, - combineWith: dragging.combineWith, - combineTargetFor: null, - }), - ); - - getSecondarySnapshot = memoizeOne( - (secondary: SecondaryMapProps): StateSnapshot => ({ - isDragging: false, - isDropAnimating: false, - dropAnimation: null, - mode: null, - draggingOver: null, - combineTargetFor: secondary.combineTargetFor, - combineWith: null, - }), - ); - - renderChildren = (dragHandleProps: ?DragHandleProps): Node | null => { - const dragging: ?DraggingMapProps = this.props.dragging; - const secondary: ?SecondaryMapProps = this.props.secondary; - const children: ChildrenFn = this.props.children; - - if (dragging) { - return children( - this.getDraggingProvided(dragging, dragHandleProps), - this.getDraggingSnapshot(dragging), - ); - } - - invariant( - secondary, - 'If no DraggingMapProps are provided, then SecondaryMapProps are required', - ); - - return children( - this.getSecondaryProvided(secondary, dragHandleProps), - this.getSecondarySnapshot(secondary), - ); - }; - - render() { - const { - draggableId, - index, - dragging, - isDragDisabled, - disableInteractiveElementBlocking, - } = this.props; - const droppableId: DroppableId = this.context[droppableIdKey]; - const type: TypeId = this.context[droppableTypeKey]; - const isDragging: boolean = Boolean(dragging); - const isDropAnimating: boolean = Boolean(dragging && dragging.dropping); - - return ( - - - {this.renderChildren} - - - ); - } -} diff --git a/src/view/draggable/use-draggable.js b/src/view/draggable/use-draggable.js deleted file mode 100644 index e5bc6a2709..0000000000 --- a/src/view/draggable/use-draggable.js +++ /dev/null @@ -1,244 +0,0 @@ -// @flow -import { useMemo, useRef, useCallback } from 'react'; -import { type Position } from 'css-box-model'; -import invariant from 'tiny-invariant'; -import getStyle from './get-style'; -import useDragHandle from '../use-drag-handle/use-drag-handle'; -import type { - Args as DragHandleArgs, - Callbacks as DragHandleCallbacks, - DragHandleProps, -} from '../use-drag-handle/drag-handle-types'; -import type { MovementMode } from '../../types'; -import useDraggableDimensionPublisher, { - type Args as DimensionPublisherArgs, -} from '../use-draggable-dimension-publisher/use-draggable-dimension-publisher'; -import * as timings from '../../debug/timings'; -import type { - Props, - Provided, - StateSnapshot, - DraggableStyle, -} from './draggable-types'; -import getWindowScroll from '../window/get-window-scroll'; -// import throwIfRefIsInvalid from '../throw-if-invalid-inner-ref'; -// import checkOwnProps from './check-own-props'; -import AppContext, { type AppContextValue } from '../context/app-context'; -import DroppableContext, { - type DroppableContextValue, -} from '../context/droppable-context'; -import useRequiredContext from '../use-required-context'; - -export default function Draggable(props: Props) { - // instance members - const ref = useRef(null); - const setRef = useCallback((el: ?HTMLElement) => { - ref.current = el; - }, []); - const getRef = useCallback((): ?HTMLElement => ref.current, []); - - // context - const appContext: AppContextValue = useRequiredContext(AppContext); - const droppableContext: DroppableContextValue = useRequiredContext( - DroppableContext, - ); - - // props - const { - // ownProps - children, - draggableId, - isDragDisabled, - shouldRespectForceTouch, - disableInteractiveElementBlocking: canDragInteractiveElements, - index, - - // mapProps - dragging, - secondary, - - // dispatchProps - moveUp: moveUpAction, - move: moveAction, - drop: dropAction, - moveDown: moveDownAction, - moveRight: moveRightAction, - moveLeft: moveLeftAction, - moveByWindowScroll: moveByWindowScrollAction, - lift: liftAction, - dropAnimationFinished: dropAnimationFinishedAction, - } = props; - - // The dimension publisher - const publisherArgs: DimensionPublisherArgs = useMemo( - () => ({ - draggableId, - droppableId: droppableContext.droppableId, - type: droppableContext.type, - index, - getDraggableRef: getRef, - }), - [ - draggableId, - droppableContext.droppableId, - droppableContext.type, - getRef, - index, - ], - ); - - useDraggableDimensionPublisher(publisherArgs); - - // The Drag handle - - const onLift = useCallback( - () => (options: { - clientSelection: Position, - movementMode: MovementMode, - }) => { - timings.start('LIFT'); - const el: ?HTMLElement = ref.current; - invariant(el); - invariant(!isDragDisabled, 'Cannot lift a Draggable when it is disabled'); - const { clientSelection, movementMode } = options; - - liftAction({ - id: draggableId, - clientSelection, - movementMode, - }); - timings.finish('LIFT'); - }, - [draggableId, isDragDisabled, liftAction], - ); - - const getShouldRespectForceTouch = useCallback( - () => shouldRespectForceTouch, - [shouldRespectForceTouch], - ); - - const callbacks: DragHandleCallbacks = useMemo( - () => ({ - onLift, - onMove: (clientSelection: Position) => - moveAction({ client: clientSelection }), - onDrop: () => dropAction({ reason: 'DROP' }), - onCancel: () => dropAction({ reason: 'CANCEL' }), - onMoveUp: moveUpAction, - onMoveDown: moveDownAction, - onMoveRight: moveRightAction, - onMoveLeft: moveLeftAction, - onWindowScroll: () => - moveByWindowScrollAction({ - newScroll: getWindowScroll(), - }), - }), - [ - dropAction, - moveAction, - moveByWindowScrollAction, - moveDownAction, - moveLeftAction, - moveRightAction, - moveUpAction, - onLift, - ], - ); - - const isDragging: boolean = Boolean(dragging); - const isDropAnimating: boolean = Boolean(dragging && dragging.dropping); - - const dragHandleArgs: DragHandleArgs = useMemo( - () => ({ - draggableId, - isDragging, - isDropAnimating, - isEnabled: !isDragDisabled, - callbacks, - getDraggableRef: getRef, - canDragInteractiveElements, - getShouldRespectForceTouch, - }), - [ - callbacks, - canDragInteractiveElements, - draggableId, - getRef, - getShouldRespectForceTouch, - isDragDisabled, - isDragging, - isDropAnimating, - ], - ); - - const dragHandleProps: DragHandleProps = useDragHandle(dragHandleArgs); - - const onMoveEnd = useCallback( - (event: TransitionEvent) => { - const isDropping: boolean = Boolean(dragging && dragging.dropping); - - if (!isDropping) { - return; - } - - // There might be other properties on the element that are - // being transitioned. We do not want those to end a drop animation! - if (event.propertyName !== 'transform') { - return; - } - - dropAnimationFinishedAction(); - }, - [dragging, dropAnimationFinishedAction], - ); - - const provided: Provided = useMemo(() => { - const style: DraggableStyle = getStyle(dragging, secondary); - const onTransitionEnd = dragging && dragging.dropping ? onMoveEnd : null; - - const result: Provided = { - innerRef: setRef, - draggableProps: { - 'data-react-beautiful-dnd-draggable': appContext.style, - style, - onTransitionEnd, - }, - dragHandleProps, - }; - - return result; - }, [ - appContext.style, - dragHandleProps, - dragging, - onMoveEnd, - secondary, - setRef, - ]); - - const snapshot: StateSnapshot = useMemo(() => { - if (dragging) { - return { - isDragging: true, - isDropAnimating: Boolean(dragging.dropping), - dropAnimation: dragging.dropping, - mode: dragging.mode, - draggingOver: dragging.draggingOver, - combineWith: dragging.combineWith, - combineTargetFor: null, - }; - } - invariant(secondary, 'Expected dragging or secondary snapshot'); - return { - isDragging: false, - isDropAnimating: false, - dropAnimation: null, - mode: null, - draggingOver: null, - combineTargetFor: secondary.combineTargetFor, - combineWith: null, - }; - }, [dragging, secondary]); - - return children(provided, snapshot); -} diff --git a/src/view/use-drag-handle/drag-handle.jsx b/src/view/use-drag-handle/drag-handle.jsx deleted file mode 100644 index 05ea0e5d1b..0000000000 --- a/src/view/use-drag-handle/drag-handle.jsx +++ /dev/null @@ -1,268 +0,0 @@ -// @flow -import { Component } from 'react'; -import PropTypes from 'prop-types'; -import memoizeOne from 'memoize-one'; -import invariant from 'tiny-invariant'; -import getWindowFromEl from '../window/get-window-from-el'; -import getDragHandleRef from './util/get-drag-handle-ref'; -import type { Props, DragHandleProps } from './drag-handle-types'; -import type { - MouseSensor, - KeyboardSensor, - TouchSensor, - CreateSensorArgs, -} from './sensor/sensor-types'; -import type { DraggableId } from '../../types'; -import { styleKey, canLiftKey } from '../context-keys'; -import focusRetainer from './util/focus-retainer'; -import shouldAllowDraggingFromTarget from './util/should-allow-dragging-from-target'; -import createMouseSensor from './sensor/create-mouse-sensor'; -import createKeyboardSensor from './sensor/create-keyboard-sensor'; -import createTouchSensor from './sensor/create-touch-sensor'; -import { warning } from '../../dev-warning'; - -const preventHtml5Dnd = (event: DragEvent) => { - event.preventDefault(); -}; - -type Sensor = MouseSensor | KeyboardSensor | TouchSensor; - -export default class DragHandle extends Component { - /* eslint-disable react/sort-comp */ - mouseSensor: MouseSensor; - keyboardSensor: KeyboardSensor; - touchSensor: TouchSensor; - sensors: Sensor[]; - styleContext: string; - canLift: (id: DraggableId) => boolean; - isFocused: boolean = false; - lastDraggableRef: ?HTMLElement; - - // Need to declare contextTypes without flow - // https://github.com/brigand/babel-plugin-flow-react-proptypes/issues/22 - static contextTypes = { - [styleKey]: PropTypes.string.isRequired, - [canLiftKey]: PropTypes.func.isRequired, - }; - - constructor(props: Props, context: Object) { - super(props, context); - - const getWindow = (): HTMLElement => - getWindowFromEl(this.props.getDraggableRef()); - - const args: CreateSensorArgs = { - callbacks: this.props.callbacks, - getDraggableRef: this.props.getDraggableRef, - getWindow, - canStartCapturing: this.canStartCapturing, - getShouldRespectForceTouch: this.props.getShouldRespectForceTouch, - }; - - this.mouseSensor = createMouseSensor(args); - this.keyboardSensor = createKeyboardSensor(args); - this.touchSensor = createTouchSensor(args); - this.sensors = [this.mouseSensor, this.keyboardSensor, this.touchSensor]; - this.styleContext = context[styleKey]; - - // The canLift function is read directly off the context - // and will communicate with the store. This is done to avoid - // needing to query a property from the store and re-render this component - // with that value. By putting it as a function on the context we are able - // to avoid re-rendering to pass this information while still allowing - // drag-handles to obtain this state if they need it. - this.canLift = context[canLiftKey]; - } - - componentDidMount() { - const draggableRef: ?HTMLElement = this.props.getDraggableRef(); - - // storing a reference for later - this.lastDraggableRef = draggableRef; - - invariant(draggableRef, 'Cannot get draggable ref from drag handle'); - - // drag handle ref will not be available when not enabled - if (!this.props.isEnabled) { - return; - } - - const dragHandleRef: HTMLElement = getDragHandleRef(draggableRef); - - focusRetainer.tryRestoreFocus(this.props.draggableId, dragHandleRef); - } - - componentDidUpdate(prevProps: Props) { - const ref: ?HTMLElement = this.props.getDraggableRef(); - - // 1. focus on element if required - if (ref !== this.lastDraggableRef) { - this.lastDraggableRef = ref; - - // After a ref change we might need to manually force focus onto the ref. - // When moving something into or out of a portal the element loses focus - // https://github.com/facebook/react/issues/12454 - - if (ref && this.isFocused && this.props.isEnabled) { - getDragHandleRef(ref).focus(); - } - } - - // 2. should we kill the any capturing? - - const isCapturing: boolean = this.isAnySensorCapturing(); - - // not capturing was happening - so we dont need to do anything - if (!isCapturing) { - return; - } - - const isBeingDisabled: boolean = - prevProps.isEnabled && !this.props.isEnabled; - - if (isBeingDisabled) { - this.sensors.forEach((sensor: Sensor) => { - if (!sensor.isCapturing()) { - return; - } - const wasDragging: boolean = sensor.isDragging(); - sensor.kill(); - - // It is fine for a draggable to be disabled while a drag is pending - if (wasDragging) { - warning( - 'You have disabled dragging on a Draggable while it was dragging. The drag has been cancelled', - ); - this.props.callbacks.onCancel(); - } - }); - } - - // Drag has stopped due to somewhere else in the system - const isDragAborted: boolean = - prevProps.isDragging && !this.props.isDragging; - if (isDragAborted) { - // We need to unbind the handlers - this.sensors.forEach((sensor: Sensor) => { - if (sensor.isCapturing()) { - sensor.kill(); - // not firing any cancel event as the drag is already over - } - }); - } - } - - componentWillUnmount() { - this.sensors.forEach((sensor: Sensor) => { - // kill the current drag and fire a cancel event if - const wasDragging: boolean = sensor.isDragging(); - - sensor.unmount(); - // cancel if drag was occurring - if (wasDragging) { - this.props.callbacks.onCancel(); - } - }); - - const shouldRetainFocus: boolean = (() => { - if (!this.props.isEnabled) { - return false; - } - - // not already focused - if (!this.isFocused) { - return false; - } - - // a drag is finishing - return this.props.isDragging || this.props.isDropAnimating; - })(); - - if (shouldRetainFocus) { - focusRetainer.retain(this.props.draggableId); - } - } - - onFocus = () => { - this.isFocused = true; - }; - - onBlur = () => { - this.isFocused = false; - }; - - onKeyDown = (event: KeyboardEvent) => { - // let the other sensors deal with it - if (this.mouseSensor.isCapturing() || this.touchSensor.isCapturing()) { - return; - } - - this.keyboardSensor.onKeyDown(event); - }; - - onMouseDown = (event: MouseEvent) => { - // let the other sensors deal with it - if (this.keyboardSensor.isCapturing() || this.mouseSensor.isCapturing()) { - return; - } - - this.mouseSensor.onMouseDown(event); - }; - - onTouchStart = (event: TouchEvent) => { - // let the keyboard sensor deal with it - if (this.mouseSensor.isCapturing() || this.keyboardSensor.isCapturing()) { - return; - } - - this.touchSensor.onTouchStart(event); - }; - - canStartCapturing = (event: Event) => { - // this might be before a drag has started - isolated to this element - if (this.isAnySensorCapturing()) { - return false; - } - - // this will check if anything else in the system is dragging - if (!this.canLift(this.props.draggableId)) { - return false; - } - - // check if we are dragging an interactive element - return shouldAllowDraggingFromTarget(event, this.props); - }; - - isAnySensorCapturing = (): boolean => - this.sensors.some((sensor: Sensor) => sensor.isCapturing()); - - getProvided = memoizeOne( - (isEnabled: boolean): ?DragHandleProps => { - if (!isEnabled) { - return null; - } - - const provided: DragHandleProps = { - onMouseDown: this.onMouseDown, - onKeyDown: this.onKeyDown, - onTouchStart: this.onTouchStart, - onFocus: this.onFocus, - onBlur: this.onBlur, - tabIndex: 0, - 'data-react-beautiful-dnd-drag-handle': this.styleContext, - // English default. Consumers are welcome to add their own start instruction - 'aria-roledescription': 'Draggable item. Press space bar to lift', - draggable: false, - onDragStart: preventHtml5Dnd, - }; - - return provided; - }, - ); - - render() { - const { children, isEnabled } = this.props; - - return children(this.getProvided(isEnabled)); - } -} diff --git a/src/view/use-style-marshal/use-style-marshal.js b/src/view/use-style-marshal/use-style-marshal.js index ef745d3e8c..cbae0680c7 100644 --- a/src/view/use-style-marshal/use-style-marshal.js +++ b/src/view/use-style-marshal/use-style-marshal.js @@ -46,7 +46,7 @@ export default function useStyleMarshal(uniqueId: number) { useEffect(() => { invariant( - alwaysRef.current || dynamicRef.current, + !alwaysRef.current && !dynamicRef.current, 'style elements already mounted', ); From 2e3bdfd36332a74a99d16392dbf129eda2cf3acd Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Mon, 18 Mar 2019 14:46:22 +1100 Subject: [PATCH 022/117] yolo --- src/view/use-drag-handle/use-drag-handle.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/view/use-drag-handle/use-drag-handle.js b/src/view/use-drag-handle/use-drag-handle.js index f0a5fc1da5..65ced89309 100644 --- a/src/view/use-drag-handle/use-drag-handle.js +++ b/src/view/use-drag-handle/use-drag-handle.js @@ -23,6 +23,7 @@ function preventHtml5Dnd(event: DragEvent) { } export default function useDragHandle(args: Args): DragHandleProps { + console.log('rendering drag handle'); const { canLift, style }: AppContextValue = useRequiredContext(AppContext); const { draggableId, From 7b9e2fd8d5b0f288699504def655a8fe7c0d48ec Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Mon, 18 Mar 2019 15:43:20 +1100 Subject: [PATCH 023/117] constant down --- .../sensor/create-keyboard-sensor.js | 13 ++-- .../sensor/create-mouse-sensor.js | 15 ++--- .../sensor/create-touch-sensor.js | 13 ++-- .../use-drag-handle/sensor/sensor-types.js | 2 +- .../sensor/use-mouse-sensor.js | 31 ++++++++++ src/view/use-drag-handle/use-drag-handle.js | 61 ++++++++++--------- .../view/drag-handle/touch-sensor.spec.js | 4 +- 7 files changed, 87 insertions(+), 52 deletions(-) create mode 100644 src/view/use-drag-handle/sensor/use-mouse-sensor.js diff --git a/src/view/use-drag-handle/sensor/create-keyboard-sensor.js b/src/view/use-drag-handle/sensor/create-keyboard-sensor.js index ac90fff403..477f8815a5 100644 --- a/src/view/use-drag-handle/sensor/create-keyboard-sensor.js +++ b/src/view/use-drag-handle/sensor/create-keyboard-sensor.js @@ -29,7 +29,7 @@ const scrollJumpKeys: KeyMap = { const noop = () => {}; export default ({ - callbacks, + getCallbacks, getWindow, getDraggableRef, canStartCapturing, @@ -59,10 +59,11 @@ export default ({ } }; const cancel = () => { - stopDragging(callbacks.onCancel); + stopDragging(getCallbacks().onCancel); }; const isDragging = (): boolean => state.isDragging; - const schedule = createScheduler(callbacks); + // TODO: we assume here that the callbacks can never change + const schedule = createScheduler(getCallbacks()); const onKeyDown = (event: KeyboardEvent) => { // not yet dragging @@ -93,7 +94,7 @@ export default ({ // we are using this event for part of the drag event.preventDefault(); startDragging(() => - callbacks.onLift({ + getCallbacks().onLift({ clientSelection: center, movementMode: 'SNAP', }), @@ -112,7 +113,7 @@ export default ({ if (event.keyCode === keyCodes.space) { // need to stop parent Draggable's thinking this is a lift event.preventDefault(); - stopDragging(callbacks.onDrop); + stopDragging(getCallbacks().onDrop); return; } @@ -200,7 +201,7 @@ export default ({ return; } - callbacks.onWindowScroll(); + getCallbacks().onWindowScroll(); }, }, // Cancel on page visibility change diff --git a/src/view/use-drag-handle/sensor/create-mouse-sensor.js b/src/view/use-drag-handle/sensor/create-mouse-sensor.js index b4e3892c26..ff51d663e7 100644 --- a/src/view/use-drag-handle/sensor/create-mouse-sensor.js +++ b/src/view/use-drag-handle/sensor/create-mouse-sensor.js @@ -36,7 +36,7 @@ const noop = () => {}; const mouseDownMarshal: EventMarshal = createEventMarshal(); export default ({ - callbacks, + getCallbacks, getWindow, canStartCapturing, }: CreateSensorArgs): MouseSensor => { @@ -49,7 +49,8 @@ export default ({ }; const isDragging = (): boolean => state.isDragging; const isCapturing = (): boolean => Boolean(state.pending || state.isDragging); - const schedule = createScheduler(callbacks); + // TODO: we assume here that the callbacks can never change + const schedule = createScheduler(getCallbacks()); const postDragEventPreventer: EventPreventer = createPostDragEventPreventer( getWindow, ); @@ -101,7 +102,7 @@ export default ({ }; const cancel = () => { - kill(callbacks.onCancel); + kill(getCallbacks().onCancel); }; const windowBindings: EventBinding[] = [ @@ -146,7 +147,7 @@ export default ({ // preventing default as we are using this event event.preventDefault(); startDragging(() => - callbacks.onLift({ + getCallbacks().onLift({ clientSelection: point, movementMode: 'FLUID', }), @@ -163,7 +164,7 @@ export default ({ // preventing default as we are using this event event.preventDefault(); - stopDragging(callbacks.onDrop); + stopDragging(getCallbacks().onDrop); }, }, { @@ -175,7 +176,7 @@ export default ({ event.preventDefault(); } - stopDragging(callbacks.onCancel); + stopDragging(getCallbacks().onCancel); }, }, { @@ -225,7 +226,7 @@ export default ({ stopPendingDrag(); return; } - // callbacks.onWindowScroll(); + // getCallbacks().onWindowScroll(); schedule.windowScrollMove(); }, }, diff --git a/src/view/use-drag-handle/sensor/create-touch-sensor.js b/src/view/use-drag-handle/sensor/create-touch-sensor.js index 9c35610f4d..05123e3140 100644 --- a/src/view/use-drag-handle/sensor/create-touch-sensor.js +++ b/src/view/use-drag-handle/sensor/create-touch-sensor.js @@ -103,7 +103,7 @@ const initial: State = { }; export default ({ - callbacks, + getCallbacks, getWindow, canStartCapturing, getShouldRespectForceTouch, @@ -119,7 +119,8 @@ export default ({ const isDragging = (): boolean => state.isDragging; const isCapturing = (): boolean => Boolean(state.pending || state.isDragging || state.longPressTimerId); - const schedule = createScheduler(callbacks); + // TODO: assuming they do not change reference... + const schedule = createScheduler(getCallbacks()); const postDragEventPreventer: EventPreventer = createPostDragEventPreventer( getWindow, ); @@ -143,7 +144,7 @@ export default ({ longPressTimerId: null, }); - callbacks.onLift({ + getCallbacks().onLift({ clientSelection: pending, movementMode: 'FLUID', }); @@ -208,7 +209,7 @@ export default ({ }; const cancel = () => { - kill(callbacks.onCancel); + kill(getCallbacks().onCancel); }; const windowBindings: EventBinding[] = [ @@ -257,7 +258,7 @@ export default ({ // already dragging - this event is directly ending a drag event.preventDefault(); - stopDragging(callbacks.onDrop); + stopDragging(getCallbacks().onDrop); }, }, { @@ -271,7 +272,7 @@ export default ({ // already dragging - this event is directly ending a drag event.preventDefault(); - stopDragging(callbacks.onCancel); + stopDragging(getCallbacks().onCancel); }, }, // another touch start should not happen without a diff --git a/src/view/use-drag-handle/sensor/sensor-types.js b/src/view/use-drag-handle/sensor/sensor-types.js index 1fb635c141..c66fe8fc35 100644 --- a/src/view/use-drag-handle/sensor/sensor-types.js +++ b/src/view/use-drag-handle/sensor/sensor-types.js @@ -14,7 +14,7 @@ type SensorBase = {| |}; export type CreateSensorArgs = {| - callbacks: Callbacks, + getCallbacks: () => Callbacks, getDraggableRef: () => ?HTMLElement, getWindow: () => HTMLElement, canStartCapturing: (event: Event) => boolean, diff --git a/src/view/use-drag-handle/sensor/use-mouse-sensor.js b/src/view/use-drag-handle/sensor/use-mouse-sensor.js new file mode 100644 index 0000000000..91b6ace8cb --- /dev/null +++ b/src/view/use-drag-handle/sensor/use-mouse-sensor.js @@ -0,0 +1,31 @@ +// @flow +import type { Position } from 'css-box-model'; +import { useRef, useCallback } from 'react'; + +type Result = (event: MouseEvent) => void; + +export default function useMouseSensor(args: Args): Result { + const pendingRef = useRef(null); + const isDraggingRef = useRef(false); + + const bindWindowEvents = useCallback((point: Position) => {}); + + const startPendingDrag = useCallback((point: Position) => { + pendingRef.current = point; + }, []); + + const onMouseDown = useCallback( + (event: MouseEvent) => { + event.preventDefault(); + const point: Position = { + x: event.clientX, + y: event.clientY, + }; + + startPendingDrag(point); + }, + [startPendingDrag], + ); + + return onMouseDown; +} diff --git a/src/view/use-drag-handle/use-drag-handle.js b/src/view/use-drag-handle/use-drag-handle.js index 65ced89309..4efc8eb5ae 100644 --- a/src/view/use-drag-handle/use-drag-handle.js +++ b/src/view/use-drag-handle/use-drag-handle.js @@ -7,7 +7,7 @@ import type { KeyboardSensor, TouchSensor, } from './sensor/sensor-types'; -import type { Args, DragHandleProps } from './drag-handle-types'; +import type { Args, DragHandleProps, Callbacks } from './drag-handle-types'; import { useConstant, useConstantFn } from '../use-constant'; import createKeyboardSensor from './sensor/create-keyboard-sensor'; import createTouchSensor from './sensor/create-touch-sensor'; @@ -23,21 +23,16 @@ function preventHtml5Dnd(event: DragEvent) { } export default function useDragHandle(args: Args): DragHandleProps { - console.log('rendering drag handle'); const { canLift, style }: AppContextValue = useRequiredContext(AppContext); - const { - draggableId, - callbacks, - isEnabled, - isDragging, - canDragInteractiveElements, - getShouldRespectForceTouch, - getDraggableRef, - } = args; + + // side effect on every render + const latestArgsRef = useRef(args); + latestArgsRef.current = args; + const getLatestArgs = useConstantFn(() => latestArgsRef.current); // TODO: things will go bad if getDraggableRef changes const getWindow = useConstantFn( - (): HTMLElement => getWindowFromEl(getDraggableRef()), + (): HTMLElement => getWindowFromEl(getLatestArgs().getDraggableRef()), ); const isFocusedRef = useRef(false); @@ -48,13 +43,12 @@ export default function useDragHandle(args: Args): DragHandleProps { isFocusedRef.current = false; }); - let sensors: Sensor[]; - const isAnySensorCapturing = useConstantFn(() => - sensors.some((sensor: Sensor): boolean => sensor.isCapturing()), + // using getSensors before it is defined + // eslint-disable-next-line no-use-before-define + getSensors().some((sensor: Sensor): boolean => sensor.isCapturing()), ); - // TODO: this will break if draggableId changes const canStartCapturing = useConstantFn((event: Event) => { // this might be before a drag has started - isolated to this element if (isAnySensorCapturing()) { @@ -62,20 +56,27 @@ export default function useDragHandle(args: Args): DragHandleProps { } // this will check if anything else in the system is dragging - if (!canLift(draggableId)) { + if (!canLift(getLatestArgs().draggableId)) { return false; } // check if we are dragging an interactive element - return shouldAllowDraggingFromTarget(event, canDragInteractiveElements); + return shouldAllowDraggingFromTarget( + event, + getLatestArgs().canDragInteractiveElements, + ); }); + const getCallbacks = useConstantFn( + (): Callbacks => getLatestArgs().callbacks, + ); + const createArgs: CreateSensorArgs = useConstant(() => ({ - callbacks, - getDraggableRef, + getCallbacks, + getDraggableRef: getLatestArgs().getDraggableRef, canStartCapturing, getWindow, - getShouldRespectForceTouch, + getShouldRespectForceTouch: getLatestArgs().getShouldRespectForceTouch, })); const mouse: MouseSensor = useConstant(() => createMouseSensor(createArgs)); @@ -83,7 +84,7 @@ export default function useDragHandle(args: Args): DragHandleProps { createKeyboardSensor(createArgs), ); const touch: TouchSensor = useConstant(() => createTouchSensor(createArgs)); - sensors = useConstant(() => [mouse, keyboard, touch]); + const getSensors = useConstantFn(() => [mouse, keyboard, touch]); const onKeyDown = useConstantFn((event: KeyboardEvent) => { // let the other sensors deal with it @@ -119,14 +120,14 @@ export default function useDragHandle(args: Args): DragHandleProps { useLayoutEffect(() => { // Just a cleanup function return () => { - sensors.forEach((sensor: Sensor) => { + getSensors().forEach((sensor: Sensor) => { // kill the current drag and fire a cancel event if const wasDragging: boolean = sensor.isDragging(); sensor.unmount(); // Cancel if drag was occurring if (wasDragging) { - callbacks.onCancel(); + latestArgsRef.current.callbacks.onCancel(); } }); }; @@ -136,7 +137,7 @@ export default function useDragHandle(args: Args): DragHandleProps { // Checking for disabled changes during a drag useLayoutEffect(() => { - sensors.forEach((sensor: Sensor) => { + getSensors().forEach((sensor: Sensor) => { if (!sensor.isCapturing()) { return; } @@ -148,23 +149,23 @@ export default function useDragHandle(args: Args): DragHandleProps { warning( 'You have disabled dragging on a Draggable while it was dragging. The drag has been cancelled', ); - callbacks.onCancel(); + getLatestArgs().callbacks.onCancel(); } }); - }, [callbacks, isEnabled, sensors]); + }, [getLatestArgs, getSensors]); // Drag aborted elsewhere in application useLayoutEffect(() => { - if (isDragging) { + if (getLatestArgs().isDragging) { return; } - sensors.forEach((sensor: Sensor) => { + getSensors().forEach((sensor: Sensor) => { if (sensor.isCapturing()) { sensor.kill(); } }); - }, [isDragging, sensors]); + }, [getLatestArgs, getSensors]); const props: DragHandleProps = useConstant(() => ({ onMouseDown, diff --git a/test/unit/view/drag-handle/touch-sensor.spec.js b/test/unit/view/drag-handle/touch-sensor.spec.js index da03b37975..bac9b88cc4 100644 --- a/test/unit/view/drag-handle/touch-sensor.spec.js +++ b/test/unit/view/drag-handle/touch-sensor.spec.js @@ -8,7 +8,7 @@ import setWindowScroll from '../../../utils/set-window-scroll'; import { timeForLongPress, forcePressThreshold, -} from '../../../../src/view/drag-handle/sensor/create-touch-sensor'; +} from '../../../../src/view/use-drag-handle/sensor/create-touch-sensor'; import { dispatchWindowEvent, dispatchWindowKeyDownEvent, @@ -27,7 +27,7 @@ import { windowTouchStart, } from './util/events'; import { getWrapper } from './util/wrappers'; -import type { Callbacks } from '../../../../src/view/drag-handle/drag-handle-types'; +import type { Callbacks } from '../../../../src/view/use-drag-handle/drag-handle-types'; const origin: Position = { x: 0, y: 0 }; let callbacks: Callbacks; From 0d771e513bb2b88d7237427d14f11313a6789215 Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Mon, 18 Mar 2019 16:06:04 +1100 Subject: [PATCH 024/117] sprinkling love --- src/view/check-is-valid-inner-ref.js | 15 +++++++ src/view/draggable/draggable-types.js | 2 +- src/view/draggable/draggable.jsx | 9 ++-- src/view/draggable/use-validation.js | 29 ++++++++++++ src/view/droppable/droppable.jsx | 25 +++++------ .../droppable/{check.js => use-validation.js} | 36 ++++++++------- .../get-dimension.js | 44 +++++++++++++++++++ .../use-draggable-dimension-publisher.js | 43 ++---------------- .../use-droppable-dimension-publisher.js | 8 ++-- 9 files changed, 131 insertions(+), 80 deletions(-) create mode 100644 src/view/check-is-valid-inner-ref.js create mode 100644 src/view/draggable/use-validation.js rename src/view/droppable/{check.js => use-validation.js} (62%) create mode 100644 src/view/use-draggable-dimension-publisher/get-dimension.js diff --git a/src/view/check-is-valid-inner-ref.js b/src/view/check-is-valid-inner-ref.js new file mode 100644 index 0000000000..ac54133dde --- /dev/null +++ b/src/view/check-is-valid-inner-ref.js @@ -0,0 +1,15 @@ +// @flow +import invariant from 'tiny-invariant'; +import isHtmlElement from './is-type-of-element/is-html-element'; + +export default function checkIsValidInnerRef(el: ?HTMLElement) { + invariant( + el && isHtmlElement(el), + ` + provided.innerRef has not been provided with a HTMLElement. + + You can find a guide on using the innerRef callback functions at: + https://github.com/atlassian/react-beautiful-dnd/blob/master/docs/guides/using-inner-ref.md + `, + ); +} diff --git a/src/view/draggable/draggable-types.js b/src/view/draggable/draggable-types.js index 4c0b0b369d..e3164fc2b9 100644 --- a/src/view/draggable/draggable-types.js +++ b/src/view/draggable/draggable-types.js @@ -19,7 +19,7 @@ import { drop, dropAnimationFinished, } from '../../state/action-creators'; -import type { DragHandleProps } from '../drag-handle/drag-handle-types'; +import type { DragHandleProps } from '../use-drag-handle/drag-handle-types'; export type DraggingStyle = {| position: 'fixed', diff --git a/src/view/draggable/draggable.jsx b/src/view/draggable/draggable.jsx index 3c9a67568a..02c3baced7 100644 --- a/src/view/draggable/draggable.jsx +++ b/src/view/draggable/draggable.jsx @@ -30,7 +30,7 @@ import DroppableContext, { import useRequiredContext from '../use-required-context'; export default function Draggable(props: Props) { - // instance members + // reference to DOM node const ref = useRef(null); const setRef = useCallback((el: ?HTMLElement) => { ref.current = el; @@ -69,8 +69,8 @@ export default function Draggable(props: Props) { dropAnimationFinished: dropAnimationFinishedAction, } = props; - // The dimension publisher - const publisherArgs: DimensionPublisherArgs = useMemo( + // The dimension publisher: talks to the marshal + const forPublisher: DimensionPublisherArgs = useMemo( () => ({ draggableId, droppableId: droppableContext.droppableId, @@ -86,8 +86,7 @@ export default function Draggable(props: Props) { index, ], ); - - useDraggableDimensionPublisher(publisherArgs); + useDraggableDimensionPublisher(forPublisher); // The Drag handle diff --git a/src/view/draggable/use-validation.js b/src/view/draggable/use-validation.js new file mode 100644 index 0000000000..cdf5991982 --- /dev/null +++ b/src/view/draggable/use-validation.js @@ -0,0 +1,29 @@ +// @flow +import { useEffect } from 'react'; +import invariant from 'tiny-invariant'; +import type { Props } from './draggable-types'; +import checkIsValidInnerRef from '../check-is-valid-inner-ref'; + +function checkOwnProps(props: Props) { + // Number.isInteger will be provided by @babel/runtime-corejs2 + invariant( + Number.isInteger(props.index), + 'Draggable requires an integer index prop', + ); + invariant(props.draggableId, 'Draggable requires a draggableId'); + invariant( + typeof props.isDragDisabled === 'boolean', + 'isDragDisabled must be a boolean', + ); +} + +export default function useValidation(props: Props, el: ?HTMLElement) { + // running after every update in development + useEffect(() => { + if (process.env.NODE_ENV === 'production') { + return; + } + checkOwnProps(props); + checkIsValidInnerRef(el); + }); +} diff --git a/src/view/droppable/droppable.jsx b/src/view/droppable/droppable.jsx index e4af7b513e..1a08c8b4e2 100644 --- a/src/view/droppable/droppable.jsx +++ b/src/view/droppable/droppable.jsx @@ -1,7 +1,6 @@ // @flow import invariant from 'tiny-invariant'; import React, { - useEffect, useMemo, useRef, useCallback, @@ -19,8 +18,7 @@ import useAnimateInOut, { type AnimateProvided, } from '../use-animate-in-out/use-animate-in-out'; import getMaxWindowScroll from '../window/get-max-window-scroll'; -import { useConstantFn } from '../use-constant'; -import { checkOwnProps, checkPlaceholder, checkProvidedRef } from './check'; +import useValidation from './use-validation'; export default function Droppable(props: Props) { const appContext: ?AppContextValue = useContext(AppContext); @@ -29,12 +27,7 @@ export default function Droppable(props: Props) { const droppableRef = useRef(null); const placeholderRef = useRef(null); - // validating setup - useEffect(() => { - checkOwnProps(props); - checkPlaceholder(props, placeholderRef.current); - checkProvidedRef(droppableRef.current); - }); + useValidation(props, droppableRef.current, placeholderRef.current); const { // own props @@ -53,18 +46,20 @@ export default function Droppable(props: Props) { updateViewportMaxScroll, } = props; - const getDroppableRef = useConstantFn( + const getDroppableRef = useCallback( (): ?HTMLElement => droppableRef.current, + [], ); - const getPlaceholderRef = useConstantFn( + const getPlaceholderRef = useCallback( (): ?HTMLElement => placeholderRef.current, + [], ); - const setDroppableRef = useConstantFn((value: ?HTMLElement) => { + const setDroppableRef = useCallback((value: ?HTMLElement) => { droppableRef.current = value; - }); - const setPlaceholderRef = useConstantFn((value: ?HTMLElement) => { + }, []); + const setPlaceholderRef = useCallback((value: ?HTMLElement) => { placeholderRef.current = value; - }); + }, []); const onPlaceholderTransitionEnd = useCallback(() => { // A placeholder change can impact the window's max scroll diff --git a/src/view/droppable/check.js b/src/view/droppable/use-validation.js similarity index 62% rename from src/view/droppable/check.js rename to src/view/droppable/use-validation.js index f89290c854..34d61b2e65 100644 --- a/src/view/droppable/check.js +++ b/src/view/droppable/use-validation.js @@ -1,10 +1,11 @@ // @flow +import { useEffect } from 'react'; import invariant from 'tiny-invariant'; import type { Props } from './droppable-types'; -import isHtmlElement from '../is-type-of-element/is-html-element'; import { warning } from '../../dev-warning'; +import checkIsValidInnerRef from '../check-is-valid-inner-ref'; -export function checkOwnProps(props: Props) { +function checkOwnProps(props: Props) { invariant(props.droppableId, 'A Droppable requires a droppableId prop'); invariant( typeof props.isDropDisabled === 'boolean', @@ -20,11 +21,7 @@ export function checkOwnProps(props: Props) { ); } -export function checkPlaceholder(props: Props, placeholderEl: ?HTMLElement) { - if (process.env.NODE_ENV === 'production') { - return; - } - +function checkPlaceholderRef(props: Props, placeholderEl: ?HTMLElement) { if (!props.placeholder) { return; } @@ -42,14 +39,19 @@ export function checkPlaceholder(props: Props, placeholderEl: ?HTMLElement) { `); } -export function checkProvidedRef(ref: ?mixed) { - invariant( - ref && isHtmlElement(ref), - ` - provided.innerRef has not been provided with a HTMLElement. - - You can find a guide on using the innerRef callback functions at: - https://github.com/atlassian/react-beautiful-dnd/blob/master/docs/guides/using-inner-ref.md - `, - ); +export default function useValidation( + props: Props, + droppableRef: ?HTMLElement, + placeholderRef: ?HTMLElement, +) { + // Running on every update + useEffect(() => { + if (process.env.NODE_ENV === 'production') { + return; + } + + checkOwnProps(props); + checkPlaceholderRef(props, placeholderRef); + checkIsValidInnerRef(droppableRef); + }); } diff --git a/src/view/use-draggable-dimension-publisher/get-dimension.js b/src/view/use-draggable-dimension-publisher/get-dimension.js new file mode 100644 index 0000000000..b589b95045 --- /dev/null +++ b/src/view/use-draggable-dimension-publisher/get-dimension.js @@ -0,0 +1,44 @@ +// @flow +import { + type BoxModel, + type Position, + calculateBox, + withScroll, +} from 'css-box-model'; +import type { + DraggableDescriptor, + DraggableDimension, + Placeholder, +} from '../../types'; +import { origin } from '../../state/position'; + +export default function getDimension( + descriptor: DraggableDescriptor, + el: HTMLElement, + windowScroll?: Position = origin, +): DraggableDimension { + const computedStyles: CSSStyleDeclaration = window.getComputedStyle(el); + const borderBox: ClientRect = el.getBoundingClientRect(); + const client: BoxModel = calculateBox(borderBox, computedStyles); + const page: BoxModel = withScroll(client, windowScroll); + + const placeholder: Placeholder = { + client, + tagName: el.tagName.toLowerCase(), + display: computedStyles.display, + }; + const displaceBy: Position = { + x: client.marginBox.width, + y: client.marginBox.height, + }; + + const dimension: DraggableDimension = { + descriptor, + placeholder, + displaceBy, + client, + page, + }; + + return dimension; +} diff --git a/src/view/use-draggable-dimension-publisher/use-draggable-dimension-publisher.js b/src/view/use-draggable-dimension-publisher/use-draggable-dimension-publisher.js index d47a2215e2..13192090c8 100644 --- a/src/view/use-draggable-dimension-publisher/use-draggable-dimension-publisher.js +++ b/src/view/use-draggable-dimension-publisher/use-draggable-dimension-publisher.js @@ -1,24 +1,18 @@ // @flow import { useMemo, useCallback, useLayoutEffect } from 'react'; -import { - calculateBox, - withScroll, - type BoxModel, - type Position, -} from 'css-box-model'; +import { type Position } from 'css-box-model'; import invariant from 'tiny-invariant'; import type { DraggableDescriptor, DraggableDimension, - Placeholder, DraggableId, DroppableId, TypeId, } from '../../types'; import type { DimensionMarshal } from '../../state/dimension-marshal/dimension-marshal-types'; -import { origin } from '../../state/position'; import useRequiredContext from '../use-required-context'; import AppContext, { type AppContextValue } from '../context/app-context'; +import getDimension from './get-dimension'; export type Args = {| draggableId: DraggableId, @@ -28,37 +22,6 @@ export type Args = {| getDraggableRef: () => ?HTMLElement, |}; -function getDimension( - descriptor: DraggableDescriptor, - el: HTMLElement, - windowScroll?: Position = origin, -): DraggableDimension { - const computedStyles: CSSStyleDeclaration = window.getComputedStyle(el); - const borderBox: ClientRect = el.getBoundingClientRect(); - const client: BoxModel = calculateBox(borderBox, computedStyles); - const page: BoxModel = withScroll(client, windowScroll); - - const placeholder: Placeholder = { - client, - tagName: el.tagName.toLowerCase(), - display: computedStyles.display, - }; - const displaceBy: Position = { - x: client.marginBox.width, - y: client.marginBox.height, - }; - - const dimension: DraggableDimension = { - descriptor, - placeholder, - displaceBy, - client, - page, - }; - - return dimension; -} - export default function useDraggableDimensionPublisher(args: Args) { const { draggableId, droppableId, type, index, getDraggableRef } = args; const appContext: AppContextValue = useRequiredContext(AppContext); @@ -83,6 +46,8 @@ export default function useDraggableDimensionPublisher(args: Args) { [descriptor, getDraggableRef], ); + // Communicate with the marshal + // TODO: should it be an "update"? useLayoutEffect(() => { marshal.registerDraggable(descriptor, makeDimension); return () => marshal.unregisterDraggable(descriptor); diff --git a/src/view/use-droppable-dimension-publisher/use-droppable-dimension-publisher.js b/src/view/use-droppable-dimension-publisher/use-droppable-dimension-publisher.js index 21ed532231..dffab93a85 100644 --- a/src/view/use-droppable-dimension-publisher/use-droppable-dimension-publisher.js +++ b/src/view/use-droppable-dimension-publisher/use-droppable-dimension-publisher.js @@ -25,6 +25,7 @@ import AppContext, { type AppContextValue } from '../context/app-context'; import withoutPlaceholder from './without-placeholder'; import { warning } from '../../dev-warning'; import getListenerOptions from './get-listener-options'; +import useRequiredContext from '../use-required-context'; type Props = {| droppableId: DroppableId, @@ -49,8 +50,7 @@ const getClosestScrollableFromDrag = (dragging: ?WhileDragging): ?Element => export default function useDroppableDimensionPublisher(args: Props) { const whileDraggingRef = useRef(null); - const appContext: ?AppContextValue = useContext(AppContext); - invariant(appContext, 'Could not find app content'); + const appContext: AppContextValue = useRequiredContext(AppContext); const marshal: DimensionMarshal = appContext.marshal; const { @@ -229,7 +229,9 @@ export default function useDroppableDimensionPublisher(args: Props) { getListenerOptions(dragging.scrollOptions), ); }, [onClosestScroll, scheduleScrollUpdate]); - const scroll = useCallback(() => {}); + const scroll = useCallback(() => { + invariant('TODO'); + }); const callbacks: DroppableCallbacks = useMemo( () => ({ From dd72662b34856f2aaecf6b9c4806ad566acecd31 Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Mon, 18 Mar 2019 16:12:53 +1100 Subject: [PATCH 025/117] drag drop context --- .../drag-drop-context/drag-drop-context.jsx | 13 +++---------- .../drag-drop-context/use-startup-validation.js | 17 +++++++++++++++++ src/view/draggable/draggable.jsx | 4 ++++ 3 files changed, 24 insertions(+), 10 deletions(-) create mode 100644 src/view/drag-drop-context/use-startup-validation.js diff --git a/src/view/drag-drop-context/drag-drop-context.jsx b/src/view/drag-drop-context/drag-drop-context.jsx index e4b32bf10d..8e08da4f14 100644 --- a/src/view/drag-drop-context/drag-drop-context.jsx +++ b/src/view/drag-drop-context/drag-drop-context.jsx @@ -33,12 +33,10 @@ import { collectionStarting, } from '../../state/action-creators'; import { getFormattedMessage } from '../../dev-warning'; -import { peerDependencies } from '../../../package.json'; -import checkReactVersion from './check-react-version'; -import checkDoctype from './check-doctype'; import isMovementAllowed from '../../state/is-movement-allowed'; import useAnnouncer from '../use-announcer'; import AppContext, { type AppContextValue } from '../context/app-context'; +import useStartupValidation from './use-startup-validation'; type Props = {| ...Responders, @@ -81,17 +79,12 @@ export function resetServerContext() { } export default function DragDropContext(props: Props) { + // We do not want this to change const uniqueId: number = useConstant((): number => count++); let storeRef: MutableRefObject; - // some validation when mounting - useEffect(() => { - if (process.env.NODE_ENV !== 'production') { - checkReactVersion(peerDependencies.react, React.version); - checkDoctype(document); - } - }, []); + useStartupValidation(); // lazy collection of responders using a ref - update on ever render const lastPropsRef = useRef(props); diff --git a/src/view/drag-drop-context/use-startup-validation.js b/src/view/drag-drop-context/use-startup-validation.js new file mode 100644 index 0000000000..863cc20901 --- /dev/null +++ b/src/view/drag-drop-context/use-startup-validation.js @@ -0,0 +1,17 @@ +// @flow +import React, { useEffect } from 'react'; +import { peerDependencies } from '../../../package.json'; +import checkReactVersion from './check-react-version'; +import checkDoctype from './check-doctype'; + +export default function useStartupValidation() { + // Only need to run these when mounting + useEffect(() => { + if (process.env.NODE_ENV === 'production') { + return; + } + + checkReactVersion(peerDependencies.react, React.version); + checkDoctype(document); + }, []); +} diff --git a/src/view/draggable/draggable.jsx b/src/view/draggable/draggable.jsx index 02c3baced7..1495d9f223 100644 --- a/src/view/draggable/draggable.jsx +++ b/src/view/draggable/draggable.jsx @@ -28,6 +28,7 @@ import DroppableContext, { type DroppableContextValue, } from '../context/droppable-context'; import useRequiredContext from '../use-required-context'; +import useValidation from './use-validation'; export default function Draggable(props: Props) { // reference to DOM node @@ -43,6 +44,9 @@ export default function Draggable(props: Props) { DroppableContext, ); + // Validating props and innerRef + useValidation(props, ref.current); + // props const { // ownProps From c09f29dfa6353af4f12a5a8205b8181f10daf6ae Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Mon, 18 Mar 2019 17:44:48 +1100 Subject: [PATCH 026/117] going hooks all the way down --- src/view/draggable/draggable.jsx | 2 +- src/view/draggable/use-validation.js | 7 +- src/view/droppable/droppable.jsx | 6 +- src/view/droppable/use-validation.js | 8 +- .../sensor/use-mouse-sensor.js | 335 +++++++++++++++++- src/view/use-drag-handle/use-drag-handle.js | 199 ++--------- .../use-drag-handle/use-drag-handle.old.js | 185 ++++++++++ 7 files changed, 567 insertions(+), 175 deletions(-) create mode 100644 src/view/use-drag-handle/use-drag-handle.old.js diff --git a/src/view/draggable/draggable.jsx b/src/view/draggable/draggable.jsx index 1495d9f223..f8c9f57ea2 100644 --- a/src/view/draggable/draggable.jsx +++ b/src/view/draggable/draggable.jsx @@ -45,7 +45,7 @@ export default function Draggable(props: Props) { ); // Validating props and innerRef - useValidation(props, ref.current); + useValidation(props, getRef); // props const { diff --git a/src/view/draggable/use-validation.js b/src/view/draggable/use-validation.js index cdf5991982..9abc5d52a1 100644 --- a/src/view/draggable/use-validation.js +++ b/src/view/draggable/use-validation.js @@ -17,13 +17,16 @@ function checkOwnProps(props: Props) { ); } -export default function useValidation(props: Props, el: ?HTMLElement) { +export default function useValidation( + props: Props, + getRef: () => ?HTMLElement, +) { // running after every update in development useEffect(() => { if (process.env.NODE_ENV === 'production') { return; } checkOwnProps(props); - checkIsValidInnerRef(el); + checkIsValidInnerRef(getRef()); }); } diff --git a/src/view/droppable/droppable.jsx b/src/view/droppable/droppable.jsx index 1a08c8b4e2..ef9583a0b6 100644 --- a/src/view/droppable/droppable.jsx +++ b/src/view/droppable/droppable.jsx @@ -27,7 +27,11 @@ export default function Droppable(props: Props) { const droppableRef = useRef(null); const placeholderRef = useRef(null); - useValidation(props, droppableRef.current, placeholderRef.current); + useValidation( + props, + () => droppableRef.current, + () => placeholderRef.current, + ); const { // own props diff --git a/src/view/droppable/use-validation.js b/src/view/droppable/use-validation.js index 34d61b2e65..00449e679e 100644 --- a/src/view/droppable/use-validation.js +++ b/src/view/droppable/use-validation.js @@ -41,8 +41,8 @@ function checkPlaceholderRef(props: Props, placeholderEl: ?HTMLElement) { export default function useValidation( props: Props, - droppableRef: ?HTMLElement, - placeholderRef: ?HTMLElement, + getDroppableRef: () => ?HTMLElement, + getPlaceholderRef: () => ?HTMLElement, ) { // Running on every update useEffect(() => { @@ -51,7 +51,7 @@ export default function useValidation( } checkOwnProps(props); - checkPlaceholderRef(props, placeholderRef); - checkIsValidInnerRef(droppableRef); + checkIsValidInnerRef(getDroppableRef()); + checkPlaceholderRef(props, getPlaceholderRef()); }); } diff --git a/src/view/use-drag-handle/sensor/use-mouse-sensor.js b/src/view/use-drag-handle/sensor/use-mouse-sensor.js index 91b6ace8cb..733ce28477 100644 --- a/src/view/use-drag-handle/sensor/use-mouse-sensor.js +++ b/src/view/use-drag-handle/sensor/use-mouse-sensor.js @@ -1,22 +1,339 @@ // @flow import type { Position } from 'css-box-model'; -import { useRef, useCallback } from 'react'; +import { useRef, useCallback, useMemo, useLayoutEffect } from 'react'; +import invariant from 'tiny-invariant'; +import type { EventBinding } from '../util/event-types'; +import createEventMarshal from '../util/create-event-marshal'; +import { bindEvents, unbindEvents } from '../util/bind-events'; +import createScheduler from '../util/create-scheduler'; +import { warning } from '../../../dev-warning'; +import * as keyCodes from '../../key-codes'; +import supportedPageVisibilityEventName from '../util/supported-page-visibility-event-name'; +import createPostDragEventPreventer from '../util/create-post-drag-event-preventer'; +import isSloppyClickThresholdExceeded from '../util/is-sloppy-click-threshold-exceeded'; +import preventStandardKeyEvents from '../util/prevent-standard-key-events'; +import type { Callbacks } from '../drag-handle-types'; -type Result = (event: MouseEvent) => void; +export type Args = {| + callbacks: Callbacks, + getDraggableRef: () => ?HTMLElement, + getWindow: () => HTMLElement, + canStartCapturing: (event: Event) => boolean, + getShouldRespectForceTouch: () => boolean, +|}; +export type Result = (event: MouseEvent) => void; + +// Custom event format for force press inputs +type MouseForceChangedEvent = MouseEvent & { + webkitForce?: number, +}; + +// https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/button +const primaryButton: number = 0; +const noop = () => {}; + +// shared management of mousedown without needing to call preventDefault() +const mouseDownMarshal: EventMarshal = createEventMarshal(); export default function useMouseSensor(args: Args): Result { + const { canStartCapturing, getWindow, callbacks } = args; const pendingRef = useRef(null); const isDraggingRef = useRef(false); + const unbindWindowEventsRef = useRef<() => void>(noop); + const getIsCapturing = useCallback( + () => Boolean(pendingRef.current || isDraggingRef.current), + [], + ); + const reset = useCallback(() => { + pendingRef.current = null; + isDraggingRef.current = false; + }, []); - const bindWindowEvents = useCallback((point: Position) => {}); + const schedule = useMemo(() => { + invariant( + !getIsCapturing(), + 'Should not recreate scheduler while capturing', + ); + return createScheduler(callbacks); + }, [callbacks, getIsCapturing]); - const startPendingDrag = useCallback((point: Position) => { - pendingRef.current = point; - }, []); + const postDragEventPreventer: EventPreventer = useMemo( + () => createPostDragEventPreventer(getWindow), + [getWindow], + ); + + const stop = useCallback( + (shouldBlockClick: ?boolean = true) => { + if (!getIsCapturing()) { + return; + } + + schedule.cancel(); + + unbindWindowEventsRef.current(); + mouseDownMarshal.reset(); + if (shouldBlockClick) { + postDragEventPreventer.preventNext(); + } + reset(); + }, + [getIsCapturing, postDragEventPreventer, reset, schedule], + ); + + const cancel = useCallback(() => { + const wasDragging: boolean = isDraggingRef.current; + stop(); + + if (wasDragging) { + callbacks.onCancel(); + } + }, [callbacks, stop]); + + const startDragging = useCallback(() => { + invariant(!isDraggingRef.current, 'Cannot start a drag while dragging'); + const pending: ?Position = pendingRef.current; + invariant(pending, 'Cannot start a drag without a pending drag'); + + pendingRef.current = null; + isDraggingRef.current = true; + + callbacks.onLift({ + clientSelection: pending, + movementMode: 'FLUID', + }); + }, [callbacks]); + + const windowBindings: EventBinding[] = useMemo(() => { + invariant( + !getIsCapturing(), + 'Should not recreate window bindings while capturing', + ); + + const bindings: EventBinding[] = [ + { + eventName: 'mousemove', + fn: (event: MouseEvent) => { + const { button, clientX, clientY } = event; + if (button !== primaryButton) { + return; + } + + const point: Position = { + x: clientX, + y: clientY, + }; + + // Already dragging + if (isDraggingRef.current) { + // preventing default as we are using this event + event.preventDefault(); + schedule.move(point); + return; + } + + // There should be a pending drag at this point + const pending: ?Position = pendingRef.current; + + if (!pending) { + // this should be an impossible state + // we cannot use kill directly as it checks if there is a pending drag + stop(); + invariant( + false, + 'Expected there to be an active or pending drag when window mousemove event is received', + ); + } + + // threshold not yet exceeded + if (!isSloppyClickThresholdExceeded(pending, point)) { + return; + } + + // preventing default as we are using this event + event.preventDefault(); + startDragging(); + }, + }, + { + eventName: 'mouseup', + fn: (event: MouseEvent) => { + stop(); + + if (isDraggingRef.current) { + // preventing default as we are using this event + event.preventDefault(); + callbacks.onDrop(); + } + }, + }, + { + eventName: 'mousedown', + fn: (event: MouseEvent) => { + // this can happen during a drag when the user clicks a button + // other than the primary mouse button + if (isDraggingRef.current) { + event.preventDefault(); + } + + cancel(); + }, + }, + { + eventName: 'keydown', + fn: (event: KeyboardEvent) => { + // Abort if any keystrokes while a drag is pending + if (pendingRef.current) { + stop(); + return; + } + + // cancelling a drag + if (event.keyCode === keyCodes.escape) { + event.preventDefault(); + cancel(); + return; + } + + preventStandardKeyEvents(event); + }, + }, + { + eventName: 'resize', + fn: cancel, + }, + { + eventName: 'scroll', + // ## Passive: true + // Eventual consistency is fine because we use position: fixed on the item + // ## Capture: false + // Scroll events on elements do not bubble, but they go through the capture phase + // https://twitter.com/alexandereardon/status/985994224867819520 + // Using capture: false here as we want to avoid intercepting droppable scroll requests + // TODO: can result in awkward drop position + options: { passive: true, capture: false }, + fn: (event: UIEvent) => { + // IE11 fix: + // Scrollable events still bubble up and are caught by this handler in ie11. + // We can ignore this event + if (event.currentTarget !== getWindow()) { + return; + } + + // stop a pending drag + if (pendingRef.current) { + stop(); + return; + } + // getCallbacks().onWindowScroll(); + schedule.windowScrollMove(); + }, + }, + // Need to opt out of dragging if the user is a force press + // Only for safari which has decided to introduce its own custom way of doing things + // https://developer.apple.com/library/content/documentation/AppleApplications/Conceptual/SafariJSProgTopics/RespondingtoForceTouchEventsfromJavaScript.html + { + eventName: 'webkitmouseforcechanged', + fn: (event: MouseForceChangedEvent) => { + if ( + event.webkitForce == null || + (MouseEvent: any).WEBKIT_FORCE_AT_FORCE_MOUSE_DOWN == null + ) { + warning( + 'handling a mouse force changed event when it is not supported', + ); + return; + } + + const forcePressThreshold: number = (MouseEvent: any) + .WEBKIT_FORCE_AT_FORCE_MOUSE_DOWN; + const isForcePressing: boolean = + event.webkitForce >= forcePressThreshold; + + if (isForcePressing) { + // it is considered a indirect cancel so we do not + // prevent default in any situation. + cancel(); + } + }, + }, + // Cancel on page visibility change + { + eventName: supportedPageVisibilityEventName, + fn: cancel, + }, + ]; + return bindings; + }, [ + callbacks, + cancel, + getWindow, + getIsCapturing, + schedule, + startDragging, + stop, + ]); + + const bindWindowEvents = useCallback(() => { + const win: HTMLElement = getWindow(); + const options = { capture: true }; + + // setting up our unbind before we bind + unbindWindowEventsRef.current = () => + unbindEvents(win, windowBindings, options); + + bindEvents(win, windowBindings, options); + }, [getWindow, windowBindings]); + + const startPendingDrag = useCallback( + (point: Position) => { + invariant(!pendingRef.current, 'Expected there to be no pending drag'); + pendingRef.current = point; + bindWindowEvents(); + }, + [bindWindowEvents], + ); const onMouseDown = useCallback( (event: MouseEvent) => { + if (mouseDownMarshal.isHandled()) { + return; + } + + invariant( + !getIsCapturing(), + 'Should not be able to perform a mouse down while a drag or pending drag is occurring', + ); + + // We do not need to prevent the event on a dropping draggable as + // the mouse down event will not fire due to pointer-events: none + // https://codesandbox.io/s/oxo0o775rz + if (!canStartCapturing(event)) { + return; + } + + // only starting a drag if dragging with the primary mouse button + if (event.button !== primaryButton) { + return; + } + + // Do not start a drag if any modifier key is pressed + if (event.ctrlKey || event.metaKey || event.shiftKey || event.altKey) { + return; + } + + // Registering that this event has been handled. + // This is to prevent parent draggables using this event + // to start also. + // Ideally we would not use preventDefault() as we are not sure + // if this mouse down is part of a drag interaction + // Unfortunately we do to prevent the element obtaining focus (see below). + mouseDownMarshal.handle(); + + // Unfortunately we do need to prevent the drag handle from getting focus on mousedown. + // This goes against our policy on not blocking events before a drag has started. + // See [How we use dom events](/docs/guides/how-we-use-dom-events.md). event.preventDefault(); + const point: Position = { x: event.clientX, y: event.clientY, @@ -24,8 +341,12 @@ export default function useMouseSensor(args: Args): Result { startPendingDrag(point); }, - [startPendingDrag], + [canStartCapturing, getIsCapturing, startPendingDrag], ); + useLayoutEffect(() => { + // TODO: do logic for unmounting + }, []); + return onMouseDown; } diff --git a/src/view/use-drag-handle/use-drag-handle.js b/src/view/use-drag-handle/use-drag-handle.js index 4efc8eb5ae..984b1a5bef 100644 --- a/src/view/use-drag-handle/use-drag-handle.js +++ b/src/view/use-drag-handle/use-drag-handle.js @@ -1,22 +1,12 @@ // @flow -import { useLayoutEffect, useRef } from 'react'; -import type { - Sensor, - CreateSensorArgs, - MouseSensor, - KeyboardSensor, - TouchSensor, -} from './sensor/sensor-types'; +import { useLayoutEffect, useRef, useMemo, useCallback } from 'react'; import type { Args, DragHandleProps, Callbacks } from './drag-handle-types'; -import { useConstant, useConstantFn } from '../use-constant'; -import createKeyboardSensor from './sensor/create-keyboard-sensor'; -import createTouchSensor from './sensor/create-touch-sensor'; -import { warning } from '../../dev-warning'; -import shouldAllowDraggingFromTarget from './util/should-allow-dragging-from-target'; import getWindowFromEl from '../window/get-window-from-el'; import useRequiredContext from '../use-required-context'; import AppContext, { type AppContextValue } from '../context/app-context'; -import createMouseSensor from './sensor/create-mouse-sensor'; +import useMouseSensor, { + type Args as MouseSensorArgs, +} from './sensor/use-mouse-sensor'; function preventHtml5Dnd(event: DragEvent) { event.preventDefault(); @@ -24,162 +14,51 @@ function preventHtml5Dnd(event: DragEvent) { export default function useDragHandle(args: Args): DragHandleProps { const { canLift, style }: AppContextValue = useRequiredContext(AppContext); + const { callbacks, getDraggableRef, getShouldRespectForceTouch } = args; - // side effect on every render - const latestArgsRef = useRef(args); - latestArgsRef.current = args; - const getLatestArgs = useConstantFn(() => latestArgsRef.current); - - // TODO: things will go bad if getDraggableRef changes - const getWindow = useConstantFn( - (): HTMLElement => getWindowFromEl(getLatestArgs().getDraggableRef()), + const getWindow = useCallback( + (): HTMLElement => getWindowFromEl(getDraggableRef()), + [getDraggableRef], ); const isFocusedRef = useRef(false); - const onFocus = useConstantFn(() => { + const onFocus = useCallback(() => { isFocusedRef.current = true; - }); - const onBlur = useConstantFn(() => { + }, []); + const onBlur = useCallback(() => { isFocusedRef.current = false; - }); - - const isAnySensorCapturing = useConstantFn(() => - // using getSensors before it is defined - // eslint-disable-next-line no-use-before-define - getSensors().some((sensor: Sensor): boolean => sensor.isCapturing()), - ); - - const canStartCapturing = useConstantFn((event: Event) => { - // this might be before a drag has started - isolated to this element - if (isAnySensorCapturing()) { - return false; - } - - // this will check if anything else in the system is dragging - if (!canLift(getLatestArgs().draggableId)) { - return false; - } - - // check if we are dragging an interactive element - return shouldAllowDraggingFromTarget( - event, - getLatestArgs().canDragInteractiveElements, - ); - }); + }, []); - const getCallbacks = useConstantFn( - (): Callbacks => getLatestArgs().callbacks, + const mouseArgs: MouseSensorArgs = useMemo( + () => ({ + callbacks, + getDraggableRef, + getWindow, + canStartCapturing: () => true, + getShouldRespectForceTouch, + }), + [callbacks, getDraggableRef, getShouldRespectForceTouch, getWindow], ); - const createArgs: CreateSensorArgs = useConstant(() => ({ - getCallbacks, - getDraggableRef: getLatestArgs().getDraggableRef, - canStartCapturing, - getWindow, - getShouldRespectForceTouch: getLatestArgs().getShouldRespectForceTouch, - })); - - const mouse: MouseSensor = useConstant(() => createMouseSensor(createArgs)); - const keyboard: KeyboardSensor = useConstant(() => - createKeyboardSensor(createArgs), + const onMouseDown = useMouseSensor(mouseArgs); + + const props: DragHandleProps = useMemo( + () => ({ + onMouseDown, + // TODO + onKeyDown: () => {}, + onTouchStart: () => {}, + onFocus, + onBlur, + tabIndex: 0, + 'data-react-beautiful-dnd-drag-handle': style, + // English default. Consumers are welcome to add their own start instruction + 'aria-roledescription': 'Draggable item. Press space bar to lift', + draggable: false, + onDragStart: preventHtml5Dnd, + }), + [onBlur, onFocus, onMouseDown, style], ); - const touch: TouchSensor = useConstant(() => createTouchSensor(createArgs)); - const getSensors = useConstantFn(() => [mouse, keyboard, touch]); - - const onKeyDown = useConstantFn((event: KeyboardEvent) => { - // let the other sensors deal with it - if (mouse.isCapturing() || touch.isCapturing()) { - return; - } - - keyboard.onKeyDown(event); - }); - - const onMouseDown = useConstantFn((event: MouseEvent) => { - // let the other sensors deal with it - if (keyboard.isCapturing() || touch.isCapturing()) { - return; - } - - mouse.onMouseDown(event); - }); - - const onTouchStart = useConstantFn((event: TouchEvent) => { - // let the keyboard sensor deal with it - if (mouse.isCapturing() || keyboard.isCapturing()) { - return; - } - - touch.onTouchStart(event); - }); - - // TODO: focus retention - useLayoutEffect(() => {}); - - // Cleanup any capturing sensors when unmounting - useLayoutEffect(() => { - // Just a cleanup function - return () => { - getSensors().forEach((sensor: Sensor) => { - // kill the current drag and fire a cancel event if - const wasDragging: boolean = sensor.isDragging(); - - sensor.unmount(); - // Cancel if drag was occurring - if (wasDragging) { - latestArgsRef.current.callbacks.onCancel(); - } - }); - }; - // sensors is constant - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - - // Checking for disabled changes during a drag - useLayoutEffect(() => { - getSensors().forEach((sensor: Sensor) => { - if (!sensor.isCapturing()) { - return; - } - const wasDragging: boolean = sensor.isDragging(); - sensor.kill(); - - // It is fine for a draggable to be disabled while a drag is pending - if (wasDragging) { - warning( - 'You have disabled dragging on a Draggable while it was dragging. The drag has been cancelled', - ); - getLatestArgs().callbacks.onCancel(); - } - }); - }, [getLatestArgs, getSensors]); - - // Drag aborted elsewhere in application - useLayoutEffect(() => { - if (getLatestArgs().isDragging) { - return; - } - - getSensors().forEach((sensor: Sensor) => { - if (sensor.isCapturing()) { - sensor.kill(); - } - }); - }, [getLatestArgs, getSensors]); - - const props: DragHandleProps = useConstant(() => ({ - onMouseDown, - onKeyDown, - onTouchStart, - onFocus, - onBlur, - tabIndex: 0, - 'data-react-beautiful-dnd-drag-handle': style, - // English default. Consumers are welcome to add their own start instruction - 'aria-roledescription': 'Draggable item. Press space bar to lift', - draggable: false, - onDragStart: preventHtml5Dnd, - })); return props; } diff --git a/src/view/use-drag-handle/use-drag-handle.old.js b/src/view/use-drag-handle/use-drag-handle.old.js new file mode 100644 index 0000000000..4efc8eb5ae --- /dev/null +++ b/src/view/use-drag-handle/use-drag-handle.old.js @@ -0,0 +1,185 @@ +// @flow +import { useLayoutEffect, useRef } from 'react'; +import type { + Sensor, + CreateSensorArgs, + MouseSensor, + KeyboardSensor, + TouchSensor, +} from './sensor/sensor-types'; +import type { Args, DragHandleProps, Callbacks } from './drag-handle-types'; +import { useConstant, useConstantFn } from '../use-constant'; +import createKeyboardSensor from './sensor/create-keyboard-sensor'; +import createTouchSensor from './sensor/create-touch-sensor'; +import { warning } from '../../dev-warning'; +import shouldAllowDraggingFromTarget from './util/should-allow-dragging-from-target'; +import getWindowFromEl from '../window/get-window-from-el'; +import useRequiredContext from '../use-required-context'; +import AppContext, { type AppContextValue } from '../context/app-context'; +import createMouseSensor from './sensor/create-mouse-sensor'; + +function preventHtml5Dnd(event: DragEvent) { + event.preventDefault(); +} + +export default function useDragHandle(args: Args): DragHandleProps { + const { canLift, style }: AppContextValue = useRequiredContext(AppContext); + + // side effect on every render + const latestArgsRef = useRef(args); + latestArgsRef.current = args; + const getLatestArgs = useConstantFn(() => latestArgsRef.current); + + // TODO: things will go bad if getDraggableRef changes + const getWindow = useConstantFn( + (): HTMLElement => getWindowFromEl(getLatestArgs().getDraggableRef()), + ); + + const isFocusedRef = useRef(false); + const onFocus = useConstantFn(() => { + isFocusedRef.current = true; + }); + const onBlur = useConstantFn(() => { + isFocusedRef.current = false; + }); + + const isAnySensorCapturing = useConstantFn(() => + // using getSensors before it is defined + // eslint-disable-next-line no-use-before-define + getSensors().some((sensor: Sensor): boolean => sensor.isCapturing()), + ); + + const canStartCapturing = useConstantFn((event: Event) => { + // this might be before a drag has started - isolated to this element + if (isAnySensorCapturing()) { + return false; + } + + // this will check if anything else in the system is dragging + if (!canLift(getLatestArgs().draggableId)) { + return false; + } + + // check if we are dragging an interactive element + return shouldAllowDraggingFromTarget( + event, + getLatestArgs().canDragInteractiveElements, + ); + }); + + const getCallbacks = useConstantFn( + (): Callbacks => getLatestArgs().callbacks, + ); + + const createArgs: CreateSensorArgs = useConstant(() => ({ + getCallbacks, + getDraggableRef: getLatestArgs().getDraggableRef, + canStartCapturing, + getWindow, + getShouldRespectForceTouch: getLatestArgs().getShouldRespectForceTouch, + })); + + const mouse: MouseSensor = useConstant(() => createMouseSensor(createArgs)); + const keyboard: KeyboardSensor = useConstant(() => + createKeyboardSensor(createArgs), + ); + const touch: TouchSensor = useConstant(() => createTouchSensor(createArgs)); + const getSensors = useConstantFn(() => [mouse, keyboard, touch]); + + const onKeyDown = useConstantFn((event: KeyboardEvent) => { + // let the other sensors deal with it + if (mouse.isCapturing() || touch.isCapturing()) { + return; + } + + keyboard.onKeyDown(event); + }); + + const onMouseDown = useConstantFn((event: MouseEvent) => { + // let the other sensors deal with it + if (keyboard.isCapturing() || touch.isCapturing()) { + return; + } + + mouse.onMouseDown(event); + }); + + const onTouchStart = useConstantFn((event: TouchEvent) => { + // let the keyboard sensor deal with it + if (mouse.isCapturing() || keyboard.isCapturing()) { + return; + } + + touch.onTouchStart(event); + }); + + // TODO: focus retention + useLayoutEffect(() => {}); + + // Cleanup any capturing sensors when unmounting + useLayoutEffect(() => { + // Just a cleanup function + return () => { + getSensors().forEach((sensor: Sensor) => { + // kill the current drag and fire a cancel event if + const wasDragging: boolean = sensor.isDragging(); + + sensor.unmount(); + // Cancel if drag was occurring + if (wasDragging) { + latestArgsRef.current.callbacks.onCancel(); + } + }); + }; + // sensors is constant + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + // Checking for disabled changes during a drag + useLayoutEffect(() => { + getSensors().forEach((sensor: Sensor) => { + if (!sensor.isCapturing()) { + return; + } + const wasDragging: boolean = sensor.isDragging(); + sensor.kill(); + + // It is fine for a draggable to be disabled while a drag is pending + if (wasDragging) { + warning( + 'You have disabled dragging on a Draggable while it was dragging. The drag has been cancelled', + ); + getLatestArgs().callbacks.onCancel(); + } + }); + }, [getLatestArgs, getSensors]); + + // Drag aborted elsewhere in application + useLayoutEffect(() => { + if (getLatestArgs().isDragging) { + return; + } + + getSensors().forEach((sensor: Sensor) => { + if (sensor.isCapturing()) { + sensor.kill(); + } + }); + }, [getLatestArgs, getSensors]); + + const props: DragHandleProps = useConstant(() => ({ + onMouseDown, + onKeyDown, + onTouchStart, + onFocus, + onBlur, + tabIndex: 0, + 'data-react-beautiful-dnd-drag-handle': style, + // English default. Consumers are welcome to add their own start instruction + 'aria-roledescription': 'Draggable item. Press space bar to lift', + draggable: false, + onDragStart: preventHtml5Dnd, + })); + + return props; +} From 379addfff01a3afbd5f9a31be0464871e83ea001 Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Tue, 19 Mar 2019 09:04:03 +1100 Subject: [PATCH 027/117] validation for droppable --- src/view/draggable/draggable.jsx | 6 +- src/view/droppable/droppable.jsx | 15 +++-- src/view/droppable/use-validation.js | 16 ++++- src/view/placeholder/placeholder.jsx | 67 +++++++++---------- .../use-animate-in-out/use-animate-in-out.js | 7 +- .../sensor/use-mouse-sensor.js | 3 +- src/view/use-drag-handle/use-drag-handle.js | 8 ++- .../use-droppable-dimension-publisher.js | 23 ++++--- 8 files changed, 82 insertions(+), 63 deletions(-) diff --git a/src/view/draggable/draggable.jsx b/src/view/draggable/draggable.jsx index f8c9f57ea2..54bffee067 100644 --- a/src/view/draggable/draggable.jsx +++ b/src/view/draggable/draggable.jsx @@ -95,11 +95,9 @@ export default function Draggable(props: Props) { // The Drag handle const onLift = useCallback( - () => (options: { - clientSelection: Position, - movementMode: MovementMode, - }) => { + (options: { clientSelection: Position, movementMode: MovementMode }) => { timings.start('LIFT'); + console.warn('ON_LIFT'); const el: ?HTMLElement = ref.current; invariant(el); invariant(!isDragDisabled, 'Cannot lift a Draggable when it is disabled'); diff --git a/src/view/droppable/droppable.jsx b/src/view/droppable/droppable.jsx index ef9583a0b6..c3565d0b3f 100644 --- a/src/view/droppable/droppable.jsx +++ b/src/view/droppable/droppable.jsx @@ -27,11 +27,7 @@ export default function Droppable(props: Props) { const droppableRef = useRef(null); const placeholderRef = useRef(null); - useValidation( - props, - () => droppableRef.current, - () => placeholderRef.current, - ); + // Note: Running validation at the end as it uses some placeholder things const { // own props @@ -126,6 +122,15 @@ export default function Droppable(props: Props) { [droppableId, type], ); + useValidation({ + props, + getDroppableRef: () => droppableRef.current, + // Not checking on the first placement :( + // The droppable placeholder is not set yet as the useLayoutEffect + setState has not finished + shouldCheckPlaceholder: Boolean(instruction), + getPlaceholderRef: () => placeholderRef.current, + }); + return ( {children(provided, snapshot)} diff --git a/src/view/droppable/use-validation.js b/src/view/droppable/use-validation.js index 00449e679e..489044a596 100644 --- a/src/view/droppable/use-validation.js +++ b/src/view/droppable/use-validation.js @@ -39,11 +39,19 @@ function checkPlaceholderRef(props: Props, placeholderEl: ?HTMLElement) { `); } -export default function useValidation( +type Args = {| props: Props, getDroppableRef: () => ?HTMLElement, + shouldCheckPlaceholder: boolean, getPlaceholderRef: () => ?HTMLElement, -) { +|}; + +export default function useValidation({ + props, + getDroppableRef, + shouldCheckPlaceholder, + getPlaceholderRef, +}: Args) { // Running on every update useEffect(() => { if (process.env.NODE_ENV === 'production') { @@ -52,6 +60,8 @@ export default function useValidation( checkOwnProps(props); checkIsValidInnerRef(getDroppableRef()); - checkPlaceholderRef(props, getPlaceholderRef()); + if (shouldCheckPlaceholder) { + checkPlaceholderRef(props, getPlaceholderRef()); + } }); } diff --git a/src/view/placeholder/placeholder.jsx b/src/view/placeholder/placeholder.jsx index 971aafc502..b277b24d0c 100644 --- a/src/view/placeholder/placeholder.jsx +++ b/src/view/placeholder/placeholder.jsx @@ -1,5 +1,10 @@ // @flow -import React, { useState, useRef, useEffect } from 'react'; +import React, { + useState, + useCallback, + useEffect, + useLayoutEffect, +} from 'react'; import type { Spacing } from 'css-box-model'; import type { Placeholder as PlaceholderType, @@ -109,49 +114,37 @@ const getStyle = ({ }; }; -const Placeholder = React.memo(function Placeholder(props: Props) { - const mountTimer = useRef(null); - +function Placeholder(props: Props) { + const { animate, onTransitionEnd, onClose } = props; const [isAnimatingOpenOnMount, setIsAnimatingOpenOnMount] = useState( props.animate === 'open', ); + // will run after a render is flushed useEffect(() => { - const clear = () => { - if (mountTimer.current) { - clearTimeout(mountTimer.current); - mountTimer.current = null; - } - }; - - if (!isAnimatingOpenOnMount) { - return clear; - } - - mountTimer.current = setTimeout(() => { - mountTimer.current = null; - + if (isAnimatingOpenOnMount) { setIsAnimatingOpenOnMount(false); - }); - - return clear; + } }, [isAnimatingOpenOnMount]); - const onTransitionEnd = (event: TransitionEvent) => { - // We transition height, width and margin - // each of those transitions will independently call this callback - // Because they all have the same duration we can just respond to one of them - // 'height' was chosen for no particular reason :D - if (event.propertyName !== 'height') { - return; - } + const onSizeChangeEnd = useCallback( + (event: TransitionEvent) => { + // We transition height, width and margin + // each of those transitions will independently call this callback + // Because they all have the same duration we can just respond to one of them + // 'height' was chosen for no particular reason :D + if (event.propertyName !== 'height') { + return; + } - props.onTransitionEnd(); + onTransitionEnd(); - if (props.animate === 'close') { - props.onClose(); - } - }; + if (animate === 'close') { + onClose(); + } + }, + [animate, onClose, onTransitionEnd], + ); const style: PlaceholderStyle = getStyle({ isAnimatingOpenOnMount, @@ -161,9 +154,9 @@ const Placeholder = React.memo(function Placeholder(props: Props) { return React.createElement(props.placeholder.tagName, { style, - onTransitionEnd, + onTransitionEnd: onSizeChangeEnd, ref: props.innerRef, }); -}); +} -export default Placeholder; +export default React.memo(Placeholder); diff --git a/src/view/use-animate-in-out/use-animate-in-out.js b/src/view/use-animate-in-out/use-animate-in-out.js index dd481cb72f..4ab3eb6454 100644 --- a/src/view/use-animate-in-out/use-animate-in-out.js +++ b/src/view/use-animate-in-out/use-animate-in-out.js @@ -1,5 +1,5 @@ // @flow -import { useMemo, useCallback, useEffect, useState } from 'react'; +import { useMemo, useCallback, useLayoutEffect, useState } from 'react'; import type { InOutAnimationMode } from '../../types'; export type AnimateProvided = {| @@ -20,7 +20,10 @@ export default function useAnimateInOut(args: Args): ?AnimateProvided { args.shouldAnimate && args.on ? 'open' : 'none', ); - useEffect(() => { + console.warn('useAnimateInOut: render'); + + useLayoutEffect(() => { + console.warn('useAnimateInOut: layout effect'); if (!args.shouldAnimate) { setIsVisible(Boolean(args.on)); setData(args.on); diff --git a/src/view/use-drag-handle/sensor/use-mouse-sensor.js b/src/view/use-drag-handle/sensor/use-mouse-sensor.js index 733ce28477..62897dfcdd 100644 --- a/src/view/use-drag-handle/sensor/use-mouse-sensor.js +++ b/src/view/use-drag-handle/sensor/use-mouse-sensor.js @@ -157,9 +157,10 @@ export default function useMouseSensor(args: Args): Result { { eventName: 'mouseup', fn: (event: MouseEvent) => { + const wasDragging: boolean = isDraggingRef.current; stop(); - if (isDraggingRef.current) { + if (wasDragging) { // preventing default as we are using this event event.preventDefault(); callbacks.onDrop(); diff --git a/src/view/use-drag-handle/use-drag-handle.js b/src/view/use-drag-handle/use-drag-handle.js index 984b1a5bef..0dbac40c6b 100644 --- a/src/view/use-drag-handle/use-drag-handle.js +++ b/src/view/use-drag-handle/use-drag-handle.js @@ -13,7 +13,9 @@ function preventHtml5Dnd(event: DragEvent) { } export default function useDragHandle(args: Args): DragHandleProps { - const { canLift, style }: AppContextValue = useRequiredContext(AppContext); + const { canLift, style: styleContext }: AppContextValue = useRequiredContext( + AppContext, + ); const { callbacks, getDraggableRef, getShouldRespectForceTouch } = args; const getWindow = useCallback( @@ -51,13 +53,13 @@ export default function useDragHandle(args: Args): DragHandleProps { onFocus, onBlur, tabIndex: 0, - 'data-react-beautiful-dnd-drag-handle': style, + 'data-react-beautiful-dnd-drag-handle': styleContext, // English default. Consumers are welcome to add their own start instruction 'aria-roledescription': 'Draggable item. Press space bar to lift', draggable: false, onDragStart: preventHtml5Dnd, }), - [onBlur, onFocus, onMouseDown, style], + [onBlur, onFocus, onMouseDown, styleContext], ); return props; diff --git a/src/view/use-droppable-dimension-publisher/use-droppable-dimension-publisher.js b/src/view/use-droppable-dimension-publisher/use-droppable-dimension-publisher.js index dffab93a85..e6439aae2c 100644 --- a/src/view/use-droppable-dimension-publisher/use-droppable-dimension-publisher.js +++ b/src/view/use-droppable-dimension-publisher/use-droppable-dimension-publisher.js @@ -1,5 +1,12 @@ // @flow -import { useCallback, useMemo, useEffect, useContext, useRef } from 'react'; +import { + useCallback, + useMemo, + useEffect, + useContext, + useLayoutEffect, + useRef, +} from 'react'; import invariant from 'tiny-invariant'; import { type Position } from 'css-box-model'; import rafSchedule from 'raf-schd'; @@ -231,22 +238,22 @@ export default function useDroppableDimensionPublisher(args: Props) { }, [onClosestScroll, scheduleScrollUpdate]); const scroll = useCallback(() => { invariant('TODO'); - }); + }, []); - const callbacks: DroppableCallbacks = useMemo( - () => ({ + const callbacks: DroppableCallbacks = useMemo(() => { + console.log('breaking callbacks memo'); + return { getDimensionAndWatchScroll, recollect, dragStopped, scroll, - }), - [dragStopped, getDimensionAndWatchScroll, recollect, scroll], - ); + }; + }, [dragStopped, getDimensionAndWatchScroll, recollect, scroll]); // Register with the marshal and let it know of: // - any descriptor changes // - when it unmounts - useEffect(() => { + useLayoutEffect(() => { marshal.registerDroppable(descriptor, callbacks); return () => { From 437ac051c0dff7e76105e4624e1acf3f73594dbe Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Tue, 19 Mar 2019 09:04:41 +1100 Subject: [PATCH 028/117] removing some logging --- src/view/draggable/draggable.jsx | 1 - src/view/use-animate-in-out/use-animate-in-out.js | 3 --- 2 files changed, 4 deletions(-) diff --git a/src/view/draggable/draggable.jsx b/src/view/draggable/draggable.jsx index 54bffee067..68a9e74735 100644 --- a/src/view/draggable/draggable.jsx +++ b/src/view/draggable/draggable.jsx @@ -97,7 +97,6 @@ export default function Draggable(props: Props) { const onLift = useCallback( (options: { clientSelection: Position, movementMode: MovementMode }) => { timings.start('LIFT'); - console.warn('ON_LIFT'); const el: ?HTMLElement = ref.current; invariant(el); invariant(!isDragDisabled, 'Cannot lift a Draggable when it is disabled'); diff --git a/src/view/use-animate-in-out/use-animate-in-out.js b/src/view/use-animate-in-out/use-animate-in-out.js index 4ab3eb6454..9f8eb0bb70 100644 --- a/src/view/use-animate-in-out/use-animate-in-out.js +++ b/src/view/use-animate-in-out/use-animate-in-out.js @@ -20,10 +20,7 @@ export default function useAnimateInOut(args: Args): ?AnimateProvided { args.shouldAnimate && args.on ? 'open' : 'none', ); - console.warn('useAnimateInOut: render'); - useLayoutEffect(() => { - console.warn('useAnimateInOut: layout effect'); if (!args.shouldAnimate) { setIsVisible(Boolean(args.on)); setData(args.on); From 96a1792077c6327918de34347ce91004f5627820 Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Tue, 19 Mar 2019 09:54:32 +1100 Subject: [PATCH 029/117] aborting drags --- src/state/create-store.js | 2 +- .../sensor/use-mouse-sensor.js | 26 ++++-- src/view/use-drag-handle/use-drag-handle.js | 81 +++++++++++++++++-- 3 files changed, 97 insertions(+), 12 deletions(-) diff --git a/src/state/create-store.js b/src/state/create-store.js index 1bca665478..dd55a4c5b8 100644 --- a/src/state/create-store.js +++ b/src/state/create-store.js @@ -11,7 +11,7 @@ import dimensionMarshalStopper from './middleware/dimension-marshal-stopper'; import autoScroll from './middleware/auto-scroll'; import pendingDrop from './middleware/pending-drop'; import type { DimensionMarshal } from './dimension-marshal/dimension-marshal-types'; -import type { StyleMarshal } from '../view/style-marshal/style-marshal-types'; +import type { StyleMarshal } from '../view/use-style-marshal/style-marshal-types'; import type { AutoScroller } from './auto-scroller/auto-scroller-types'; import type { Responders, Announce } from '../types'; import type { Store } from './store-types'; diff --git a/src/view/use-drag-handle/sensor/use-mouse-sensor.js b/src/view/use-drag-handle/sensor/use-mouse-sensor.js index 62897dfcdd..b890794c07 100644 --- a/src/view/use-drag-handle/sensor/use-mouse-sensor.js +++ b/src/view/use-drag-handle/sensor/use-mouse-sensor.js @@ -3,13 +3,17 @@ import type { Position } from 'css-box-model'; import { useRef, useCallback, useMemo, useLayoutEffect } from 'react'; import invariant from 'tiny-invariant'; import type { EventBinding } from '../util/event-types'; -import createEventMarshal from '../util/create-event-marshal'; +import createEventMarshal, { + type EventMarshal, +} from '../util/create-event-marshal'; import { bindEvents, unbindEvents } from '../util/bind-events'; import createScheduler from '../util/create-scheduler'; import { warning } from '../../../dev-warning'; import * as keyCodes from '../../key-codes'; import supportedPageVisibilityEventName from '../util/supported-page-visibility-event-name'; -import createPostDragEventPreventer from '../util/create-post-drag-event-preventer'; +import createPostDragEventPreventer, { + type EventPreventer, +} from '../util/create-post-drag-event-preventer'; import isSloppyClickThresholdExceeded from '../util/is-sloppy-click-threshold-exceeded'; import preventStandardKeyEvents from '../util/prevent-standard-key-events'; import type { Callbacks } from '../drag-handle-types'; @@ -20,8 +24,12 @@ export type Args = {| getWindow: () => HTMLElement, canStartCapturing: (event: Event) => boolean, getShouldRespectForceTouch: () => boolean, + shouldAbortCapture: boolean, |}; -export type Result = (event: MouseEvent) => void; +export type Result = { + onMouseDown: (event: MouseEvent) => void, + isCapturing: boolean, +}; // Custom event format for force press inputs type MouseForceChangedEvent = MouseEvent & { @@ -36,7 +44,7 @@ const noop = () => {}; const mouseDownMarshal: EventMarshal = createEventMarshal(); export default function useMouseSensor(args: Args): Result { - const { canStartCapturing, getWindow, callbacks } = args; + const { canStartCapturing, getWindow, callbacks, shouldAbortCapture } = args; const pendingRef = useRef(null); const isDraggingRef = useRef(false); const unbindWindowEventsRef = useRef<() => void>(noop); @@ -80,6 +88,11 @@ export default function useMouseSensor(args: Args): Result { [getIsCapturing, postDragEventPreventer, reset, schedule], ); + // instructed to stop capturing + if (shouldAbortCapture && getIsCapturing()) { + stop(); + } + const cancel = useCallback(() => { const wasDragging: boolean = isDraggingRef.current; stop(); @@ -349,5 +362,8 @@ export default function useMouseSensor(args: Args): Result { // TODO: do logic for unmounting }, []); - return onMouseDown; + return { + onMouseDown, + isCapturing: getIsCapturing(), + }; } diff --git a/src/view/use-drag-handle/use-drag-handle.js b/src/view/use-drag-handle/use-drag-handle.js index 0dbac40c6b..d82a9804b5 100644 --- a/src/view/use-drag-handle/use-drag-handle.js +++ b/src/view/use-drag-handle/use-drag-handle.js @@ -1,22 +1,40 @@ // @flow -import { useLayoutEffect, useRef, useMemo, useCallback } from 'react'; -import type { Args, DragHandleProps, Callbacks } from './drag-handle-types'; +import { useLayoutEffect, useRef, useMemo, useState, useCallback } from 'react'; +import type { Args, DragHandleProps } from './drag-handle-types'; import getWindowFromEl from '../window/get-window-from-el'; import useRequiredContext from '../use-required-context'; import AppContext, { type AppContextValue } from '../context/app-context'; import useMouseSensor, { type Args as MouseSensorArgs, } from './sensor/use-mouse-sensor'; +import shouldAllowDraggingFromTarget from './util/should-allow-dragging-from-target'; function preventHtml5Dnd(event: DragEvent) { event.preventDefault(); } export default function useDragHandle(args: Args): DragHandleProps { + // Capturing + const isAnythingCapturingRef = useRef(false); + const [shouldAbortCapture, setShouldAbortCapture] = useState(false); + const recordCapture = useCallback((isCapturingList: boolean[]) => { + isAnythingCapturingRef.current = isCapturingList.some( + (isCapturing: boolean) => isCapturing, + ); + }, []); + const { canLift, style: styleContext }: AppContextValue = useRequiredContext( AppContext, ); - const { callbacks, getDraggableRef, getShouldRespectForceTouch } = args; + const { + isDragging, + isEnabled, + draggableId, + callbacks, + getDraggableRef, + getShouldRespectForceTouch, + canDragInteractiveElements, + } = args; const getWindow = useCallback( (): HTMLElement => getWindowFromEl(getDraggableRef()), @@ -31,18 +49,69 @@ export default function useDragHandle(args: Args): DragHandleProps { isFocusedRef.current = false; }, []); + const canStartCapturing = useCallback( + (event: Event) => { + // Something on this element might be capturing but a drag has not started yet + // We want to prevent anything else from capturing + if (isAnythingCapturingRef.current) { + return false; + } + // Do not drag if anything else in the system is dragging + if (!canLift(draggableId)) { + return false; + } + + // Check if we are dragging an interactive element + return shouldAllowDraggingFromTarget(event, canDragInteractiveElements); + }, + [canDragInteractiveElements, canLift, draggableId], + ); + const mouseArgs: MouseSensorArgs = useMemo( () => ({ callbacks, getDraggableRef, getWindow, - canStartCapturing: () => true, + canStartCapturing, getShouldRespectForceTouch, + shouldAbortCapture, }), - [callbacks, getDraggableRef, getShouldRespectForceTouch, getWindow], + [ + shouldAbortCapture, + callbacks, + canStartCapturing, + getDraggableRef, + getShouldRespectForceTouch, + getWindow, + ], + ); + + const { isCapturing: isMouseCapturing, onMouseDown } = useMouseSensor( + mouseArgs, ); + recordCapture([isMouseCapturing]); + + // handle aborting + useLayoutEffect(() => { + if (!isDragging && isAnythingCapturingRef.current) { + setShouldAbortCapture(true); + } + }, [isDragging]); + + // handle is being disabled + useLayoutEffect(() => { + // nothing capturing - we are all good + if (!isEnabled && isAnythingCapturingRef.current) { + setShouldAbortCapture(true); + } + }, [isEnabled]); - const onMouseDown = useMouseSensor(mouseArgs); + // flip the abort capture flag back to true after use + useLayoutEffect(() => { + if (shouldAbortCapture) { + setShouldAbortCapture(false); + } + }, [shouldAbortCapture]); const props: DragHandleProps = useMemo( () => ({ From 7597757634ac31a3d8a27d25fa150b048c07b07d Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Tue, 19 Mar 2019 09:56:04 +1100 Subject: [PATCH 030/117] more comments --- src/view/use-drag-handle/use-drag-handle.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/view/use-drag-handle/use-drag-handle.js b/src/view/use-drag-handle/use-drag-handle.js index d82a9804b5..fc90bc4a3a 100644 --- a/src/view/use-drag-handle/use-drag-handle.js +++ b/src/view/use-drag-handle/use-drag-handle.js @@ -93,6 +93,7 @@ export default function useDragHandle(args: Args): DragHandleProps { // handle aborting useLayoutEffect(() => { + // No longer dragging but still capturing: need to abort if (!isDragging && isAnythingCapturingRef.current) { setShouldAbortCapture(true); } @@ -100,7 +101,7 @@ export default function useDragHandle(args: Args): DragHandleProps { // handle is being disabled useLayoutEffect(() => { - // nothing capturing - we are all good + // No longer enabled but still capturing: need to abort if (!isEnabled && isAnythingCapturingRef.current) { setShouldAbortCapture(true); } @@ -125,6 +126,7 @@ export default function useDragHandle(args: Args): DragHandleProps { 'data-react-beautiful-dnd-drag-handle': styleContext, // English default. Consumers are welcome to add their own start instruction 'aria-roledescription': 'Draggable item. Press space bar to lift', + // Opting out of html5 drag and drops draggable: false, onDragStart: preventHtml5Dnd, }), From bb0060c67c571d4f0835bc9a18a0b3c9fa1b340b Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Tue, 19 Mar 2019 11:34:25 +1100 Subject: [PATCH 031/117] wip --- package.json | 2 +- src/animation.js | 28 +++++------ .../drag-drop-context/drag-drop-context.jsx | 16 +++++-- src/view/error-boundary/error-boundary.jsx | 47 +++++++++++++++++++ src/view/error-boundary/index.js | 2 + src/view/use-drag-handle/use-drag-handle.js | 7 +++ .../use-draggable-dimension-publisher.js | 11 +++-- .../use-droppable-dimension-publisher.js | 1 - stories/1-single-vertical-list.stories.js | 2 +- stories/src/data.js | 4 +- yarn.lock | 24 +++++----- 11 files changed, 105 insertions(+), 39 deletions(-) create mode 100644 src/view/error-boundary/error-boundary.jsx create mode 100644 src/view/error-boundary/index.js diff --git a/package.json b/package.json index aac8d5ae9c..5dc290c14f 100644 --- a/package.json +++ b/package.json @@ -58,7 +58,7 @@ "css-box-model": "^1.1.1", "memoize-one": "^5.0.0", "raf-schd": "^4.0.0", - "react-redux": "npm:@acemarke/react-redux@next", + "react-redux": "6.0.1", "redux": "^4.0.1", "tiny-invariant": "^1.0.3" }, diff --git a/src/animation.js b/src/animation.js index 00967732dc..c734b418db 100644 --- a/src/animation.js +++ b/src/animation.js @@ -19,26 +19,26 @@ export const combine = { }, }; -export const timings = { - outOfTheWay: 0.2, - // greater than the out of the way time - // so that when the drop ends everything will - // have to be out of the way - minDropTime: 0.33, - maxDropTime: 0.55, -}; - -// slow timings -// uncomment to use // export const timings = { -// outOfTheWay: 2, +// outOfTheWay: 0.2, // // greater than the out of the way time // // so that when the drop ends everything will // // have to be out of the way -// minDropTime: 3, -// maxDropTime: 4, +// minDropTime: 0.33, +// maxDropTime: 0.55, // }; +// slow timings +// uncomment to use +export const timings = { + outOfTheWay: 2, + // greater than the out of the way time + // so that when the drop ends everything will + // have to be out of the way + minDropTime: 3, + maxDropTime: 4, +}; + const outOfTheWayTiming: string = `${timings.outOfTheWay}s ${ curves.outOfTheWay }`; diff --git a/src/view/drag-drop-context/drag-drop-context.jsx b/src/view/drag-drop-context/drag-drop-context.jsx index 8e08da4f14..6a4ae1757c 100644 --- a/src/view/drag-drop-context/drag-drop-context.jsx +++ b/src/view/drag-drop-context/drag-drop-context.jsx @@ -37,6 +37,7 @@ import isMovementAllowed from '../../state/is-movement-allowed'; import useAnnouncer from '../use-announcer'; import AppContext, { type AppContextValue } from '../context/app-context'; import useStartupValidation from './use-startup-validation'; +import ErrorBoundary from '../error-boundary'; type Props = {| ...Responders, @@ -163,11 +164,20 @@ export default function DragDropContext(props: Props) { isMovementAllowed: getIsMovementAllowed, })); + const recoverStoreFromError = useConstantFn(() => { + const state: State = storeRef.current.getState(); + if (state.phase !== 'IDLE') { + store.dispatch(clean()); + } + }); + return ( - - {props.children} - + + + {props.children} + + ); } diff --git a/src/view/error-boundary/error-boundary.jsx b/src/view/error-boundary/error-boundary.jsx new file mode 100644 index 0000000000..99f0d4fa97 --- /dev/null +++ b/src/view/error-boundary/error-boundary.jsx @@ -0,0 +1,47 @@ +// @flow +import React, { type Node } from 'react'; +import { getFormattedMessage } from '../../dev-warning'; + +type Props = {| + onError: () => void, + children: Node | null, +|}; + +function printFatalError(error: Error) { + if (process.env.NODE_ENV === 'production') { + return; + } + // eslint-disable-next-line no-console + console.error( + ...getFormattedMessage( + ` + An error has occurred while a drag is occurring. + Any existing drag will be cancelled. + + > ${error.message} + `, + ), + ); + // eslint-disable-next-line no-console + console.error('raw', error); +} + +export default class ErrorBoundary extends React.Component { + componentDidCatch(error: Error) { + printFatalError(error); + this.props.onError(); + + // If the failure was due to an invariant failure - then we handle the error + if (error.message.indexOf('Invariant failed') !== -1) { + this.setState({}); + return; + } + + // Error is more serious and we throw it + throw error; + } + + render() { + return this.props.children; + } +} diff --git a/src/view/error-boundary/index.js b/src/view/error-boundary/index.js new file mode 100644 index 0000000000..8b92d731a3 --- /dev/null +++ b/src/view/error-boundary/index.js @@ -0,0 +1,2 @@ +// @flow +export { default } from './error-boundary'; diff --git a/src/view/use-drag-handle/use-drag-handle.js b/src/view/use-drag-handle/use-drag-handle.js index fc90bc4a3a..0a04865479 100644 --- a/src/view/use-drag-handle/use-drag-handle.js +++ b/src/view/use-drag-handle/use-drag-handle.js @@ -1,13 +1,16 @@ // @flow import { useLayoutEffect, useRef, useMemo, useState, useCallback } from 'react'; +import invariant from 'tiny-invariant'; import type { Args, DragHandleProps } from './drag-handle-types'; import getWindowFromEl from '../window/get-window-from-el'; import useRequiredContext from '../use-required-context'; import AppContext, { type AppContextValue } from '../context/app-context'; +import focusRetainer from './util/focus-retainer'; import useMouseSensor, { type Args as MouseSensorArgs, } from './sensor/use-mouse-sensor'; import shouldAllowDraggingFromTarget from './util/should-allow-dragging-from-target'; +import getDragHandleRef from './util/get-drag-handle-ref'; function preventHtml5Dnd(event: DragEvent) { event.preventDefault(); @@ -28,6 +31,7 @@ export default function useDragHandle(args: Args): DragHandleProps { ); const { isDragging, + isDropAnimating, isEnabled, draggableId, callbacks, @@ -91,6 +95,9 @@ export default function useDragHandle(args: Args): DragHandleProps { ); recordCapture([isMouseCapturing]); + // mounting focus retention + useLayoutEffect(() => {}); + // handle aborting useLayoutEffect(() => { // No longer dragging but still capturing: need to abort diff --git a/src/view/use-draggable-dimension-publisher/use-draggable-dimension-publisher.js b/src/view/use-draggable-dimension-publisher/use-draggable-dimension-publisher.js index 13192090c8..6f4a949f0a 100644 --- a/src/view/use-draggable-dimension-publisher/use-draggable-dimension-publisher.js +++ b/src/view/use-draggable-dimension-publisher/use-draggable-dimension-publisher.js @@ -27,15 +27,16 @@ export default function useDraggableDimensionPublisher(args: Args) { const appContext: AppContextValue = useRequiredContext(AppContext); const marshal: DimensionMarshal = appContext.marshal; - const descriptor: DraggableDescriptor = useMemo( - () => ({ + const descriptor: DraggableDescriptor = useMemo(() => { + const result = { id: draggableId, droppableId, type, index, - }), - [draggableId, droppableId, index, type], - ); + }; + console.log('creating new descriptor', result); + return result; + }, [draggableId, droppableId, index, type]); const makeDimension = useCallback( (windowScroll?: Position): DraggableDimension => { diff --git a/src/view/use-droppable-dimension-publisher/use-droppable-dimension-publisher.js b/src/view/use-droppable-dimension-publisher/use-droppable-dimension-publisher.js index e6439aae2c..c6a03a0122 100644 --- a/src/view/use-droppable-dimension-publisher/use-droppable-dimension-publisher.js +++ b/src/view/use-droppable-dimension-publisher/use-droppable-dimension-publisher.js @@ -241,7 +241,6 @@ export default function useDroppableDimensionPublisher(args: Props) { }, []); const callbacks: DroppableCallbacks = useMemo(() => { - console.log('breaking callbacks memo'); return { getDimensionAndWatchScroll, recollect, diff --git a/stories/1-single-vertical-list.stories.js b/stories/1-single-vertical-list.stories.js index 764070b7d6..d0e1a2735f 100644 --- a/stories/1-single-vertical-list.stories.js +++ b/stories/1-single-vertical-list.stories.js @@ -7,7 +7,7 @@ import { quotes, getQuotes } from './src/data'; import { grid } from './src/constants'; const data = { - small: quotes, + small: quotes.slice(0, 2), medium: getQuotes(40), large: getQuotes(500), }; diff --git a/stories/src/data.js b/stories/src/data.js index f167807b55..efd6512a94 100644 --- a/stories/src/data.js +++ b/stories/src/data.js @@ -54,12 +54,12 @@ export const authors: Author[] = [jake, BMO, finn, princess]; export const quotes: Quote[] = [ { - id: '1', + id: 'item:0', content: 'Sometimes life is scary and dark', author: BMO, }, { - id: '2', + id: 'item:1', content: 'Sucking at something is the first step towards being sorta good at something.', author: jake, diff --git a/yarn.lock b/yarn.lock index bf0262f8de..84a70d3e37 100644 --- a/yarn.lock +++ b/yarn.lock @@ -874,7 +874,7 @@ dependencies: regenerator-runtime "^0.12.0" -"@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.2.0", "@babel/runtime@^7.3.4": +"@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.2.0", "@babel/runtime@^7.3.1", "@babel/runtime@^7.3.4": version "7.3.4" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.3.4.tgz#73d12ba819e365fcf7fd152aed56d6df97d21c83" integrity sha512-IvfvnMdSaLBateu0jfsYIpZTxAc2cKEXEMiezGGN75QcBcecDUKd3PgLAncT0oOgxKy8dd8hrJKj9MfzgfZd6g== @@ -5884,7 +5884,7 @@ hoist-non-react-statics@^2.3.1, hoist-non-react-statics@^2.5.0: resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-2.5.5.tgz#c5903cf409c0dfd908f388e619d86b9c1174cb47" integrity sha512-rqcy4pJo55FTTLWt+bU8ukscqHeE/e9KWvsOW2b/a3afxQZhwkQdT1rPPCJ0rYXdj4vNcasY8zHTH+jF/qStxw== -hoist-non-react-statics@^3.2.1: +hoist-non-react-statics@^3.3.0: version "3.3.0" resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.0.tgz#b09178f0122184fb95acf525daaecb4d8f45958b" integrity sha512-0XsbTXxgiaCDYDIWFcwkmerZPSwywfUqYmwT4jzewKTQSWoE6FCMoUVOeBJWK3E/CrWbxRG3m5GzY4lnIwGRBA== @@ -9074,7 +9074,7 @@ prompts@^2.0.1: kleur "^3.0.2" sisteransi "^1.0.0" -prop-types@^15.5.10, prop-types@^15.5.4, prop-types@^15.6.0, prop-types@^15.6.1, prop-types@^15.6.2: +prop-types@^15.5.10, prop-types@^15.5.4, prop-types@^15.6.0, prop-types@^15.6.1, prop-types@^15.6.2, prop-types@^15.7.2: version "15.7.2" resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.7.2.tgz#52c41e75b8c87e72b9d9360e0206b99dcbffa6c5" integrity sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ== @@ -9415,7 +9415,7 @@ react-is@^16.3.1, react-is@^16.7.0, react-is@^16.8.1, react-is@^16.8.3: resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.8.3.tgz#4ad8b029c2a718fc0cfc746c8d4e1b7221e5387d" integrity sha512-Y4rC1ZJmsxxkkPuMLwvKvlL1Zfpbcu+Bf4ZigkHup3v9EfdYhAlWAaVyA19olXq2o2mGn0w+dFKvk3pVVlYcIA== -react-is@^16.8.4: +react-is@^16.8.2, react-is@^16.8.4: version "16.8.4" resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.8.4.tgz#90f336a68c3a29a096a3d648ab80e87ec61482a2" integrity sha512-PVadd+WaUDOAciICm/J1waJaSvgq+4rHE/K70j0PFqKhkTBsPv/82UGQJNXAngz1fOQLLxI6z1sEDmJDQhCTAA== @@ -9455,17 +9455,17 @@ react-popper@^1.3.3: typed-styles "^0.0.7" warning "^4.0.2" -"react-redux@npm:@acemarke/react-redux@next": - version "7.0.0-alpha.5" - resolved "https://registry.yarnpkg.com/@acemarke/react-redux/-/react-redux-7.0.0-alpha.5.tgz#af25d17b11f06eb23dc68462b9af868a64635d43" - integrity sha512-iKuJ9yaIPD3LQXnCIuUjOweMiWm5j0xBOUqSi6F6ip7gbmy1lK6Febik2KgWfYvX7/ZOTgEXUQ8upbuX392/xw== +react-redux@6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-6.0.1.tgz#0d423e2c1cb10ada87293d47e7de7c329623ba4d" + integrity sha512-T52I52Kxhbqy/6TEfBv85rQSDz6+Y28V/pf52vDWs1YRXG19mcFOGfHnY2HsNFHyhP+ST34Aih98fvt6tqwVcQ== dependencies: - "@babel/runtime" "^7.2.0" - hoist-non-react-statics "^3.2.1" + "@babel/runtime" "^7.3.1" + hoist-non-react-statics "^3.3.0" invariant "^2.2.4" loose-envify "^1.4.0" - prop-types "^15.6.2" - react-is "^16.7.0" + prop-types "^15.7.2" + react-is "^16.8.2" react-resize-detector@^3.2.1: version "3.4.0" From b0a94e28e90f75120eb162f33899861012542a26 Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Tue, 19 Mar 2019 12:48:02 +1100 Subject: [PATCH 032/117] for show --- package.json | 2 +- src/state/create-store.js | 2 +- stories/src/vertical/quote-app.jsx | 1 + yarn.lock | 24 ++++++++++++------------ 4 files changed, 15 insertions(+), 14 deletions(-) diff --git a/package.json b/package.json index 5dc290c14f..aac8d5ae9c 100644 --- a/package.json +++ b/package.json @@ -58,7 +58,7 @@ "css-box-model": "^1.1.1", "memoize-one": "^5.0.0", "raf-schd": "^4.0.0", - "react-redux": "6.0.1", + "react-redux": "npm:@acemarke/react-redux@next", "redux": "^4.0.1", "tiny-invariant": "^1.0.3" }, diff --git a/src/state/create-store.js b/src/state/create-store.js index dd55a4c5b8..702d79dd4e 100644 --- a/src/state/create-store.js +++ b/src/state/create-store.js @@ -49,7 +49,7 @@ export default ({ // > uncomment to use // debugging logger - // require('../debug/middleware/log').default, + require('../debug/middleware/log').default, // user timing api // require('../debug/middleware/user-timing').default, // debugging timer diff --git a/stories/src/vertical/quote-app.jsx b/stories/src/vertical/quote-app.jsx index 913610853e..f8150ac11c 100644 --- a/stories/src/vertical/quote-app.jsx +++ b/stories/src/vertical/quote-app.jsx @@ -69,6 +69,7 @@ export default class QuoteApp extends Component { result.destination.index, ); + console.warn('REORDERING QUOTES'); this.setState({ quotes, }); diff --git a/yarn.lock b/yarn.lock index 84a70d3e37..bf0262f8de 100644 --- a/yarn.lock +++ b/yarn.lock @@ -874,7 +874,7 @@ dependencies: regenerator-runtime "^0.12.0" -"@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.2.0", "@babel/runtime@^7.3.1", "@babel/runtime@^7.3.4": +"@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.2.0", "@babel/runtime@^7.3.4": version "7.3.4" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.3.4.tgz#73d12ba819e365fcf7fd152aed56d6df97d21c83" integrity sha512-IvfvnMdSaLBateu0jfsYIpZTxAc2cKEXEMiezGGN75QcBcecDUKd3PgLAncT0oOgxKy8dd8hrJKj9MfzgfZd6g== @@ -5884,7 +5884,7 @@ hoist-non-react-statics@^2.3.1, hoist-non-react-statics@^2.5.0: resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-2.5.5.tgz#c5903cf409c0dfd908f388e619d86b9c1174cb47" integrity sha512-rqcy4pJo55FTTLWt+bU8ukscqHeE/e9KWvsOW2b/a3afxQZhwkQdT1rPPCJ0rYXdj4vNcasY8zHTH+jF/qStxw== -hoist-non-react-statics@^3.3.0: +hoist-non-react-statics@^3.2.1: version "3.3.0" resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.0.tgz#b09178f0122184fb95acf525daaecb4d8f45958b" integrity sha512-0XsbTXxgiaCDYDIWFcwkmerZPSwywfUqYmwT4jzewKTQSWoE6FCMoUVOeBJWK3E/CrWbxRG3m5GzY4lnIwGRBA== @@ -9074,7 +9074,7 @@ prompts@^2.0.1: kleur "^3.0.2" sisteransi "^1.0.0" -prop-types@^15.5.10, prop-types@^15.5.4, prop-types@^15.6.0, prop-types@^15.6.1, prop-types@^15.6.2, prop-types@^15.7.2: +prop-types@^15.5.10, prop-types@^15.5.4, prop-types@^15.6.0, prop-types@^15.6.1, prop-types@^15.6.2: version "15.7.2" resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.7.2.tgz#52c41e75b8c87e72b9d9360e0206b99dcbffa6c5" integrity sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ== @@ -9415,7 +9415,7 @@ react-is@^16.3.1, react-is@^16.7.0, react-is@^16.8.1, react-is@^16.8.3: resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.8.3.tgz#4ad8b029c2a718fc0cfc746c8d4e1b7221e5387d" integrity sha512-Y4rC1ZJmsxxkkPuMLwvKvlL1Zfpbcu+Bf4ZigkHup3v9EfdYhAlWAaVyA19olXq2o2mGn0w+dFKvk3pVVlYcIA== -react-is@^16.8.2, react-is@^16.8.4: +react-is@^16.8.4: version "16.8.4" resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.8.4.tgz#90f336a68c3a29a096a3d648ab80e87ec61482a2" integrity sha512-PVadd+WaUDOAciICm/J1waJaSvgq+4rHE/K70j0PFqKhkTBsPv/82UGQJNXAngz1fOQLLxI6z1sEDmJDQhCTAA== @@ -9455,17 +9455,17 @@ react-popper@^1.3.3: typed-styles "^0.0.7" warning "^4.0.2" -react-redux@6.0.1: - version "6.0.1" - resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-6.0.1.tgz#0d423e2c1cb10ada87293d47e7de7c329623ba4d" - integrity sha512-T52I52Kxhbqy/6TEfBv85rQSDz6+Y28V/pf52vDWs1YRXG19mcFOGfHnY2HsNFHyhP+ST34Aih98fvt6tqwVcQ== +"react-redux@npm:@acemarke/react-redux@next": + version "7.0.0-alpha.5" + resolved "https://registry.yarnpkg.com/@acemarke/react-redux/-/react-redux-7.0.0-alpha.5.tgz#af25d17b11f06eb23dc68462b9af868a64635d43" + integrity sha512-iKuJ9yaIPD3LQXnCIuUjOweMiWm5j0xBOUqSi6F6ip7gbmy1lK6Febik2KgWfYvX7/ZOTgEXUQ8upbuX392/xw== dependencies: - "@babel/runtime" "^7.3.1" - hoist-non-react-statics "^3.3.0" + "@babel/runtime" "^7.2.0" + hoist-non-react-statics "^3.2.1" invariant "^2.2.4" loose-envify "^1.4.0" - prop-types "^15.7.2" - react-is "^16.8.2" + prop-types "^15.6.2" + react-is "^16.7.0" react-resize-detector@^3.2.1: version "3.4.0" From 697aae9045184a5f2b5d1208d7973a47734914d8 Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Tue, 19 Mar 2019 14:53:43 +1100 Subject: [PATCH 033/117] moving back to stable react-redux --- package.json | 2 +- src/state/create-store.js | 4 ++-- stories/src/vertical/quote-app.jsx | 1 - yarn.lock | 24 ++++++++++++------------ 4 files changed, 15 insertions(+), 16 deletions(-) diff --git a/package.json b/package.json index aac8d5ae9c..5dc290c14f 100644 --- a/package.json +++ b/package.json @@ -58,7 +58,7 @@ "css-box-model": "^1.1.1", "memoize-one": "^5.0.0", "raf-schd": "^4.0.0", - "react-redux": "npm:@acemarke/react-redux@next", + "react-redux": "6.0.1", "redux": "^4.0.1", "tiny-invariant": "^1.0.3" }, diff --git a/src/state/create-store.js b/src/state/create-store.js index 702d79dd4e..231c831db8 100644 --- a/src/state/create-store.js +++ b/src/state/create-store.js @@ -49,8 +49,8 @@ export default ({ // > uncomment to use // debugging logger - require('../debug/middleware/log').default, - // user timing api + // require('../debug/middleware/log').default, + // // user timing api // require('../debug/middleware/user-timing').default, // debugging timer // require('../debug/middleware/action-timing').default, diff --git a/stories/src/vertical/quote-app.jsx b/stories/src/vertical/quote-app.jsx index f8150ac11c..913610853e 100644 --- a/stories/src/vertical/quote-app.jsx +++ b/stories/src/vertical/quote-app.jsx @@ -69,7 +69,6 @@ export default class QuoteApp extends Component { result.destination.index, ); - console.warn('REORDERING QUOTES'); this.setState({ quotes, }); diff --git a/yarn.lock b/yarn.lock index bf0262f8de..84a70d3e37 100644 --- a/yarn.lock +++ b/yarn.lock @@ -874,7 +874,7 @@ dependencies: regenerator-runtime "^0.12.0" -"@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.2.0", "@babel/runtime@^7.3.4": +"@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.2.0", "@babel/runtime@^7.3.1", "@babel/runtime@^7.3.4": version "7.3.4" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.3.4.tgz#73d12ba819e365fcf7fd152aed56d6df97d21c83" integrity sha512-IvfvnMdSaLBateu0jfsYIpZTxAc2cKEXEMiezGGN75QcBcecDUKd3PgLAncT0oOgxKy8dd8hrJKj9MfzgfZd6g== @@ -5884,7 +5884,7 @@ hoist-non-react-statics@^2.3.1, hoist-non-react-statics@^2.5.0: resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-2.5.5.tgz#c5903cf409c0dfd908f388e619d86b9c1174cb47" integrity sha512-rqcy4pJo55FTTLWt+bU8ukscqHeE/e9KWvsOW2b/a3afxQZhwkQdT1rPPCJ0rYXdj4vNcasY8zHTH+jF/qStxw== -hoist-non-react-statics@^3.2.1: +hoist-non-react-statics@^3.3.0: version "3.3.0" resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.0.tgz#b09178f0122184fb95acf525daaecb4d8f45958b" integrity sha512-0XsbTXxgiaCDYDIWFcwkmerZPSwywfUqYmwT4jzewKTQSWoE6FCMoUVOeBJWK3E/CrWbxRG3m5GzY4lnIwGRBA== @@ -9074,7 +9074,7 @@ prompts@^2.0.1: kleur "^3.0.2" sisteransi "^1.0.0" -prop-types@^15.5.10, prop-types@^15.5.4, prop-types@^15.6.0, prop-types@^15.6.1, prop-types@^15.6.2: +prop-types@^15.5.10, prop-types@^15.5.4, prop-types@^15.6.0, prop-types@^15.6.1, prop-types@^15.6.2, prop-types@^15.7.2: version "15.7.2" resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.7.2.tgz#52c41e75b8c87e72b9d9360e0206b99dcbffa6c5" integrity sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ== @@ -9415,7 +9415,7 @@ react-is@^16.3.1, react-is@^16.7.0, react-is@^16.8.1, react-is@^16.8.3: resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.8.3.tgz#4ad8b029c2a718fc0cfc746c8d4e1b7221e5387d" integrity sha512-Y4rC1ZJmsxxkkPuMLwvKvlL1Zfpbcu+Bf4ZigkHup3v9EfdYhAlWAaVyA19olXq2o2mGn0w+dFKvk3pVVlYcIA== -react-is@^16.8.4: +react-is@^16.8.2, react-is@^16.8.4: version "16.8.4" resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.8.4.tgz#90f336a68c3a29a096a3d648ab80e87ec61482a2" integrity sha512-PVadd+WaUDOAciICm/J1waJaSvgq+4rHE/K70j0PFqKhkTBsPv/82UGQJNXAngz1fOQLLxI6z1sEDmJDQhCTAA== @@ -9455,17 +9455,17 @@ react-popper@^1.3.3: typed-styles "^0.0.7" warning "^4.0.2" -"react-redux@npm:@acemarke/react-redux@next": - version "7.0.0-alpha.5" - resolved "https://registry.yarnpkg.com/@acemarke/react-redux/-/react-redux-7.0.0-alpha.5.tgz#af25d17b11f06eb23dc68462b9af868a64635d43" - integrity sha512-iKuJ9yaIPD3LQXnCIuUjOweMiWm5j0xBOUqSi6F6ip7gbmy1lK6Febik2KgWfYvX7/ZOTgEXUQ8upbuX392/xw== +react-redux@6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-6.0.1.tgz#0d423e2c1cb10ada87293d47e7de7c329623ba4d" + integrity sha512-T52I52Kxhbqy/6TEfBv85rQSDz6+Y28V/pf52vDWs1YRXG19mcFOGfHnY2HsNFHyhP+ST34Aih98fvt6tqwVcQ== dependencies: - "@babel/runtime" "^7.2.0" - hoist-non-react-statics "^3.2.1" + "@babel/runtime" "^7.3.1" + hoist-non-react-statics "^3.3.0" invariant "^2.2.4" loose-envify "^1.4.0" - prop-types "^15.6.2" - react-is "^16.7.0" + prop-types "^15.7.2" + react-is "^16.8.2" react-resize-detector@^3.2.1: version "3.4.0" From 9853c124e096bf0841d1ea767174c7c41c89b23a Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Tue, 19 Mar 2019 15:53:46 +1100 Subject: [PATCH 034/117] wip --- src/state/action-creators.js | 12 +- src/state/middleware/style.js | 2 +- src/state/reducer.js | 5 +- .../drag-drop-context/drag-drop-context.jsx | 2 +- src/view/draggable/draggable.jsx | 3 + src/view/droppable/connected-droppable.js | 6 + src/view/error-boundary/error-boundary.jsx | 13 +- .../util/should-allow-dragging-from-target.js | 5 +- .../droppable-dimension-publisher.jsx | 358 ------------------ .../use-style-marshal/use-style-marshal.js | 1 - 10 files changed, 38 insertions(+), 369 deletions(-) delete mode 100644 src/view/use-droppable-dimension-publisher/droppable-dimension-publisher.jsx diff --git a/src/state/action-creators.js b/src/state/action-creators.js index fcdb4d529b..e5e7d19437 100644 --- a/src/state/action-creators.js +++ b/src/state/action-creators.js @@ -229,14 +229,20 @@ export const moveLeft = (): MoveLeftAction => ({ payload: null, }); +type CleanActionArgs = {| + shouldFlush: boolean, +|}; + type CleanAction = {| type: 'CLEAN', - payload: null, + payload: CleanActionArgs, |}; -export const clean = (): CleanAction => ({ +export const clean = ( + args?: CleanActionArgs = { shouldFlush: false }, +): CleanAction => ({ type: 'CLEAN', - payload: null, + payload: args, }); export type AnimateDropArgs = {| diff --git a/src/state/middleware/style.js b/src/state/middleware/style.js index 97c82aa3cf..2b9820cdd4 100644 --- a/src/state/middleware/style.js +++ b/src/state/middleware/style.js @@ -1,6 +1,6 @@ // @flow import type { Action, Dispatch } from '../store-types'; -import type { StyleMarshal } from '../../view/style-marshal/style-marshal-types'; +import type { StyleMarshal } from '../../view/use-style-marshal/style-marshal-types'; export default (marshal: StyleMarshal) => () => (next: Dispatch) => ( action: Action, diff --git a/src/state/reducer.js b/src/state/reducer.js index 0efb37e637..1c21f7416c 100644 --- a/src/state/reducer.js +++ b/src/state/reducer.js @@ -60,7 +60,10 @@ const idle: IdleState = { phase: 'IDLE', completed: null, shouldFlush: false }; export default (state: State = idle, action: Action): State => { if (action.type === 'CLEAN') { - return idle; + return { + ...idle, + shouldFlush: action.payload.shouldFlush, + }; } if (action.type === 'INITIAL_PUBLISH') { diff --git a/src/view/drag-drop-context/drag-drop-context.jsx b/src/view/drag-drop-context/drag-drop-context.jsx index 6a4ae1757c..5b74c9ce3b 100644 --- a/src/view/drag-drop-context/drag-drop-context.jsx +++ b/src/view/drag-drop-context/drag-drop-context.jsx @@ -167,7 +167,7 @@ export default function DragDropContext(props: Props) { const recoverStoreFromError = useConstantFn(() => { const state: State = storeRef.current.getState(); if (state.phase !== 'IDLE') { - store.dispatch(clean()); + store.dispatch(clean({ shouldFlush: true })); } }); diff --git a/src/view/draggable/draggable.jsx b/src/view/draggable/draggable.jsx index 68a9e74735..77f981564e 100644 --- a/src/view/draggable/draggable.jsx +++ b/src/view/draggable/draggable.jsx @@ -97,6 +97,9 @@ export default function Draggable(props: Props) { const onLift = useCallback( (options: { clientSelection: Position, movementMode: MovementMode }) => { timings.start('LIFT'); + setTimeout(() => { + throw new Error('YOLO'); + }, 1000); const el: ?HTMLElement = ref.current; invariant(el); invariant(!isDragDisabled, 'Cannot lift a Draggable when it is disabled'); diff --git a/src/view/droppable/connected-droppable.js b/src/view/droppable/connected-droppable.js index e0d4e2d356..d22bdb9159 100644 --- a/src/view/droppable/connected-droppable.js +++ b/src/view/droppable/connected-droppable.js @@ -155,6 +155,12 @@ export const makeMapStateToProps = (): Selector => { ); } + // An error occurred and we need to clear everything + // TODO: validate and add test + if (state.phase === 'IDLE' && !state.completed && state.shouldFlush) { + return idleWithoutAnimation; + } + if (state.phase === 'IDLE' && state.completed) { const completed: CompletedDrag = state.completed; if (!isMatchingType(type, completed.critical)) { diff --git a/src/view/error-boundary/error-boundary.jsx b/src/view/error-boundary/error-boundary.jsx index 99f0d4fa97..92c42b52ef 100644 --- a/src/view/error-boundary/error-boundary.jsx +++ b/src/view/error-boundary/error-boundary.jsx @@ -27,7 +27,14 @@ function printFatalError(error: Error) { } export default class ErrorBoundary extends React.Component { - componentDidCatch(error: Error) { + componentDidMount() { + window.addEventListener('error', this.onFatalError); + } + componentWillUnmount() { + window.removeEventListener('error', this.onFatalError); + } + + onFatalError = (error: Error) => { printFatalError(error); this.props.onError(); @@ -39,6 +46,10 @@ export default class ErrorBoundary extends React.Component { // Error is more serious and we throw it throw error; + }; + + componentDidCatch(error: Error) { + this.onFatalError(error); } render() { diff --git a/src/view/use-drag-handle/util/should-allow-dragging-from-target.js b/src/view/use-drag-handle/util/should-allow-dragging-from-target.js index d1b2141eb4..fe43adce04 100644 --- a/src/view/use-drag-handle/util/should-allow-dragging-from-target.js +++ b/src/view/use-drag-handle/util/should-allow-dragging-from-target.js @@ -1,5 +1,4 @@ // @flow -import type { Props } from '../drag-handle-types'; import isElement from '../../is-type-of-element/is-element'; export type TagNameMap = { @@ -56,9 +55,9 @@ const isAnInteractiveElement = ( return isAnInteractiveElement(parent, current.parentElement); }; -export default (event: Event, props: Props): boolean => { +export default (event: Event, canDragInteractiveElements: boolean): boolean => { // Allowing drag with all element types - if (props.canDragInteractiveElements) { + if (canDragInteractiveElements) { return true; } diff --git a/src/view/use-droppable-dimension-publisher/droppable-dimension-publisher.jsx b/src/view/use-droppable-dimension-publisher/droppable-dimension-publisher.jsx deleted file mode 100644 index c4f7c4dfc9..0000000000 --- a/src/view/use-droppable-dimension-publisher/droppable-dimension-publisher.jsx +++ /dev/null @@ -1,358 +0,0 @@ -// @flow -import React, { - useCallback, - useMemo, - useEffect, - useContext, - useRef, -} from 'react'; -import PropTypes from 'prop-types'; -import memoizeOne from 'memoize-one'; -import invariant from 'tiny-invariant'; -import { type Position } from 'css-box-model'; -import rafSchedule from 'raf-schd'; -import checkForNestedScrollContainers from './check-for-nested-scroll-container'; -import { dimensionMarshalKey } from '../context-keys'; -import { origin } from '../../state/position'; -import getScroll from './get-scroll'; -import type { - DimensionMarshal, - DroppableCallbacks, - RecollectDroppableOptions, -} from '../../state/dimension-marshal/dimension-marshal-types'; -import getEnv, { type Env } from './get-env'; -import type { - DroppableId, - TypeId, - DroppableDimension, - DroppableDescriptor, - Direction, - ScrollOptions, -} from '../../types'; -import getDimension from './get-dimension'; -import { warning } from '../../dev-warning'; - -type Props = {| - droppableId: DroppableId, - type: TypeId, - direction: Direction, - isDropDisabled: boolean, - isCombineEnabled: boolean, - ignoreContainerClipping: boolean, - getPlaceholderRef: () => ?HTMLElement, - getDroppableRef: () => ?HTMLElement, - children: Node, -|}; - -type WhileDragging = {| - ref: HTMLElement, - descriptor: DroppableDescriptor, - env: Env, - scrollOptions: ScrollOptions, -|}; - -const getClosestScrollable = (dragging: ?WhileDragging): ?Element => - (dragging && dragging.env.closestScrollable) || null; - -const immediate = { - passive: false, -}; -const delayed = { - passive: true, -}; - -const getListenerOptions = (options: ScrollOptions) => - options.shouldPublishImmediately ? immediate : delayed; - -const withoutPlaceholder = ( - placeholder: ?HTMLElement, - fn: () => DroppableDimension, -): DroppableDimension => { - if (!placeholder) { - return fn(); - } - - const last: string = placeholder.style.display; - placeholder.style.display = 'none'; - const result: DroppableDimension = fn(); - placeholder.style.display = last; - - return result; -}; - -export default class DroppableDimensionPublisher extends React.Component { - /* eslint-disable react/sort-comp */ - dragging: ?WhileDragging; - callbacks: DroppableCallbacks; - publishedDescriptor: ?DroppableDescriptor = null; - - constructor(props: Props, context: mixed) { - super(props, context); - const callbacks: DroppableCallbacks = { - getDimensionAndWatchScroll: this.getDimensionAndWatchScroll, - recollect: this.recollect, - dragStopped: this.dragStopped, - scroll: this.scroll, - }; - this.callbacks = callbacks; - } - - static contextTypes = { - [dimensionMarshalKey]: PropTypes.object.isRequired, - }; - - getClosestScroll = (): Position => { - const dragging: ?WhileDragging = this.dragging; - if (!dragging || !dragging.env.closestScrollable) { - return origin; - } - - return getScroll(dragging.env.closestScrollable); - }; - - memoizedUpdateScroll = memoizeOne((x: number, y: number) => { - invariant( - this.publishedDescriptor, - 'Cannot update scroll on unpublished droppable', - ); - - const newScroll: Position = { x, y }; - const marshal: DimensionMarshal = this.context[dimensionMarshalKey]; - marshal.updateDroppableScroll(this.publishedDescriptor.id, newScroll); - }); - - updateScroll = () => { - const scroll: Position = this.getClosestScroll(); - this.memoizedUpdateScroll(scroll.x, scroll.y); - }; - - scheduleScrollUpdate = rafSchedule(this.updateScroll); - - onClosestScroll = () => { - const dragging: ?WhileDragging = this.dragging; - const closest: ?Element = getClosestScrollable(this.dragging); - - invariant( - dragging && closest, - 'Could not find scroll options while scrolling', - ); - const options: ScrollOptions = dragging.scrollOptions; - if (options.shouldPublishImmediately) { - this.updateScroll(); - return; - } - this.scheduleScrollUpdate(); - }; - - scroll = (change: Position) => { - const closest: ?Element = getClosestScrollable(this.dragging); - invariant(closest, 'Cannot scroll a droppable with no closest scrollable'); - closest.scrollTop += change.y; - closest.scrollLeft += change.x; - }; - - dragStopped = () => { - const dragging: ?WhileDragging = this.dragging; - invariant(dragging, 'Cannot stop drag when no active drag'); - const closest: ?Element = getClosestScrollable(dragging); - - // goodbye old friend - this.dragging = null; - - if (!closest) { - return; - } - - // unwatch scroll - this.scheduleScrollUpdate.cancel(); - closest.removeEventListener( - 'scroll', - this.onClosestScroll, - getListenerOptions(dragging.scrollOptions), - ); - }; - - componentDidMount() { - this.publish(); - - // Note: not calling `marshal.updateDroppableIsEnabled()` - // If the dimension marshal needs to get the dimension immediately - // then it will get the enabled state of the dimension at that point - } - - componentDidUpdate(prevProps: Props) { - // Update the descriptor if needed - this.publish(); - - // Do not need to update the marshal if no drag is occurring - if (!this.dragging) { - return; - } - - // Need to update the marshal if an enabled state is changing - - const isDisabledChanged: boolean = - this.props.isDropDisabled !== prevProps.isDropDisabled; - const isCombineChanged: boolean = - this.props.isCombineEnabled !== prevProps.isCombineEnabled; - - if (!isDisabledChanged && !isCombineChanged) { - return; - } - - const marshal: DimensionMarshal = this.context[dimensionMarshalKey]; - - if (isDisabledChanged) { - marshal.updateDroppableIsEnabled( - this.props.droppableId, - !this.props.isDropDisabled, - ); - } - - if (isCombineChanged) { - marshal.updateDroppableIsCombineEnabled( - this.props.droppableId, - this.props.isCombineEnabled, - ); - } - } - - componentWillUnmount() { - if (this.dragging) { - warning('unmounting droppable while a drag is occurring'); - this.dragStopped(); - } - - this.unpublish(); - } - - getMemoizedDescriptor = memoizeOne( - (id: DroppableId, type: TypeId): DroppableDescriptor => ({ - id, - type, - }), - ); - - publish = () => { - const marshal: DimensionMarshal = this.context[dimensionMarshalKey]; - const descriptor: DroppableDescriptor = this.getMemoizedDescriptor( - this.props.droppableId, - this.props.type, - ); - - if (!this.publishedDescriptor) { - marshal.registerDroppable(descriptor, this.callbacks); - this.publishedDescriptor = descriptor; - return; - } - - // already published - and no changes - if (this.publishedDescriptor === descriptor) { - return; - } - - // already published and there has been changes - marshal.updateDroppable( - this.publishedDescriptor, - descriptor, - this.callbacks, - ); - this.publishedDescriptor = descriptor; - }; - - unpublish = () => { - invariant( - this.publishedDescriptor, - 'Cannot unpublish descriptor when none is published', - ); - - // Using the previously published id to unpublish. This is to guard - // against the case where the id dynamically changes. This is not - // supported during a drag - but it is good to guard against. - const marshal: DimensionMarshal = this.context[dimensionMarshalKey]; - marshal.unregisterDroppable(this.publishedDescriptor); - this.publishedDescriptor = null; - }; - - // Used when Draggables are added or removed from a Droppable during a drag - recollect = (options: RecollectDroppableOptions): DroppableDimension => { - const dragging: ?WhileDragging = this.dragging; - const closest: ?Element = getClosestScrollable(dragging); - invariant( - dragging && closest, - 'Can only recollect Droppable client for Droppables that have a scroll container', - ); - - const execute = (): DroppableDimension => - getDimension({ - ref: dragging.ref, - descriptor: dragging.descriptor, - env: dragging.env, - windowScroll: origin, - direction: this.props.direction, - isDropDisabled: this.props.isDropDisabled, - isCombineEnabled: this.props.isCombineEnabled, - shouldClipSubject: !this.props.ignoreContainerClipping, - }); - - if (!options.withoutPlaceholder) { - return execute(); - } - - return withoutPlaceholder(this.props.getPlaceholderRef(), execute); - }; - - getDimensionAndWatchScroll = ( - windowScroll: Position, - options: ScrollOptions, - ): DroppableDimension => { - invariant( - !this.dragging, - 'Cannot collect a droppable while a drag is occurring', - ); - const descriptor: ?DroppableDescriptor = this.publishedDescriptor; - invariant(descriptor, 'Cannot get dimension for unpublished droppable'); - const ref: ?HTMLElement = this.props.getDroppableRef(); - invariant(ref, 'Cannot collect without a droppable ref'); - const env: Env = getEnv(ref); - - const dragging: WhileDragging = { - ref, - descriptor, - env, - scrollOptions: options, - }; - this.dragging = dragging; - - const dimension: DroppableDimension = getDimension({ - ref, - descriptor, - env, - windowScroll, - direction: this.props.direction, - isDropDisabled: this.props.isDropDisabled, - isCombineEnabled: this.props.isCombineEnabled, - shouldClipSubject: !this.props.ignoreContainerClipping, - }); - - if (env.closestScrollable) { - // bind scroll listener - - env.closestScrollable.addEventListener( - 'scroll', - this.onClosestScroll, - getListenerOptions(dragging.scrollOptions), - ); - // print a debug warning if using an unsupported nested scroll container setup - if (process.env.NODE_ENV !== 'production') { - checkForNestedScrollContainers(env.closestScrollable); - } - } - - return dimension; - }; - - render() { - return this.props.children; - } -} diff --git a/src/view/use-style-marshal/use-style-marshal.js b/src/view/use-style-marshal/use-style-marshal.js index cbae0680c7..6eeaff4637 100644 --- a/src/view/use-style-marshal/use-style-marshal.js +++ b/src/view/use-style-marshal/use-style-marshal.js @@ -27,7 +27,6 @@ export default function useStyleMarshal(uniqueId: number) { const alwaysRef = useRef(null); const dynamicRef = useRef(null); - // TODO: need to check if the memoize one is working const setDynamicStyle = useCallback( // Using memoizeOne to prevent frequent updates to textContext memoizeOne((proposed: string) => { From 93b64dedf26ea8977cfb35e871d737ebaab36859 Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Tue, 19 Mar 2019 16:05:24 +1100 Subject: [PATCH 035/117] fixing flow --- .../drag-drop-context/drag-drop-context.jsx | 26 +++---------------- src/view/droppable/droppable.jsx | 2 +- src/view/placeholder/placeholder.jsx | 11 +++----- 3 files changed, 7 insertions(+), 32 deletions(-) diff --git a/src/view/drag-drop-context/drag-drop-context.jsx b/src/view/drag-drop-context/drag-drop-context.jsx index 5b74c9ce3b..ae2a271ed9 100644 --- a/src/view/drag-drop-context/drag-drop-context.jsx +++ b/src/view/drag-drop-context/drag-drop-context.jsx @@ -45,26 +45,6 @@ type Props = {| children: Node | null, |}; -// TODO: handle errors -const printFatalDevError = (error: Error) => { - if (process.env.NODE_ENV === 'production') { - return; - } - // eslint-disable-next-line no-console - console.error( - ...getFormattedMessage( - ` - An error has occurred while a drag is occurring. - Any existing drag will be cancelled. - - > ${error.message} - `, - ), - ); - // eslint-disable-next-line no-console - console.error('raw', error); -}; - const createResponders = (props: Props): Responders => ({ onBeforeDragStart: props.onBeforeDragStart, onDragStart: props.onDragStart, @@ -72,16 +52,16 @@ const createResponders = (props: Props): Responders => ({ onDragUpdate: props.onDragUpdate, }); -let count: number = 0; +let instanceCount: number = 0; // Reset any context that gets persisted across server side renders export function resetServerContext() { - count = 0; + instanceCount = 0; } export default function DragDropContext(props: Props) { // We do not want this to change - const uniqueId: number = useConstant((): number => count++); + const uniqueId: number = useConstant((): number => instanceCount++); let storeRef: MutableRefObject; diff --git a/src/view/droppable/droppable.jsx b/src/view/droppable/droppable.jsx index c3565d0b3f..e8888f4518 100644 --- a/src/view/droppable/droppable.jsx +++ b/src/view/droppable/droppable.jsx @@ -86,7 +86,7 @@ export default function Droppable(props: Props) { const placeholder: Node | null = instruction ? ( ( props.animate === 'open', @@ -159,4 +154,4 @@ function Placeholder(props: Props) { }); } -export default React.memo(Placeholder); +export default React.memo(Placeholder); From a7fb2c1e344e931fe8d27feec915586afd866921 Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Tue, 19 Mar 2019 16:21:47 +1100 Subject: [PATCH 036/117] correctly handling unmounting from mouse sensor --- src/view/draggable/draggable.jsx | 3 --- src/view/use-drag-handle/sensor/use-mouse-sensor.js | 5 +++-- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/src/view/draggable/draggable.jsx b/src/view/draggable/draggable.jsx index 77f981564e..68a9e74735 100644 --- a/src/view/draggable/draggable.jsx +++ b/src/view/draggable/draggable.jsx @@ -97,9 +97,6 @@ export default function Draggable(props: Props) { const onLift = useCallback( (options: { clientSelection: Position, movementMode: MovementMode }) => { timings.start('LIFT'); - setTimeout(() => { - throw new Error('YOLO'); - }, 1000); const el: ?HTMLElement = ref.current; invariant(el); invariant(!isDragDisabled, 'Cannot lift a Draggable when it is disabled'); diff --git a/src/view/use-drag-handle/sensor/use-mouse-sensor.js b/src/view/use-drag-handle/sensor/use-mouse-sensor.js index b890794c07..9d865a4e65 100644 --- a/src/view/use-drag-handle/sensor/use-mouse-sensor.js +++ b/src/view/use-drag-handle/sensor/use-mouse-sensor.js @@ -358,9 +358,10 @@ export default function useMouseSensor(args: Args): Result { [canStartCapturing, getIsCapturing, startPendingDrag], ); + // When unmounting - cancel useLayoutEffect(() => { - // TODO: do logic for unmounting - }, []); + return () => cancel(); + }, [cancel]); return { onMouseDown, From f9915db6f27f2114477f0945a6381fcf347704e2 Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Tue, 19 Mar 2019 16:40:11 +1100 Subject: [PATCH 037/117] removing debug statement --- .../sensor/use-mouse-sensor.js | 24 +++++++++++-------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/src/view/use-drag-handle/sensor/use-mouse-sensor.js b/src/view/use-drag-handle/sensor/use-mouse-sensor.js index 9d865a4e65..544afaa5ac 100644 --- a/src/view/use-drag-handle/sensor/use-mouse-sensor.js +++ b/src/view/use-drag-handle/sensor/use-mouse-sensor.js @@ -44,7 +44,13 @@ const noop = () => {}; const mouseDownMarshal: EventMarshal = createEventMarshal(); export default function useMouseSensor(args: Args): Result { - const { canStartCapturing, getWindow, callbacks, shouldAbortCapture } = args; + const { + canStartCapturing, + getWindow, + callbacks, + shouldAbortCapture, + getShouldRespectForceTouch, + } = args; const pendingRef = useRef(null); const isDraggingRef = useRef(false); const unbindWindowEventsRef = useRef<() => void>(noop); @@ -258,6 +264,12 @@ export default function useMouseSensor(args: Args): Result { return; } + // New behaviour + if (!getShouldRespectForceTouch()) { + event.preventDefault(); + return; + } + const forcePressThreshold: number = (MouseEvent: any) .WEBKIT_FORCE_AT_FORCE_MOUSE_DOWN; const isForcePressing: boolean = @@ -277,15 +289,7 @@ export default function useMouseSensor(args: Args): Result { }, ]; return bindings; - }, [ - callbacks, - cancel, - getWindow, - getIsCapturing, - schedule, - startDragging, - stop, - ]); + }, [getIsCapturing, cancel, startDragging, schedule, stop, callbacks, getWindow, getShouldRespectForceTouch]); const bindWindowEvents = useCallback(() => { const win: HTMLElement = getWindow(); From 4eabc0bb9b036353f166076b71df85839ad8103a Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Tue, 19 Mar 2019 17:10:23 +1100 Subject: [PATCH 038/117] use keyboard sensor --- .../sensor/use-keyboard-sensor.js | 277 ++++++++++++++++++ .../sensor/use-mouse-sensor.js | 56 ++-- src/view/use-drag-handle/use-drag-handle.js | 30 +- 3 files changed, 334 insertions(+), 29 deletions(-) create mode 100644 src/view/use-drag-handle/sensor/use-keyboard-sensor.js diff --git a/src/view/use-drag-handle/sensor/use-keyboard-sensor.js b/src/view/use-drag-handle/sensor/use-keyboard-sensor.js new file mode 100644 index 0000000000..814e57e7b2 --- /dev/null +++ b/src/view/use-drag-handle/sensor/use-keyboard-sensor.js @@ -0,0 +1,277 @@ +// @flow +import type { Position } from 'css-box-model'; +import { useRef, useCallback, useMemo, useLayoutEffect } from 'react'; +import invariant from 'tiny-invariant'; +import type { EventBinding } from '../util/event-types'; +import createEventMarshal, { + type EventMarshal, +} from '../util/create-event-marshal'; +import { bindEvents, unbindEvents } from '../util/bind-events'; +import createScheduler from '../util/create-scheduler'; +import { warning } from '../../../dev-warning'; +import * as keyCodes from '../../key-codes'; +import supportedPageVisibilityEventName from '../util/supported-page-visibility-event-name'; +import createPostDragEventPreventer, { + type EventPreventer, +} from '../util/create-post-drag-event-preventer'; +import isSloppyClickThresholdExceeded from '../util/is-sloppy-click-threshold-exceeded'; +import preventStandardKeyEvents from '../util/prevent-standard-key-events'; +import type { Callbacks } from '../drag-handle-types'; +import getBorderBoxCenterPosition from '../../get-border-box-center-position'; + +export type Args = {| + callbacks: Callbacks, + getDraggableRef: () => ?HTMLElement, + getWindow: () => HTMLElement, + canStartCapturing: (event: Event) => boolean, + shouldAbortCapture: boolean, +|}; +export type Result = { + onKeyDown: (event: KeyboardEvent) => void, + isCapturing: boolean, +}; + +type KeyMap = { + [key: number]: true, +}; + +const scrollJumpKeys: KeyMap = { + [keyCodes.pageDown]: true, + [keyCodes.pageUp]: true, + [keyCodes.home]: true, + [keyCodes.end]: true, +}; + +function noop() {} + +export default function useKeyboardSensor(args: Args): Result { + const { + canStartCapturing, + getWindow, + callbacks, + shouldAbortCapture, + getDraggableRef, + } = args; + const isDraggingRef = useRef(false); + const unbindWindowEventsRef = useRef<() => void>(noop); + + const getIsDragging = useCallback(() => isDraggingRef.current, []); + + const schedule = useMemo(() => { + invariant( + !getIsDragging(), + 'Should not recreate scheduler while capturing', + ); + return createScheduler(callbacks); + }, [callbacks, getIsDragging]); + + const stop = useCallback(() => { + if (!getIsDragging()) { + return; + } + + schedule.cancel(); + unbindWindowEventsRef.current(); + isDraggingRef.current = false; + }, [getIsDragging, schedule]); + + // instructed to stop capturing + if (shouldAbortCapture && getIsDragging()) { + stop(); + } + + const cancel = useCallback(() => { + const wasDragging: boolean = isDraggingRef.current; + stop(); + + if (wasDragging) { + callbacks.onCancel(); + } + }, [callbacks, stop]); + + const windowBindings: EventBinding[] = useMemo(() => { + invariant( + !getIsDragging(), + 'Should not recreate window bindings when dragging', + ); + return [ + // any mouse actions kills a drag + { + eventName: 'mousedown', + fn: cancel, + }, + { + eventName: 'mouseup', + fn: cancel, + }, + { + eventName: 'click', + fn: cancel, + }, + { + eventName: 'touchstart', + fn: cancel, + }, + // resizing the browser kills a drag + { + eventName: 'resize', + fn: cancel, + }, + // kill if the user is using the mouse wheel + // We are not supporting wheel / trackpad scrolling with keyboard dragging + { + eventName: 'wheel', + fn: cancel, + // chrome says it is a violation for this to not be passive + // it is fine for it to be passive as we just cancel as soon as we get + // any event + options: { passive: true }, + }, + // Need to respond instantly to a jump scroll request + // Not using the scheduler + { + eventName: 'scroll', + // Scroll events on elements do not bubble, but they go through the capture phase + // https://twitter.com/alexandereardon/status/985994224867819520 + // Using capture: false here as we want to avoid intercepting droppable scroll requests + options: { capture: false }, + fn: (event: UIEvent) => { + // IE11 fix: + // Scrollable events still bubble up and are caught by this handler in ie11. + // We can ignore this event + if (event.currentTarget !== getWindow()) { + return; + } + + callbacks.onWindowScroll(); + }, + }, + // Cancel on page visibility change + { + eventName: supportedPageVisibilityEventName, + fn: cancel, + }, + ]; + }, [callbacks, cancel, getIsDragging, getWindow]); + + const bindWindowEvents = useCallback(() => { + const win: HTMLElement = getWindow(); + const options = { capture: true }; + + // setting up our unbind before we bind + unbindWindowEventsRef.current = () => + unbindEvents(win, windowBindings, options); + + bindEvents(win, windowBindings, options); + }, [getWindow, windowBindings]); + + const startDragging = useCallback(() => { + invariant(!isDraggingRef.current, 'Cannot start a drag while dragging'); + + const ref: ?HTMLElement = getDraggableRef(); + invariant(ref, 'Cannot start a keyboard drag without a draggable ref'); + isDraggingRef.current = true; + + bindWindowEvents(); + + const center: Position = getBorderBoxCenterPosition(ref); + callbacks.onLift({ + clientSelection: center, + movementMode: 'SNAP', + }); + }, [bindWindowEvents, callbacks, getDraggableRef]); + + const onKeyDown = useCallback( + (event: KeyboardEvent) => { + // not dragging yet + if (!getIsDragging()) { + // We may already be lifting on a child draggable. + // We do not need to use an EventMarshal here as + // we always call preventDefault on the first input + if (event.defaultPrevented) { + return; + } + + // Cannot lift at this time + if (!canStartCapturing(event)) { + return; + } + + if (event.keyCode !== keyCodes.space) { + return; + } + + // Calling preventDefault as we are consuming the event + event.preventDefault(); + startDragging(); + return; + } + + // already dragging + + // Cancelling + if (event.keyCode === keyCodes.escape) { + event.preventDefault(); + cancel(); + return; + } + + // Dropping + if (event.keyCode === keyCodes.space) { + // need to stop parent Draggable's thinking this is a lift + event.preventDefault(); + stop(); + callbacks.onDrop(); + return; + } + + // Movement + + if (event.keyCode === keyCodes.arrowDown) { + event.preventDefault(); + schedule.moveDown(); + return; + } + + if (event.keyCode === keyCodes.arrowUp) { + event.preventDefault(); + schedule.moveUp(); + return; + } + + if (event.keyCode === keyCodes.arrowRight) { + event.preventDefault(); + schedule.moveRight(); + return; + } + + if (event.keyCode === keyCodes.arrowLeft) { + event.preventDefault(); + schedule.moveLeft(); + return; + } + + // preventing scroll jumping at this time + if (scrollJumpKeys[event.keyCode]) { + event.preventDefault(); + return; + } + + preventStandardKeyEvents(event); + }, + [ + callbacks, + canStartCapturing, + cancel, + getIsDragging, + schedule, + startDragging, + stop, + ], + ); + + return { + onKeyDown, + isCapturing: getIsDragging(), + }; +} diff --git a/src/view/use-drag-handle/sensor/use-mouse-sensor.js b/src/view/use-drag-handle/sensor/use-mouse-sensor.js index 544afaa5ac..84d46ee5d3 100644 --- a/src/view/use-drag-handle/sensor/use-mouse-sensor.js +++ b/src/view/use-drag-handle/sensor/use-mouse-sensor.js @@ -58,10 +58,6 @@ export default function useMouseSensor(args: Args): Result { () => Boolean(pendingRef.current || isDraggingRef.current), [], ); - const reset = useCallback(() => { - pendingRef.current = null; - isDraggingRef.current = false; - }, []); const schedule = useMemo(() => { invariant( @@ -76,23 +72,24 @@ export default function useMouseSensor(args: Args): Result { [getWindow], ); - const stop = useCallback( - (shouldBlockClick: ?boolean = true) => { - if (!getIsCapturing()) { - return; - } + const stop = useCallback(() => { + if (!getIsCapturing()) { + return; + } - schedule.cancel(); + schedule.cancel(); + unbindWindowEventsRef.current(); - unbindWindowEventsRef.current(); - mouseDownMarshal.reset(); - if (shouldBlockClick) { - postDragEventPreventer.preventNext(); - } - reset(); - }, - [getIsCapturing, postDragEventPreventer, reset, schedule], - ); + const shouldBlockClick: boolean = isDraggingRef.current; + + mouseDownMarshal.reset(); + if (shouldBlockClick) { + postDragEventPreventer.preventNext(); + } + // resettting refs + pendingRef.current = null; + isDraggingRef.current = false; + }, [getIsCapturing, postDragEventPreventer, schedule]); // instructed to stop capturing if (shouldAbortCapture && getIsCapturing()) { @@ -264,17 +261,17 @@ export default function useMouseSensor(args: Args): Result { return; } + const forcePressThreshold: number = (MouseEvent: any) + .WEBKIT_FORCE_AT_FORCE_MOUSE_DOWN; + const isForcePressing: boolean = + event.webkitForce >= forcePressThreshold; + // New behaviour if (!getShouldRespectForceTouch()) { event.preventDefault(); return; } - const forcePressThreshold: number = (MouseEvent: any) - .WEBKIT_FORCE_AT_FORCE_MOUSE_DOWN; - const isForcePressing: boolean = - event.webkitForce >= forcePressThreshold; - if (isForcePressing) { // it is considered a indirect cancel so we do not // prevent default in any situation. @@ -289,7 +286,16 @@ export default function useMouseSensor(args: Args): Result { }, ]; return bindings; - }, [getIsCapturing, cancel, startDragging, schedule, stop, callbacks, getWindow, getShouldRespectForceTouch]); + }, [ + getIsCapturing, + cancel, + startDragging, + schedule, + stop, + callbacks, + getWindow, + getShouldRespectForceTouch, + ]); const bindWindowEvents = useCallback(() => { const win: HTMLElement = getWindow(); diff --git a/src/view/use-drag-handle/use-drag-handle.js b/src/view/use-drag-handle/use-drag-handle.js index 0a04865479..baa0ccc362 100644 --- a/src/view/use-drag-handle/use-drag-handle.js +++ b/src/view/use-drag-handle/use-drag-handle.js @@ -11,6 +11,9 @@ import useMouseSensor, { } from './sensor/use-mouse-sensor'; import shouldAllowDraggingFromTarget from './util/should-allow-dragging-from-target'; import getDragHandleRef from './util/get-drag-handle-ref'; +import useKeyboardSensor, { + type Args as KeyboardSensorArgs, +} from './sensor/use-keyboard-sensor'; function preventHtml5Dnd(event: DragEvent) { event.preventDefault(); @@ -89,11 +92,30 @@ export default function useDragHandle(args: Args): DragHandleProps { getWindow, ], ); - const { isCapturing: isMouseCapturing, onMouseDown } = useMouseSensor( mouseArgs, ); - recordCapture([isMouseCapturing]); + + const keyboardArgs: KeyboardSensorArgs = useMemo( + () => ({ + callbacks, + getDraggableRef, + getWindow, + canStartCapturing, + shouldAbortCapture, + }), + [ + callbacks, + canStartCapturing, + getDraggableRef, + getWindow, + shouldAbortCapture, + ], + ); + const { isCapturing: isKeyboardCapturing, onKeyDown } = useKeyboardSensor( + keyboardArgs, + ); + recordCapture([isMouseCapturing, isKeyboardCapturing]); // mounting focus retention useLayoutEffect(() => {}); @@ -124,8 +146,8 @@ export default function useDragHandle(args: Args): DragHandleProps { const props: DragHandleProps = useMemo( () => ({ onMouseDown, + onKeyDown, // TODO - onKeyDown: () => {}, onTouchStart: () => {}, onFocus, onBlur, @@ -137,7 +159,7 @@ export default function useDragHandle(args: Args): DragHandleProps { draggable: false, onDragStart: preventHtml5Dnd, }), - [onBlur, onFocus, onMouseDown, styleContext], + [onBlur, onFocus, onKeyDown, onMouseDown, styleContext], ); return props; From 70342a78e49b6e74a1f9f717514dd56e6afe80b5 Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Tue, 19 Mar 2019 17:16:39 +1100 Subject: [PATCH 039/117] keyboard sensor --- .../use-drag-handle/sensor/use-keyboard-sensor.js | 13 +++++-------- src/view/use-drag-handle/sensor/use-mouse-sensor.js | 2 +- 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/src/view/use-drag-handle/sensor/use-keyboard-sensor.js b/src/view/use-drag-handle/sensor/use-keyboard-sensor.js index 814e57e7b2..5c146258a4 100644 --- a/src/view/use-drag-handle/sensor/use-keyboard-sensor.js +++ b/src/view/use-drag-handle/sensor/use-keyboard-sensor.js @@ -3,18 +3,10 @@ import type { Position } from 'css-box-model'; import { useRef, useCallback, useMemo, useLayoutEffect } from 'react'; import invariant from 'tiny-invariant'; import type { EventBinding } from '../util/event-types'; -import createEventMarshal, { - type EventMarshal, -} from '../util/create-event-marshal'; import { bindEvents, unbindEvents } from '../util/bind-events'; import createScheduler from '../util/create-scheduler'; -import { warning } from '../../../dev-warning'; import * as keyCodes from '../../key-codes'; import supportedPageVisibilityEventName from '../util/supported-page-visibility-event-name'; -import createPostDragEventPreventer, { - type EventPreventer, -} from '../util/create-post-drag-event-preventer'; -import isSloppyClickThresholdExceeded from '../util/is-sloppy-click-threshold-exceeded'; import preventStandardKeyEvents from '../util/prevent-standard-key-events'; import type { Callbacks } from '../drag-handle-types'; import getBorderBoxCenterPosition from '../../get-border-box-center-position'; @@ -270,6 +262,11 @@ export default function useKeyboardSensor(args: Args): Result { ], ); + // When unmounting - cancel + useLayoutEffect(() => { + return cancel; + }, [cancel]); + return { onKeyDown, isCapturing: getIsDragging(), diff --git a/src/view/use-drag-handle/sensor/use-mouse-sensor.js b/src/view/use-drag-handle/sensor/use-mouse-sensor.js index 84d46ee5d3..22d824cf86 100644 --- a/src/view/use-drag-handle/sensor/use-mouse-sensor.js +++ b/src/view/use-drag-handle/sensor/use-mouse-sensor.js @@ -370,7 +370,7 @@ export default function useMouseSensor(args: Args): Result { // When unmounting - cancel useLayoutEffect(() => { - return () => cancel(); + return cancel; }, [cancel]); return { From 0edd91827292c4b9154198c1fc40f0757a78cdb4 Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Wed, 20 Mar 2019 08:40:56 +1100 Subject: [PATCH 040/117] touch sensor --- .../sensor/use-touch-sensor.js | 445 ++++++++++++++++++ 1 file changed, 445 insertions(+) create mode 100644 src/view/use-drag-handle/sensor/use-touch-sensor.js diff --git a/src/view/use-drag-handle/sensor/use-touch-sensor.js b/src/view/use-drag-handle/sensor/use-touch-sensor.js new file mode 100644 index 0000000000..fccfd1b40d --- /dev/null +++ b/src/view/use-drag-handle/sensor/use-touch-sensor.js @@ -0,0 +1,445 @@ +// @flow +import type { Position } from 'css-box-model'; +import { useRef, useCallback, useMemo, useLayoutEffect } from 'react'; +import invariant from 'tiny-invariant'; +import type { EventBinding } from '../util/event-types'; +import createEventMarshal, { + type EventMarshal, +} from '../util/create-event-marshal'; +import { bindEvents, unbindEvents } from '../util/bind-events'; +import createScheduler from '../util/create-scheduler'; +import * as keyCodes from '../../key-codes'; +import supportedPageVisibilityEventName from '../util/supported-page-visibility-event-name'; +import createPostDragEventPreventer, { + type EventPreventer, +} from '../util/create-post-drag-event-preventer'; +import type { Callbacks } from '../drag-handle-types'; + +export type Args = {| + callbacks: Callbacks, + getDraggableRef: () => ?HTMLElement, + getWindow: () => HTMLElement, + canStartCapturing: (event: Event) => boolean, + getShouldRespectForceTouch: () => boolean, + shouldAbortCapture: boolean, +|}; +export type Result = { + onTouchStart: (event: TouchEvent) => void, + isCapturing: boolean, +}; + +type PendingDrag = {| + longPressTimerId: TimeoutID, + point: Position, +|}; + +type TouchWithForce = Touch & { + force: number, +}; + +type WebkitHack = {| + preventTouchMove: () => void, + releaseTouchMove: () => void, +|}; + +export const timeForLongPress: number = 150; +export const forcePressThreshold: number = 0.15; +const touchStartMarshal: EventMarshal = createEventMarshal(); +const noop = (): void => {}; + +// Webkit does not allow event.preventDefault() in dynamically added handlers +// So we add an always listening event handler to get around this :( +// webkit bug: https://bugs.webkit.org/show_bug.cgi?id=184250 +const webkitHack: WebkitHack = (() => { + const stub: WebkitHack = { + preventTouchMove: noop, + releaseTouchMove: noop, + }; + + // Do nothing when server side rendering + if (typeof window === 'undefined') { + return stub; + } + + // Device has no touch support - no point adding the touch listener + if (!('ontouchstart' in window)) { + return stub; + } + + // Not adding any user agent testing as everything pretends to be webkit + + let isBlocking: boolean = false; + + // Adding a persistent event handler + window.addEventListener( + 'touchmove', + (event: TouchEvent) => { + // We let the event go through as normal as nothing + // is blocking the touchmove + if (!isBlocking) { + return; + } + + // Our event handler would have worked correctly if the browser + // was not webkit based, or an older version of webkit. + if (event.defaultPrevented) { + return; + } + + // Okay, now we need to step in and fix things + event.preventDefault(); + + // Forcing this to be non-passive so we can get every touchmove + // Not activating in the capture phase like the dynamic touchmove we add. + // Technically it would not matter if we did this in the capture phase + }, + { passive: false, capture: false }, + ); + + const preventTouchMove = () => { + isBlocking = true; + }; + const releaseTouchMove = () => { + isBlocking = false; + }; + + return { preventTouchMove, releaseTouchMove }; +})(); + +export default function useTouchSensor(args: Args): Result { + const { + callbacks, + getWindow, + canStartCapturing, + getShouldRespectForceTouch, + shouldAbortCapture, + } = args; + const pendingRef = useRef(null); + const isDraggingRef = useRef(false); + const hasMovedRef = useRef(false); + const unbindWindowEventsRef = useRef<() => void>(noop); + const getIsCapturing = useCallback( + () => Boolean(pendingRef.current || isDraggingRef.current), + [], + ); + const postDragClickPreventer: EventPreventer = useMemo( + () => createPostDragEventPreventer(getWindow), + [getWindow], + ); + + const schedule = useMemo(() => { + invariant( + !getIsCapturing(), + 'Should not recreate scheduler while capturing', + ); + return createScheduler(callbacks); + }, [callbacks, getIsCapturing]); + + const stop = useCallback(() => { + if (!getIsCapturing()) { + return; + } + + schedule.cancel(); + unbindWindowEventsRef.current(); + touchStartMarshal.reset(); + webkitHack.releaseTouchMove(); + hasMovedRef.current = false; + + // if dragging - prevent the next click + if (isDraggingRef.current) { + postDragClickPreventer.preventNext(); + isDraggingRef.current = false; + return; + } + + const pending: ?PendingDrag = pendingRef.current; + invariant(pending, 'Expected a pending drag'); + + clearTimeout(pending.longPressTimerId); + pendingRef.current = null; + }, [getIsCapturing, postDragClickPreventer, schedule]); + + const cancel = useCallback(() => { + const wasDragging: boolean = isDraggingRef.current; + stop(); + + if (wasDragging) { + callbacks.onCancel(); + } + }, [callbacks, stop]); + + // instructed to stop capturing + if (shouldAbortCapture && getIsCapturing()) { + stop(); + } + + const windowBindings: EventBinding[] = useMemo(() => { + invariant( + !getIsCapturing(), + 'Should not recreate window bindings while capturing', + ); + + const bindings: EventBinding[] = [ + { + eventName: 'touchmove', + // Opting out of passive touchmove (default) so as to prevent scrolling while moving + // Not worried about performance as effect of move is throttled in requestAnimationFrame + options: { passive: false }, + fn: (event: TouchEvent) => { + // Drag has not yet started and we are waiting for a long press. + if (!isDraggingRef.current) { + stop(); + return; + } + + // At this point we are dragging + + if (!hasMovedRef.current) { + hasMovedRef.current = true; + } + + const { clientX, clientY } = event.touches[0]; + + const point: Position = { + x: clientX, + y: clientY, + }; + + // We need to prevent the default event in order to block native scrolling + // Also because we are using it as part of a drag we prevent the default action + // as a sign that we are using the event + event.preventDefault(); + schedule.move(point); + }, + }, + { + eventName: 'touchend', + fn: (event: TouchEvent) => { + // drag had not started yet - do not prevent the default action + if (!isDraggingRef.current) { + stop(); + return; + } + + // already dragging - this event is directly ending a drag + event.preventDefault(); + stop(); + callbacks.onDrop(); + }, + }, + { + eventName: 'touchcancel', + fn: (event: TouchEvent) => { + // drag had not started yet - do not prevent the default action + if (!isDraggingRef.current) { + stop(); + return; + } + + // already dragging - this event is directly ending a drag + event.preventDefault(); + cancel(); + }, + }, + // another touch start should not happen without a + // touchend or touchcancel. However, just being super safe + { + eventName: 'touchstart', + fn: cancel, + }, + // If the orientation of the device changes - kill the drag + // https://davidwalsh.name/orientation-change + { + eventName: 'orientationchange', + fn: cancel, + }, + // some devices fire resize if the orientation changes + { + eventName: 'resize', + fn: cancel, + }, + // ## Passive: true + // For scroll events we are okay with eventual consistency. + // Passive scroll listeners is the default behavior for mobile + // but we are being really clear here + // ## Capture: false + // Scroll events on elements do not bubble, but they go through the capture phase + // https://twitter.com/alexandereardon/status/985994224867819520 + // Using capture: false here as we want to avoid intercepting droppable scroll requests + { + eventName: 'scroll', + options: { passive: true, capture: false }, + fn: () => { + // stop a pending drag + if (pendingRef.current) { + stop(); + return; + } + schedule.windowScrollMove(); + }, + }, + // Long press can bring up a context menu + // need to opt out of this behavior + { + eventName: 'contextmenu', + fn: (event: Event) => { + // always opting out of context menu events + event.preventDefault(); + }, + }, + // On some devices it is possible to have a touch interface with a keyboard. + // On any keyboard event we cancel a touch drag + { + eventName: 'keydown', + fn: (event: KeyboardEvent) => { + if (!isDraggingRef.current) { + cancel(); + return; + } + + // direct cancel: we are preventing the default action + // indirect cancel: we are not preventing the default action + + // escape is a direct cancel + if (event.keyCode === keyCodes.escape) { + event.preventDefault(); + } + cancel(); + }, + }, + // Need to opt out of dragging if the user is a force press + // Only for webkit which has decided to introduce its own custom way of doing things + // https://developer.apple.com/library/content/documentation/AppleApplications/Conceptual/SafariJSProgTopics/RespondingtoForceTouchEventsfromJavaScript.html + { + eventName: 'touchforcechange', + fn: (event: TouchEvent) => { + // A force push action will no longer fire after a touchmove + if (hasMovedRef.current) { + // This is being super safe. While this situation should not occur we + // are still expressing that we want to opt out of force pressing + event.preventDefault(); + return; + } + + // A drag could be pending or has already started but no movement has occurred + + // Not respecting force touches - prevent the event + if (!getShouldRespectForceTouch()) { + event.preventDefault(); + return; + } + + const touch: TouchWithForce = (event.touches[0]: any); + + if (touch.force >= forcePressThreshold) { + // this is an indirect cancel so we do not preventDefault + // we also want to allow the force press to occur + cancel(); + } + }, + }, + // Cancel on page visibility change + { + eventName: supportedPageVisibilityEventName, + fn: cancel, + }, + ]; + return bindings; + }, [ + callbacks, + cancel, + getIsCapturing, + getShouldRespectForceTouch, + schedule, + stop, + ]); + + const bindWindowEvents = useCallback(() => { + const win: HTMLElement = getWindow(); + const options = { capture: true }; + + // setting up our unbind before we bind + unbindWindowEventsRef.current = () => + unbindEvents(win, windowBindings, options); + + bindEvents(win, windowBindings, options); + }, [getWindow, windowBindings]); + + const startDragging = useCallback(() => { + const pending: ?PendingDrag = pendingRef.current; + invariant(pending, 'Cannot start a drag without a pending drag'); + + isDraggingRef.current = true; + pendingRef.current = null; + hasMovedRef.current = false; + + callbacks.onLift({ + clientSelection: pending.point, + movementMode: 'FLUID', + }); + }, [callbacks]); + + const startPendingDrag = useCallback( + (event: TouchEvent) => { + invariant(!pendingRef.current, 'Expected there to be no pending drag'); + const touch: Touch = event.touches[0]; + const { clientX, clientY } = touch; + const point: Position = { + x: clientX, + y: clientY, + }; + const longPressTimerId: TimeoutID = setTimeout( + startDragging, + timeForLongPress, + ); + + const pending: PendingDrag = { + point, + longPressTimerId, + }; + + pendingRef.current = pending; + bindWindowEvents(); + }, + [bindWindowEvents, startDragging], + ); + + const onTouchStart = (event: TouchEvent) => { + if (touchStartMarshal.isHandled()) { + return; + } + + invariant( + !getIsCapturing(), + 'Should not be able to perform a touch start while a drag or pending drag is occurring', + ); + + // We do not need to prevent the event on a dropping draggable as + // the touchstart event will not fire due to pointer-events: none + // https://codesandbox.io/s/oxo0o775rz + if (!canStartCapturing(event)) { + return; + } + + // We need to stop parents from responding to this event - which may cause a double lift + // We also need to NOT call event.preventDefault() so as to maintain as much standard + // browser interactions as possible. + // This includes navigation on anchors which we want to preserve + touchStartMarshal.handle(); + + // A webkit only hack to prevent touch move events + webkitHack.preventTouchMove(); + startPendingDrag(event); + }; + + // When unmounting - cancel + useLayoutEffect(() => { + return cancel; + }, [cancel]); + + return { + onTouchStart, + isCapturing: getIsCapturing(), + }; +} From bdb046c40c5c5cd32d42ac636fea4e4ea9f9ea0a Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Wed, 20 Mar 2019 08:43:06 +1100 Subject: [PATCH 041/117] linking in touch handle --- src/view/use-drag-handle/use-drag-handle.js | 31 ++++++++++++++++++--- 1 file changed, 27 insertions(+), 4 deletions(-) diff --git a/src/view/use-drag-handle/use-drag-handle.js b/src/view/use-drag-handle/use-drag-handle.js index baa0ccc362..3d6950ccf1 100644 --- a/src/view/use-drag-handle/use-drag-handle.js +++ b/src/view/use-drag-handle/use-drag-handle.js @@ -14,6 +14,9 @@ import getDragHandleRef from './util/get-drag-handle-ref'; import useKeyboardSensor, { type Args as KeyboardSensorArgs, } from './sensor/use-keyboard-sensor'; +import useTouchSensor, { + type Args as TouchSensorArgs, +} from './sensor/use-touch-sensor'; function preventHtml5Dnd(event: DragEvent) { event.preventDefault(); @@ -115,7 +118,28 @@ export default function useDragHandle(args: Args): DragHandleProps { const { isCapturing: isKeyboardCapturing, onKeyDown } = useKeyboardSensor( keyboardArgs, ); - recordCapture([isMouseCapturing, isKeyboardCapturing]); + const touchArgs: TouchSensorArgs = useMemo( + () => ({ + callbacks, + getDraggableRef, + getWindow, + canStartCapturing, + getShouldRespectForceTouch, + shouldAbortCapture, + }), + [ + shouldAbortCapture, + callbacks, + canStartCapturing, + getDraggableRef, + getShouldRespectForceTouch, + getWindow, + ], + ); + const { isCapturing: isTouchCapturing, onTouchStart } = useTouchSensor( + touchArgs, + ); + recordCapture([isMouseCapturing, isKeyboardCapturing, isTouchCapturing]); // mounting focus retention useLayoutEffect(() => {}); @@ -147,8 +171,7 @@ export default function useDragHandle(args: Args): DragHandleProps { () => ({ onMouseDown, onKeyDown, - // TODO - onTouchStart: () => {}, + onTouchStart, onFocus, onBlur, tabIndex: 0, @@ -159,7 +182,7 @@ export default function useDragHandle(args: Args): DragHandleProps { draggable: false, onDragStart: preventHtml5Dnd, }), - [onBlur, onFocus, onKeyDown, onMouseDown, styleContext], + [onBlur, onFocus, onKeyDown, onMouseDown, onTouchStart, styleContext], ); return props; From 62c3f6c4887620855952b6120319a422463c99fa Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Wed, 20 Mar 2019 09:09:30 +1100 Subject: [PATCH 042/117] removing unused things --- src/view/use-drag-handle/use-drag-handle.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/view/use-drag-handle/use-drag-handle.js b/src/view/use-drag-handle/use-drag-handle.js index 3d6950ccf1..d0a3aa4798 100644 --- a/src/view/use-drag-handle/use-drag-handle.js +++ b/src/view/use-drag-handle/use-drag-handle.js @@ -1,6 +1,5 @@ // @flow import { useLayoutEffect, useRef, useMemo, useState, useCallback } from 'react'; -import invariant from 'tiny-invariant'; import type { Args, DragHandleProps } from './drag-handle-types'; import getWindowFromEl from '../window/get-window-from-el'; import useRequiredContext from '../use-required-context'; @@ -10,7 +9,6 @@ import useMouseSensor, { type Args as MouseSensorArgs, } from './sensor/use-mouse-sensor'; import shouldAllowDraggingFromTarget from './util/should-allow-dragging-from-target'; -import getDragHandleRef from './util/get-drag-handle-ref'; import useKeyboardSensor, { type Args as KeyboardSensorArgs, } from './sensor/use-keyboard-sensor'; From c31e94661ad43c27a5c28ce3d548054d31098ec9 Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Wed, 20 Mar 2019 10:01:38 +1100 Subject: [PATCH 043/117] wip --- .../use-draggable-dimension-publisher.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/view/use-draggable-dimension-publisher/use-draggable-dimension-publisher.js b/src/view/use-draggable-dimension-publisher/use-draggable-dimension-publisher.js index 6f4a949f0a..177412a47f 100644 --- a/src/view/use-draggable-dimension-publisher/use-draggable-dimension-publisher.js +++ b/src/view/use-draggable-dimension-publisher/use-draggable-dimension-publisher.js @@ -34,7 +34,7 @@ export default function useDraggableDimensionPublisher(args: Args) { type, index, }; - console.log('creating new descriptor', result); + // console.log('creating new descriptor', result); return result; }, [draggableId, droppableId, index, type]); From 6805ee068c98422ec44f3341ad67d84ffd3c44b5 Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Wed, 20 Mar 2019 15:06:37 +1100 Subject: [PATCH 044/117] fixing flow errors --- src/index.js | 8 +++---- .../drag-drop-context/drag-drop-context.jsx | 12 ++++------ src/view/draggable/connected-draggable.js | 1 + src/view/droppable/connected-droppable.js | 1 + src/view/use-drag-handle/use-drag-handle.js | 2 -- .../use-droppable-dimension-publisher.js | 22 +++++++++-------- .../responders-integration.spec.js | 10 ++++++-- .../unit/state/middleware/auto-scroll.spec.js | 6 ++--- .../dimension-marshal-stopper.spec.js | 10 ++++---- test/unit/state/middleware/lift.spec.js | 24 ++++++++++++++----- test/unit/state/middleware/style.spec.js | 2 +- 11 files changed, 56 insertions(+), 42 deletions(-) diff --git a/src/index.js b/src/index.js index b103d1b7fe..30d1f6a912 100644 --- a/src/index.js +++ b/src/index.js @@ -30,14 +30,14 @@ export type { OnDragEndResponder, } from './types'; -// Droppable +// Droppable types export type { Provided as DroppableProvided, StateSnapshot as DroppableStateSnapshot, DroppableProps, } from './view/droppable/droppable-types'; -// Draggable +// Draggable types export type { Provided as DraggableProvided, StateSnapshot as DraggableStateSnapshot, @@ -48,5 +48,5 @@ export type { NotDraggingStyle, } from './view/draggable/draggable-types'; -// DragHandle -export type { DragHandleProps } from './view/drag-handle/drag-handle-types'; +// DragHandle types +export type { DragHandleProps } from './view/use-drag-handle/drag-handle-types'; diff --git a/src/view/drag-drop-context/drag-drop-context.jsx b/src/view/drag-drop-context/drag-drop-context.jsx index ae2a271ed9..79862a4456 100644 --- a/src/view/drag-drop-context/drag-drop-context.jsx +++ b/src/view/drag-drop-context/drag-drop-context.jsx @@ -1,10 +1,5 @@ // @flow -import React, { - useEffect, - useRef, - type Node, - type MutableRefObject, -} from 'react'; +import React, { useEffect, useRef, type Node } from 'react'; import { bindActionCreators } from 'redux'; import { Provider } from 'react-redux'; import createStore from '../../state/create-store'; @@ -32,7 +27,6 @@ import { updateDroppableIsCombineEnabled, collectionStarting, } from '../../state/action-creators'; -import { getFormattedMessage } from '../../dev-warning'; import isMovementAllowed from '../../state/is-movement-allowed'; import useAnnouncer from '../use-announcer'; import AppContext, { type AppContextValue } from '../context/app-context'; @@ -63,7 +57,9 @@ export default function DragDropContext(props: Props) { // We do not want this to change const uniqueId: number = useConstant((): number => instanceCount++); - let storeRef: MutableRefObject; + // flow does not support MutableRefObject + // let storeRef: MutableRefObject; + let storeRef; useStartupValidation(); diff --git a/src/view/draggable/connected-draggable.js b/src/view/draggable/connected-draggable.js index 2aceb98ffa..677d53f23a 100644 --- a/src/view/draggable/connected-draggable.js +++ b/src/view/draggable/connected-draggable.js @@ -280,6 +280,7 @@ class DraggableType extends Component { // Leaning heavily on the default shallow equality checking // that `connect` provides. // It avoids needing to do it own within `Draggable` +// $ExpectError - incorrect flowtype for react-redux version const ConnectedDraggable: typeof DraggableType = (connect( // returning a function so each component can do its own memoization makeMapStateToProps, diff --git a/src/view/droppable/connected-droppable.js b/src/view/droppable/connected-droppable.js index d22bdb9159..721eb646c1 100644 --- a/src/view/droppable/connected-droppable.js +++ b/src/view/droppable/connected-droppable.js @@ -218,6 +218,7 @@ class DroppableType extends Component { // Leaning heavily on the default shallow equality checking // that `connect` provides. // It avoids needing to do it own within `Droppable` +// $ExpectError - incorrect flowtype for react-redux version const ConnectedDroppable: typeof DroppableType = (connect( // returning a function so each component can do its own memoization makeMapStateToProps, diff --git a/src/view/use-drag-handle/use-drag-handle.js b/src/view/use-drag-handle/use-drag-handle.js index d0a3aa4798..b5fd5a3b1b 100644 --- a/src/view/use-drag-handle/use-drag-handle.js +++ b/src/view/use-drag-handle/use-drag-handle.js @@ -4,7 +4,6 @@ import type { Args, DragHandleProps } from './drag-handle-types'; import getWindowFromEl from '../window/get-window-from-el'; import useRequiredContext from '../use-required-context'; import AppContext, { type AppContextValue } from '../context/app-context'; -import focusRetainer from './util/focus-retainer'; import useMouseSensor, { type Args as MouseSensorArgs, } from './sensor/use-mouse-sensor'; @@ -35,7 +34,6 @@ export default function useDragHandle(args: Args): DragHandleProps { ); const { isDragging, - isDropAnimating, isEnabled, draggableId, callbacks, diff --git a/src/view/use-droppable-dimension-publisher/use-droppable-dimension-publisher.js b/src/view/use-droppable-dimension-publisher/use-droppable-dimension-publisher.js index c6a03a0122..e78a085c85 100644 --- a/src/view/use-droppable-dimension-publisher/use-droppable-dimension-publisher.js +++ b/src/view/use-droppable-dimension-publisher/use-droppable-dimension-publisher.js @@ -1,12 +1,5 @@ // @flow -import { - useCallback, - useMemo, - useEffect, - useContext, - useLayoutEffect, - useRef, -} from 'react'; +import { useCallback, useMemo, useLayoutEffect, useRef } from 'react'; import invariant from 'tiny-invariant'; import { type Position } from 'css-box-model'; import rafSchedule from 'raf-schd'; @@ -236,8 +229,17 @@ export default function useDroppableDimensionPublisher(args: Props) { getListenerOptions(dragging.scrollOptions), ); }, [onClosestScroll, scheduleScrollUpdate]); - const scroll = useCallback(() => { - invariant('TODO'); + + const scroll = useCallback((change: Position) => { + // arrange + const dragging: ?WhileDragging = whileDraggingRef.current; + invariant(dragging, 'Cannot scroll when there is no drag'); + const closest: ?Element = getClosestScrollableFromDrag(dragging); + invariant(closest, 'Cannot scroll a droppable with no closest scrollable'); + + // act + closest.scrollTop += change.y; + closest.scrollLeft += change.x; }, []); const callbacks: DroppableCallbacks = useMemo(() => { diff --git a/test/unit/integration/responders-integration.spec.js b/test/unit/integration/responders-integration.spec.js index d74b291e6f..065d09476e 100644 --- a/test/unit/integration/responders-integration.spec.js +++ b/test/unit/integration/responders-integration.spec.js @@ -4,7 +4,7 @@ import React from 'react'; import { mount } from 'enzyme'; import { getRect, type Rect, type Position } from 'css-box-model'; import { DragDropContext, Draggable, Droppable } from '../../../src'; -import { sloppyClickThreshold } from '../../../src/view/drag-handle/util/is-sloppy-click-threshold-exceeded'; +import { sloppyClickThreshold } from '../../../src/view/use-drag-handle/util/is-sloppy-click-threshold-exceeded'; import { dispatchWindowMouseEvent, dispatchWindowKeyDownEvent, @@ -151,7 +151,13 @@ describe('responders integration', () => { const waitForReturnToHome = () => { // cheating - wrapper.find(Draggable).simulate('transitionEnd'); + console.log( + 'find', + wrapper.find('[data-react-beautiful-dnd-draggable]').length, + ); + wrapper + .find('[data-react-beautiful-dnd-draggable]') + .simulate('onMoveEnd'); }; const stop = () => { diff --git a/test/unit/state/middleware/auto-scroll.spec.js b/test/unit/state/middleware/auto-scroll.spec.js index cc46b6f0ef..f56cf6e848 100644 --- a/test/unit/state/middleware/auto-scroll.spec.js +++ b/test/unit/state/middleware/auto-scroll.spec.js @@ -38,7 +38,7 @@ const getScrollerStub = (): AutoScroller => ({ shouldCancelPending.forEach((action: Action) => { it(`should cancel a pending scroll when a ${action.type} is fired`, () => { const scroller: AutoScroller = getScrollerStub(); - const store: Store = createStore(middleware(() => scroller)); + const store: Store = createStore(middleware(scroller)); store.dispatch(initialPublish(initialPublishArgs)); expect(store.getState().phase).toBe('DRAGGING'); @@ -53,7 +53,7 @@ shouldCancelPending.forEach((action: Action) => { shouldStop.forEach((action: Action) => { it(`should stop the auto scroller when a ${action.type} is fired`, () => { const scroller: AutoScroller = getScrollerStub(); - const store: Store = createStore(middleware(() => scroller)); + const store: Store = createStore(middleware(scroller)); store.dispatch(initialPublish(initialPublishArgs)); expect(store.getState().phase).toBe('DRAGGING'); @@ -66,7 +66,7 @@ shouldStop.forEach((action: Action) => { it('should fire a scroll when there is an update', () => { const scroller: AutoScroller = getScrollerStub(); - const store: Store = createStore(middleware(() => scroller)); + const store: Store = createStore(middleware(scroller)); store.dispatch(initialPublish(initialPublishArgs)); expect(scroller.start).toHaveBeenCalledWith(store.getState()); diff --git a/test/unit/state/middleware/dimension-marshal-stopper.spec.js b/test/unit/state/middleware/dimension-marshal-stopper.spec.js index e5ee6a5c9f..a56c5489bb 100644 --- a/test/unit/state/middleware/dimension-marshal-stopper.spec.js +++ b/test/unit/state/middleware/dimension-marshal-stopper.spec.js @@ -28,9 +28,7 @@ const getMarshal = (stopPublishing: Function): DimensionMarshal => { it('should stop a collection if a drag is aborted', () => { const stopPublishing = jest.fn(); - const store: Store = createStore( - middleware(() => getMarshal(stopPublishing)), - ); + const store: Store = createStore(middleware(getMarshal(stopPublishing))); store.dispatch(initialPublish(initialPublishArgs)); @@ -42,7 +40,7 @@ it('should stop a collection if a drag is aborted', () => { it('should not stop a collection if a drop is pending', () => { const stopPublishing = jest.fn(); const store: Store = createStore( - middleware(() => getMarshal(stopPublishing)), + middleware(getMarshal(stopPublishing)), // will convert the drop into a drop pending dropMiddleware, ); @@ -62,7 +60,7 @@ it('should not stop a collection if a drop is pending', () => { it('should stop a collection if a drag is complete', () => { const stopPublishing = jest.fn(); const store: Store = createStore( - middleware(() => getMarshal(stopPublishing)), + middleware(getMarshal(stopPublishing)), // will convert the drop into a drop pending dropMiddleware, ); @@ -80,7 +78,7 @@ it('should stop a collection if a drag is complete', () => { it('should stop a collection if a drop animation starts', () => { const stopPublishing = jest.fn(); const store: Store = createStore( - middleware(() => getMarshal(stopPublishing)), + middleware(getMarshal(stopPublishing)), // will convert the drop into a drop pending dropMiddleware, ); diff --git a/test/unit/state/middleware/lift.spec.js b/test/unit/state/middleware/lift.spec.js index 1bbf1f9fc9..d5dc87d017 100644 --- a/test/unit/state/middleware/lift.spec.js +++ b/test/unit/state/middleware/lift.spec.js @@ -1,6 +1,6 @@ // @flow import type { CompletedDrag } from '../../../../src/types'; -import type { Store } from '../../../../src/state/store-types'; +import type { Action, Store } from '../../../../src/state/store-types'; import type { DimensionMarshal } from '../../../../src/state/dimension-marshal/dimension-marshal-types'; import middleware from '../../../../src/state/middleware/lift'; import createStore from './util/create-store'; @@ -23,8 +23,8 @@ import { getCompletedArgs, } from '../../../utils/preset-action-args'; -const getMarshal = (store: Store): DimensionMarshal => { - const marshal: DimensionMarshal = getDimensionMarshal(store.dispatch); +const getMarshal = (dispatch: Action => void): DimensionMarshal => { + const marshal: DimensionMarshal = getDimensionMarshal(dispatch); populateMarshal(marshal); return marshal; @@ -45,7 +45,11 @@ it('should throw if a drag cannot be started when a lift action occurs', () => { const mock = jest.fn(); const store: Store = createStore( passThrough(mock), - middleware(() => getMarshal(store)), + middleware( + getMarshal((action: Action) => { + store.dispatch(action); + }), + ), ); // first lift is all good @@ -61,7 +65,11 @@ it('should flush any animating drops', () => { const mock = jest.fn(); const store: Store = createStore( passThrough(mock), - middleware(() => getMarshal(store)), + middleware( + getMarshal((action: Action) => { + store.dispatch(action); + }), + ), ); // start a drag @@ -95,7 +103,11 @@ it('should publish the initial dimensions when lifting', () => { const mock = jest.fn(); const store: Store = createStore( passThrough(mock), - middleware(() => getMarshal(store)), + middleware( + getMarshal((action: Action) => { + store.dispatch(action); + }), + ), ); // first lift is preparing diff --git a/test/unit/state/middleware/style.spec.js b/test/unit/state/middleware/style.spec.js index 577bbfad62..b435ccbbee 100644 --- a/test/unit/state/middleware/style.spec.js +++ b/test/unit/state/middleware/style.spec.js @@ -1,6 +1,6 @@ // @flow import middleware from '../../../../src/state/middleware/style'; -import type { StyleMarshal } from '../../../../src/view/style-marshal/style-marshal-types'; +import type { StyleMarshal } from '../../../../src/view/use-style-marshal/style-marshal-types'; import type { DropReason } from '../../../../src/types'; import type { Store } from '../../../../src/state/store-types'; import createStore from './util/create-store'; From 41f33d1815ab7ff7d05e466daa1cdc09126e6892 Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Thu, 21 Mar 2019 08:20:04 +1100 Subject: [PATCH 045/117] trying to write some tests --- package.json | 1 + .../use-drag-handle/use-drag-handle.old.js | 185 ------------------ .../draggable-dimension-publisher.jsx | 150 -------------- src/view/use-memo-one.js | 40 ++++ .../draggable/drag-handle-connection.spec.js | 2 +- .../sensors/use-mouse-sensor.spec.js | 80 ++++++++ yarn.lock | 46 +++++ 7 files changed, 168 insertions(+), 336 deletions(-) delete mode 100644 src/view/use-drag-handle/use-drag-handle.old.js delete mode 100644 src/view/use-draggable-dimension-publisher/draggable-dimension-publisher.jsx create mode 100644 src/view/use-memo-one.js create mode 100644 test/unit/view/draggable/drag-handle/sensors/use-mouse-sensor.spec.js diff --git a/package.json b/package.json index 5dc290c14f..89afcbd832 100644 --- a/package.json +++ b/package.json @@ -110,6 +110,7 @@ "react": "16.8.4", "react-dom": "16.8.4", "react-test-renderer": "16.8.4", + "react-testing-library": "^6.0.1", "rimraf": "^2.6.3", "rollup": "^1.6.0", "rollup-plugin-babel": "^4.3.2", diff --git a/src/view/use-drag-handle/use-drag-handle.old.js b/src/view/use-drag-handle/use-drag-handle.old.js deleted file mode 100644 index 4efc8eb5ae..0000000000 --- a/src/view/use-drag-handle/use-drag-handle.old.js +++ /dev/null @@ -1,185 +0,0 @@ -// @flow -import { useLayoutEffect, useRef } from 'react'; -import type { - Sensor, - CreateSensorArgs, - MouseSensor, - KeyboardSensor, - TouchSensor, -} from './sensor/sensor-types'; -import type { Args, DragHandleProps, Callbacks } from './drag-handle-types'; -import { useConstant, useConstantFn } from '../use-constant'; -import createKeyboardSensor from './sensor/create-keyboard-sensor'; -import createTouchSensor from './sensor/create-touch-sensor'; -import { warning } from '../../dev-warning'; -import shouldAllowDraggingFromTarget from './util/should-allow-dragging-from-target'; -import getWindowFromEl from '../window/get-window-from-el'; -import useRequiredContext from '../use-required-context'; -import AppContext, { type AppContextValue } from '../context/app-context'; -import createMouseSensor from './sensor/create-mouse-sensor'; - -function preventHtml5Dnd(event: DragEvent) { - event.preventDefault(); -} - -export default function useDragHandle(args: Args): DragHandleProps { - const { canLift, style }: AppContextValue = useRequiredContext(AppContext); - - // side effect on every render - const latestArgsRef = useRef(args); - latestArgsRef.current = args; - const getLatestArgs = useConstantFn(() => latestArgsRef.current); - - // TODO: things will go bad if getDraggableRef changes - const getWindow = useConstantFn( - (): HTMLElement => getWindowFromEl(getLatestArgs().getDraggableRef()), - ); - - const isFocusedRef = useRef(false); - const onFocus = useConstantFn(() => { - isFocusedRef.current = true; - }); - const onBlur = useConstantFn(() => { - isFocusedRef.current = false; - }); - - const isAnySensorCapturing = useConstantFn(() => - // using getSensors before it is defined - // eslint-disable-next-line no-use-before-define - getSensors().some((sensor: Sensor): boolean => sensor.isCapturing()), - ); - - const canStartCapturing = useConstantFn((event: Event) => { - // this might be before a drag has started - isolated to this element - if (isAnySensorCapturing()) { - return false; - } - - // this will check if anything else in the system is dragging - if (!canLift(getLatestArgs().draggableId)) { - return false; - } - - // check if we are dragging an interactive element - return shouldAllowDraggingFromTarget( - event, - getLatestArgs().canDragInteractiveElements, - ); - }); - - const getCallbacks = useConstantFn( - (): Callbacks => getLatestArgs().callbacks, - ); - - const createArgs: CreateSensorArgs = useConstant(() => ({ - getCallbacks, - getDraggableRef: getLatestArgs().getDraggableRef, - canStartCapturing, - getWindow, - getShouldRespectForceTouch: getLatestArgs().getShouldRespectForceTouch, - })); - - const mouse: MouseSensor = useConstant(() => createMouseSensor(createArgs)); - const keyboard: KeyboardSensor = useConstant(() => - createKeyboardSensor(createArgs), - ); - const touch: TouchSensor = useConstant(() => createTouchSensor(createArgs)); - const getSensors = useConstantFn(() => [mouse, keyboard, touch]); - - const onKeyDown = useConstantFn((event: KeyboardEvent) => { - // let the other sensors deal with it - if (mouse.isCapturing() || touch.isCapturing()) { - return; - } - - keyboard.onKeyDown(event); - }); - - const onMouseDown = useConstantFn((event: MouseEvent) => { - // let the other sensors deal with it - if (keyboard.isCapturing() || touch.isCapturing()) { - return; - } - - mouse.onMouseDown(event); - }); - - const onTouchStart = useConstantFn((event: TouchEvent) => { - // let the keyboard sensor deal with it - if (mouse.isCapturing() || keyboard.isCapturing()) { - return; - } - - touch.onTouchStart(event); - }); - - // TODO: focus retention - useLayoutEffect(() => {}); - - // Cleanup any capturing sensors when unmounting - useLayoutEffect(() => { - // Just a cleanup function - return () => { - getSensors().forEach((sensor: Sensor) => { - // kill the current drag and fire a cancel event if - const wasDragging: boolean = sensor.isDragging(); - - sensor.unmount(); - // Cancel if drag was occurring - if (wasDragging) { - latestArgsRef.current.callbacks.onCancel(); - } - }); - }; - // sensors is constant - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - - // Checking for disabled changes during a drag - useLayoutEffect(() => { - getSensors().forEach((sensor: Sensor) => { - if (!sensor.isCapturing()) { - return; - } - const wasDragging: boolean = sensor.isDragging(); - sensor.kill(); - - // It is fine for a draggable to be disabled while a drag is pending - if (wasDragging) { - warning( - 'You have disabled dragging on a Draggable while it was dragging. The drag has been cancelled', - ); - getLatestArgs().callbacks.onCancel(); - } - }); - }, [getLatestArgs, getSensors]); - - // Drag aborted elsewhere in application - useLayoutEffect(() => { - if (getLatestArgs().isDragging) { - return; - } - - getSensors().forEach((sensor: Sensor) => { - if (sensor.isCapturing()) { - sensor.kill(); - } - }); - }, [getLatestArgs, getSensors]); - - const props: DragHandleProps = useConstant(() => ({ - onMouseDown, - onKeyDown, - onTouchStart, - onFocus, - onBlur, - tabIndex: 0, - 'data-react-beautiful-dnd-drag-handle': style, - // English default. Consumers are welcome to add their own start instruction - 'aria-roledescription': 'Draggable item. Press space bar to lift', - draggable: false, - onDragStart: preventHtml5Dnd, - })); - - return props; -} diff --git a/src/view/use-draggable-dimension-publisher/draggable-dimension-publisher.jsx b/src/view/use-draggable-dimension-publisher/draggable-dimension-publisher.jsx deleted file mode 100644 index 2db8e33f59..0000000000 --- a/src/view/use-draggable-dimension-publisher/draggable-dimension-publisher.jsx +++ /dev/null @@ -1,150 +0,0 @@ -// @flow -import { - calculateBox, - withScroll, - type BoxModel, - type Position, -} from 'css-box-model'; -import memoizeOne from 'memoize-one'; -import PropTypes from 'prop-types'; -import { Component, type Node } from 'react'; -import invariant from 'tiny-invariant'; -import { dimensionMarshalKey } from '../context-keys'; -import { origin } from '../../state/position'; -import type { - DraggableDescriptor, - DraggableDimension, - Placeholder, - DraggableId, - DroppableId, - TypeId, -} from '../../types'; -import type { DimensionMarshal } from '../../state/dimension-marshal/dimension-marshal-types'; - -type Props = {| - draggableId: DraggableId, - droppableId: DroppableId, - type: TypeId, - index: number, - getDraggableRef: () => ?HTMLElement, - children: Node, -|}; - -export default class DraggableDimensionPublisher extends Component { - /* eslint-disable react/sort-comp */ - static contextTypes = { - [dimensionMarshalKey]: PropTypes.object.isRequired, - }; - - publishedDescriptor: ?DraggableDescriptor = null; - - componentDidMount() { - this.publish(); - } - - componentDidUpdate() { - this.publish(); - } - - componentWillUnmount() { - this.unpublish(); - } - - getMemoizedDescriptor = memoizeOne( - ( - id: DraggableId, - index: number, - droppableId: DroppableId, - type: TypeId, - ): DraggableDescriptor => ({ - id, - index, - droppableId, - type, - }), - ); - - publish = () => { - const marshal: DimensionMarshal = this.context[dimensionMarshalKey]; - const descriptor: DraggableDescriptor = this.getMemoizedDescriptor( - this.props.draggableId, - this.props.index, - this.props.droppableId, - this.props.type, - ); - - if (!this.publishedDescriptor) { - marshal.registerDraggable(descriptor, this.getDimension); - this.publishedDescriptor = descriptor; - return; - } - - // No changes to the descriptor - if (descriptor === this.publishedDescriptor) { - return; - } - - marshal.updateDraggable( - this.publishedDescriptor, - descriptor, - this.getDimension, - ); - this.publishedDescriptor = descriptor; - }; - - unpublish = () => { - invariant( - this.publishedDescriptor, - 'Cannot unpublish descriptor when none is published', - ); - - // Using the previously published id to unpublish. This is to guard - // against the case where the id dynamically changes. This is not - // supported during a drag - but it is good to guard against. - const marshal: DimensionMarshal = this.context[dimensionMarshalKey]; - marshal.unregisterDraggable(this.publishedDescriptor); - this.publishedDescriptor = null; - }; - - getDimension = (windowScroll?: Position = origin): DraggableDimension => { - const targetRef: ?HTMLElement = this.props.getDraggableRef(); - const descriptor: ?DraggableDescriptor = this.publishedDescriptor; - - invariant( - targetRef, - 'DraggableDimensionPublisher cannot calculate a dimension when not attached to the DOM', - ); - invariant(descriptor, 'Cannot get dimension for unpublished draggable'); - - const computedStyles: CSSStyleDeclaration = window.getComputedStyle( - targetRef, - ); - const borderBox: ClientRect = targetRef.getBoundingClientRect(); - const client: BoxModel = calculateBox(borderBox, computedStyles); - const page: BoxModel = withScroll(client, windowScroll); - - const placeholder: Placeholder = { - client, - tagName: targetRef.tagName.toLowerCase(), - display: computedStyles.display, - }; - const displaceBy: Position = { - x: client.marginBox.width, - y: client.marginBox.height, - }; - - const dimension: DraggableDimension = { - descriptor, - placeholder, - displaceBy, - client, - page, - }; - - return dimension; - }; - - render() { - return this.props.children; - } -} diff --git a/src/view/use-memo-one.js b/src/view/use-memo-one.js new file mode 100644 index 0000000000..8514c1cb79 --- /dev/null +++ b/src/view/use-memo-one.js @@ -0,0 +1,40 @@ +// @flow +import { useRef } from 'react'; + +const isShallowEqual = (newValue: mixed, oldValue: mixed): boolean => + newValue === oldValue; + +const isEqual = (newInputs: mixed[], lastInputs: mixed[]): boolean => + newInputs.length === lastInputs.length && + newInputs.every( + (newArg: mixed, index: number): boolean => + isShallowEqual(newArg, lastInputs[index]), + ); + +export default function useMemoOne( + // getResult changes on every call, + getResult: () => T, + // the inputs array changes on every call + inputs?: mixed[] = [], +): T { + const isFirstCallRef = useRef(true); + const lastInputsRef = useRef(inputs); + // Cannot lazy create a ref value, so setting to null + // $ExpectError - T is not null + const resultRef = useRef(null); + + // on first call return the initial result + if (isFirstCallRef.current) { + resultRef.current = getResult(); + return resultRef.current; + } + + // Don't recalculate result if the inputs have not changed + if (isEqual(inputs, lastInputsRef.current)) { + return resultRef.current; + } + + lastInputsRef.current = inputs; + resultRef.current = getResult(); + return resultRef.current; +} diff --git a/test/unit/view/draggable/drag-handle-connection.spec.js b/test/unit/view/draggable/drag-handle-connection.spec.js index 6e212f6fc6..76ecd0918d 100644 --- a/test/unit/view/draggable/drag-handle-connection.spec.js +++ b/test/unit/view/draggable/drag-handle-connection.spec.js @@ -19,7 +19,7 @@ import { getPreset } from '../../../utils/dimension'; import { setViewport } from '../../../utils/viewport'; import mount from './util/mount'; import Item from './util/item'; -import DragHandle from '../../../../src/view/drag-handle'; +import DragHandle from '../../../../src/view/use-drag-handle'; import { withKeyboard } from '../../../utils/user-input-util'; import * as keyCodes from '../../../../src/view/key-codes'; diff --git a/test/unit/view/draggable/drag-handle/sensors/use-mouse-sensor.spec.js b/test/unit/view/draggable/drag-handle/sensors/use-mouse-sensor.spec.js new file mode 100644 index 0000000000..7525eccc8d --- /dev/null +++ b/test/unit/view/draggable/drag-handle/sensors/use-mouse-sensor.spec.js @@ -0,0 +1,80 @@ +// @flow +import React from 'react'; +import invariant from 'tiny-invariant'; +import type { Position } from 'css-box-model'; +import { render, fireEvent } from 'react-testing-library'; +import { + Draggable, + DragDropContext, + Droppable, + type DraggableStateSnapshot, +} from '../../../../../../src'; +import DroppableContext, { + type DroppableContextValue, +} from '../../../../../../src/view/context/droppable-context'; +import { primaryButton } from '../../../drag-handle/util/events'; +import { origin } from '../../../../../../src/state/position'; +import { sloppyClickThreshold } from '../../../../../../src/view/use-drag-handle/util/is-sloppy-click-threshold-exceeded'; +import { getPreset, getComputedSpacing } from '../../../../../utils/dimension'; +import getWindowFromEl from '../../../../../../src/view/window/get-window-from-el'; + +it('should start a drag if there was sufficient mouse movement', () => { + let lastSnapshot: DraggableStateSnapshot; + + jest + .spyOn(Element.prototype, 'getBoundingClientRect') + .mockImplementation(() => getPreset().inHome1.client.borderBox); + + jest + .spyOn(window, 'getComputedStyle') + .mockImplementation(() => getComputedSpacing({})); + + const { getByText } = render( + {}}> + + {droppableProvided => ( +
+ + {(provided, snapshot) => { + lastSnapshot = snapshot; + + return ( +
+ Drag handle +
+ ); + }} +
+ {droppableProvided.placeholder} +
+ )} +
+
, + ); + + invariant(lastSnapshot); + const handle = getByText('Drag handle'); + fireEvent.mouseDown(handle, { + button: primaryButton, + clientX: origin.x, + clientY: origin.y, + }); + + // not dragging yet + expect(lastSnapshot.isDragging).toBe(false); + + const point: Position = { x: 0, y: sloppyClickThreshold }; + fireEvent.mouseMove(handle.parentElement, { + button: primaryButton, + clientX: point.x, + clientY: point.y, + }); + expect(lastSnapshot.isDragging).toBe(true); +}); diff --git a/yarn.lock b/yarn.lock index 84a70d3e37..ea22e99e1a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1178,6 +1178,14 @@ "@types/istanbul-lib-coverage" "^1.1.0" "@types/yargs" "^12.0.9" +"@jest/types@^24.5.0": + version "24.5.0" + resolved "https://registry.yarnpkg.com/@jest/types/-/types-24.5.0.tgz#feee214a4d0167b0ca447284e95a57aa10b3ee95" + integrity sha512-kN7RFzNMf2R8UDadPOl6ReyI+MT8xfqRuAnuVL+i4gwjv/zubdDK+EDeLHYwq1j0CSSR2W/MmgaRlMZJzXdmVA== + dependencies: + "@types/istanbul-lib-coverage" "^1.1.0" + "@types/yargs" "^12.0.9" + "@mrmlnc/readdir-enhanced@^2.2.1": version "2.2.1" resolved "https://registry.yarnpkg.com/@mrmlnc/readdir-enhanced/-/readdir-enhanced-2.2.1.tgz#524af240d1a360527b730475ecfa1344aa540dde" @@ -1202,6 +1210,11 @@ react-lifecycles-compat "^3.0.4" warning "^3.0.0" +"@sheerun/mutationobserver-shim@^0.3.2": + version "0.3.2" + resolved "https://registry.yarnpkg.com/@sheerun/mutationobserver-shim/-/mutationobserver-shim-0.3.2.tgz#8013f2af54a2b7d735f71560ff360d3a8176a87b" + integrity sha512-vTCdPp/T/Q3oSqwHmZ5Kpa9oI7iLtGl3RQaA/NyLHikvcrPxACkkKVr/XzkSPJWXHRhKGzVvb0urJsbMlRxi1Q== + "@storybook/addons@5.0.1": version "5.0.1" resolved "https://registry.yarnpkg.com/@storybook/addons/-/addons-5.0.1.tgz#24ae1c80cea973de3e0125614a015fbb0629ac34" @@ -4305,6 +4318,16 @@ dom-serializer@0, dom-serializer@~0.1.0: domelementtype "^1.3.0" entities "^1.1.1" +dom-testing-library@^3.13.1: + version "3.18.1" + resolved "https://registry.yarnpkg.com/dom-testing-library/-/dom-testing-library-3.18.1.tgz#04f1ded8a83007eb305184ed953baf34fb2fec86" + integrity sha512-tp7doYDfPkbExBHnqnJFXQOeqKbTlxvyP8njIjSe9JYwAFDh5XvRi8zV7XdFigdd8KTsQaeLFZCUH5JDTukmvQ== + dependencies: + "@babel/runtime" "^7.3.4" + "@sheerun/mutationobserver-shim" "^0.3.2" + pretty-format "^24.5.0" + wait-for-expect "^1.1.0" + dom-walk@^0.1.0: version "0.1.1" resolved "https://registry.yarnpkg.com/dom-walk/-/dom-walk-0.1.1.tgz#672226dc74c8f799ad35307df936aba11acd6018" @@ -9003,6 +9026,16 @@ pretty-format@^24.3.1: ansi-styles "^3.2.0" react-is "^16.8.4" +pretty-format@^24.5.0: + version "24.5.0" + resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-24.5.0.tgz#cc69a0281a62cd7242633fc135d6930cd889822d" + integrity sha512-/3RuSghukCf8Riu5Ncve0iI+BzVkbRU5EeUoArKARZobREycuH5O4waxvaNIloEXdb0qwgmEAed5vTpX1HNROQ== + dependencies: + "@jest/types" "^24.5.0" + ansi-regex "^4.0.0" + ansi-styles "^3.2.0" + react-is "^16.8.4" + pretty-hrtime@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/pretty-hrtime/-/pretty-hrtime-1.0.3.tgz#b7e3ea42435a4c9b2759d99e0f201eb195802ee1" @@ -9508,6 +9541,14 @@ react-test-renderer@^16.0.0-0: react-is "^16.8.3" scheduler "^0.13.3" +react-testing-library@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/react-testing-library/-/react-testing-library-6.0.1.tgz#0ddf155cb609529e37359a82cc63eb3f830397fd" + integrity sha512-Asyrmdj059WnD8q4pVsKoPtvWfXEk+OCCNKSo9bh5tZ0pb80iXvkr4oppiL8H2qWL+MJUV2PTMneHYxsTeAa/A== + dependencies: + "@babel/runtime" "^7.3.1" + dom-testing-library "^3.13.1" + react-textarea-autosize@^7.0.4: version "7.1.0" resolved "https://registry.yarnpkg.com/react-textarea-autosize/-/react-textarea-autosize-7.1.0.tgz#3132cb77e65d94417558d37c0bfe415a5afd3445" @@ -11637,6 +11678,11 @@ w3c-hr-time@^1.0.1: dependencies: browser-process-hrtime "^0.1.2" +wait-for-expect@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/wait-for-expect/-/wait-for-expect-1.1.0.tgz#6607375c3f79d32add35cd2c87ce13f351a3d453" + integrity sha512-vQDokqxyMyknfX3luCDn16bSaRcOyH6gGuUXMIbxBLeTo6nWuEWYqMTT9a+44FmW8c2m6TRWBdNvBBjA1hwEKg== + wait-port@^0.2.2: version "0.2.2" resolved "https://registry.yarnpkg.com/wait-port/-/wait-port-0.2.2.tgz#d51a491e484a17bf75a947e711a2f012b4e6f2e3" From e97f2785bfee2da35c377199c96c1f58a8ecf318 Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Thu, 21 Mar 2019 09:14:50 +1100 Subject: [PATCH 046/117] creating custom memo functions --- src/view/use-custom-memo/are-inputs-equal.js | 16 ++++++++++ src/view/use-custom-memo/use-callback-one.js | 32 +++++++++++++++++++ .../{ => use-custom-memo}/use-memo-one.js | 14 ++------ .../sensor/use-mouse-sensor.js | 24 +++++++------- 4 files changed, 64 insertions(+), 22 deletions(-) create mode 100644 src/view/use-custom-memo/are-inputs-equal.js create mode 100644 src/view/use-custom-memo/use-callback-one.js rename src/view/{ => use-custom-memo}/use-memo-one.js (67%) diff --git a/src/view/use-custom-memo/are-inputs-equal.js b/src/view/use-custom-memo/are-inputs-equal.js new file mode 100644 index 0000000000..dbf521be54 --- /dev/null +++ b/src/view/use-custom-memo/are-inputs-equal.js @@ -0,0 +1,16 @@ +// @flow +const isShallowEqual = (newValue: mixed, oldValue: mixed): boolean => + newValue === oldValue; + +export default function areInputsEqual( + newInputs: mixed[], + lastInputs: mixed[], +) { + return ( + newInputs.length === lastInputs.length && + newInputs.every( + (newArg: mixed, index: number): boolean => + isShallowEqual(newArg, lastInputs[index]), + ) + ); +} diff --git a/src/view/use-custom-memo/use-callback-one.js b/src/view/use-custom-memo/use-callback-one.js new file mode 100644 index 0000000000..5f3f43bb22 --- /dev/null +++ b/src/view/use-custom-memo/use-callback-one.js @@ -0,0 +1,32 @@ +// @flow +import { useRef } from 'react'; +import areInputsEqual from './are-inputs-equal'; + +export default function useCallbackOne( + // getResult changes on every call, + callback: T, + // the inputs array changes on every call + inputs?: mixed[] = [], +): T { + const isFirstCallRef = useRef(true); + const lastInputsRef = useRef(inputs); + // Cannot lazy create a ref value, so setting to null + // $ExpectError - T is not null + const resultRef = useRef(null); + + // on first call return the initial result + if (isFirstCallRef.current) { + isFirstCallRef.current = false; + resultRef.current = callback; + return resultRef.current; + } + + // Don't recalculate result if the inputs have not changed + if (areInputsEqual(inputs, lastInputsRef.current)) { + return resultRef.current; + } + + lastInputsRef.current = inputs; + resultRef.current = callback; + return resultRef.current; +} diff --git a/src/view/use-memo-one.js b/src/view/use-custom-memo/use-memo-one.js similarity index 67% rename from src/view/use-memo-one.js rename to src/view/use-custom-memo/use-memo-one.js index 8514c1cb79..04daeced92 100644 --- a/src/view/use-memo-one.js +++ b/src/view/use-custom-memo/use-memo-one.js @@ -1,15 +1,6 @@ // @flow import { useRef } from 'react'; - -const isShallowEqual = (newValue: mixed, oldValue: mixed): boolean => - newValue === oldValue; - -const isEqual = (newInputs: mixed[], lastInputs: mixed[]): boolean => - newInputs.length === lastInputs.length && - newInputs.every( - (newArg: mixed, index: number): boolean => - isShallowEqual(newArg, lastInputs[index]), - ); +import areInputsEqual from './are-inputs-equal'; export default function useMemoOne( // getResult changes on every call, @@ -25,12 +16,13 @@ export default function useMemoOne( // on first call return the initial result if (isFirstCallRef.current) { + isFirstCallRef.current = false; resultRef.current = getResult(); return resultRef.current; } // Don't recalculate result if the inputs have not changed - if (isEqual(inputs, lastInputsRef.current)) { + if (areInputsEqual(inputs, lastInputsRef.current)) { return resultRef.current; } diff --git a/src/view/use-drag-handle/sensor/use-mouse-sensor.js b/src/view/use-drag-handle/sensor/use-mouse-sensor.js index 22d824cf86..5c2f6030b8 100644 --- a/src/view/use-drag-handle/sensor/use-mouse-sensor.js +++ b/src/view/use-drag-handle/sensor/use-mouse-sensor.js @@ -1,6 +1,6 @@ // @flow import type { Position } from 'css-box-model'; -import { useRef, useCallback, useMemo, useLayoutEffect } from 'react'; +import { useRef, useLayoutEffect } from 'react'; import invariant from 'tiny-invariant'; import type { EventBinding } from '../util/event-types'; import createEventMarshal, { @@ -17,6 +17,8 @@ import createPostDragEventPreventer, { import isSloppyClickThresholdExceeded from '../util/is-sloppy-click-threshold-exceeded'; import preventStandardKeyEvents from '../util/prevent-standard-key-events'; import type { Callbacks } from '../drag-handle-types'; +import useMemoOne from '../../use-custom-memo/use-memo-one'; +import useCallbackOne from '../../use-custom-memo/use-callback-one'; export type Args = {| callbacks: Callbacks, @@ -54,12 +56,12 @@ export default function useMouseSensor(args: Args): Result { const pendingRef = useRef(null); const isDraggingRef = useRef(false); const unbindWindowEventsRef = useRef<() => void>(noop); - const getIsCapturing = useCallback( + const getIsCapturing = useCallbackOne( () => Boolean(pendingRef.current || isDraggingRef.current), [], ); - const schedule = useMemo(() => { + const schedule = useMemoOne(() => { invariant( !getIsCapturing(), 'Should not recreate scheduler while capturing', @@ -67,12 +69,12 @@ export default function useMouseSensor(args: Args): Result { return createScheduler(callbacks); }, [callbacks, getIsCapturing]); - const postDragEventPreventer: EventPreventer = useMemo( + const postDragEventPreventer: EventPreventer = useMemoOne( () => createPostDragEventPreventer(getWindow), [getWindow], ); - const stop = useCallback(() => { + const stop = useCallbackOne(() => { if (!getIsCapturing()) { return; } @@ -96,7 +98,7 @@ export default function useMouseSensor(args: Args): Result { stop(); } - const cancel = useCallback(() => { + const cancel = useCallbackOne(() => { const wasDragging: boolean = isDraggingRef.current; stop(); @@ -105,7 +107,7 @@ export default function useMouseSensor(args: Args): Result { } }, [callbacks, stop]); - const startDragging = useCallback(() => { + const startDragging = useCallbackOne(() => { invariant(!isDraggingRef.current, 'Cannot start a drag while dragging'); const pending: ?Position = pendingRef.current; invariant(pending, 'Cannot start a drag without a pending drag'); @@ -119,7 +121,7 @@ export default function useMouseSensor(args: Args): Result { }); }, [callbacks]); - const windowBindings: EventBinding[] = useMemo(() => { + const windowBindings: EventBinding[] = useMemoOne(() => { invariant( !getIsCapturing(), 'Should not recreate window bindings while capturing', @@ -297,7 +299,7 @@ export default function useMouseSensor(args: Args): Result { getShouldRespectForceTouch, ]); - const bindWindowEvents = useCallback(() => { + const bindWindowEvents = useCallbackOne(() => { const win: HTMLElement = getWindow(); const options = { capture: true }; @@ -308,7 +310,7 @@ export default function useMouseSensor(args: Args): Result { bindEvents(win, windowBindings, options); }, [getWindow, windowBindings]); - const startPendingDrag = useCallback( + const startPendingDrag = useCallbackOne( (point: Position) => { invariant(!pendingRef.current, 'Expected there to be no pending drag'); pendingRef.current = point; @@ -317,7 +319,7 @@ export default function useMouseSensor(args: Args): Result { [bindWindowEvents], ); - const onMouseDown = useCallback( + const onMouseDown = useCallbackOne( (event: MouseEvent) => { if (mouseDownMarshal.isHandled()) { return; From 4747abf201a319ef93cfb24444f7b516932e98e4 Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Thu, 21 Mar 2019 13:32:43 +1100 Subject: [PATCH 047/117] adding goodness --- .../drag-drop-context/drag-drop-context.jsx | 18 ++++-- src/view/draggable/draggable-types.js | 2 +- src/view/draggable/draggable.jsx | 64 +++++-------------- 3 files changed, 28 insertions(+), 56 deletions(-) diff --git a/src/view/drag-drop-context/drag-drop-context.jsx b/src/view/drag-drop-context/drag-drop-context.jsx index 79862a4456..5d0bb8b761 100644 --- a/src/view/drag-drop-context/drag-drop-context.jsx +++ b/src/view/drag-drop-context/drag-drop-context.jsx @@ -17,7 +17,6 @@ import type { import type { DraggableId, State, Responders, Announce } from '../../types'; import type { Store, Action } from '../../state/store-types'; import StoreContext from '../context/store-context'; -import { useConstant, useConstantFn } from '../use-constant'; import { clean, move, @@ -32,6 +31,8 @@ import useAnnouncer from '../use-announcer'; import AppContext, { type AppContextValue } from '../context/app-context'; import useStartupValidation from './use-startup-validation'; import ErrorBoundary from '../error-boundary'; +import useMemoOne from '../use-custom-memo/use-memo-one'; +import useCallbackOne from '../use-custom-memo/use-callback-one'; type Props = {| ...Responders, @@ -125,31 +126,36 @@ export default function DragDropContext(props: Props) { storeRef = useRef(store); - const getCanLift = useConstantFn((id: DraggableId) => + const getCanLift = useCallbackOne((id: DraggableId) => canStartDrag(storeRef.current.getState(), id), ); - const getIsMovementAllowed = useConstantFn(() => + const getIsMovementAllowed = useCallbackOne(() => isMovementAllowed(storeRef.current.getState()), ); - const appContext: AppContextValue = useConstant(() => ({ + const appContext: AppContextValue = useMemoOne(() => ({ marshal: dimensionMarshal, style: styleMarshal.styleContext, canLift: getCanLift, isMovementAllowed: getIsMovementAllowed, })); - const recoverStoreFromError = useConstantFn(() => { + const tryResetStore = useCallbackOne(() => { const state: State = storeRef.current.getState(); if (state.phase !== 'IDLE') { store.dispatch(clean({ shouldFlush: true })); } }); + // Clean store when unmounting + useEffect(() => { + return tryResetStore; + }, [tryResetStore]); + return ( - + {props.children} diff --git a/src/view/draggable/draggable-types.js b/src/view/draggable/draggable-types.js index 1e13807bf2..7a05e8f565 100644 --- a/src/view/draggable/draggable-types.js +++ b/src/view/draggable/draggable-types.js @@ -19,7 +19,7 @@ import { drop, dropAnimationFinished, } from '../../state/action-creators'; -import type { DragHandleProps } from '../drag-handle/drag-handle-types'; +import type { DragHandleProps } from '../use-drag-handle/drag-handle-types'; export type DraggingStyle = {| position: 'fixed', diff --git a/src/view/draggable/draggable.jsx b/src/view/draggable/draggable.jsx index 68a9e74735..0a2c241eb8 100644 --- a/src/view/draggable/draggable.jsx +++ b/src/view/draggable/draggable.jsx @@ -14,12 +14,7 @@ import useDraggableDimensionPublisher, { type Args as DimensionPublisherArgs, } from '../use-draggable-dimension-publisher/use-draggable-dimension-publisher'; import * as timings from '../../debug/timings'; -import type { - Props, - Provided, - StateSnapshot, - DraggableStyle, -} from './draggable-types'; +import type { Props, Provided, DraggableStyle } from './draggable-types'; import getWindowScroll from '../window/get-window-scroll'; // import throwIfRefIsInvalid from '../throw-if-invalid-inner-ref'; // import checkOwnProps from './check-own-props'; @@ -58,8 +53,7 @@ export default function Draggable(props: Props) { index, // mapProps - dragging, - secondary, + mapped, // dispatchProps moveUp: moveUpAction, @@ -145,8 +139,9 @@ export default function Draggable(props: Props) { ], ); - const isDragging: boolean = Boolean(dragging); - const isDropAnimating: boolean = Boolean(dragging && dragging.dropping); + const isDragging: boolean = mapped.type === 'DRAGGING'; + const isDropAnimating: boolean = + mapped.type === 'DRAGGING' && Boolean(mapped.dropping); const dragHandleArgs: DragHandleArgs = useMemo( () => ({ @@ -175,9 +170,11 @@ export default function Draggable(props: Props) { const onMoveEnd = useCallback( (event: TransitionEvent) => { - const isDropping: boolean = Boolean(dragging && dragging.dropping); + if (mapped.type !== 'DRAGGING') { + return; + } - if (!isDropping) { + if (!mapped.dropping) { return; } @@ -189,12 +186,13 @@ export default function Draggable(props: Props) { dropAnimationFinishedAction(); }, - [dragging, dropAnimationFinishedAction], + [dropAnimationFinishedAction, mapped], ); const provided: Provided = useMemo(() => { - const style: DraggableStyle = getStyle(dragging, secondary); - const onTransitionEnd = dragging && dragging.dropping ? onMoveEnd : null; + const style: DraggableStyle = getStyle(mapped); + const onTransitionEnd = + mapped.type === 'DRAGGING' && mapped.dropping ? onMoveEnd : null; const result: Provided = { innerRef: setRef, @@ -207,39 +205,7 @@ export default function Draggable(props: Props) { }; return result; - }, [ - appContext.style, - dragHandleProps, - dragging, - onMoveEnd, - secondary, - setRef, - ]); - - // TODO: this could be done in the connected component - const snapshot: StateSnapshot = useMemo(() => { - if (dragging) { - return { - isDragging: true, - isDropAnimating: Boolean(dragging.dropping), - dropAnimation: dragging.dropping, - mode: dragging.mode, - draggingOver: dragging.draggingOver, - combineWith: dragging.combineWith, - combineTargetFor: null, - }; - } - invariant(secondary, 'Expected dragging or secondary snapshot'); - return { - isDragging: false, - isDropAnimating: false, - dropAnimation: null, - mode: null, - draggingOver: null, - combineTargetFor: secondary.combineTargetFor, - combineWith: null, - }; - }, [dragging, secondary]); + }, [appContext.style, dragHandleProps, mapped, onMoveEnd, setRef]); - return children(provided, snapshot); + return children(provided, mapped.snapshot); } From cadebc5bff39606705025b5f59ea5bd95c4a71be Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Fri, 22 Mar 2019 08:33:17 +1100 Subject: [PATCH 048/117] fixing import --- src/view/drag-drop-context/drag-drop-context.jsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/view/drag-drop-context/drag-drop-context.jsx b/src/view/drag-drop-context/drag-drop-context.jsx index 5d0bb8b761..9c1d6c3ecf 100644 --- a/src/view/drag-drop-context/drag-drop-context.jsx +++ b/src/view/drag-drop-context/drag-drop-context.jsx @@ -33,6 +33,7 @@ import useStartupValidation from './use-startup-validation'; import ErrorBoundary from '../error-boundary'; import useMemoOne from '../use-custom-memo/use-memo-one'; import useCallbackOne from '../use-custom-memo/use-callback-one'; +import { useConstant, useConstantFn } from '../use-constant'; type Props = {| ...Responders, From fbcef22088af50d74e8ce86826e60861d4e8a4e7 Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Fri, 22 Mar 2019 15:53:58 +1100 Subject: [PATCH 049/117] redux v7 --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index f325819890..20bc3bddda 100644 --- a/package.json +++ b/package.json @@ -58,7 +58,7 @@ "css-box-model": "^1.1.1", "memoize-one": "^5.0.0", "raf-schd": "^4.0.0", - "react-redux": "6.0.1", + "react-redux": "7.0.0-beta.0", "redux": "^4.0.1", "tiny-invariant": "^1.0.3" }, diff --git a/yarn.lock b/yarn.lock index ea22e99e1a..55f9c330ec 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9488,10 +9488,10 @@ react-popper@^1.3.3: typed-styles "^0.0.7" warning "^4.0.2" -react-redux@6.0.1: - version "6.0.1" - resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-6.0.1.tgz#0d423e2c1cb10ada87293d47e7de7c329623ba4d" - integrity sha512-T52I52Kxhbqy/6TEfBv85rQSDz6+Y28V/pf52vDWs1YRXG19mcFOGfHnY2HsNFHyhP+ST34Aih98fvt6tqwVcQ== +react-redux@7.0.0-beta.0: + version "7.0.0-beta.0" + resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-7.0.0-beta.0.tgz#46ea289a0b0cf18f864b251a1d2bff4f110b71e0" + integrity sha512-vQSWFBeSxXm0RPuUqZMcYyN9CPapFs3cJltgUHQe943joTgcpC/nZAmiBMWLmte6BE7xYeE66diO2Z/87kWarw== dependencies: "@babel/runtime" "^7.3.1" hoist-non-react-statics "^3.3.0" From 7fb94987639494452e0ad6eabde5a20336901ef5 Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Fri, 22 Mar 2019 16:16:22 +1100 Subject: [PATCH 050/117] bumping deps. using snapshot --- package.json | 4 ++-- src/view/droppable/droppable.jsx | 15 ++------------- yarn.lock | 33 ++++++++++++++++---------------- 3 files changed, 21 insertions(+), 31 deletions(-) diff --git a/package.json b/package.json index 20bc3bddda..72dce073af 100644 --- a/package.json +++ b/package.json @@ -85,7 +85,7 @@ "cross-env": "^5.2.0", "cypress": "^3.1.5", "enzyme": "^3.9.0", - "enzyme-adapter-react-16": "^1.10.0", + "enzyme-adapter-react-16": "^1.11.2", "eslint": "^5.15.1", "eslint-config-airbnb": "^17.1.0", "eslint-config-prettier": "^4.1.0", @@ -97,7 +97,7 @@ "eslint-plugin-jsx-a11y": "^6.2.1", "eslint-plugin-prettier": "^3.0.1", "eslint-plugin-react": "^7.12.4", - "eslint-plugin-react-hooks": "^1.5.0", + "eslint-plugin-react-hooks": "^1.5.1", "flow-bin": "0.94.0", "fs-extra": "^7.0.1", "globby": "^9.1.0", diff --git a/src/view/droppable/droppable.jsx b/src/view/droppable/droppable.jsx index e8888f4518..2bc59e6b42 100644 --- a/src/view/droppable/droppable.jsx +++ b/src/view/droppable/droppable.jsx @@ -7,7 +7,7 @@ import React, { useContext, type Node, } from 'react'; -import type { Props, Provided, StateSnapshot } from './droppable-types'; +import type { Props, Provided } from './droppable-types'; import useDroppableDimensionPublisher from '../use-droppable-dimension-publisher'; import Placeholder from '../placeholder'; import AppContext, { type AppContextValue } from '../context/app-context'; @@ -39,9 +39,7 @@ export default function Droppable(props: Props) { isDropDisabled, isCombineEnabled, // map props - isDraggingOver, - draggingOverWith, - draggingFromThisWith, + snapshot, // dispatch props updateViewportMaxScroll, } = props; @@ -105,15 +103,6 @@ export default function Droppable(props: Props) { [placeholder, setDroppableRef, styleContext], ); - const snapshot: StateSnapshot = useMemo( - () => ({ - isDraggingOver, - draggingOverWith, - draggingFromThisWith, - }), - [draggingFromThisWith, draggingOverWith, isDraggingOver], - ); - const droppableContext: ?DroppableContextValue = useMemo( () => ({ droppableId, diff --git a/yarn.lock b/yarn.lock index 55f9c330ec..97c8c20097 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4524,27 +4524,28 @@ entities@^1.1.1, entities@~1.1.1: resolved "https://registry.yarnpkg.com/entities/-/entities-1.1.2.tgz#bdfa735299664dfafd34529ed4f8522a275fea56" integrity sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w== -enzyme-adapter-react-16@^1.10.0: - version "1.10.0" - resolved "https://registry.yarnpkg.com/enzyme-adapter-react-16/-/enzyme-adapter-react-16-1.10.0.tgz#12e5b6f4be84f9a2ef374acc2555f829f351fc6e" - integrity sha512-0QqwEZcBv1xEEla+a3H7FMci+y4ybLia9cZzsdIrId7qcig4MK0kqqf6iiCILH1lsKS6c6AVqL3wGPhCevv5aQ== +enzyme-adapter-react-16@^1.11.2: + version "1.11.2" + resolved "https://registry.yarnpkg.com/enzyme-adapter-react-16/-/enzyme-adapter-react-16-1.11.2.tgz#8efeafb27e96873a5492fdef3f423693182eb9d4" + integrity sha512-2ruTTCPRb0lPuw/vKTXGVZVBZqh83MNDnakMhzxhpJcIbneEwNy2Cv0KvL97pl57/GOazJHflWNLjwWhex5AAA== dependencies: - enzyme-adapter-utils "^1.10.0" + enzyme-adapter-utils "^1.10.1" object.assign "^4.1.0" object.values "^1.1.0" - prop-types "^15.6.2" - react-is "^16.7.0" + prop-types "^15.7.2" + react-is "^16.8.4" react-test-renderer "^16.0.0-0" + semver "^5.6.0" -enzyme-adapter-utils@^1.10.0: - version "1.10.0" - resolved "https://registry.yarnpkg.com/enzyme-adapter-utils/-/enzyme-adapter-utils-1.10.0.tgz#5836169f68b9e8733cb5b69cad5da2a49e34f550" - integrity sha512-VnIXJDYVTzKGbdW+lgK8MQmYHJquTQZiGzu/AseCZ7eHtOMAj4Rtvk8ZRopodkfPves0EXaHkXBDkVhPa3t0jA== +enzyme-adapter-utils@^1.10.1: + version "1.10.1" + resolved "https://registry.yarnpkg.com/enzyme-adapter-utils/-/enzyme-adapter-utils-1.10.1.tgz#58264efa19a7befdbf964fb7981a108a5452ac96" + integrity sha512-oasinhhLoBuZsIkTe8mx0HiudtfErUtG0Ooe1FOplu/t4c9rOmyG5gtrBASK6u4whHIRWvv0cbZMElzNTR21SA== dependencies: function.prototype.name "^1.1.0" object.assign "^4.1.0" object.fromentries "^2.0.0" - prop-types "^15.6.2" + prop-types "^15.7.2" semver "^5.6.0" enzyme@^3.9.0: @@ -4743,10 +4744,10 @@ eslint-plugin-prettier@^3.0.1: dependencies: prettier-linter-helpers "^1.0.0" -eslint-plugin-react-hooks@^1.5.0: - version "1.5.0" - resolved "https://registry.yarnpkg.com/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-1.5.0.tgz#cdd958cfff55bd5fa4f84db90d1490fb5ca4ae2b" - integrity sha512-iwDuWR2ReRgvJsNm8fXPtTKdg78IVQF8I4+am3ntztPf/+nPnWZfArFu6aXpaC75/iCYRrkqI8nPCYkxJstmpA== +eslint-plugin-react-hooks@^1.5.1: + version "1.5.1" + resolved "https://registry.yarnpkg.com/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-1.5.1.tgz#3c601326914ee0e1fedd709115db4940bdbbed4a" + integrity sha512-i3dIrmZ+Ssrm0LrbbtuGcRf7EEpe1FaMuL8XnnpZO0X4tk3dZNzevWxD0/7nMAFa5yZQfNnYkfEP0MmwLvbdHw== eslint-plugin-react@^7.12.4: version "7.12.4" From 3c19b947d3e92b8f01ee9561a039b39b1d9e9d3a Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Sun, 24 Mar 2019 21:10:33 +1100 Subject: [PATCH 051/117] getting mouse sensors tests mostly working --- src/view/context-keys.js | 11 ---- .../sensor/use-keyboard-sensor.js | 22 ++++---- .../sensor/use-mouse-sensor.js | 21 ++++--- .../sensor/use-touch-sensor.js | 22 ++++---- src/view/use-drag-handle/use-drag-handle.js | 54 +++++++++++------- .../view/drag-handle/keyboard-sensor.spec.js | 2 +- .../view/drag-handle/mouse-sensor.spec.js | 15 ++--- .../unit/view/drag-handle/util/app-context.js | 12 ++++ .../view/drag-handle/util/basic-context.js | 9 --- test/unit/view/drag-handle/util/controls.js | 4 +- test/unit/view/drag-handle/util/wrappers.js | 55 ++++++++++++------- 11 files changed, 128 insertions(+), 99 deletions(-) delete mode 100644 src/view/context-keys.js create mode 100644 test/unit/view/drag-handle/util/app-context.js delete mode 100644 test/unit/view/drag-handle/util/basic-context.js diff --git a/src/view/context-keys.js b/src/view/context-keys.js deleted file mode 100644 index e5091d964f..0000000000 --- a/src/view/context-keys.js +++ /dev/null @@ -1,11 +0,0 @@ -// @flow -const prefix = (key: string): string => - `private-react-beautiful-dnd-key-do-not-use-${key}`; - -export const storeKey: string = prefix('store'); -export const droppableIdKey: string = prefix('droppable-id'); -export const droppableTypeKey: string = prefix('droppable-type'); -export const dimensionMarshalKey: string = prefix('dimension-marshal'); -export const styleKey: string = prefix('style'); -export const canLiftKey: string = prefix('can-lift'); -export const isMovementAllowedKey: string = prefix('is-movement-allowed'); diff --git a/src/view/use-drag-handle/sensor/use-keyboard-sensor.js b/src/view/use-drag-handle/sensor/use-keyboard-sensor.js index 5c146258a4..db7459e84c 100644 --- a/src/view/use-drag-handle/sensor/use-keyboard-sensor.js +++ b/src/view/use-drag-handle/sensor/use-keyboard-sensor.js @@ -17,11 +17,10 @@ export type Args = {| getWindow: () => HTMLElement, canStartCapturing: (event: Event) => boolean, shouldAbortCapture: boolean, + onCaptureStart: () => void, + onCaptureEnd: () => void, |}; -export type Result = { - onKeyDown: (event: KeyboardEvent) => void, - isCapturing: boolean, -}; +export type OnKeyDown = (event: KeyboardEvent) => void; type KeyMap = { [key: number]: true, @@ -36,12 +35,14 @@ const scrollJumpKeys: KeyMap = { function noop() {} -export default function useKeyboardSensor(args: Args): Result { +export default function useKeyboardSensor(args: Args): OnKeyDown { const { canStartCapturing, getWindow, callbacks, shouldAbortCapture, + onCaptureStart, + onCaptureEnd, getDraggableRef, } = args; const isDraggingRef = useRef(false); @@ -65,7 +66,8 @@ export default function useKeyboardSensor(args: Args): Result { schedule.cancel(); unbindWindowEventsRef.current(); isDraggingRef.current = false; - }, [getIsDragging, schedule]); + onCaptureEnd(); + }, [getIsDragging, onCaptureEnd, schedule]); // instructed to stop capturing if (shouldAbortCapture && getIsDragging()) { @@ -164,6 +166,7 @@ export default function useKeyboardSensor(args: Args): Result { invariant(ref, 'Cannot start a keyboard drag without a draggable ref'); isDraggingRef.current = true; + onCaptureStart(); bindWindowEvents(); const center: Position = getBorderBoxCenterPosition(ref); @@ -171,7 +174,7 @@ export default function useKeyboardSensor(args: Args): Result { clientSelection: center, movementMode: 'SNAP', }); - }, [bindWindowEvents, callbacks, getDraggableRef]); + }, [bindWindowEvents, callbacks, getDraggableRef, onCaptureStart]); const onKeyDown = useCallback( (event: KeyboardEvent) => { @@ -267,8 +270,5 @@ export default function useKeyboardSensor(args: Args): Result { return cancel; }, [cancel]); - return { - onKeyDown, - isCapturing: getIsDragging(), - }; + return onKeyDown; } diff --git a/src/view/use-drag-handle/sensor/use-mouse-sensor.js b/src/view/use-drag-handle/sensor/use-mouse-sensor.js index 5c2f6030b8..a608bd05f6 100644 --- a/src/view/use-drag-handle/sensor/use-mouse-sensor.js +++ b/src/view/use-drag-handle/sensor/use-mouse-sensor.js @@ -22,16 +22,16 @@ import useCallbackOne from '../../use-custom-memo/use-callback-one'; export type Args = {| callbacks: Callbacks, + onCaptureStart: () => void, + onCaptureEnd: () => void, getDraggableRef: () => ?HTMLElement, getWindow: () => HTMLElement, canStartCapturing: (event: Event) => boolean, getShouldRespectForceTouch: () => boolean, shouldAbortCapture: boolean, |}; -export type Result = { - onMouseDown: (event: MouseEvent) => void, - isCapturing: boolean, -}; + +export type OnMouseDown = (event: MouseEvent) => void; // Custom event format for force press inputs type MouseForceChangedEvent = MouseEvent & { @@ -45,13 +45,15 @@ const noop = () => {}; // shared management of mousedown without needing to call preventDefault() const mouseDownMarshal: EventMarshal = createEventMarshal(); -export default function useMouseSensor(args: Args): Result { +export default function useMouseSensor(args: Args): OnMouseDown { const { canStartCapturing, getWindow, callbacks, shouldAbortCapture, getShouldRespectForceTouch, + onCaptureStart, + onCaptureEnd, } = args; const pendingRef = useRef(null); const isDraggingRef = useRef(false); @@ -91,6 +93,9 @@ export default function useMouseSensor(args: Args): Result { // resettting refs pendingRef.current = null; isDraggingRef.current = false; + + // releasing the capture + onCaptureEnd(); }, [getIsCapturing, postDragEventPreventer, schedule]); // instructed to stop capturing @@ -314,6 +319,7 @@ export default function useMouseSensor(args: Args): Result { (point: Position) => { invariant(!pendingRef.current, 'Expected there to be no pending drag'); pendingRef.current = point; + onCaptureStart(); bindWindowEvents(); }, [bindWindowEvents], @@ -375,8 +381,5 @@ export default function useMouseSensor(args: Args): Result { return cancel; }, [cancel]); - return { - onMouseDown, - isCapturing: getIsCapturing(), - }; + return onMouseDown; } diff --git a/src/view/use-drag-handle/sensor/use-touch-sensor.js b/src/view/use-drag-handle/sensor/use-touch-sensor.js index fccfd1b40d..571ad9409a 100644 --- a/src/view/use-drag-handle/sensor/use-touch-sensor.js +++ b/src/view/use-drag-handle/sensor/use-touch-sensor.js @@ -22,11 +22,10 @@ export type Args = {| canStartCapturing: (event: Event) => boolean, getShouldRespectForceTouch: () => boolean, shouldAbortCapture: boolean, + onCaptureStart: () => void, + onCaptureEnd: () => void, |}; -export type Result = { - onTouchStart: (event: TouchEvent) => void, - isCapturing: boolean, -}; +export type OnTouchStart = (event: TouchEvent) => void; type PendingDrag = {| longPressTimerId: TimeoutID, @@ -106,12 +105,14 @@ const webkitHack: WebkitHack = (() => { return { preventTouchMove, releaseTouchMove }; })(); -export default function useTouchSensor(args: Args): Result { +export default function useTouchSensor(args: Args): OnTouchStart { const { callbacks, getWindow, canStartCapturing, getShouldRespectForceTouch, + onCaptureStart, + onCaptureEnd, shouldAbortCapture, } = args; const pendingRef = useRef(null); @@ -145,6 +146,7 @@ export default function useTouchSensor(args: Args): Result { touchStartMarshal.reset(); webkitHack.releaseTouchMove(); hasMovedRef.current = false; + onCaptureEnd(); // if dragging - prevent the next click if (isDraggingRef.current) { @@ -158,7 +160,7 @@ export default function useTouchSensor(args: Args): Result { clearTimeout(pending.longPressTimerId); pendingRef.current = null; - }, [getIsCapturing, postDragClickPreventer, schedule]); + }, [getIsCapturing, onCaptureEnd, postDragClickPreventer, schedule]); const cancel = useCallback(() => { const wasDragging: boolean = isDraggingRef.current; @@ -400,9 +402,10 @@ export default function useTouchSensor(args: Args): Result { }; pendingRef.current = pending; + onCaptureStart(); bindWindowEvents(); }, - [bindWindowEvents, startDragging], + [bindWindowEvents, onCaptureStart, startDragging], ); const onTouchStart = (event: TouchEvent) => { @@ -438,8 +441,5 @@ export default function useTouchSensor(args: Args): Result { return cancel; }, [cancel]); - return { - onTouchStart, - isCapturing: getIsCapturing(), - }; + return onTouchStart; } diff --git a/src/view/use-drag-handle/use-drag-handle.js b/src/view/use-drag-handle/use-drag-handle.js index b5fd5a3b1b..d8a5d94337 100644 --- a/src/view/use-drag-handle/use-drag-handle.js +++ b/src/view/use-drag-handle/use-drag-handle.js @@ -1,4 +1,5 @@ // @flow +import invariant from 'tiny-invariant'; import { useLayoutEffect, useRef, useMemo, useState, useCallback } from 'react'; import type { Args, DragHandleProps } from './drag-handle-types'; import getWindowFromEl from '../window/get-window-from-el'; @@ -23,10 +24,19 @@ export default function useDragHandle(args: Args): DragHandleProps { // Capturing const isAnythingCapturingRef = useRef(false); const [shouldAbortCapture, setShouldAbortCapture] = useState(false); - const recordCapture = useCallback((isCapturingList: boolean[]) => { - isAnythingCapturingRef.current = isCapturingList.some( - (isCapturing: boolean) => isCapturing, + const onCaptureStart = useCallback(() => { + invariant( + !isAnythingCapturingRef.current, + 'Cannot start capturing while something else is', ); + isAnythingCapturingRef.current = true; + }, []); + const onCaptureEnd = useCallback(() => { + invariant( + isAnythingCapturingRef.current, + 'Cannot stop capturing while nothing is capturing', + ); + isAnythingCapturingRef.current = false; }, []); const { canLift, style: styleContext }: AppContextValue = useRequiredContext( @@ -79,21 +89,23 @@ export default function useDragHandle(args: Args): DragHandleProps { getDraggableRef, getWindow, canStartCapturing, + onCaptureStart, + onCaptureEnd, getShouldRespectForceTouch, shouldAbortCapture, }), [ - shouldAbortCapture, callbacks, - canStartCapturing, getDraggableRef, - getShouldRespectForceTouch, getWindow, + canStartCapturing, + onCaptureStart, + onCaptureEnd, + getShouldRespectForceTouch, + shouldAbortCapture, ], ); - const { isCapturing: isMouseCapturing, onMouseDown } = useMouseSensor( - mouseArgs, - ); + const onMouseDown = useMouseSensor(mouseArgs); const keyboardArgs: KeyboardSensorArgs = useMemo( () => ({ @@ -102,18 +114,21 @@ export default function useDragHandle(args: Args): DragHandleProps { getWindow, canStartCapturing, shouldAbortCapture, + onCaptureStart, + onCaptureEnd, }), [ callbacks, canStartCapturing, getDraggableRef, getWindow, + onCaptureEnd, + onCaptureStart, shouldAbortCapture, ], ); - const { isCapturing: isKeyboardCapturing, onKeyDown } = useKeyboardSensor( - keyboardArgs, - ); + const onKeyDown = useKeyboardSensor(keyboardArgs); + const touchArgs: TouchSensorArgs = useMemo( () => ({ callbacks, @@ -122,20 +137,21 @@ export default function useDragHandle(args: Args): DragHandleProps { canStartCapturing, getShouldRespectForceTouch, shouldAbortCapture, + onCaptureStart, + onCaptureEnd, }), [ - shouldAbortCapture, callbacks, - canStartCapturing, getDraggableRef, - getShouldRespectForceTouch, getWindow, + canStartCapturing, + getShouldRespectForceTouch, + shouldAbortCapture, + onCaptureStart, + onCaptureEnd, ], ); - const { isCapturing: isTouchCapturing, onTouchStart } = useTouchSensor( - touchArgs, - ); - recordCapture([isMouseCapturing, isKeyboardCapturing, isTouchCapturing]); + const onTouchStart = useTouchSensor(touchArgs); // mounting focus retention useLayoutEffect(() => {}); diff --git a/test/unit/view/drag-handle/keyboard-sensor.spec.js b/test/unit/view/drag-handle/keyboard-sensor.spec.js index 790090aef2..e5709a731f 100644 --- a/test/unit/view/drag-handle/keyboard-sensor.spec.js +++ b/test/unit/view/drag-handle/keyboard-sensor.spec.js @@ -30,7 +30,7 @@ import { windowMouseMove, } from './util/events'; import { getWrapper } from './util/wrappers'; -import type { Callbacks } from '../../../../src/view/drag-handle/drag-handle-types'; +import type { Callbacks } from '../../../../src/view/use-drag-handle/drag-handle-types'; const origin: Position = { x: 0, y: 0 }; diff --git a/test/unit/view/drag-handle/mouse-sensor.spec.js b/test/unit/view/drag-handle/mouse-sensor.spec.js index 2cd03f86b4..559227cbd0 100644 --- a/test/unit/view/drag-handle/mouse-sensor.spec.js +++ b/test/unit/view/drag-handle/mouse-sensor.spec.js @@ -1,8 +1,7 @@ // @flow import { type Position } from 'css-box-model'; import { type ReactWrapper } from 'enzyme'; -import { canLiftKey, styleKey } from '../../../../src/view/context-keys'; -import { sloppyClickThreshold } from '../../../../src/view/drag-handle/util/is-sloppy-click-threshold-exceeded'; +import { sloppyClickThreshold } from '../../../../src/view/use-drag-handle/util/is-sloppy-click-threshold-exceeded'; import * as keyCodes from '../../../../src/view/key-codes'; import getWindowScroll from '../../../../src/view/window/get-window-scroll'; import setWindowScroll from '../../../utils/set-window-scroll'; @@ -37,7 +36,9 @@ import { windowTab, } from './util/events'; import { getWrapper } from './util/wrappers'; -import type { Callbacks } from '../../../../src/view/drag-handle/drag-handle-types'; +import type { Callbacks } from '../../../../src/view/use-drag-handle/drag-handle-types'; +import type { AppContextValue } from '../../../../src/view/context/app-context'; +import basicContext from './util/app-context'; const origin: Position = { x: 0, y: 0 }; @@ -83,7 +84,7 @@ describe('initiation', () => { windowMouseMove(point); expect(customCallbacks.onLift).toHaveBeenCalledWith({ - clientSelection: point, + clientSelection: origin, movementMode: 'FLUID', }); @@ -202,9 +203,9 @@ describe('initiation', () => { it('should not start a drag if the state says that a drag cannot start', () => { const customCallbacks: Callbacks = getStubCallbacks(); - const customContext = { - [styleKey]: 'hello', - [canLiftKey]: () => false, + const customContext: AppContextValue = { + ...basicContext, + canLift: () => false, }; const customWrapper = getWrapper(customCallbacks, customContext); const mock: MockEvent = createMockEvent(); diff --git a/test/unit/view/drag-handle/util/app-context.js b/test/unit/view/drag-handle/util/app-context.js new file mode 100644 index 0000000000..b527568b2b --- /dev/null +++ b/test/unit/view/drag-handle/util/app-context.js @@ -0,0 +1,12 @@ +// @flow +import { getMarshalStub } from '../../../../utils/dimension-marshal'; +import { type AppContextValue } from '../../../../../src/view/context/app-context'; + +const value: AppContextValue = { + marshal: getMarshalStub(), + style: '1', + canLift: () => true, + isMovementAllowed: () => true, +}; + +export default value; diff --git a/test/unit/view/drag-handle/util/basic-context.js b/test/unit/view/drag-handle/util/basic-context.js deleted file mode 100644 index 9d0610aea6..0000000000 --- a/test/unit/view/drag-handle/util/basic-context.js +++ /dev/null @@ -1,9 +0,0 @@ -// @flow -import { styleKey, canLiftKey } from '../../../../../src/view/context-keys'; - -const basicContext = { - [styleKey]: 'hello', - [canLiftKey]: () => true, -}; - -export default basicContext; diff --git a/test/unit/view/drag-handle/util/controls.js b/test/unit/view/drag-handle/util/controls.js index 92c85eeca4..8a304d79f2 100644 --- a/test/unit/view/drag-handle/util/controls.js +++ b/test/unit/view/drag-handle/util/controls.js @@ -1,7 +1,7 @@ // @flow import type { ReactWrapper } from 'enzyme'; -import { sloppyClickThreshold } from '../../../../../src/view/drag-handle/util/is-sloppy-click-threshold-exceeded'; -import { timeForLongPress } from '../../../../../src/view/drag-handle/sensor/create-touch-sensor'; +import { sloppyClickThreshold } from '../../../../../src/view/use-drag-handle/util/is-sloppy-click-threshold-exceeded'; +import { timeForLongPress } from '../../../../../src/view/use-drag-handle/sensor/create-touch-sensor'; import { primaryButton, touchStart, diff --git a/test/unit/view/drag-handle/util/wrappers.js b/test/unit/view/drag-handle/util/wrappers.js index 824363e580..c7b8f8e8ca 100644 --- a/test/unit/view/drag-handle/util/wrappers.js +++ b/test/unit/view/drag-handle/util/wrappers.js @@ -1,12 +1,16 @@ // @flow import React, { type Node } from 'react'; import { mount, type ReactWrapper } from 'enzyme'; -import DragHandle from '../../../../../src/view/drag-handle/drag-handle'; +import useDragHandle from '../../../../../src/view/use-drag-handle'; import type { + Args, Callbacks, DragHandleProps, -} from '../../../../../src/view/drag-handle/drag-handle-types'; -import basicContext from './basic-context'; +} from '../../../../../src/view/use-drag-handle/drag-handle-types'; +import basicContext from './app-context'; +import AppContext, { + type AppContextValue, +} from '../../../../../src/view/context/app-context'; type ChildProps = {| dragHandleProps: ?DragHandleProps, @@ -42,28 +46,41 @@ export const createRef = () => { return { ref, setRef, getRef }; }; +type WithDragHandleProps = {| + ...Args, + children: (value: ?DragHandleProps) => Node | null, +|}; + +function WithDragHandle(props: WithDragHandleProps) { + // strip the children prop out + const { children, ...args } = props; + const result: ?DragHandleProps = useDragHandle(args); + return props.children(result); +} + export const getWrapper = ( callbacks: Callbacks, - context?: Object = basicContext, + appContext?: AppContextValue = basicContext, shouldRespectForceTouch?: boolean = true, ): ReactWrapper<*> => { const ref = createRef(); return mount( - shouldRespectForceTouch} - > - {(dragHandleProps: ?DragHandleProps) => ( - - )} - , - { context }, + + shouldRespectForceTouch} + > + {(dragHandleProps: ?DragHandleProps) => ( + + )} + + , ); }; From 4aa5a7585640c81de4236762b0ae6b24a6cc3742 Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Sun, 24 Mar 2019 21:58:27 +1100 Subject: [PATCH 052/117] mouse sensor tests passing --- .../sensor/use-keyboard-sensor.js | 13 +--- .../sensor/use-mouse-sensor.js | 42 +++++------- .../sensor/use-touch-sensor.js | 13 +--- src/view/use-drag-handle/use-drag-handle.js | 64 +++++++++---------- .../view/drag-handle/mouse-sensor.spec.js | 3 + test/unit/view/drag-handle/util/wrappers.js | 47 +++++++++----- 6 files changed, 86 insertions(+), 96 deletions(-) diff --git a/src/view/use-drag-handle/sensor/use-keyboard-sensor.js b/src/view/use-drag-handle/sensor/use-keyboard-sensor.js index db7459e84c..ff96044bfe 100644 --- a/src/view/use-drag-handle/sensor/use-keyboard-sensor.js +++ b/src/view/use-drag-handle/sensor/use-keyboard-sensor.js @@ -16,8 +16,7 @@ export type Args = {| getDraggableRef: () => ?HTMLElement, getWindow: () => HTMLElement, canStartCapturing: (event: Event) => boolean, - shouldAbortCapture: boolean, - onCaptureStart: () => void, + onCaptureStart: (abort: () => void) => void, onCaptureEnd: () => void, |}; export type OnKeyDown = (event: KeyboardEvent) => void; @@ -40,7 +39,6 @@ export default function useKeyboardSensor(args: Args): OnKeyDown { canStartCapturing, getWindow, callbacks, - shouldAbortCapture, onCaptureStart, onCaptureEnd, getDraggableRef, @@ -69,11 +67,6 @@ export default function useKeyboardSensor(args: Args): OnKeyDown { onCaptureEnd(); }, [getIsDragging, onCaptureEnd, schedule]); - // instructed to stop capturing - if (shouldAbortCapture && getIsDragging()) { - stop(); - } - const cancel = useCallback(() => { const wasDragging: boolean = isDraggingRef.current; stop(); @@ -166,7 +159,7 @@ export default function useKeyboardSensor(args: Args): OnKeyDown { invariant(ref, 'Cannot start a keyboard drag without a draggable ref'); isDraggingRef.current = true; - onCaptureStart(); + onCaptureStart(stop); bindWindowEvents(); const center: Position = getBorderBoxCenterPosition(ref); @@ -174,7 +167,7 @@ export default function useKeyboardSensor(args: Args): OnKeyDown { clientSelection: center, movementMode: 'SNAP', }); - }, [bindWindowEvents, callbacks, getDraggableRef, onCaptureStart]); + }, [bindWindowEvents, callbacks, getDraggableRef, onCaptureStart, stop]); const onKeyDown = useCallback( (event: KeyboardEvent) => { diff --git a/src/view/use-drag-handle/sensor/use-mouse-sensor.js b/src/view/use-drag-handle/sensor/use-mouse-sensor.js index a608bd05f6..7db2ea960b 100644 --- a/src/view/use-drag-handle/sensor/use-mouse-sensor.js +++ b/src/view/use-drag-handle/sensor/use-mouse-sensor.js @@ -1,6 +1,6 @@ // @flow import type { Position } from 'css-box-model'; -import { useRef, useLayoutEffect } from 'react'; +import { useRef, useLayoutEffect, useCallback, useMemo } from 'react'; import invariant from 'tiny-invariant'; import type { EventBinding } from '../util/event-types'; import createEventMarshal, { @@ -17,18 +17,16 @@ import createPostDragEventPreventer, { import isSloppyClickThresholdExceeded from '../util/is-sloppy-click-threshold-exceeded'; import preventStandardKeyEvents from '../util/prevent-standard-key-events'; import type { Callbacks } from '../drag-handle-types'; -import useMemoOne from '../../use-custom-memo/use-memo-one'; -import useCallbackOne from '../../use-custom-memo/use-callback-one'; +// import useMemo from '../../use-custom-memo/use-memo-one'; export type Args = {| callbacks: Callbacks, - onCaptureStart: () => void, + onCaptureStart: (abort: Function) => void, onCaptureEnd: () => void, getDraggableRef: () => ?HTMLElement, getWindow: () => HTMLElement, canStartCapturing: (event: Event) => boolean, getShouldRespectForceTouch: () => boolean, - shouldAbortCapture: boolean, |}; export type OnMouseDown = (event: MouseEvent) => void; @@ -50,7 +48,6 @@ export default function useMouseSensor(args: Args): OnMouseDown { canStartCapturing, getWindow, callbacks, - shouldAbortCapture, getShouldRespectForceTouch, onCaptureStart, onCaptureEnd, @@ -58,12 +55,12 @@ export default function useMouseSensor(args: Args): OnMouseDown { const pendingRef = useRef(null); const isDraggingRef = useRef(false); const unbindWindowEventsRef = useRef<() => void>(noop); - const getIsCapturing = useCallbackOne( + const getIsCapturing = useCallback( () => Boolean(pendingRef.current || isDraggingRef.current), [], ); - const schedule = useMemoOne(() => { + const schedule = useMemo(() => { invariant( !getIsCapturing(), 'Should not recreate scheduler while capturing', @@ -71,12 +68,12 @@ export default function useMouseSensor(args: Args): OnMouseDown { return createScheduler(callbacks); }, [callbacks, getIsCapturing]); - const postDragEventPreventer: EventPreventer = useMemoOne( + const postDragEventPreventer: EventPreventer = useMemo( () => createPostDragEventPreventer(getWindow), [getWindow], ); - const stop = useCallbackOne(() => { + const stop = useCallback(() => { if (!getIsCapturing()) { return; } @@ -90,20 +87,15 @@ export default function useMouseSensor(args: Args): OnMouseDown { if (shouldBlockClick) { postDragEventPreventer.preventNext(); } - // resettting refs + // resetting refs pendingRef.current = null; isDraggingRef.current = false; // releasing the capture onCaptureEnd(); - }, [getIsCapturing, postDragEventPreventer, schedule]); + }, [getIsCapturing, onCaptureEnd, postDragEventPreventer, schedule]); - // instructed to stop capturing - if (shouldAbortCapture && getIsCapturing()) { - stop(); - } - - const cancel = useCallbackOne(() => { + const cancel = useCallback(() => { const wasDragging: boolean = isDraggingRef.current; stop(); @@ -112,7 +104,7 @@ export default function useMouseSensor(args: Args): OnMouseDown { } }, [callbacks, stop]); - const startDragging = useCallbackOne(() => { + const startDragging = useCallback(() => { invariant(!isDraggingRef.current, 'Cannot start a drag while dragging'); const pending: ?Position = pendingRef.current; invariant(pending, 'Cannot start a drag without a pending drag'); @@ -126,7 +118,7 @@ export default function useMouseSensor(args: Args): OnMouseDown { }); }, [callbacks]); - const windowBindings: EventBinding[] = useMemoOne(() => { + const windowBindings: EventBinding[] = useMemo(() => { invariant( !getIsCapturing(), 'Should not recreate window bindings while capturing', @@ -304,7 +296,7 @@ export default function useMouseSensor(args: Args): OnMouseDown { getShouldRespectForceTouch, ]); - const bindWindowEvents = useCallbackOne(() => { + const bindWindowEvents = useCallback(() => { const win: HTMLElement = getWindow(); const options = { capture: true }; @@ -315,17 +307,17 @@ export default function useMouseSensor(args: Args): OnMouseDown { bindEvents(win, windowBindings, options); }, [getWindow, windowBindings]); - const startPendingDrag = useCallbackOne( + const startPendingDrag = useCallback( (point: Position) => { invariant(!pendingRef.current, 'Expected there to be no pending drag'); pendingRef.current = point; - onCaptureStart(); + onCaptureStart(stop); bindWindowEvents(); }, - [bindWindowEvents], + [bindWindowEvents, onCaptureStart, stop], ); - const onMouseDown = useCallbackOne( + const onMouseDown = useCallback( (event: MouseEvent) => { if (mouseDownMarshal.isHandled()) { return; diff --git a/src/view/use-drag-handle/sensor/use-touch-sensor.js b/src/view/use-drag-handle/sensor/use-touch-sensor.js index 571ad9409a..8b8cc6e91c 100644 --- a/src/view/use-drag-handle/sensor/use-touch-sensor.js +++ b/src/view/use-drag-handle/sensor/use-touch-sensor.js @@ -21,8 +21,7 @@ export type Args = {| getWindow: () => HTMLElement, canStartCapturing: (event: Event) => boolean, getShouldRespectForceTouch: () => boolean, - shouldAbortCapture: boolean, - onCaptureStart: () => void, + onCaptureStart: (abort: () => void) => void, onCaptureEnd: () => void, |}; export type OnTouchStart = (event: TouchEvent) => void; @@ -113,7 +112,6 @@ export default function useTouchSensor(args: Args): OnTouchStart { getShouldRespectForceTouch, onCaptureStart, onCaptureEnd, - shouldAbortCapture, } = args; const pendingRef = useRef(null); const isDraggingRef = useRef(false); @@ -171,11 +169,6 @@ export default function useTouchSensor(args: Args): OnTouchStart { } }, [callbacks, stop]); - // instructed to stop capturing - if (shouldAbortCapture && getIsCapturing()) { - stop(); - } - const windowBindings: EventBinding[] = useMemo(() => { invariant( !getIsCapturing(), @@ -402,10 +395,10 @@ export default function useTouchSensor(args: Args): OnTouchStart { }; pendingRef.current = pending; - onCaptureStart(); + onCaptureStart(stop); bindWindowEvents(); }, - [bindWindowEvents, onCaptureStart, startDragging], + [bindWindowEvents, onCaptureStart, startDragging, stop], ); const onTouchStart = (event: TouchEvent) => { diff --git a/src/view/use-drag-handle/use-drag-handle.js b/src/view/use-drag-handle/use-drag-handle.js index d8a5d94337..7bb6c12eb8 100644 --- a/src/view/use-drag-handle/use-drag-handle.js +++ b/src/view/use-drag-handle/use-drag-handle.js @@ -1,6 +1,6 @@ // @flow import invariant from 'tiny-invariant'; -import { useLayoutEffect, useRef, useMemo, useState, useCallback } from 'react'; +import { useLayoutEffect, useRef, useMemo, useCallback } from 'react'; import type { Args, DragHandleProps } from './drag-handle-types'; import getWindowFromEl from '../window/get-window-from-el'; import useRequiredContext from '../use-required-context'; @@ -20,23 +20,32 @@ function preventHtml5Dnd(event: DragEvent) { event.preventDefault(); } +type Capturing = {| + abort: () => void, +|}; + export default function useDragHandle(args: Args): DragHandleProps { // Capturing - const isAnythingCapturingRef = useRef(false); - const [shouldAbortCapture, setShouldAbortCapture] = useState(false); - const onCaptureStart = useCallback(() => { + const capturingRef = useRef(null); + const onCaptureStart = useCallback((abort: () => void) => { invariant( - !isAnythingCapturingRef.current, + !capturingRef.current, 'Cannot start capturing while something else is', ); - isAnythingCapturingRef.current = true; + capturingRef.current = { + abort, + }; }, []); const onCaptureEnd = useCallback(() => { invariant( - isAnythingCapturingRef.current, + capturingRef.current, 'Cannot stop capturing while nothing is capturing', ); - isAnythingCapturingRef.current = false; + capturingRef.current = null; + }, []); + const abortCapture = useCallback(() => { + invariant(capturingRef.current, 'Cannot abort capture when there is none'); + capturingRef.current.abort(); }, []); const { canLift, style: styleContext }: AppContextValue = useRequiredContext( @@ -69,7 +78,7 @@ export default function useDragHandle(args: Args): DragHandleProps { (event: Event) => { // Something on this element might be capturing but a drag has not started yet // We want to prevent anything else from capturing - if (isAnythingCapturingRef.current) { + if (capturingRef.current) { return false; } // Do not drag if anything else in the system is dragging @@ -92,7 +101,6 @@ export default function useDragHandle(args: Args): DragHandleProps { onCaptureStart, onCaptureEnd, getShouldRespectForceTouch, - shouldAbortCapture, }), [ callbacks, @@ -102,7 +110,6 @@ export default function useDragHandle(args: Args): DragHandleProps { onCaptureStart, onCaptureEnd, getShouldRespectForceTouch, - shouldAbortCapture, ], ); const onMouseDown = useMouseSensor(mouseArgs); @@ -113,7 +120,6 @@ export default function useDragHandle(args: Args): DragHandleProps { getDraggableRef, getWindow, canStartCapturing, - shouldAbortCapture, onCaptureStart, onCaptureEnd, }), @@ -124,7 +130,6 @@ export default function useDragHandle(args: Args): DragHandleProps { getWindow, onCaptureEnd, onCaptureStart, - shouldAbortCapture, ], ); const onKeyDown = useKeyboardSensor(keyboardArgs); @@ -136,7 +141,6 @@ export default function useDragHandle(args: Args): DragHandleProps { getWindow, canStartCapturing, getShouldRespectForceTouch, - shouldAbortCapture, onCaptureStart, onCaptureEnd, }), @@ -146,7 +150,6 @@ export default function useDragHandle(args: Args): DragHandleProps { getWindow, canStartCapturing, getShouldRespectForceTouch, - shouldAbortCapture, onCaptureStart, onCaptureEnd, ], @@ -157,27 +160,18 @@ export default function useDragHandle(args: Args): DragHandleProps { useLayoutEffect(() => {}); // handle aborting - useLayoutEffect(() => { - // No longer dragging but still capturing: need to abort - if (!isDragging && isAnythingCapturingRef.current) { - setShouldAbortCapture(true); - } - }, [isDragging]); - - // handle is being disabled - useLayoutEffect(() => { - // No longer enabled but still capturing: need to abort - if (!isEnabled && isAnythingCapturingRef.current) { - setShouldAbortCapture(true); - } - }, [isEnabled]); - - // flip the abort capture flag back to true after use - useLayoutEffect(() => { - if (shouldAbortCapture) { - setShouldAbortCapture(false); + // No longer dragging but still capturing: need to abort + if (!isDragging && capturingRef.current) { + abortCapture(); + } + + // No longer enabled but still capturing: need to abort and cancel if needed + if (!isEnabled && capturingRef.current) { + abortCapture(); + if (isDragging) { + callbacks.onCancel(); } - }, [shouldAbortCapture]); + } const props: DragHandleProps = useMemo( () => ({ diff --git a/test/unit/view/drag-handle/mouse-sensor.spec.js b/test/unit/view/drag-handle/mouse-sensor.spec.js index 559227cbd0..31f3ddd32f 100644 --- a/test/unit/view/drag-handle/mouse-sensor.spec.js +++ b/test/unit/view/drag-handle/mouse-sensor.spec.js @@ -1005,6 +1005,7 @@ describe('disabled mid drag', () => { // lift mouseDown(wrapper); windowMouseMove({ x: 0, y: sloppyClickThreshold }); + wrapper.setProps({ isDragging: true }); expect(callbacksCalled(callbacks)({ onLift: 1 })).toBe(true); @@ -1025,6 +1026,7 @@ describe('disabled mid drag', () => { // lift mouseDown(wrapper); windowMouseMove({ x: 0, y: sloppyClickThreshold }); + wrapper.setProps({ isDragging: true }); // move windowMouseMove({ x: 0, y: sloppyClickThreshold + 1 }); requestAnimationFrame.step(); @@ -1051,6 +1053,7 @@ describe('disabled mid drag', () => { // lift mouseDown(wrapper); windowMouseMove({ x: 0, y: sloppyClickThreshold + 1 }); + wrapper.setProps({ isDragging: true }); // move windowMouseMove({ x: 0, y: sloppyClickThreshold + 1 }); requestAnimationFrame.step(); diff --git a/test/unit/view/drag-handle/util/wrappers.js b/test/unit/view/drag-handle/util/wrappers.js index c7b8f8e8ca..b3f7a5a314 100644 --- a/test/unit/view/drag-handle/util/wrappers.js +++ b/test/unit/view/drag-handle/util/wrappers.js @@ -58,6 +58,13 @@ function WithDragHandle(props: WithDragHandleProps) { return props.children(result); } +class PassThrough extends React.Component<*> { + render() { + const { children, ...rest } = this.props; + return children(rest); + } +} + export const getWrapper = ( callbacks: Callbacks, appContext?: AppContextValue = basicContext, @@ -65,22 +72,30 @@ export const getWrapper = ( ): ReactWrapper<*> => { const ref = createRef(); + // stopping this from creating a new reference and breaking the memoization during a drag + const getShouldRespectForceTouch = () => shouldRespectForceTouch; + return mount( - - shouldRespectForceTouch} - > - {(dragHandleProps: ?DragHandleProps) => ( - - )} - - , + + {(outer: any) => ( + + + {(dragHandleProps: ?DragHandleProps) => ( + + )} + + + )} + , ); }; From 85ceb1bcd94eb217f93af12b5ec8e59813f8e576 Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Mon, 25 Mar 2019 09:08:28 +1100 Subject: [PATCH 053/117] fixing tests. improving drag handle --- .../sensor/use-keyboard-sensor.js | 7 +---- .../sensor/use-mouse-sensor.js | 6 ++--- .../sensor/use-touch-sensor.js | 5 ---- src/view/use-drag-handle/use-drag-handle.js | 27 ++++++++++++++++--- .../{use-previous.js => use-previous-ref.js} | 6 +++-- .../view/drag-handle/keyboard-sensor.spec.js | 14 +++++++--- .../view/drag-handle/mouse-sensor.spec.js | 1 + .../view/drag-handle/touch-sensor.spec.js | 10 ++++--- 8 files changed, 49 insertions(+), 27 deletions(-) rename src/view/{use-previous.js => use-previous-ref.js} (51%) diff --git a/src/view/use-drag-handle/sensor/use-keyboard-sensor.js b/src/view/use-drag-handle/sensor/use-keyboard-sensor.js index ff96044bfe..bd1c3609db 100644 --- a/src/view/use-drag-handle/sensor/use-keyboard-sensor.js +++ b/src/view/use-drag-handle/sensor/use-keyboard-sensor.js @@ -169,7 +169,7 @@ export default function useKeyboardSensor(args: Args): OnKeyDown { }); }, [bindWindowEvents, callbacks, getDraggableRef, onCaptureStart, stop]); - const onKeyDown = useCallback( + const onKeyDown: OnKeyDown = useCallback( (event: KeyboardEvent) => { // not dragging yet if (!getIsDragging()) { @@ -258,10 +258,5 @@ export default function useKeyboardSensor(args: Args): OnKeyDown { ], ); - // When unmounting - cancel - useLayoutEffect(() => { - return cancel; - }, [cancel]); - return onKeyDown; } diff --git a/src/view/use-drag-handle/sensor/use-mouse-sensor.js b/src/view/use-drag-handle/sensor/use-mouse-sensor.js index 7db2ea960b..62c52f0b9b 100644 --- a/src/view/use-drag-handle/sensor/use-mouse-sensor.js +++ b/src/view/use-drag-handle/sensor/use-mouse-sensor.js @@ -369,9 +369,9 @@ export default function useMouseSensor(args: Args): OnMouseDown { ); // When unmounting - cancel - useLayoutEffect(() => { - return cancel; - }, [cancel]); + // useLayoutEffect(() => { + // return cancel; + // }, [cancel]); return onMouseDown; } diff --git a/src/view/use-drag-handle/sensor/use-touch-sensor.js b/src/view/use-drag-handle/sensor/use-touch-sensor.js index 8b8cc6e91c..6a6e3f5d99 100644 --- a/src/view/use-drag-handle/sensor/use-touch-sensor.js +++ b/src/view/use-drag-handle/sensor/use-touch-sensor.js @@ -429,10 +429,5 @@ export default function useTouchSensor(args: Args): OnTouchStart { startPendingDrag(event); }; - // When unmounting - cancel - useLayoutEffect(() => { - return cancel; - }, [cancel]); - return onTouchStart; } diff --git a/src/view/use-drag-handle/use-drag-handle.js b/src/view/use-drag-handle/use-drag-handle.js index 7bb6c12eb8..5040ed0af6 100644 --- a/src/view/use-drag-handle/use-drag-handle.js +++ b/src/view/use-drag-handle/use-drag-handle.js @@ -15,6 +15,7 @@ import useKeyboardSensor, { import useTouchSensor, { type Args as TouchSensorArgs, } from './sensor/use-touch-sensor'; +import usePreviousRef from '../use-previous-ref'; function preventHtml5Dnd(event: DragEvent) { event.preventDefault(); @@ -76,11 +77,16 @@ export default function useDragHandle(args: Args): DragHandleProps { const canStartCapturing = useCallback( (event: Event) => { + // Cannot lift when disabled + if (!isEnabled) { + return false; + } // Something on this element might be capturing but a drag has not started yet // We want to prevent anything else from capturing if (capturingRef.current) { return false; } + // Do not drag if anything else in the system is dragging if (!canLift(draggableId)) { return false; @@ -89,7 +95,7 @@ export default function useDragHandle(args: Args): DragHandleProps { // Check if we are dragging an interactive element return shouldAllowDraggingFromTarget(event, canDragInteractiveElements); }, - [canDragInteractiveElements, canLift, draggableId], + [canDragInteractiveElements, canLift, draggableId, isEnabled], ); const mouseArgs: MouseSensorArgs = useMemo( @@ -156,8 +162,23 @@ export default function useDragHandle(args: Args): DragHandleProps { ); const onTouchStart = useTouchSensor(touchArgs); - // mounting focus retention - useLayoutEffect(() => {}); + // aborting on unmount + const lastArgsRef = usePreviousRef(args); + useLayoutEffect(() => { + // only when unmounting + return () => { + if (!capturingRef.current) { + return; + } + abortCapture(); + + if (lastArgsRef.current.isDragging) { + // eslint-disable-next-line react-hooks/exhaustive-deps + lastArgsRef.current.callbacks.onCancel(); + } + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); // handle aborting // No longer dragging but still capturing: need to abort diff --git a/src/view/use-previous.js b/src/view/use-previous-ref.js similarity index 51% rename from src/view/use-previous.js rename to src/view/use-previous-ref.js index 6eb0dfc43a..4289e5517d 100644 --- a/src/view/use-previous.js +++ b/src/view/use-previous-ref.js @@ -1,7 +1,9 @@ // @flow import { useRef, useEffect } from 'react'; -export default function usePrevious(current: T): T { +// Should return MutableRefObject but I cannot import this type from 'react'; +// $ExpectError - MutableRefObject I want you +export default function usePrevious(current: T): MutableRefObject { const ref = useRef(current); // will be updated on the next render @@ -10,5 +12,5 @@ export default function usePrevious(current: T): T { }); // return the existing current (pre render) - return ref.current; + return ref; } diff --git a/test/unit/view/drag-handle/keyboard-sensor.spec.js b/test/unit/view/drag-handle/keyboard-sensor.spec.js index e5709a731f..105a0991c5 100644 --- a/test/unit/view/drag-handle/keyboard-sensor.spec.js +++ b/test/unit/view/drag-handle/keyboard-sensor.spec.js @@ -1,7 +1,6 @@ // @flow import { getRect, type Position } from 'css-box-model'; import { type ReactWrapper } from 'enzyme'; -import { canLiftKey, styleKey } from '../../../../src/view/context-keys'; import * as keyCodes from '../../../../src/view/key-codes'; import { withKeyboard } from '../../../utils/user-input-util'; import { @@ -31,6 +30,8 @@ import { } from './util/events'; import { getWrapper } from './util/wrappers'; import type { Callbacks } from '../../../../src/view/use-drag-handle/drag-handle-types'; +import type { AppContextValue } from '../../../../src/view/context/app-context'; +import basicContext from './util/app-context'; const origin: Position = { x: 0, y: 0 }; @@ -131,9 +132,9 @@ describe('initiation', () => { it('should not lift if the state does not currently allow lifting', () => { const customCallbacks: Callbacks = getStubCallbacks(); - const customContext = { - [styleKey]: 'hello', - [canLiftKey]: () => false, + const customContext: AppContextValue = { + ...basicContext, + canLift: () => false, }; const customWrapper = getWrapper(customCallbacks, customContext); const mock: MockEvent = createMockEvent(); @@ -519,6 +520,7 @@ describe('cancel', () => { describe('disabled mid drag', () => { it('should cancel the current drag', () => { pressSpacebar(wrapper); + wrapper.setProps({ isDragging: true }); wrapper.setProps({ isEnabled: false, @@ -535,6 +537,8 @@ describe('disabled mid drag', () => { it('should drop any pending movements', () => { // lift pressSpacebar(wrapper); + wrapper.setProps({ isDragging: true }); + wrapper.setProps({ isDragging: true }); expect(callbacks.onLift).toHaveBeenCalledTimes(1); pressArrowUp(wrapper); @@ -566,6 +570,7 @@ describe('disabled mid drag', () => { it('should stop preventing default action on events', () => { // setup pressSpacebar(wrapper); + wrapper.setProps({ isDragging: true }); wrapper.setProps({ isEnabled: false, }); @@ -633,6 +638,7 @@ describe('cancelled elsewhere in the app mid drag', () => { it('should call the onCancel prop if unmounted mid drag', () => { pressSpacebar(wrapper); + wrapper.setProps({ isDragging: true }); wrapper.unmount(); diff --git a/test/unit/view/drag-handle/mouse-sensor.spec.js b/test/unit/view/drag-handle/mouse-sensor.spec.js index 31f3ddd32f..f0f91281f8 100644 --- a/test/unit/view/drag-handle/mouse-sensor.spec.js +++ b/test/unit/view/drag-handle/mouse-sensor.spec.js @@ -1137,6 +1137,7 @@ describe('unmounted mid drag', () => { beforeEach(() => { mouseDown(wrapper); windowMouseMove({ x: 0, y: sloppyClickThreshold }); + wrapper.setProps({ isDragging: true }); wrapper.unmount(); }); diff --git a/test/unit/view/drag-handle/touch-sensor.spec.js b/test/unit/view/drag-handle/touch-sensor.spec.js index bac9b88cc4..47cba37e48 100644 --- a/test/unit/view/drag-handle/touch-sensor.spec.js +++ b/test/unit/view/drag-handle/touch-sensor.spec.js @@ -1,7 +1,6 @@ // @flow import { type Position } from 'css-box-model'; import { type ReactWrapper } from 'enzyme'; -import { canLiftKey, styleKey } from '../../../../src/view/context-keys'; import * as keyCodes from '../../../../src/view/key-codes'; import getWindowScroll from '../../../../src/view/window/get-window-scroll'; import setWindowScroll from '../../../utils/set-window-scroll'; @@ -28,6 +27,8 @@ import { } from './util/events'; import { getWrapper } from './util/wrappers'; import type { Callbacks } from '../../../../src/view/use-drag-handle/drag-handle-types'; +import type { AppContextValue } from '../../../../src/view/context/app-context'; +import basicContext from './util/app-context'; const origin: Position = { x: 0, y: 0 }; let callbacks: Callbacks; @@ -57,6 +58,7 @@ afterEach(() => { const start = () => { touchStart(wrapper, origin); jest.runTimersToTime(timeForLongPress); + wrapper.setProps({ isDragging: true }); }; const end = () => windowTouchEnd(); const move = (point?: Position = { x: 5, y: 20 }) => { @@ -90,9 +92,9 @@ describe('initiation', () => { it('should not start a drag if the application state does not allow it', () => { const customCallbacks: Callbacks = getStubCallbacks(); - const customContext = { - [styleKey]: 'hello', - [canLiftKey]: () => false, + const customContext: AppContextValue = { + ...basicContext, + canLift: () => false, }; const customWrapper = getWrapper(customCallbacks, customContext); const mock: MockEvent = createMockEvent(); From afbd2bd678b56468da0877c0e9b4959081f5a7a6 Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Mon, 25 Mar 2019 09:22:37 +1100 Subject: [PATCH 054/117] more drag handle tests --- src/view/use-drag-handle/use-drag-handle.js | 26 ++++++++++++++----- test/unit/view/drag-handle/attributes.spec.js | 5 ++-- .../disabled-while-capturing.spec.js | 2 +- .../drag-handle/interactive-elements.spec.js | 6 ++--- 4 files changed, 26 insertions(+), 13 deletions(-) diff --git a/src/view/use-drag-handle/use-drag-handle.js b/src/view/use-drag-handle/use-drag-handle.js index 5040ed0af6..8949129d07 100644 --- a/src/view/use-drag-handle/use-drag-handle.js +++ b/src/view/use-drag-handle/use-drag-handle.js @@ -16,6 +16,7 @@ import useTouchSensor, { type Args as TouchSensorArgs, } from './sensor/use-touch-sensor'; import usePreviousRef from '../use-previous-ref'; +import { warning } from '../../dev-warning'; function preventHtml5Dnd(event: DragEvent) { event.preventDefault(); @@ -25,7 +26,7 @@ type Capturing = {| abort: () => void, |}; -export default function useDragHandle(args: Args): DragHandleProps { +export default function useDragHandle(args: Args): ?DragHandleProps { // Capturing const capturingRef = useRef(null); const onCaptureStart = useCallback((abort: () => void) => { @@ -190,12 +191,18 @@ export default function useDragHandle(args: Args): DragHandleProps { if (!isEnabled && capturingRef.current) { abortCapture(); if (isDragging) { + warning( + 'You have disabled dragging on a Draggable while it was dragging. The drag has been cancelled', + ); callbacks.onCancel(); } } - const props: DragHandleProps = useMemo( - () => ({ + const props: ?DragHandleProps = useMemo(() => { + if (!isEnabled) { + return null; + } + return { onMouseDown, onKeyDown, onTouchStart, @@ -208,9 +215,16 @@ export default function useDragHandle(args: Args): DragHandleProps { // Opting out of html5 drag and drops draggable: false, onDragStart: preventHtml5Dnd, - }), - [onBlur, onFocus, onKeyDown, onMouseDown, onTouchStart, styleContext], - ); + }; + }, [ + isEnabled, + onBlur, + onFocus, + onKeyDown, + onMouseDown, + onTouchStart, + styleContext, + ]); return props; } diff --git a/test/unit/view/drag-handle/attributes.spec.js b/test/unit/view/drag-handle/attributes.spec.js index 03225866e1..69f22b6938 100644 --- a/test/unit/view/drag-handle/attributes.spec.js +++ b/test/unit/view/drag-handle/attributes.spec.js @@ -1,8 +1,7 @@ // @flow import { getWrapper, Child } from './util/wrappers'; import { getStubCallbacks } from './util/callbacks'; -import basicContext from './util/basic-context'; -import { styleKey } from '../../../../src/view/context-keys'; +import basicContext from './util/app-context'; it('should apply the style context to a data-attribute', () => { expect( @@ -10,7 +9,7 @@ it('should apply the style context to a data-attribute', () => { .find(Child) .getDOMNode() .getAttribute('data-react-beautiful-dnd-drag-handle'), - ).toEqual(basicContext[styleKey]); + ).toEqual(basicContext.style); }); it('should apply a default aria roledescription containing lift instructions', () => { diff --git a/test/unit/view/drag-handle/disabled-while-capturing.spec.js b/test/unit/view/drag-handle/disabled-while-capturing.spec.js index 473883a5f0..0ccfcb849f 100644 --- a/test/unit/view/drag-handle/disabled-while-capturing.spec.js +++ b/test/unit/view/drag-handle/disabled-while-capturing.spec.js @@ -3,7 +3,7 @@ import type { ReactWrapper } from 'enzyme'; import { forEach, type Control } from './util/controls'; import { getWrapper } from './util/wrappers'; import { getStubCallbacks, callbacksCalled } from './util/callbacks'; -import type { Callbacks } from '../../../../src/view/drag-handle/drag-handle-types'; +import type { Callbacks } from '../../../../src/view/use-drag-handle/drag-handle-types'; const expectMidDragDisabledWarning = (fn: Function) => { // arrange diff --git a/test/unit/view/drag-handle/interactive-elements.spec.js b/test/unit/view/drag-handle/interactive-elements.spec.js index 0a127f6053..0fe2b4f732 100644 --- a/test/unit/view/drag-handle/interactive-elements.spec.js +++ b/test/unit/view/drag-handle/interactive-elements.spec.js @@ -7,9 +7,9 @@ import { resetCallbacks, } from './util/callbacks'; import { getWrapper } from './util/wrappers'; -import { interactiveTagNames } from '../../../../src/view/drag-handle/util/should-allow-dragging-from-target'; -import type { Callbacks } from '../../../../src/view/drag-handle/drag-handle-types'; -import type { TagNameMap } from '../../../../src/view/drag-handle/util/should-allow-dragging-from-target'; +import { interactiveTagNames } from '../../../../src/view/use-drag-handle/util/should-allow-dragging-from-target'; +import type { Callbacks } from '../../../../src/view/use-drag-handle/drag-handle-types'; +import type { TagNameMap } from '../../../../src/view/use-drag-handle/util/should-allow-dragging-from-target'; const mixedCase = (map: TagNameMap): string[] => [ ...Object.keys(map).map((tagName: string) => tagName.toLowerCase()), From 10c7540498f2a1c7f21f891fc143c419a870a882 Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Mon, 25 Mar 2019 14:43:39 +1100 Subject: [PATCH 055/117] tests for integration --- src/view/draggable/draggable.jsx | 2 +- .../use-style-marshal/use-style-marshal.js | 11 ++-- .../responders-integration.spec.js | 56 +++++++++++++------ 3 files changed, 46 insertions(+), 23 deletions(-) diff --git a/src/view/draggable/draggable.jsx b/src/view/draggable/draggable.jsx index 0a2c241eb8..0aabef7483 100644 --- a/src/view/draggable/draggable.jsx +++ b/src/view/draggable/draggable.jsx @@ -166,7 +166,7 @@ export default function Draggable(props: Props) { ], ); - const dragHandleProps: DragHandleProps = useDragHandle(dragHandleArgs); + const dragHandleProps: ?DragHandleProps = useDragHandle(dragHandleArgs); const onMoveEnd = useCallback( (event: TransitionEvent) => { diff --git a/src/view/use-style-marshal/use-style-marshal.js b/src/view/use-style-marshal/use-style-marshal.js index 6eeaff4637..ba3d6e78f7 100644 --- a/src/view/use-style-marshal/use-style-marshal.js +++ b/src/view/use-style-marshal/use-style-marshal.js @@ -101,10 +101,13 @@ export default function useStyleMarshal(uniqueId: number) { }, [setDynamicStyle, styles.dropAnimating, styles.userCancel], ); - const resting = useCallback(() => setDynamicStyle(styles.resting), [ - setDynamicStyle, - styles.resting, - ]); + const resting = useCallback(() => { + // Can be called defensively + if (!dynamicRef.current) { + return; + } + setDynamicStyle(styles.resting); + }, [setDynamicStyle, styles.resting]); const marshal: StyleMarshal = useMemo( () => ({ diff --git a/test/unit/integration/responders-integration.spec.js b/test/unit/integration/responders-integration.spec.js index 065d09476e..09cec6ae02 100644 --- a/test/unit/integration/responders-integration.spec.js +++ b/test/unit/integration/responders-integration.spec.js @@ -49,14 +49,21 @@ describe('responders integration', () => { const getMountedApp = () => { // Both list and item will have the same dimensions - jest - .spyOn(Element.prototype, 'getBoundingClientRect') - .mockImplementation(() => borderBox); - // Stubbing out totally - not including margins in this - jest - .spyOn(window, 'getComputedStyle') - .mockImplementation(() => getComputedSpacing({})); + const setRefDimensions = (ref: ?HTMLElement) => { + if (!ref) { + return; + } + + jest + .spyOn(ref, 'getBoundingClientRect') + .mockImplementation(() => borderBox); + + // Stubbing out totally - not including margins in this + jest + .spyOn(window, 'getComputedStyle') + .mockImplementation(() => getComputedSpacing({})); + }; return mount( { {(droppableProvided: DroppableProvided) => (
{ + setRefDimensions(ref); + droppableProvided.innerRef(ref); + }} {...droppableProvided.droppableProps} >

Droppable

@@ -76,7 +86,10 @@ describe('responders integration', () => { {(draggableProvided: DraggableProvided) => (
{ + setRefDimensions(ref); + draggableProvided.innerRef(ref); + }} {...draggableProvided.draggableProps} {...draggableProvided.dragHandleProps} > @@ -143,30 +156,37 @@ describe('responders integration', () => { const move = () => { windowMouseMove({ x: dragMove.x, - y: dragMove.y + sloppyClickThreshold + 1, + y: dragMove.y, }); - // movements are scheduled with setTimeout + // movements are scheduled in an animation frame + requestAnimationFrame.step(); + // responder updates are scheduled with setTimeout jest.runOnlyPendingTimers(); }; const waitForReturnToHome = () => { - // cheating - console.log( - 'find', - wrapper.find('[data-react-beautiful-dnd-draggable]').length, - ); - wrapper + // could not get this right just using window events + const props = wrapper .find('[data-react-beautiful-dnd-draggable]') - .simulate('onMoveEnd'); + .first() + .props(); + + if (props.onTransitionEnd) { + props.onTransitionEnd({ propertyName: 'transform' }); + } }; const stop = () => { windowMouseUp(); + // tell enzyme the onTransitionEnd prop has changed + wrapper.update(); waitForReturnToHome(); }; const cancel = () => { cancelWithKeyboard(); + // tell enzyme the onTransitionEnd prop has changed + wrapper.update(); waitForReturnToHome(); }; From dab43548e4934562cc11d0f43ac59cc5e7b6b2c3 Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Mon, 25 Mar 2019 14:44:06 +1100 Subject: [PATCH 056/117] naming improvement --- test/unit/integration/responders-integration.spec.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/unit/integration/responders-integration.spec.js b/test/unit/integration/responders-integration.spec.js index 09cec6ae02..5fd714a9bc 100644 --- a/test/unit/integration/responders-integration.spec.js +++ b/test/unit/integration/responders-integration.spec.js @@ -164,7 +164,7 @@ describe('responders integration', () => { jest.runOnlyPendingTimers(); }; - const waitForReturnToHome = () => { + const tryFlushDropAnimation = () => { // could not get this right just using window events const props = wrapper .find('[data-react-beautiful-dnd-draggable]') @@ -180,14 +180,14 @@ describe('responders integration', () => { windowMouseUp(); // tell enzyme the onTransitionEnd prop has changed wrapper.update(); - waitForReturnToHome(); + tryFlushDropAnimation(); }; const cancel = () => { cancelWithKeyboard(); // tell enzyme the onTransitionEnd prop has changed wrapper.update(); - waitForReturnToHome(); + tryFlushDropAnimation(); }; const perform = () => { From 52883143d0ab2485aa62c1074a0a6f48223f1aee Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Mon, 25 Mar 2019 17:09:51 +1100 Subject: [PATCH 057/117] tests for controls --- package.json | 1 - .../responders-integration.spec.js | 2 +- ...start-when-something-else-dragging.spec.js | 34 ++------ test/unit/view/drag-handle/util/callbacks.js | 2 +- test/unit/view/drag-handle/util/controls.js | 19 +++-- test/unit/view/drag-handle/util/wrappers.js | 2 +- .../sensors/use-mouse-sensor.spec.js | 80 ------------------- yarn.lock | 46 ----------- 8 files changed, 23 insertions(+), 163 deletions(-) delete mode 100644 test/unit/view/draggable/drag-handle/sensors/use-mouse-sensor.spec.js diff --git a/package.json b/package.json index 72dce073af..8741ef4601 100644 --- a/package.json +++ b/package.json @@ -110,7 +110,6 @@ "react": "16.8.4", "react-dom": "16.8.4", "react-test-renderer": "16.8.4", - "react-testing-library": "^6.0.1", "rimraf": "^2.6.3", "rollup": "^1.6.0", "rollup-plugin-babel": "^4.3.2", diff --git a/test/unit/integration/responders-integration.spec.js b/test/unit/integration/responders-integration.spec.js index 5fd714a9bc..f017ce010e 100644 --- a/test/unit/integration/responders-integration.spec.js +++ b/test/unit/integration/responders-integration.spec.js @@ -178,7 +178,7 @@ describe('responders integration', () => { const stop = () => { windowMouseUp(); - // tell enzyme the onTransitionEnd prop has changed + // tell enzyme the onTransitionEnd prop has chan`ged wrapper.update(); tryFlushDropAnimation(); }; diff --git a/test/unit/view/drag-handle/block-drag-start-when-something-else-dragging.spec.js b/test/unit/view/drag-handle/block-drag-start-when-something-else-dragging.spec.js index 5c3a10428e..26e85b1abc 100644 --- a/test/unit/view/drag-handle/block-drag-start-when-something-else-dragging.spec.js +++ b/test/unit/view/drag-handle/block-drag-start-when-something-else-dragging.spec.js @@ -1,48 +1,30 @@ // @flow import React from 'react'; import { mount } from 'enzyme'; -import DragHandle from '../../../../src/view/drag-handle/drag-handle'; import type { DragHandleProps } from '../../../../src/view/drag-handle/drag-handle-types'; import { forEach, type Control } from './util/controls'; import { getStubCallbacks, callbacksCalled } from './util/callbacks'; -import basicContext from './util/basic-context'; -import { Child, createRef } from './util/wrappers'; -import { canLiftKey } from '../../../../src/view/context-keys'; +import { Child, createRef, getWrapper } from './util/wrappers'; +import type { AppContextValue } from '../../../../src/view/context/app-context'; +import basicContext from './util/app-context'; forEach((control: Control) => { it('should not start a drag if something else is already dragging in the system', () => { - const ref = createRef(); // faking a 'false' response const canLift = jest.fn().mockImplementation(() => false); - const customContext = { + const customContext: AppContextValue = { ...basicContext, - [canLiftKey]: canLift, + canLift, }; - const customCallbacks = getStubCallbacks(); - const wrapper = mount( - true} - canDragInteractiveElements={false} - > - {(dragHandleProps: ?DragHandleProps) => ( - - )} - , - { context: customContext }, - ); + const callbacks = getStubCallbacks(); + const wrapper = getWrapper(callbacks, customContext); control.preLift(wrapper); control.lift(wrapper); control.drop(wrapper); expect( - callbacksCalled(customCallbacks)({ + callbacksCalled(callbacks)({ onLift: 0, }), ).toBe(true); diff --git a/test/unit/view/drag-handle/util/callbacks.js b/test/unit/view/drag-handle/util/callbacks.js index e0e1959714..f1f5d48b9f 100644 --- a/test/unit/view/drag-handle/util/callbacks.js +++ b/test/unit/view/drag-handle/util/callbacks.js @@ -1,5 +1,5 @@ // @flow -import type { Callbacks } from '../../../../../src/view/drag-handle/drag-handle-types'; +import type { Callbacks } from '../../../../../src/view/use-drag-handle/drag-handle-types'; export const getStubCallbacks = (): Callbacks => ({ onLift: jest.fn(), diff --git a/test/unit/view/drag-handle/util/controls.js b/test/unit/view/drag-handle/util/controls.js index 8a304d79f2..13256cc24e 100644 --- a/test/unit/view/drag-handle/util/controls.js +++ b/test/unit/view/drag-handle/util/controls.js @@ -2,6 +2,7 @@ import type { ReactWrapper } from 'enzyme'; import { sloppyClickThreshold } from '../../../../../src/view/use-drag-handle/util/is-sloppy-click-threshold-exceeded'; import { timeForLongPress } from '../../../../../src/view/use-drag-handle/sensor/create-touch-sensor'; +import * as attributes from '../../../../../src/view/data-attributes'; import { primaryButton, touchStart, @@ -40,12 +41,15 @@ const trySetIsDragging = (wrapper: ReactWrapper<*>) => { wrapper.setProps({ isDragging: true }); }; +const getDragHandle = (wrapper: ReactWrapper<*>) => + wrapper.find(`[${attributes.dragHandle}]`); + export const touch: Control = { name: 'touch', hasPostDragClickBlocking: true, hasPreLift: true, preLift: (wrapper: ReactWrapper<*>, options?: Object = {}) => - touchStart(wrapper, { x: 0, y: 0 }, 0, options), + touchStart(getDragHandle(wrapper), { x: 0, y: 0 }, 0, options), lift: (wrapper: ReactWrapper<*>) => { jest.runTimersToTime(timeForLongPress); trySetIsDragging(wrapper); @@ -68,16 +72,16 @@ export const keyboard: Control = { // no pre lift required preLift: () => {}, lift: (wrap: ReactWrapper<*>, options?: Object = {}) => { - pressSpacebar(wrap, options); + pressSpacebar(getDragHandle(wrap), options); trySetIsDragging(wrap); }, move: (wrap: ReactWrapper<*>) => { - pressArrowDown(wrap); + pressArrowDown(getDragHandle(wrap)); }, drop: (wrap: ReactWrapper<*>) => { // only want to fire the event if dragging - otherwise it might start a drag if (wrap.props().isDragging) { - pressSpacebar(wrap); + pressSpacebar(getDragHandle(wrap)); } }, // no cleanup required @@ -88,8 +92,9 @@ export const mouse: Control = { name: 'mouse', hasPostDragClickBlocking: true, hasPreLift: true, - preLift: (wrap: ReactWrapper<*>, options?: Object = {}) => - mouseDown(wrap, { x: 0, y: 0 }, primaryButton, options), + preLift: (wrap: ReactWrapper<*>, options?: Object = {}) => { + mouseDown(getDragHandle(wrap), { x: 0, y: 0 }, primaryButton, options); + }, lift: (wrap: ReactWrapper<*>) => { windowMouseMove({ x: 0, y: sloppyClickThreshold }); trySetIsDragging(wrap); @@ -108,7 +113,7 @@ export const mouse: Control = { export const controls: Control[] = [mouse, keyboard, touch]; export const forEach = (fn: (control: Control) => void) => { - controls.forEach((control: Control) => { + controls.slice(0).forEach((control: Control) => { describe(`with: ${control.name}`, () => { beforeEach(() => { jest.useFakeTimers(); diff --git a/test/unit/view/drag-handle/util/wrappers.js b/test/unit/view/drag-handle/util/wrappers.js index b3f7a5a314..6a2072e28f 100644 --- a/test/unit/view/drag-handle/util/wrappers.js +++ b/test/unit/view/drag-handle/util/wrappers.js @@ -80,7 +80,7 @@ export const getWrapper = ( {(outer: any) => ( { - let lastSnapshot: DraggableStateSnapshot; - - jest - .spyOn(Element.prototype, 'getBoundingClientRect') - .mockImplementation(() => getPreset().inHome1.client.borderBox); - - jest - .spyOn(window, 'getComputedStyle') - .mockImplementation(() => getComputedSpacing({})); - - const { getByText } = render( - {}}> - - {droppableProvided => ( -
- - {(provided, snapshot) => { - lastSnapshot = snapshot; - - return ( -
- Drag handle -
- ); - }} -
- {droppableProvided.placeholder} -
- )} -
-
, - ); - - invariant(lastSnapshot); - const handle = getByText('Drag handle'); - fireEvent.mouseDown(handle, { - button: primaryButton, - clientX: origin.x, - clientY: origin.y, - }); - - // not dragging yet - expect(lastSnapshot.isDragging).toBe(false); - - const point: Position = { x: 0, y: sloppyClickThreshold }; - fireEvent.mouseMove(handle.parentElement, { - button: primaryButton, - clientX: point.x, - clientY: point.y, - }); - expect(lastSnapshot.isDragging).toBe(true); -}); diff --git a/yarn.lock b/yarn.lock index 97c8c20097..af9f1c8b1e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1178,14 +1178,6 @@ "@types/istanbul-lib-coverage" "^1.1.0" "@types/yargs" "^12.0.9" -"@jest/types@^24.5.0": - version "24.5.0" - resolved "https://registry.yarnpkg.com/@jest/types/-/types-24.5.0.tgz#feee214a4d0167b0ca447284e95a57aa10b3ee95" - integrity sha512-kN7RFzNMf2R8UDadPOl6ReyI+MT8xfqRuAnuVL+i4gwjv/zubdDK+EDeLHYwq1j0CSSR2W/MmgaRlMZJzXdmVA== - dependencies: - "@types/istanbul-lib-coverage" "^1.1.0" - "@types/yargs" "^12.0.9" - "@mrmlnc/readdir-enhanced@^2.2.1": version "2.2.1" resolved "https://registry.yarnpkg.com/@mrmlnc/readdir-enhanced/-/readdir-enhanced-2.2.1.tgz#524af240d1a360527b730475ecfa1344aa540dde" @@ -1210,11 +1202,6 @@ react-lifecycles-compat "^3.0.4" warning "^3.0.0" -"@sheerun/mutationobserver-shim@^0.3.2": - version "0.3.2" - resolved "https://registry.yarnpkg.com/@sheerun/mutationobserver-shim/-/mutationobserver-shim-0.3.2.tgz#8013f2af54a2b7d735f71560ff360d3a8176a87b" - integrity sha512-vTCdPp/T/Q3oSqwHmZ5Kpa9oI7iLtGl3RQaA/NyLHikvcrPxACkkKVr/XzkSPJWXHRhKGzVvb0urJsbMlRxi1Q== - "@storybook/addons@5.0.1": version "5.0.1" resolved "https://registry.yarnpkg.com/@storybook/addons/-/addons-5.0.1.tgz#24ae1c80cea973de3e0125614a015fbb0629ac34" @@ -4318,16 +4305,6 @@ dom-serializer@0, dom-serializer@~0.1.0: domelementtype "^1.3.0" entities "^1.1.1" -dom-testing-library@^3.13.1: - version "3.18.1" - resolved "https://registry.yarnpkg.com/dom-testing-library/-/dom-testing-library-3.18.1.tgz#04f1ded8a83007eb305184ed953baf34fb2fec86" - integrity sha512-tp7doYDfPkbExBHnqnJFXQOeqKbTlxvyP8njIjSe9JYwAFDh5XvRi8zV7XdFigdd8KTsQaeLFZCUH5JDTukmvQ== - dependencies: - "@babel/runtime" "^7.3.4" - "@sheerun/mutationobserver-shim" "^0.3.2" - pretty-format "^24.5.0" - wait-for-expect "^1.1.0" - dom-walk@^0.1.0: version "0.1.1" resolved "https://registry.yarnpkg.com/dom-walk/-/dom-walk-0.1.1.tgz#672226dc74c8f799ad35307df936aba11acd6018" @@ -9027,16 +9004,6 @@ pretty-format@^24.3.1: ansi-styles "^3.2.0" react-is "^16.8.4" -pretty-format@^24.5.0: - version "24.5.0" - resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-24.5.0.tgz#cc69a0281a62cd7242633fc135d6930cd889822d" - integrity sha512-/3RuSghukCf8Riu5Ncve0iI+BzVkbRU5EeUoArKARZobREycuH5O4waxvaNIloEXdb0qwgmEAed5vTpX1HNROQ== - dependencies: - "@jest/types" "^24.5.0" - ansi-regex "^4.0.0" - ansi-styles "^3.2.0" - react-is "^16.8.4" - pretty-hrtime@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/pretty-hrtime/-/pretty-hrtime-1.0.3.tgz#b7e3ea42435a4c9b2759d99e0f201eb195802ee1" @@ -9542,14 +9509,6 @@ react-test-renderer@^16.0.0-0: react-is "^16.8.3" scheduler "^0.13.3" -react-testing-library@^6.0.1: - version "6.0.1" - resolved "https://registry.yarnpkg.com/react-testing-library/-/react-testing-library-6.0.1.tgz#0ddf155cb609529e37359a82cc63eb3f830397fd" - integrity sha512-Asyrmdj059WnD8q4pVsKoPtvWfXEk+OCCNKSo9bh5tZ0pb80iXvkr4oppiL8H2qWL+MJUV2PTMneHYxsTeAa/A== - dependencies: - "@babel/runtime" "^7.3.1" - dom-testing-library "^3.13.1" - react-textarea-autosize@^7.0.4: version "7.1.0" resolved "https://registry.yarnpkg.com/react-textarea-autosize/-/react-textarea-autosize-7.1.0.tgz#3132cb77e65d94417558d37c0bfe415a5afd3445" @@ -11679,11 +11638,6 @@ w3c-hr-time@^1.0.1: dependencies: browser-process-hrtime "^0.1.2" -wait-for-expect@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/wait-for-expect/-/wait-for-expect-1.1.0.tgz#6607375c3f79d32add35cd2c87ce13f351a3d453" - integrity sha512-vQDokqxyMyknfX3luCDn16bSaRcOyH6gGuUXMIbxBLeTo6nWuEWYqMTT9a+44FmW8c2m6TRWBdNvBBjA1hwEKg== - wait-port@^0.2.2: version "0.2.2" resolved "https://registry.yarnpkg.com/wait-port/-/wait-port-0.2.2.tgz#d51a491e484a17bf75a947e711a2f012b4e6f2e3" From 4eec4ffe5049146b2e8d33b63503c57cee96eb06 Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Mon, 25 Mar 2019 17:40:01 +1100 Subject: [PATCH 058/117] tests for drag handles --- src/view/use-drag-handle/use-drag-handle.js | 14 +++++----- ...start-when-something-else-dragging.spec.js | 5 +--- test/unit/view/drag-handle/util/controls.js | 26 +++++-------------- test/unit/view/drag-handle/util/wrappers.js | 2 +- 4 files changed, 15 insertions(+), 32 deletions(-) diff --git a/src/view/use-drag-handle/use-drag-handle.js b/src/view/use-drag-handle/use-drag-handle.js index 8949129d07..ca4c810549 100644 --- a/src/view/use-drag-handle/use-drag-handle.js +++ b/src/view/use-drag-handle/use-drag-handle.js @@ -181,16 +181,10 @@ export default function useDragHandle(args: Args): ?DragHandleProps { // eslint-disable-next-line react-hooks/exhaustive-deps }, []); - // handle aborting - // No longer dragging but still capturing: need to abort - if (!isDragging && capturingRef.current) { - abortCapture(); - } - // No longer enabled but still capturing: need to abort and cancel if needed if (!isEnabled && capturingRef.current) { abortCapture(); - if (isDragging) { + if (lastArgsRef.current.isDragging) { warning( 'You have disabled dragging on a Draggable while it was dragging. The drag has been cancelled', ); @@ -198,6 +192,12 @@ export default function useDragHandle(args: Args): ?DragHandleProps { } } + // handle aborting + // No longer dragging but still capturing: need to abort + if (!isDragging && capturingRef.current) { + abortCapture(); + } + const props: ?DragHandleProps = useMemo(() => { if (!isEnabled) { return null; diff --git a/test/unit/view/drag-handle/block-drag-start-when-something-else-dragging.spec.js b/test/unit/view/drag-handle/block-drag-start-when-something-else-dragging.spec.js index 26e85b1abc..982644f582 100644 --- a/test/unit/view/drag-handle/block-drag-start-when-something-else-dragging.spec.js +++ b/test/unit/view/drag-handle/block-drag-start-when-something-else-dragging.spec.js @@ -1,10 +1,7 @@ // @flow -import React from 'react'; -import { mount } from 'enzyme'; -import type { DragHandleProps } from '../../../../src/view/drag-handle/drag-handle-types'; import { forEach, type Control } from './util/controls'; import { getStubCallbacks, callbacksCalled } from './util/callbacks'; -import { Child, createRef, getWrapper } from './util/wrappers'; +import { getWrapper } from './util/wrappers'; import type { AppContextValue } from '../../../../src/view/context/app-context'; import basicContext from './util/app-context'; diff --git a/test/unit/view/drag-handle/util/controls.js b/test/unit/view/drag-handle/util/controls.js index 13256cc24e..6f4d18209d 100644 --- a/test/unit/view/drag-handle/util/controls.js +++ b/test/unit/view/drag-handle/util/controls.js @@ -2,7 +2,6 @@ import type { ReactWrapper } from 'enzyme'; import { sloppyClickThreshold } from '../../../../../src/view/use-drag-handle/util/is-sloppy-click-threshold-exceeded'; import { timeForLongPress } from '../../../../../src/view/use-drag-handle/sensor/create-touch-sensor'; -import * as attributes from '../../../../../src/view/data-attributes'; import { primaryButton, touchStart, @@ -27,22 +26,9 @@ export type Control = {| cleanup: () => void, |}; -const trySetIsDragging = (wrapper: ReactWrapper<*>) => { - // potentially not looking at the root wrapper - if (!wrapper.props().callbacks) { - return; - } - - // lift was not successful - this can happen when not allowed to lift - if (!wrapper.props().callbacks.onLift.mock.calls.length) { - return; - } - // would be set during a drag - wrapper.setProps({ isDragging: true }); -}; - +// using the class rather than the attribute as the attribute will not be present when disabled const getDragHandle = (wrapper: ReactWrapper<*>) => - wrapper.find(`[${attributes.dragHandle}]`); + wrapper.find('.drag-handle'); export const touch: Control = { name: 'touch', @@ -52,7 +38,7 @@ export const touch: Control = { touchStart(getDragHandle(wrapper), { x: 0, y: 0 }, 0, options), lift: (wrapper: ReactWrapper<*>) => { jest.runTimersToTime(timeForLongPress); - trySetIsDragging(wrapper); + wrapper.setProps({ isDragging: true }); }, move: () => { windowTouchMove({ x: 100, y: 200 }); @@ -73,7 +59,7 @@ export const keyboard: Control = { preLift: () => {}, lift: (wrap: ReactWrapper<*>, options?: Object = {}) => { pressSpacebar(getDragHandle(wrap), options); - trySetIsDragging(wrap); + wrap.setProps({ isDragging: true }); }, move: (wrap: ReactWrapper<*>) => { pressArrowDown(getDragHandle(wrap)); @@ -97,7 +83,7 @@ export const mouse: Control = { }, lift: (wrap: ReactWrapper<*>) => { windowMouseMove({ x: 0, y: sloppyClickThreshold }); - trySetIsDragging(wrap); + wrap.setProps({ isDragging: true }); }, move: () => { windowMouseMove({ x: 100, y: 200 }); @@ -113,7 +99,7 @@ export const mouse: Control = { export const controls: Control[] = [mouse, keyboard, touch]; export const forEach = (fn: (control: Control) => void) => { - controls.slice(0).forEach((control: Control) => { + controls.forEach((control: Control) => { describe(`with: ${control.name}`, () => { beforeEach(() => { jest.useFakeTimers(); diff --git a/test/unit/view/drag-handle/util/wrappers.js b/test/unit/view/drag-handle/util/wrappers.js index 6a2072e28f..1f656fa8bd 100644 --- a/test/unit/view/drag-handle/util/wrappers.js +++ b/test/unit/view/drag-handle/util/wrappers.js @@ -25,7 +25,7 @@ export class Child extends React.Component {
Drag me! {this.props.children} From 7e387c8f43fd801526c9402c658b4507b2c4265f Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Tue, 26 Mar 2019 08:56:49 +1100 Subject: [PATCH 059/117] more drag handle work --- .../sensor/use-keyboard-sensor.js | 2 +- src/view/use-drag-handle/use-drag-handle.js | 6 +- src/view/use-drag-handle/use-validation.js | 17 ++ .../view/drag-handle/contenteditable.spec.js | 261 ++++++++++-------- .../drag-handle/nested-drag-handles.spec.js | 87 +++--- .../view/drag-handle/throw-if-svg.spec.js | 46 ++- test/unit/view/drag-handle/util/controls.js | 20 +- test/unit/view/drag-handle/util/wrappers.js | 4 +- .../view/drag-handle/window-bindings.spec.js | 2 +- 9 files changed, 250 insertions(+), 195 deletions(-) create mode 100644 src/view/use-drag-handle/use-validation.js diff --git a/src/view/use-drag-handle/sensor/use-keyboard-sensor.js b/src/view/use-drag-handle/sensor/use-keyboard-sensor.js index bd1c3609db..c74893fe8e 100644 --- a/src/view/use-drag-handle/sensor/use-keyboard-sensor.js +++ b/src/view/use-drag-handle/sensor/use-keyboard-sensor.js @@ -1,6 +1,6 @@ // @flow import type { Position } from 'css-box-model'; -import { useRef, useCallback, useMemo, useLayoutEffect } from 'react'; +import { useRef, useCallback, useMemo } from 'react'; import invariant from 'tiny-invariant'; import type { EventBinding } from '../util/event-types'; import { bindEvents, unbindEvents } from '../util/bind-events'; diff --git a/src/view/use-drag-handle/use-drag-handle.js b/src/view/use-drag-handle/use-drag-handle.js index ca4c810549..679141fad9 100644 --- a/src/view/use-drag-handle/use-drag-handle.js +++ b/src/view/use-drag-handle/use-drag-handle.js @@ -17,6 +17,7 @@ import useTouchSensor, { } from './sensor/use-touch-sensor'; import usePreviousRef from '../use-previous-ref'; import { warning } from '../../dev-warning'; +import useValidation from './use-validation'; function preventHtml5Dnd(event: DragEvent) { event.preventDefault(); @@ -63,6 +64,8 @@ export default function useDragHandle(args: Args): ?DragHandleProps { canDragInteractiveElements, } = args; + useValidation(getDraggableRef); + const getWindow = useCallback( (): HTMLElement => getWindowFromEl(getDraggableRef()), [getDraggableRef], @@ -82,7 +85,8 @@ export default function useDragHandle(args: Args): ?DragHandleProps { if (!isEnabled) { return false; } - // Something on this element might be capturing but a drag has not started yet + // Something on this element might be capturing. + // A drag might not have started yet // We want to prevent anything else from capturing if (capturingRef.current) { return false; diff --git a/src/view/use-drag-handle/use-validation.js b/src/view/use-drag-handle/use-validation.js new file mode 100644 index 0000000000..ed72349534 --- /dev/null +++ b/src/view/use-drag-handle/use-validation.js @@ -0,0 +1,17 @@ +// @flow +import { useEffect } from 'react'; +import invariant from 'tiny-invariant'; +import getDragHandleRef from './util/get-drag-handle-ref'; + +export default function useValidation(getDraggableRef: () => ?HTMLElement) { + // validate ref on mount + useEffect(() => { + if (process.env.NODE_ENV === 'production') { + return; + } + const draggableRef: ?HTMLElement = getDraggableRef(); + invariant(draggableRef, 'Drag handle was unable to find draggable ref'); + + getDragHandleRef(draggableRef); + }, [getDraggableRef]); +} diff --git a/test/unit/view/drag-handle/contenteditable.spec.js b/test/unit/view/drag-handle/contenteditable.spec.js index 1fd984e167..a9e3e67168 100644 --- a/test/unit/view/drag-handle/contenteditable.spec.js +++ b/test/unit/view/drag-handle/contenteditable.spec.js @@ -2,15 +2,15 @@ import React from 'react'; import { mount } from 'enzyme'; import { forEach, type Control } from './util/controls'; -import { createRef } from './util/wrappers'; +import type { DragHandleProps } from '../../../../src/view/use-drag-handle/drag-handle-types'; +import { createRef, WithDragHandle } from './util/wrappers'; import { getStubCallbacks, callbacksCalled, whereAnyCallbacksCalled, } from './util/callbacks'; -import DragHandle from '../../../../src/view/drag-handle/drag-handle'; -import basicContext from './util/basic-context'; -import type { DragHandleProps } from '../../../../src/view/drag-handle/drag-handle-types'; +import basicContext from './util/app-context'; +import AppContext from '../../../../src/view/context/app-context'; const draggableId = 'draggable'; @@ -27,21 +27,27 @@ forEach((control: Control) => { const callbacks = getStubCallbacks(); const ref = createRef(); const wrapper = mount( - true} - > - {(dragHandleProps: ?DragHandleProps) => ( -
- )} - , - { context: basicContext }, + + true} + > + {(dragHandleProps: ?DragHandleProps) => ( +
+ )} + + , ); const target = wrapper.getDOMNode(); const options = { @@ -65,23 +71,28 @@ forEach((control: Control) => { const customCallbacks = getStubCallbacks(); const ref = createRef(); const customWrapper = mount( - true} - > - {(dragHandleProps: ?DragHandleProps) => ( -
-
-
- )} - , - { context: basicContext }, + + true} + > + {(dragHandleProps: ?DragHandleProps) => ( +
+
+
+ )} + + , ); const target = customWrapper.getDOMNode().querySelector('.editable'); if (!target) { @@ -104,26 +115,31 @@ forEach((control: Control) => { const customCallbacks = getStubCallbacks(); const ref = createRef(); const customWrapper = mount( - true} - > - {(dragHandleProps: ?DragHandleProps) => ( -
-
-

hello there

- Edit me! + + true} + > + {(dragHandleProps: ?DragHandleProps) => ( +
+
+

hello there

+ Edit me! +
-
- )} - , - { context: basicContext }, + )} + + , ); const target = customWrapper.getDOMNode().querySelector('.target'); if (!target) { @@ -150,26 +166,31 @@ forEach((control: Control) => { const customCallbacks = getStubCallbacks(); const ref = createRef(); const customWrapper = mount( - true} - > - {(dragHandleProps: ?DragHandleProps) => ( -
-
-

hello there

- Edit me! + + true} + > + {(dragHandleProps: ?DragHandleProps) => ( +
+
+

hello there

+ Edit me! +
-
- )} - , - { context: basicContext }, + )} + + , ); const target = customWrapper.getDOMNode().querySelector('.target'); if (!target) { @@ -181,12 +202,10 @@ forEach((control: Control) => { control.preLift(customWrapper, options); control.lift(customWrapper, options); - control.drop(customWrapper); expect( callbacksCalled(customCallbacks)({ onLift: 1, - onDrop: 1, }), ).toBe(true); @@ -199,24 +218,29 @@ forEach((control: Control) => { const customCallbacks = getStubCallbacks(); const ref = createRef(); const customWrapper = mount( - true} - // stating that we can drag - canDragInteractiveElements - > - {(dragHandleProps: ?DragHandleProps) => ( -
-
-
- )} - , - { context: basicContext }, + + true} + // stating that we can drag + canDragInteractiveElements + > + {(dragHandleProps: ?DragHandleProps) => ( +
+
+
+ )} + + , ); const target = customWrapper.getDOMNode().querySelector('.editable'); if (!target) { @@ -228,12 +252,10 @@ forEach((control: Control) => { control.preLift(customWrapper, options); control.lift(customWrapper, options); - control.drop(customWrapper); expect( callbacksCalled(customCallbacks)({ onLift: 1, - onDrop: 1, }), ).toBe(true); @@ -244,27 +266,32 @@ forEach((control: Control) => { const customCallbacks = getStubCallbacks(); const ref = createRef(); const customWrapper = mount( - true} - // stating that we can drag - canDragInteractiveElements - > - {(dragHandleProps: ?DragHandleProps) => ( -
-
-

hello there

- Edit me! + + true} + // stating that we can drag + canDragInteractiveElements + > + {(dragHandleProps: ?DragHandleProps) => ( +
+
+

hello there

+ Edit me! +
-
- )} - , - { context: basicContext }, + )} + + , ); const target = customWrapper.getDOMNode().querySelector('.target'); if (!target) { @@ -276,12 +303,10 @@ forEach((control: Control) => { control.preLift(customWrapper, options); control.lift(customWrapper, options); - control.drop(customWrapper); expect( callbacksCalled(customCallbacks)({ onLift: 1, - onDrop: 1, }), ).toBe(true); diff --git a/test/unit/view/drag-handle/nested-drag-handles.spec.js b/test/unit/view/drag-handle/nested-drag-handles.spec.js index 36186ab098..8e8874936f 100644 --- a/test/unit/view/drag-handle/nested-drag-handles.spec.js +++ b/test/unit/view/drag-handle/nested-drag-handles.spec.js @@ -2,14 +2,14 @@ import React from 'react'; import { mount, type ReactWrapper } from 'enzyme'; import { forEach, type Control } from './util/controls'; -import DragHandle from '../../../../src/view/drag-handle/drag-handle'; import { getStubCallbacks } from './util/callbacks'; -import basicContext from './util/basic-context'; +import basicContext from './util/app-context'; import type { Callbacks, DragHandleProps, -} from '../../../../src/view/drag-handle/drag-handle-types'; -import { createRef, Child } from './util/wrappers'; +} from '../../../../src/view/use-drag-handle/drag-handle-types'; +import { createRef, Child, WithDragHandle } from './util/wrappers'; +import AppContext from '../../../../src/view/context/app-context'; const getNestedWrapper = ( parentCallbacks: Callbacks, @@ -19,46 +19,47 @@ const getNestedWrapper = ( const inner = createRef(); return mount( - true} - canDragInteractiveElements={false} - > - {(parentProps: ?DragHandleProps) => ( - - true} + + true} + canDragInteractiveElements={false} + > + {(parentProps: ?DragHandleProps) => ( + - {(childProps: ?DragHandleProps) => ( - - Child! - - )} - - - )} - , - { context: basicContext }, + true} + > + {(childProps: ?DragHandleProps) => ( + + Child! + + )} + + + )} + + , ); }; diff --git a/test/unit/view/drag-handle/throw-if-svg.spec.js b/test/unit/view/drag-handle/throw-if-svg.spec.js index 2f3ffe7af3..f39695fe54 100644 --- a/test/unit/view/drag-handle/throw-if-svg.spec.js +++ b/test/unit/view/drag-handle/throw-if-svg.spec.js @@ -1,16 +1,11 @@ // @flow import React from 'react'; import { mount } from 'enzyme'; -import DragHandle from '../../../../src/view/drag-handle/drag-handle'; -import type { DragHandleProps } from '../../../../src/view/drag-handle/drag-handle-types'; -import { styleKey, canLiftKey } from '../../../../src/view/context-keys'; +import type { DragHandleProps } from '../../../../src/view/use-drag-handle/drag-handle-types'; import { getStubCallbacks } from './util/callbacks'; -import { createRef } from './util/wrappers'; - -const basicContext = { - [styleKey]: 'hello', - [canLiftKey]: () => true, -}; +import { WithDragHandle, createRef } from './util/wrappers'; +import AppContext from '../../../../src/view/context/app-context'; +import basicContext from './util/app-context'; beforeEach(() => { jest.spyOn(console, 'error').mockImplementation(() => {}); @@ -25,22 +20,23 @@ it('should throw if a help SVG message if the drag handle is a SVG', () => { const ref = createRef(); return mount( - true} - > - {(dragHandleProps: ?DragHandleProps) => ( - // $ExpectError - this fails the flow check! Success! - - )} - , - { context: basicContext }, + + true} + > + {(dragHandleProps: ?DragHandleProps) => ( + // $ExpectError - this fails the flow check! Success! + + )} + + , ); }; diff --git a/test/unit/view/drag-handle/util/controls.js b/test/unit/view/drag-handle/util/controls.js index 6f4d18209d..0153fae781 100644 --- a/test/unit/view/drag-handle/util/controls.js +++ b/test/unit/view/drag-handle/util/controls.js @@ -28,7 +28,19 @@ export type Control = {| // using the class rather than the attribute as the attribute will not be present when disabled const getDragHandle = (wrapper: ReactWrapper<*>) => - wrapper.find('.drag-handle'); + // using div. as it can return a component with the classname prop + // using .first in case there is nested handles + wrapper.find('div.drag-handle').first(); + +const trySetIsDragging = (wrapper: ReactWrapper<*>) => { + // sometimes we are dragging a wrapper that is not the root. + // this will throw an error + // So we are only setting the prop if the component would support it + + if (wrapper.props().draggableId) { + wrapper.setProps({ isDragging: true }); + } +}; export const touch: Control = { name: 'touch', @@ -38,7 +50,7 @@ export const touch: Control = { touchStart(getDragHandle(wrapper), { x: 0, y: 0 }, 0, options), lift: (wrapper: ReactWrapper<*>) => { jest.runTimersToTime(timeForLongPress); - wrapper.setProps({ isDragging: true }); + trySetIsDragging(wrapper); }, move: () => { windowTouchMove({ x: 100, y: 200 }); @@ -59,7 +71,7 @@ export const keyboard: Control = { preLift: () => {}, lift: (wrap: ReactWrapper<*>, options?: Object = {}) => { pressSpacebar(getDragHandle(wrap), options); - wrap.setProps({ isDragging: true }); + trySetIsDragging(wrap); }, move: (wrap: ReactWrapper<*>) => { pressArrowDown(getDragHandle(wrap)); @@ -83,7 +95,7 @@ export const mouse: Control = { }, lift: (wrap: ReactWrapper<*>) => { windowMouseMove({ x: 0, y: sloppyClickThreshold }); - wrap.setProps({ isDragging: true }); + trySetIsDragging(wrap); }, move: () => { windowMouseMove({ x: 100, y: 200 }); diff --git a/test/unit/view/drag-handle/util/wrappers.js b/test/unit/view/drag-handle/util/wrappers.js index 1f656fa8bd..d98692a628 100644 --- a/test/unit/view/drag-handle/util/wrappers.js +++ b/test/unit/view/drag-handle/util/wrappers.js @@ -51,14 +51,14 @@ type WithDragHandleProps = {| children: (value: ?DragHandleProps) => Node | null, |}; -function WithDragHandle(props: WithDragHandleProps) { +export function WithDragHandle(props: WithDragHandleProps) { // strip the children prop out const { children, ...args } = props; const result: ?DragHandleProps = useDragHandle(args); return props.children(result); } -class PassThrough extends React.Component<*> { +export class PassThrough extends React.Component<*> { render() { const { children, ...rest } = this.props; return children(rest); diff --git a/test/unit/view/drag-handle/window-bindings.spec.js b/test/unit/view/drag-handle/window-bindings.spec.js index 96619da1f3..146c5e6e00 100644 --- a/test/unit/view/drag-handle/window-bindings.spec.js +++ b/test/unit/view/drag-handle/window-bindings.spec.js @@ -1,6 +1,6 @@ // @flow import type { ReactWrapper } from 'enzyme'; -import type { Callbacks } from '../../../../src/view/drag-handle/drag-handle-types'; +import type { Callbacks } from '../../../../src/view/use-drag-handle/drag-handle-types'; import { forEach, type Control } from './util/controls'; import { getWrapper } from './util/wrappers'; import { getStubCallbacks } from './util/callbacks'; From e74b544dacec9ba205ccd88d95228511223ead4d Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Tue, 26 Mar 2019 09:27:30 +1100 Subject: [PATCH 060/117] drag handle tests done except for focus management --- test/unit/view/drag-handle/interactive-elements.spec.js | 2 -- test/unit/view/drag-handle/util/controls.js | 8 +++++--- test/unit/view/drag-handle/window-bindings.spec.js | 7 +++++++ 3 files changed, 12 insertions(+), 5 deletions(-) diff --git a/test/unit/view/drag-handle/interactive-elements.spec.js b/test/unit/view/drag-handle/interactive-elements.spec.js index 0fe2b4f732..53db24e272 100644 --- a/test/unit/view/drag-handle/interactive-elements.spec.js +++ b/test/unit/view/drag-handle/interactive-elements.spec.js @@ -122,7 +122,6 @@ forEach((control: Control) => { control.preLift(wrapper, options); control.lift(wrapper, options); - control.drop(wrapper); expect( callbacksCalled(callbacks)({ @@ -149,7 +148,6 @@ forEach((control: Control) => { control.preLift(wrapper, options); control.lift(wrapper, options); - control.drop(wrapper); expect( callbacksCalled(callbacks)({ diff --git a/test/unit/view/drag-handle/util/controls.js b/test/unit/view/drag-handle/util/controls.js index 0153fae781..15b7745ef0 100644 --- a/test/unit/view/drag-handle/util/controls.js +++ b/test/unit/view/drag-handle/util/controls.js @@ -35,10 +35,11 @@ const getDragHandle = (wrapper: ReactWrapper<*>) => const trySetIsDragging = (wrapper: ReactWrapper<*>) => { // sometimes we are dragging a wrapper that is not the root. // this will throw an error - // So we are only setting the prop if the component would support it - if (wrapper.props().draggableId) { + try { wrapper.setProps({ isDragging: true }); + } catch (e) { + // ignoring error } }; @@ -108,7 +109,8 @@ export const mouse: Control = { }, }; -export const controls: Control[] = [mouse, keyboard, touch]; +// export const controls: Control[] = [mouse, keyboard, touch]; +export const controls: Control[] = [keyboard]; export const forEach = (fn: (control: Control) => void) => { controls.forEach((control: Control) => { diff --git a/test/unit/view/drag-handle/window-bindings.spec.js b/test/unit/view/drag-handle/window-bindings.spec.js index 146c5e6e00..b589620c92 100644 --- a/test/unit/view/drag-handle/window-bindings.spec.js +++ b/test/unit/view/drag-handle/window-bindings.spec.js @@ -101,6 +101,13 @@ forEach((control: Control) => { // unmounting while dragging wrapper.unmount(); + if (control.hasPostDragClickBlocking) { + // cleanup is still bound + expect(getAddCount()).toBeGreaterThan(getRemoveCount()); + // cleanup performed + control.cleanup(); + } + expect(getAddCount()).toBe(getRemoveCount()); }); From 18370eb1bfff10126bf8cc571e6ff8042a222220 Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Tue, 26 Mar 2019 10:36:10 +1100 Subject: [PATCH 061/117] tests for draggable dimension publisher --- src/view/draggable/draggable.jsx | 16 +- .../use-draggable-dimension-publisher.js | 17 +- .../draggable/drag-handle-connection.spec.js | 230 +----------------- test/unit/view/draggable/mounting.spec.js | 10 +- test/unit/view/draggable/util/mount.js | 73 ++++-- ...use-draggable-dimension-publisher.spec.js} | 142 ++++++----- 6 files changed, 150 insertions(+), 338 deletions(-) rename test/unit/view/{draggable-dimension-publisher.spec.js => use-draggable-dimension-publisher.spec.js} (73%) diff --git a/src/view/draggable/draggable.jsx b/src/view/draggable/draggable.jsx index 0aabef7483..9944d60c17 100644 --- a/src/view/draggable/draggable.jsx +++ b/src/view/draggable/draggable.jsx @@ -19,9 +19,6 @@ import getWindowScroll from '../window/get-window-scroll'; // import throwIfRefIsInvalid from '../throw-if-invalid-inner-ref'; // import checkOwnProps from './check-own-props'; import AppContext, { type AppContextValue } from '../context/app-context'; -import DroppableContext, { - type DroppableContextValue, -} from '../context/droppable-context'; import useRequiredContext from '../use-required-context'; import useValidation from './use-validation'; @@ -35,9 +32,6 @@ export default function Draggable(props: Props) { // context const appContext: AppContextValue = useRequiredContext(AppContext); - const droppableContext: DroppableContextValue = useRequiredContext( - DroppableContext, - ); // Validating props and innerRef useValidation(props, getRef); @@ -71,18 +65,10 @@ export default function Draggable(props: Props) { const forPublisher: DimensionPublisherArgs = useMemo( () => ({ draggableId, - droppableId: droppableContext.droppableId, - type: droppableContext.type, index, getDraggableRef: getRef, }), - [ - draggableId, - droppableContext.droppableId, - droppableContext.type, - getRef, - index, - ], + [draggableId, getRef, index], ); useDraggableDimensionPublisher(forPublisher); diff --git a/src/view/use-draggable-dimension-publisher/use-draggable-dimension-publisher.js b/src/view/use-draggable-dimension-publisher/use-draggable-dimension-publisher.js index 177412a47f..5d941ca4e2 100644 --- a/src/view/use-draggable-dimension-publisher/use-draggable-dimension-publisher.js +++ b/src/view/use-draggable-dimension-publisher/use-draggable-dimension-publisher.js @@ -6,27 +6,33 @@ import type { DraggableDescriptor, DraggableDimension, DraggableId, - DroppableId, - TypeId, } from '../../types'; import type { DimensionMarshal } from '../../state/dimension-marshal/dimension-marshal-types'; import useRequiredContext from '../use-required-context'; import AppContext, { type AppContextValue } from '../context/app-context'; import getDimension from './get-dimension'; +import DroppableContext, { + type DroppableContextValue, +} from '../context/droppable-context'; export type Args = {| draggableId: DraggableId, - droppableId: DroppableId, - type: TypeId, index: number, getDraggableRef: () => ?HTMLElement, |}; export default function useDraggableDimensionPublisher(args: Args) { - const { draggableId, droppableId, type, index, getDraggableRef } = args; + const { draggableId, index, getDraggableRef } = args; + // App context const appContext: AppContextValue = useRequiredContext(AppContext); const marshal: DimensionMarshal = appContext.marshal; + // Droppable context + const droppableContext: DroppableContextValue = useRequiredContext( + DroppableContext, + ); + const { droppableId, type } = droppableContext; + const descriptor: DraggableDescriptor = useMemo(() => { const result = { id: draggableId, @@ -34,7 +40,6 @@ export default function useDraggableDimensionPublisher(args: Args) { type, index, }; - // console.log('creating new descriptor', result); return result; }, [draggableId, droppableId, index, type]); diff --git a/test/unit/view/draggable/drag-handle-connection.spec.js b/test/unit/view/draggable/drag-handle-connection.spec.js index 76ecd0918d..0be499ffb6 100644 --- a/test/unit/view/draggable/drag-handle-connection.spec.js +++ b/test/unit/view/draggable/drag-handle-connection.spec.js @@ -1,25 +1,16 @@ // @flow import React from 'react'; -import { type Position } from 'css-box-model'; import type { ReactWrapper } from 'enzyme'; import type { DispatchProps, Provided, } from '../../../../src/view/draggable/draggable-types'; -import { - draggable, - getDispatchPropsStub, - atRestMapProps, - disabledOwnProps, - whileDragging, -} from './util/get-props'; +import { getDispatchPropsStub } from './util/get-props'; import type { Viewport } from '../../../../src/types'; -import { origin } from '../../../../src/state/position'; import { getPreset } from '../../../utils/dimension'; import { setViewport } from '../../../utils/viewport'; import mount from './util/mount'; import Item from './util/item'; -import DragHandle from '../../../../src/view/use-drag-handle'; import { withKeyboard } from '../../../utils/user-input-util'; import * as keyCodes from '../../../../src/view/key-codes'; @@ -102,222 +93,3 @@ describe('drag handle not the same element as draggable', () => { expect(dispatchProps.lift).not.toHaveBeenCalled(); }); }); - -describe('handling drag handle events', () => { - describe('onLift', () => { - it('should throw if lifted when dragging is not enabled', () => { - const customWrapper = mount({ - ownProps: disabledOwnProps, - mapProps: atRestMapProps, - }); - - expect(() => { - customWrapper - .find(DragHandle) - .props() - .callbacks.onLift({ - clientSelection: origin, - movementMode: 'SNAP', - }); - }).toThrow(); - }); - - it('should throw if lifted when not attached to the dom', () => { - const customWrapper = mount(); - customWrapper.unmount(); - - expect(() => { - customWrapper - .find(DragHandle) - .props() - .callbacks.onLift({ - clientSelection: origin, - movementMode: 'SNAP', - }); - }).toThrow(); - }); - - it('should lift if permitted', () => { - const dispatchProps = getDispatchPropsStub(); - const wrapper = mount({ - dispatchProps, - }); - - wrapper - .find(DragHandle) - .props() - .callbacks.onLift({ - clientSelection: origin, - movementMode: 'SNAP', - }); - - expect(dispatchProps.lift).toHaveBeenCalledWith({ - id: draggable.id, - clientSelection: origin, - movementMode: 'SNAP', - }); - }); - - describe('onMove', () => { - it('should consider any mouse movement for the client coordinates', () => { - const selection: Position = { - x: 10, - y: 50, - }; - - const dispatchProps = getDispatchPropsStub(); - const wrapper = mount({ - mapProps: whileDragging, - dispatchProps, - }); - - wrapper - .find(DragHandle) - .props() - .callbacks.onMove(selection); - - expect(dispatchProps.move).toHaveBeenCalledWith({ - client: selection, - }); - }); - }); - - describe('onDrop', () => { - it('should trigger drop', () => { - const dispatchProps = getDispatchPropsStub(); - const wrapper = mount({ - dispatchProps, - }); - - wrapper - .find(DragHandle) - .props() - .callbacks.onDrop(); - - expect(dispatchProps.drop).toHaveBeenCalled(); - }); - }); - - describe('onMoveUp', () => { - it('should call the move up action', () => { - const dispatchProps = getDispatchPropsStub(); - const wrapper = mount({ - mapProps: whileDragging, - dispatchProps, - }); - - wrapper - .find(DragHandle) - .props() - .callbacks.onMoveUp(); - - expect(dispatchProps.moveUp).toHaveBeenCalled(); - }); - }); - - describe('onMoveDown', () => { - it('should call the move down action', () => { - const dispatchProps = getDispatchPropsStub(); - const wrapper = mount({ - mapProps: whileDragging, - dispatchProps, - }); - - wrapper - .find(DragHandle) - .props() - .callbacks.onMoveDown(); - - expect(dispatchProps.moveDown).toHaveBeenCalled(); - }); - }); - - describe('onMoveLeft', () => { - it('should call the cross axis move forward action', () => { - const dispatchProps = getDispatchPropsStub(); - const wrapper = mount({ - mapProps: whileDragging, - dispatchProps, - }); - - wrapper - .find(DragHandle) - .props() - .callbacks.onMoveLeft(); - - expect(dispatchProps.moveLeft).toHaveBeenCalled(); - }); - }); - - describe('onMoveRight', () => { - it('should call the move cross axis backwards action', () => { - const dispatchProps = getDispatchPropsStub(); - const wrapper = mount({ - mapProps: whileDragging, - dispatchProps, - }); - - wrapper - .find(DragHandle) - .props() - .callbacks.onMoveRight(); - - expect(dispatchProps.moveRight).toHaveBeenCalled(); - }); - }); - - describe('onCancel', () => { - it('should call the drop dispatch prop', () => { - const dispatchProps = getDispatchPropsStub(); - const wrapper = mount({ - mapProps: whileDragging, - dispatchProps, - }); - - wrapper - .find(DragHandle) - .props() - .callbacks.onCancel(); - - expect(dispatchProps.drop).toHaveBeenCalledWith({ - reason: 'CANCEL', - }); - }); - - it('should allow the action even if dragging is disabled', () => { - const dispatchProps = getDispatchPropsStub(); - const wrapper = mount({ - ownProps: disabledOwnProps, - mapProps: whileDragging, - dispatchProps, - }); - - wrapper - .find(DragHandle) - .props() - .callbacks.onCancel(); - - expect(dispatchProps.drop).toHaveBeenCalled(); - }); - }); - - describe('onWindowScroll', () => { - it('should call the moveByWindowScroll action', () => { - const dispatchProps = getDispatchPropsStub(); - const wrapper = mount({ - mapProps: whileDragging, - dispatchProps, - }); - - wrapper - .find(DragHandle) - .props() - .callbacks.onWindowScroll(); - - expect(dispatchProps.moveByWindowScroll).toHaveBeenCalledWith({ - newScroll: viewport.scroll.current, - }); - }); - }); - }); -}); diff --git a/test/unit/view/draggable/mounting.spec.js b/test/unit/view/draggable/mounting.spec.js index 8d10140e9a..31c71f585d 100644 --- a/test/unit/view/draggable/mounting.spec.js +++ b/test/unit/view/draggable/mounting.spec.js @@ -1,8 +1,6 @@ // @flow import type { ReactWrapper } from 'enzyme'; -import type { StyleMarshal } from '../../../../src/view/style-marshal/style-marshal-types'; import type { Provided } from '../../../../src/view/draggable/draggable-types'; -import createStyleMarshal from '../../../../src/view/style-marshal/style-marshal'; import mount from './util/mount'; import getStubber from './util/get-stubber'; import getLastCall from './util/get-last-call'; @@ -20,16 +18,14 @@ it('should not create any wrapping elements', () => { it('should attach a data attribute for global styling', () => { const myMock = jest.fn(); const Stubber = getStubber(myMock); - const styleMarshal: StyleMarshal = createStyleMarshal(); + const styleContext: string = 'this is a cool style context'; mount({ mapProps: atRestMapProps, WrappedComponent: Stubber, - styleMarshal, + styleContext, }); const provided: Provided = getLastCall(myMock)[0].provided; - expect(provided.draggableProps[attributes.draggable]).toEqual( - styleMarshal.styleContext, - ); + expect(provided.draggableProps[attributes.draggable]).toEqual(styleContext); }); diff --git a/test/unit/view/draggable/util/mount.js b/test/unit/view/draggable/util/mount.js index d7c71b287c..6abe278b0f 100644 --- a/test/unit/view/draggable/util/mount.js +++ b/test/unit/view/draggable/util/mount.js @@ -1,23 +1,15 @@ // @flow -import React from 'react'; +import React, { type Node } from 'react'; import { mount, type ReactWrapper } from 'enzyme'; import type { + Props, OwnProps, MapProps, DispatchProps, Provided, StateSnapshot, } from '../../../../../src/view/draggable/draggable-types'; -import type { StyleMarshal } from '../../../../../src/view/style-marshal/style-marshal-types'; -import { - combine, - withStore, - withDroppableId, - withStyleContext, - withDimensionMarshal, - withCanLift, - withDroppableType, -} from '../../../../utils/get-context-options'; +import type { StyleMarshal } from '../../../../../src/view/use-style-marshal/style-marshal-types'; import { atRestMapProps, getDispatchPropsStub, @@ -26,36 +18,67 @@ import { } from './get-props'; import Item from './item'; import Draggable from '../../../../../src/view/draggable/draggable'; +import AppContext, { + type AppContextValue, +} from '../../../../../src/view/context/app-context'; +import DroppableContext, { + type DroppableContextValue, +} from '../../../../../src/view/context/droppable-context'; +import { getMarshalStub } from '../../../../utils/dimension-marshal'; type MountConnected = {| ownProps?: OwnProps, mapProps?: MapProps, dispatchProps?: DispatchProps, WrappedComponent?: any, - styleMarshal?: StyleMarshal, + styleContext?: string, +|}; + +type PassThroughProps = {| + ...Props, + children: (props: Props) => Node, |}; +export class PassThrough extends React.Component { + render() { + const { children, ...rest } = this.props; + // $FlowFixMe - incorrectly typed child function + return this.props.children(rest); + } +} export default ({ ownProps = defaultOwnProps, mapProps = atRestMapProps, dispatchProps = getDispatchPropsStub(), WrappedComponent = Item, - styleMarshal, + styleContext = 'fake-style-context', }: MountConnected = {}): ReactWrapper<*> => { + const droppableContext: DroppableContextValue = { + droppableId: droppable.id, + type: droppable.type, + }; + + const appContext: AppContextValue = { + marshal: getMarshalStub(), + style: styleContext, + canLift: () => true, + isMovementAllowed: () => true, + }; + // Using PassThrough so that you can do .setProps on the root const wrapper: ReactWrapper<*> = mount( - - {(provided: Provided, snapshot: StateSnapshot) => ( - + + {(props: Props) => ( + + + + {(provided: Provided, snapshot: StateSnapshot) => ( + + )} + + + )} - , - combine( - withStore(), - withDroppableId(droppable.id), - withDroppableType(droppable.type), - withStyleContext(styleMarshal), - withDimensionMarshal(), - withCanLift(), - ), + , ); return wrapper; diff --git a/test/unit/view/draggable-dimension-publisher.spec.js b/test/unit/view/use-draggable-dimension-publisher.spec.js similarity index 73% rename from test/unit/view/draggable-dimension-publisher.spec.js rename to test/unit/view/use-draggable-dimension-publisher.spec.js index 2730fcef96..5f0a57d579 100644 --- a/test/unit/view/draggable-dimension-publisher.spec.js +++ b/test/unit/view/use-draggable-dimension-publisher.spec.js @@ -1,9 +1,9 @@ // @flow -import React, { Component } from 'react'; +import React, { useRef, useCallback, type Node } from 'react'; import invariant from 'tiny-invariant'; import { type Spacing, type Rect } from 'css-box-model'; import { mount, type ReactWrapper } from 'enzyme'; -import DraggableDimensionPublisher from '../../../src/view/draggable-dimension-publisher/draggable-dimension-publisher'; +import useDraggableDimensionPublisher from '../../../src/view/use-draggable-dimension-publisher'; import { getPreset, getDraggableDimension, @@ -13,7 +13,6 @@ import type { DimensionMarshal, GetDraggableDimensionFn, } from '../../../src/state/dimension-marshal/dimension-marshal-types'; -import { withDimensionMarshal } from '../../utils/get-context-options'; import forceUpdate from '../../utils/force-update'; import tryCleanPrototypeStubs from '../../utils/try-clean-prototype-stubs'; import { getMarshalStub } from '../../utils/dimension-marshal'; @@ -22,39 +21,73 @@ import type { DraggableDimension, DraggableDescriptor, } from '../../../src/types'; +import AppContext, { + type AppContextValue, +} from '../../../src/view/context/app-context'; +import DroppableContext, { + type DroppableContextValue, +} from '../../../src/view/context/droppable-context'; const preset = getPreset(); const noComputedSpacing = getComputedSpacing({}); -type Props = {| +type ItemProps = {| + index: number, + draggableId: DraggableId, +|}; + +type AppProps = {| + marshal: DimensionMarshal, index?: number, draggableId?: DraggableId, + Component?: any, |}; -class Item extends Component { - /* eslint-disable react/sort-comp */ +function Item(props: ItemProps) { + const ref = useRef(null); + const setRef = useCallback((value: ?HTMLElement) => { + ref.current = value; + }, []); + const getRef = useCallback((): ?HTMLElement => ref.current, []); + + useDraggableDimensionPublisher({ + draggableId: props.draggableId, + index: props.index, + getDraggableRef: getRef, + }); - ref: ?HTMLElement; + return
hi
; +} - setRef = (ref: ?HTMLElement) => { - this.ref = ref; +function App({ + marshal, + draggableId = preset.inHome1.descriptor.id, + index = preset.inHome1.descriptor.index, + Component = Item, +}: AppProps) { + const appContext: AppContextValue = { + marshal, + style: '1', + canLift: () => true, + isMovementAllowed: () => true, + }; + const droppableContext: DroppableContextValue = { + type: preset.inHome1.descriptor.type, + droppableId: preset.inHome1.descriptor.droppableId, }; - getRef = (): ?HTMLElement => this.ref; - - render() { - return ( - -
hi
-
- ); - } + const itemProps: ItemProps = { + draggableId, + index, + }; + + return ( + + + + + + ); } beforeEach(() => { @@ -72,7 +105,7 @@ describe('dimension registration', () => { it('should register itself when mounting', () => { const marshal: DimensionMarshal = getMarshalStub(); - mount(, withDimensionMarshal(marshal)); + mount(); expect(marshal.registerDraggable).toHaveBeenCalledTimes(1); expect(marshal.registerDraggable.mock.calls[0][0]).toEqual( @@ -83,7 +116,7 @@ describe('dimension registration', () => { it('should unregister itself when unmounting', () => { const marshal: DimensionMarshal = getMarshalStub(); - const wrapper = mount(, withDimensionMarshal(marshal)); + const wrapper = mount(); expect(marshal.registerDraggable).toHaveBeenCalled(); expect(marshal.unregisterDraggable).not.toHaveBeenCalled(); @@ -97,7 +130,7 @@ describe('dimension registration', () => { it('should update its registration when a descriptor property changes', () => { const marshal: DimensionMarshal = getMarshalStub(); - const wrapper = mount(, withDimensionMarshal(marshal)); + const wrapper = mount(); // asserting shape of original publish expect(marshal.registerDraggable.mock.calls[0][0]).toEqual( preset.inHome1.descriptor, @@ -111,8 +144,13 @@ describe('dimension registration', () => { ...preset.inHome1.descriptor, index: 1000, }; - expect(marshal.updateDraggable).toHaveBeenCalledWith( + + // Old descriptor unregistered + expect(marshal.unregisterDraggable).toHaveBeenCalledWith( preset.inHome1.descriptor, + ); + // New descriptor registered + expect(marshal.registerDraggable).toHaveBeenCalledWith( newDescriptor, expect.any(Function), ); @@ -121,7 +159,7 @@ describe('dimension registration', () => { it('should not update its registration when a descriptor property does not change on an update', () => { const marshal: DimensionMarshal = getMarshalStub(); - const wrapper = mount(, withDimensionMarshal(marshal)); + const wrapper = mount(); expect(marshal.registerDraggable).toHaveBeenCalledTimes(1); forceUpdate(wrapper); @@ -133,7 +171,7 @@ describe('dimension publishing', () => { // we are doing this rather than spying on the prototype. // Sometimes setRef was being provided with an element that did not have the mocked prototype :| const setBoundingClientRect = (wrapper: ReactWrapper<*>, borderBox: Rect) => { - const ref: ?HTMLElement = wrapper.instance().getRef(); + const ref: ?HTMLElement = wrapper.getDOMNode(); invariant(ref); // $FlowFixMe - normally a read only thing. Muhaha @@ -162,11 +200,11 @@ describe('dimension publishing', () => { const marshal: DimensionMarshal = getMarshalStub(); const wrapper: ReactWrapper<*> = mount( - , - withDimensionMarshal(marshal), ); setBoundingClientRect(wrapper, expected.client.borderBox); @@ -208,11 +246,11 @@ describe('dimension publishing', () => { const marshal: DimensionMarshal = getMarshalStub(); const wrapper: ReactWrapper<*> = mount( - , - withDimensionMarshal(marshal), ); setBoundingClientRect(wrapper, expected.client.borderBox); @@ -248,11 +286,11 @@ describe('dimension publishing', () => { .mockImplementation(() => noComputedSpacing); const wrapper: ReactWrapper<*> = mount( - , - withDimensionMarshal(marshal), ); setBoundingClientRect(wrapper, expected.client.borderBox); @@ -267,35 +305,27 @@ describe('dimension publishing', () => { }); it('should throw an error if no ref is provided when attempting to get a dimension', () => { - class NoRefItem extends Component<*> { - render() { - return ( - undefined} - > -
hi
-
- ); - } + function NoRefItem(props: ItemProps) { + const ref = useRef(null); + const getRef = useCallback((): ?HTMLElement => ref.current, []); + + useDraggableDimensionPublisher({ + draggableId: props.draggableId, + index: props.index, + getDraggableRef: getRef, + }); + + return
hi
; } const marshal: DimensionMarshal = getMarshalStub(); - const wrapper: ReactWrapper<*> = mount( - , - withDimensionMarshal(marshal), + , ); - // pull the get dimension function out const getDimension: GetDraggableDimensionFn = marshal.registerDraggable.mock.calls[0][1]; - // when we call the get dimension function without a ref things will explode expect(getDimension).toThrow(); - wrapper.unmount(); }); }); From 5ea6c1eb2013eda53c88aa710d1b585fcd016e6b Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Tue, 26 Mar 2019 10:57:10 +1100 Subject: [PATCH 062/117] tests for droppable --- .../home-list-placeholder-cleanup.spec.js | 24 +++---- test/unit/view/droppable/placeholder.spec.js | 15 ++--- .../update-max-window-scroll.spec.js | 7 +- test/unit/view/droppable/util/mount.js | 64 +++++++++++++------ 4 files changed, 66 insertions(+), 44 deletions(-) diff --git a/test/unit/view/droppable/home-list-placeholder-cleanup.spec.js b/test/unit/view/droppable/home-list-placeholder-cleanup.spec.js index e8bc3d2017..513443b59b 100644 --- a/test/unit/view/droppable/home-list-placeholder-cleanup.spec.js +++ b/test/unit/view/droppable/home-list-placeholder-cleanup.spec.js @@ -1,4 +1,5 @@ // @flow +import { act } from 'react-dom/test-utils'; import type { ReactWrapper } from 'enzyme'; import mount from './util/mount'; import { @@ -7,8 +8,6 @@ import { homeAtRest, homePostDropAnimation, } from './util/get-props'; -import Placeholder from '../../../../src/view/placeholder'; -import AnimateInOut from '../../../../src/view/animate-in-out/animate-in-out'; it('should not display a placeholder after a flushed drag end in the home list', () => { // dropping @@ -17,14 +16,14 @@ it('should not display a placeholder after a flushed drag end in the home list', mapProps: isNotOverHome, }); - expect(wrapper.find(Placeholder)).toHaveLength(1); + expect(wrapper.find('Placeholder')).toHaveLength(1); wrapper.setProps({ ...homeAtRest, }); wrapper.update(); - expect(wrapper.find(Placeholder)).toHaveLength(0); + expect(wrapper.find('Placeholder')).toHaveLength(0); }); it('should animate a placeholder closed in a home list after a drag', () => { @@ -34,26 +33,27 @@ it('should animate a placeholder closed in a home list after a drag', () => { mapProps: isNotOverHome, }); - expect(wrapper.find(Placeholder)).toHaveLength(1); + expect(wrapper.find('Placeholder')).toHaveLength(1); wrapper.setProps({ ...homePostDropAnimation, }); wrapper.update(); - expect(wrapper.find(Placeholder)).toHaveLength(1); - expect(wrapper.find(AnimateInOut).props().shouldAnimate).toBe(true); + expect(wrapper.find('Placeholder')).toHaveLength(1); expect(homePostDropAnimation.shouldAnimatePlaceholder).toBe(true); // finishing the animation - wrapper - .find(Placeholder) - .props() - .onClose(); + act(() => { + wrapper + .find('Placeholder') + .props() + .onClose(); + }); // let the wrapper know the react tree has changed wrapper.update(); // placeholder is now gone - expect(wrapper.find(Placeholder)).toHaveLength(0); + expect(wrapper.find('Placeholder')).toHaveLength(0); }); diff --git a/test/unit/view/droppable/placeholder.spec.js b/test/unit/view/droppable/placeholder.spec.js index 5fb6a2ba14..b077ddfbe1 100644 --- a/test/unit/view/droppable/placeholder.spec.js +++ b/test/unit/view/droppable/placeholder.spec.js @@ -10,7 +10,6 @@ import { homeAtRest, isNotOverForeign, } from './util/get-props'; -import Placeholder from '../../../../src/view/placeholder'; describe('home list', () => { it('should not render a placeholder when not dragging', () => { @@ -19,7 +18,7 @@ describe('home list', () => { mapProps: homeAtRest, }); - expect(wrapper.find(Placeholder)).toHaveLength(0); + expect(wrapper.find('Placeholder')).toHaveLength(0); }); it('should render a placeholder when dragging over', () => { @@ -28,7 +27,7 @@ describe('home list', () => { mapProps: isOverHome, }); - expect(wrapper.find(Placeholder)).toHaveLength(1); + expect(wrapper.find('Placeholder')).toHaveLength(1); }); it('should render a placeholder when dragging over nothing', () => { @@ -37,7 +36,7 @@ describe('home list', () => { mapProps: isNotOverHome, }); - expect(wrapper.find(Placeholder)).toHaveLength(1); + expect(wrapper.find('Placeholder')).toHaveLength(1); }); it('should render a placeholder when dragging over a foreign list', () => { @@ -46,7 +45,7 @@ describe('home list', () => { mapProps: isOverForeign, }); - expect(wrapper.find(Placeholder)).toHaveLength(1); + expect(wrapper.find('Placeholder')).toHaveLength(1); }); }); @@ -57,7 +56,7 @@ describe('foreign', () => { mapProps: homeAtRest, }); - expect(wrapper.find(Placeholder)).toHaveLength(0); + expect(wrapper.find('Placeholder')).toHaveLength(0); }); it('should render a placeholder when dragging over', () => { @@ -66,7 +65,7 @@ describe('foreign', () => { mapProps: isOverForeign, }); - expect(wrapper.find(Placeholder)).toHaveLength(1); + expect(wrapper.find('Placeholder')).toHaveLength(1); }); it('should not render a placeholder when over nothing', () => { @@ -75,6 +74,6 @@ describe('foreign', () => { mapProps: isNotOverForeign, }); - expect(wrapper.find(Placeholder)).toHaveLength(0); + expect(wrapper.find('Placeholder')).toHaveLength(0); }); }); diff --git a/test/unit/view/droppable/update-max-window-scroll.spec.js b/test/unit/view/droppable/update-max-window-scroll.spec.js index c3a9eefff6..14a7ac08b9 100644 --- a/test/unit/view/droppable/update-max-window-scroll.spec.js +++ b/test/unit/view/droppable/update-max-window-scroll.spec.js @@ -3,7 +3,6 @@ import type { ReactWrapper } from 'enzyme'; import mount from './util/mount'; import { homeOwnProps, isOverHome, isNotOverHome } from './util/get-props'; import type { DispatchProps } from '../../../../src/view/droppable/droppable-types'; -import Placeholder from '../../../../src/view/placeholder'; import getMaxWindowScroll from '../../../../src/view/window/get-max-window-scroll'; it('should update when a placeholder animation finishes', () => { @@ -18,7 +17,7 @@ it('should update when a placeholder animation finishes', () => { }); wrapper - .find(Placeholder) + .find('Placeholder') .props() .onTransitionEnd(); @@ -39,7 +38,7 @@ it('should update when a placeholder finishes and the list is not dragged over', }); wrapper - .find(Placeholder) + .find('Placeholder') .props() .onTransitionEnd(); @@ -61,7 +60,7 @@ it('should not update when dropping', () => { }); wrapper - .find(Placeholder) + .find('Placeholder') .props() .onTransitionEnd(); diff --git a/test/unit/view/droppable/util/mount.js b/test/unit/view/droppable/util/mount.js index a7636ca912..cce8364170 100644 --- a/test/unit/view/droppable/util/mount.js +++ b/test/unit/view/droppable/util/mount.js @@ -1,5 +1,5 @@ // @flow -import React from 'react'; +import React, { useMemo } from 'react'; import { mount, type ReactWrapper } from 'enzyme'; import type { MapProps, @@ -14,14 +14,11 @@ import { homeAtRest, dispatchProps as defaultDispatchProps, } from './get-props'; -import { - withStore, - combine, - withDimensionMarshal, - withStyleContext, - withIsMovementAllowed, -} from '../../../../utils/get-context-options'; import getStubber from './get-stubber'; +import { getMarshalStub } from '../../../../utils/dimension-marshal'; +import AppContext, { + type AppContextValue, +} from '../../../../../src/view/context/app-context'; type MountArgs = {| WrappedComponent?: any, @@ -31,23 +28,50 @@ type MountArgs = {| isMovementAllowed?: () => boolean, |}; +type AppProps = {| + ...OwnProps, + ...MapProps, + ...DispatchProps, + isMovementAllowed: () => boolean, + WrappedComponent: any, +|}; + +function App(props: AppProps) { + const { WrappedComponent, isMovementAllowed, ...rest } = props; + const context: AppContextValue = useMemo( + () => ({ + marshal: getMarshalStub(), + style: '1', + canLift: () => true, + isMovementAllowed, + }), + [isMovementAllowed], + ); + + return ( + + + {(provided: Provided, snapshot: StateSnapshot) => ( + + )} + + + ); +} + export default ({ WrappedComponent = getStubber(), ownProps = homeOwnProps, mapProps = homeAtRest, dispatchProps = defaultDispatchProps, - isMovementAllowed, + isMovementAllowed = () => true, }: MountArgs = {}): ReactWrapper<*> => mount( - - {(provided: Provided, snapshot: StateSnapshot) => ( - - )} - , - combine( - withStore(), - withDimensionMarshal(), - withStyleContext(), - withIsMovementAllowed(isMovementAllowed), - ), + , ); From dfa0f84b61591ab599e88297aa8d6b0fade72da4 Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Tue, 26 Mar 2019 11:16:29 +1100 Subject: [PATCH 063/117] announcer tests --- src/view/use-announcer/use-announcer.js | 8 +- test/unit/view/announcer.spec.js | 143 +++++++++++------------- 2 files changed, 69 insertions(+), 82 deletions(-) diff --git a/src/view/use-announcer/use-announcer.js b/src/view/use-announcer/use-announcer.js index e10ec326c2..5e749501d2 100644 --- a/src/view/use-announcer/use-announcer.js +++ b/src/view/use-announcer/use-announcer.js @@ -20,11 +20,11 @@ const visuallyHidden: Object = { 'clip-path': 'inset(100%)', }; +export const getId = (uniqueId: number): string => + `react-beautiful-dnd-announcement-${uniqueId}`; + export default function useAnnouncer(uniqueId: number): Announce { - const id: string = useMemo( - () => `react-beautiful-dnd-announcement-${uniqueId}`, - [uniqueId], - ); + const id: string = useMemo(() => getId(uniqueId), [uniqueId]); const ref = useRef(null); useEffect(() => { diff --git a/test/unit/view/announcer.spec.js b/test/unit/view/announcer.spec.js index 82c810a7bc..6720c3e1c8 100644 --- a/test/unit/view/announcer.spec.js +++ b/test/unit/view/announcer.spec.js @@ -1,94 +1,81 @@ // @flow -import createAnnouncer from '../../../src/view/announcer/announcer'; -import type { Announcer } from '../../../src/view/announcer/announcer-types'; - -describe('mounting', () => { - it('should not create a dom node before mount is called', () => { - const announcer: Announcer = createAnnouncer(); - - const el: ?HTMLElement = document.getElementById(announcer.id); - - expect(el).not.toBeTruthy(); - }); - - it('should create a new element when mounting', () => { - const announcer: Announcer = createAnnouncer(); - - announcer.mount(); - const el: ?HTMLElement = document.getElementById(announcer.id); - - expect(el).toBeInstanceOf(HTMLElement); - }); - - it('should throw if attempting to double mount', () => { - const announcer: Announcer = createAnnouncer(); - - announcer.mount(); - - expect(() => announcer.mount()).toThrow(); - }); - - it('should apply the appropriate aria attributes and non visibility styles', () => { - const announcer: Announcer = createAnnouncer(); - - announcer.mount(); - const el: HTMLElement = (document.getElementById(announcer.id): any); - - expect(el.getAttribute('aria-live')).toBe('assertive'); - expect(el.getAttribute('role')).toBe('log'); - expect(el.getAttribute('aria-atomic')).toBe('true'); - - // not checking all the styles - just enough to know we are doing something - expect(el.style.overflow).toBe('hidden'); - }); +import React, { type Node } from 'react'; +import invariant from 'tiny-invariant'; +import { mount } from 'enzyme'; +import type { Announce } from '../../../src/types'; +import useAnnouncer from '../../../src/view/use-announcer'; +import { getId } from '../../../src/view/use-announcer/use-announcer'; + +type Props = {| + uniqueId: number, + children: (announce: Announce) => Node, +|}; + +function WithAnnouncer(props: Props) { + const announce: Announce = useAnnouncer(props.uniqueId); + return props.children(announce); +} + +const getAnnounce = (myMock): Announce => myMock.mock.calls[0][0]; +const getMock = () => jest.fn().mockImplementation(() => null); +const getElement = (uniqueId: number): ?HTMLElement => + document.getElementById(getId(uniqueId)); + +it('should create a new element when mounting', () => { + const wrapper = mount( + {getMock()}, + ); + + const el: ?HTMLElement = getElement(5); + + expect(el).toBeTruthy(); + + wrapper.unmount(); }); -describe('unmounting', () => { - it('should remove the element when unmounting', () => { - const announcer: Announcer = createAnnouncer(); +it('should apply the appropriate aria attributes and non visibility styles', () => { + const wrapper = mount( + {getMock()}, + ); - announcer.mount(); - announcer.unmount(); - const el: ?HTMLElement = document.getElementById(announcer.id); + const el: ?HTMLElement = getElement(5); + invariant(el, 'Could not find announcer'); - expect(el).not.toBeTruthy(); - }); + expect(el.getAttribute('aria-live')).toBe('assertive'); + expect(el.getAttribute('role')).toBe('log'); + expect(el.getAttribute('aria-atomic')).toBe('true'); - it('should throw if attempting to unmount before mounting', () => { - const announcer: Announcer = createAnnouncer(); + // not checking all the styles - just enough to know we are doing something + expect(el.style.overflow).toBe('hidden'); - expect(() => announcer.unmount()).toThrow(); - }); - - it('should throw if unmounting after an unmount', () => { - const announcer: Announcer = createAnnouncer(); - - announcer.mount(); - announcer.unmount(); - - expect(() => announcer.unmount()).toThrow(); - }); + wrapper.unmount(); }); -describe('announcing', () => { - it('should warn if not mounted', () => { - jest.spyOn(console, 'warn').mockImplementation(() => {}); - const announcer: Announcer = createAnnouncer(); +it('should remove the element when unmounting', () => { + const wrapper = mount( + {getMock()}, + ); - announcer.announce('test'); + wrapper.unmount(); - expect(console.warn).toHaveBeenCalled(); + const el: ?HTMLElement = getElement(5); + expect(el).not.toBeTruthy(); +}); - console.warn.mockRestore(); - }); +it('should set the text content of the announcement element', () => { + // arrange + const mock = getMock(); + const wrapper = mount({mock}); + const el: ?HTMLElement = getElement(6); + invariant(el, 'Could not find announcer'); - it('should set the text content of the announcement element', () => { - const announcer: Announcer = createAnnouncer(); - announcer.mount(); - const el: HTMLElement = (document.getElementById(announcer.id): any); + // act + const announce: Announce = getAnnounce(mock); + announce('test'); - announcer.announce('test'); + // assert + expect(el.textContent).toBe('test'); - expect(el.textContent).toBe('test'); - }); + // cleanup + wrapper.unmount(); }); From 2da4129040bda26f44d007e4e32e3924a85bd771 Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Tue, 26 Mar 2019 12:39:09 +1100 Subject: [PATCH 064/117] assorted tests --- src/view/droppable/droppable.jsx | 1 + src/view/placeholder/placeholder.jsx | 8 +- src/view/use-style-marshal/index.js | 1 + .../view/placeholder/animated-mount.spec.js | 80 ++----- test/unit/view/placeholder/on-close.spec.js | 6 +- .../placeholder/on-transition-end.spec.js | 6 +- .../util/placeholder-with-class.js | 12 ++ .../view/style-marshal/get-styles.spec.js | 2 +- .../view/style-marshal/style-marshal.spec.js | 204 +++++++++--------- test/utils/get-context-options.js | 130 ----------- 10 files changed, 143 insertions(+), 307 deletions(-) create mode 100644 src/view/use-style-marshal/index.js create mode 100644 test/unit/view/placeholder/util/placeholder-with-class.js delete mode 100644 test/utils/get-context-options.js diff --git a/src/view/droppable/droppable.jsx b/src/view/droppable/droppable.jsx index 2bc59e6b42..ec8cd9cc04 100644 --- a/src/view/droppable/droppable.jsx +++ b/src/view/droppable/droppable.jsx @@ -88,6 +88,7 @@ export default function Droppable(props: Props) { onClose={instruction.onClose} innerRef={setPlaceholderRef} animate={instruction.animate} + styleContext={styleContext} onTransitionEnd={onPlaceholderTransitionEnd} /> ) : null; diff --git a/src/view/placeholder/placeholder.jsx b/src/view/placeholder/placeholder.jsx index 8f3f038ea4..a739f04be1 100644 --- a/src/view/placeholder/placeholder.jsx +++ b/src/view/placeholder/placeholder.jsx @@ -22,12 +22,13 @@ export type PlaceholderStyle = {| pointerEvents: 'none', transition: string, |}; -type Props = {| +export type Props = {| placeholder: PlaceholderType, animate: InOutAnimationMode, onClose: () => void, innerRef?: () => ?HTMLElement, onTransitionEnd: () => void, + styleContext: string, |}; type Size = {| @@ -110,7 +111,7 @@ const getStyle = ({ }; function Placeholder(props: Props): Node { - const { animate, onTransitionEnd, onClose } = props; + const { animate, onTransitionEnd, onClose, styleContext } = props; const [isAnimatingOpenOnMount, setIsAnimatingOpenOnMount] = useState( props.animate === 'open', ); @@ -149,9 +150,12 @@ function Placeholder(props: Props): Node { return React.createElement(props.placeholder.tagName, { style, + 'data-react-beautiful-dnd-placeholder': styleContext, onTransitionEnd: onSizeChangeEnd, ref: props.innerRef, }); } export default React.memo(Placeholder); +// enzyme does not work well with memo, so exporting the non-memo version +export const WithoutMemo = Placeholder; diff --git a/src/view/use-style-marshal/index.js b/src/view/use-style-marshal/index.js new file mode 100644 index 0000000000..361eefd61b --- /dev/null +++ b/src/view/use-style-marshal/index.js @@ -0,0 +1 @@ +export { default } from './use-style-marshal'; diff --git a/test/unit/view/placeholder/animated-mount.spec.js b/test/unit/view/placeholder/animated-mount.spec.js index 0656ec9c2d..bb991afe26 100644 --- a/test/unit/view/placeholder/animated-mount.spec.js +++ b/test/unit/view/placeholder/animated-mount.spec.js @@ -1,36 +1,24 @@ // @flow import React from 'react'; import { mount, type ReactWrapper } from 'enzyme'; +import Placeholder from './util/placeholder-with-class'; import type { PlaceholderStyle } from '../../../../src/view/placeholder/placeholder-types'; -import Placeholder from '../../../../src/view/placeholder'; import { expectIsEmpty, expectIsFull } from './util/expect'; import { placeholder } from './util/data'; import getPlaceholderStyle from './util/get-placeholder-style'; +import * as attributes from '../../../../src/view/data-attributes'; jest.useFakeTimers(); +const styleContext: string = 'hello-there'; -it('should animate a mount', () => { - const wrapper: ReactWrapper<*> = mount( - , - ); - const onMount: PlaceholderStyle = getPlaceholderStyle(wrapper); - expectIsEmpty(onMount); - - jest.runOnlyPendingTimers(); - // let enzyme know that the react tree has changed due to the set state - wrapper.update(); - - const postMount: PlaceholderStyle = getPlaceholderStyle(wrapper); - expectIsFull(postMount); -}); +const getCreatePlaceholderCalls = spy => { + return spy.mock.calls.filter(call => { + return call[1] && call[1][attributes.placeholder] === styleContext; + }); +}; -it('should not animate a mount if interrupted', () => { - jest.spyOn(Placeholder.prototype, 'render'); +it('should animate a mount', () => { + const spy = jest.spyOn(React, 'createElement'); const wrapper: ReactWrapper<*> = mount( { placeholder={placeholder} onClose={jest.fn()} onTransitionEnd={jest.fn()} + styleContext={styleContext} />, ); - const onMount: PlaceholderStyle = getPlaceholderStyle(wrapper); - expectIsEmpty(onMount); - expect(Placeholder.prototype.render).toHaveBeenCalledTimes(1); - // interrupting animation - wrapper.setProps({ - animate: 'none', - }); - expect(Placeholder.prototype.render).toHaveBeenCalledTimes(2); + const calls = getCreatePlaceholderCalls(spy); + expect(calls.length).toBe(2); - // no timers are run - // let enzyme know that the react tree has changed due to the set state - wrapper.update(); + // first call had an empty size + const onMount: PlaceholderStyle = calls[0][1].style; + expectIsEmpty(onMount); const postMount: PlaceholderStyle = getPlaceholderStyle(wrapper); expectIsFull(postMount); - - // validation - no further updates - Placeholder.prototype.render.mockClear(); - jest.runOnlyPendingTimers(); - wrapper.update(); - expectIsFull(getPlaceholderStyle(wrapper)); - expect(Placeholder.prototype.render).not.toHaveBeenCalled(); - - Placeholder.prototype.render.mockRestore(); -}); - -it('should not animate in if unmounted', () => { - jest.spyOn(console, 'error'); - - const wrapper: ReactWrapper<*> = mount( - , - ); - expectIsEmpty(getPlaceholderStyle(wrapper)); - - wrapper.unmount(); - jest.runOnlyPendingTimers(); - - // an internal setState would be triggered the timer was - // not cleared when unmounting - expect(console.error).not.toHaveBeenCalled(); - console.error.mockRestore(); }); diff --git a/test/unit/view/placeholder/on-close.spec.js b/test/unit/view/placeholder/on-close.spec.js index 134faf87a5..d7ba06e7e6 100644 --- a/test/unit/view/placeholder/on-close.spec.js +++ b/test/unit/view/placeholder/on-close.spec.js @@ -1,12 +1,12 @@ // @flow import React from 'react'; import { mount, type ReactWrapper } from 'enzyme'; -import Placeholder from '../../../../src/view/placeholder'; +import Placeholder from './util/placeholder-with-class'; import { expectIsFull } from './util/expect'; import getPlaceholderStyle from './util/get-placeholder-style'; import { placeholder } from './util/data'; -jest.useFakeTimers(); +const styleContext: string = 'yolo'; it('should only fire a single onClose event', () => { const onClose = jest.fn(); @@ -17,6 +17,7 @@ it('should only fire a single onClose event', () => { placeholder={placeholder} onClose={onClose} onTransitionEnd={jest.fn()} + styleContext={styleContext} />, ); expectIsFull(getPlaceholderStyle(wrapper)); @@ -57,6 +58,7 @@ it('should not fire an onClose if not closing when a transitionend occurs', () = placeholder={placeholder} onClose={onClose} onTransitionEnd={jest.fn()} + styleContext={styleContext} />, ); const assert = () => { diff --git a/test/unit/view/placeholder/on-transition-end.spec.js b/test/unit/view/placeholder/on-transition-end.spec.js index 3a56da8add..5e0c567a16 100644 --- a/test/unit/view/placeholder/on-transition-end.spec.js +++ b/test/unit/view/placeholder/on-transition-end.spec.js @@ -1,13 +1,11 @@ // @flow import React from 'react'; import { mount, type ReactWrapper } from 'enzyme'; -import Placeholder from '../../../../src/view/placeholder'; +import Placeholder from './util/placeholder-with-class'; import { expectIsFull } from './util/expect'; import getPlaceholderStyle from './util/get-placeholder-style'; import { placeholder } from './util/data'; -jest.useFakeTimers(); - it('should only fire a single transitionend event a single time when transitioning multiple properties', () => { const onTransitionEnd = jest.fn(); const onClose = jest.fn(); @@ -18,9 +16,9 @@ it('should only fire a single transitionend event a single time when transitioni placeholder={placeholder} onClose={onClose} onTransitionEnd={onTransitionEnd} + styleContext="hey" />, ); - jest.runOnlyPendingTimers(); // let enzyme know that the react tree has changed due to the set state wrapper.update(); expectIsFull(getPlaceholderStyle(wrapper)); diff --git a/test/unit/view/placeholder/util/placeholder-with-class.js b/test/unit/view/placeholder/util/placeholder-with-class.js new file mode 100644 index 0000000000..2c75c22d49 --- /dev/null +++ b/test/unit/view/placeholder/util/placeholder-with-class.js @@ -0,0 +1,12 @@ +// @flow +import React from 'react'; +import { WithoutMemo } from '../../../../../src/view/placeholder/placeholder'; +import type { Props } from '../../../../../src/view/placeholder/placeholder'; + +// enzyme does not work well with memo, so exporting the non-memo version +// Using PureComponent to match behaviour of React.memo +export default class PlaceholderWithClass extends React.PureComponent { + render() { + return ; + } +} diff --git a/test/unit/view/style-marshal/get-styles.spec.js b/test/unit/view/style-marshal/get-styles.spec.js index 40c4260d2d..4b7e965001 100644 --- a/test/unit/view/style-marshal/get-styles.spec.js +++ b/test/unit/view/style-marshal/get-styles.spec.js @@ -2,7 +2,7 @@ import stylelint from 'stylelint'; import getStyles, { type Styles, -} from '../../../../src/view/style-marshal/get-styles'; +} from '../../../../src/view/use-style-marshal/get-styles'; const styles: Styles = getStyles('hey'); diff --git a/test/unit/view/style-marshal/style-marshal.spec.js b/test/unit/view/style-marshal/style-marshal.spec.js index ce32a26728..464423b408 100644 --- a/test/unit/view/style-marshal/style-marshal.spec.js +++ b/test/unit/view/style-marshal/style-marshal.spec.js @@ -1,181 +1,177 @@ // @flow -import createStyleMarshal, { - resetStyleContext, -} from '../../../../src/view/style-marshal/style-marshal'; +import React, { type Node } from 'react'; +import { mount, type ReactWrapper } from 'enzyme'; +import useStyleMarshal from '../../../../src/view/use-style-marshal'; import getStyles, { type Styles, -} from '../../../../src/view/style-marshal/get-styles'; +} from '../../../../src/view/use-style-marshal/get-styles'; +import type { StyleMarshal } from '../../../../src/view/use-style-marshal/style-marshal-types'; import { prefix } from '../../../../src/view/data-attributes'; -import type { StyleMarshal } from '../../../../src/view/style-marshal/style-marshal-types'; -const getDynamicStyleTagSelector = (context: string) => - `style[${prefix}-dynamic="${context}"]`; +const getMarshal = (myMock): StyleMarshal => myMock.mock.calls[0][0]; +const getMock = () => jest.fn().mockImplementation(() => null); -const getAlwaysStyleTagSelector = (context: string) => - `style[${prefix}-always="${context}"]`; +type Props = {| + uniqueId: number, + children: (marshal: StyleMarshal) => Node, +|}; -const getStyleFromTag = (context: string): string => { - const selector: string = getDynamicStyleTagSelector(context); +function WithMarshal(props: Props) { + const marshal: StyleMarshal = useStyleMarshal(props.uniqueId); + return props.children(marshal); +} + +const getDynamicStyleTagSelector = (uniqueId: number) => + `style[${prefix}-dynamic="${uniqueId}"]`; + +const getAlwaysStyleTagSelector = (uniqueId: number) => + `style[${prefix}-always="${uniqueId}"]`; + +const getDynamicStyleFromTag = (uniqueId: number): string => { + const selector: string = getDynamicStyleTagSelector(uniqueId); const el: HTMLStyleElement = (document.querySelector(selector): any); return el.innerHTML; }; -let marshal: StyleMarshal; -let styles: Styles; -beforeEach(() => { - resetStyleContext(); - marshal = createStyleMarshal(); - styles = getStyles(marshal.styleContext); -}); - -afterEach(() => { - try { - marshal.unmount(); - } catch (e) { - // already unmounted - } -}); +const getAlwaysStyleFromTag = (uniqueId: number): string => { + const selector: string = getAlwaysStyleTagSelector(uniqueId); + const el: HTMLStyleElement = (document.querySelector(selector): any); + return el.innerHTML; +}; it('should not mount style tags until mounted', () => { - const dynamicSelector: string = getDynamicStyleTagSelector( - marshal.styleContext, - ); - const alwaysSelector: string = getAlwaysStyleTagSelector( - marshal.styleContext, - ); + const uniqueId: number = 1; + const dynamicSelector: string = getDynamicStyleTagSelector(uniqueId); + const alwaysSelector: string = getAlwaysStyleTagSelector(uniqueId); // initially there is no style tag expect(document.querySelector(dynamicSelector)).toBeFalsy(); expect(document.querySelector(alwaysSelector)).toBeFalsy(); // now mounting - marshal.mount(); + const wrapper: ReactWrapper<*> = mount( + {getMock()}, + ); + + // elements should now exist expect(document.querySelector(alwaysSelector)).toBeInstanceOf( HTMLStyleElement, ); expect(document.querySelector(dynamicSelector)).toBeInstanceOf( HTMLStyleElement, ); -}); - -it('should throw if mounting after already mounting', () => { - marshal.mount(); - expect(() => marshal.mount()).toThrow(); + wrapper.unmount(); }); -it('should apply the resting styles by default', () => { - marshal.mount(); - const active: string = getStyleFromTag(marshal.styleContext); - - expect(active).toEqual(styles.resting); -}); +it('should apply the resting dyanmic styles by default', () => { + const uniqueId: number = 2; + const wrapper: ReactWrapper<*> = mount( + {getMock()}, + ); -it('should apply the always styles when mounted', () => { - marshal.mount(); + const active: string = getDynamicStyleFromTag(uniqueId); + expect(active).toEqual(getStyles(`${uniqueId}`).resting); - const selector: string = getAlwaysStyleTagSelector(marshal.styleContext); - const el: HTMLStyleElement = (document.querySelector(selector): any); - - expect(el.innerHTML).toEqual(styles.always); + wrapper.unmount(); }); -it('should apply the resting styles when asked', () => { - marshal.mount(); +it('should apply the resting always styles by default', () => { + const uniqueId: number = 2; + const wrapper: ReactWrapper<*> = mount( + {getMock()}, + ); - marshal.resting(); - const active: string = getStyleFromTag(marshal.styleContext); + const always: string = getAlwaysStyleFromTag(uniqueId); + expect(always).toEqual(getStyles(`${uniqueId}`).always); - expect(active).toEqual(styles.resting); + wrapper.unmount(); }); it('should apply the dragging styles when asked', () => { - marshal.mount(); + const uniqueId: number = 2; + const mock = getMock(); + const wrapper: ReactWrapper<*> = mount( + {mock}, + ); + const marshal: StyleMarshal = getMarshal(mock); marshal.dragging(); - const active: string = getStyleFromTag(marshal.styleContext); - expect(active).toEqual(styles.dragging); + const active: string = getDynamicStyleFromTag(uniqueId); + expect(active).toEqual(getStyles(`${uniqueId}`).dragging); + + wrapper.unmount(); }); it('should apply the drop animating styles when asked', () => { - marshal.mount(); + const uniqueId: number = 2; + const mock = getMock(); + const wrapper: ReactWrapper<*> = mount( + {mock}, + ); + const marshal: StyleMarshal = getMarshal(mock); marshal.dropping('DROP'); - const active: string = getStyleFromTag(marshal.styleContext); + const active: string = getDynamicStyleFromTag(uniqueId); + expect(active).toEqual(getStyles(`${uniqueId}`).dropAnimating); - expect(active).toEqual(styles.dropAnimating); + wrapper.unmount(); }); it('should apply the user cancel styles when asked', () => { - marshal.mount(); + const uniqueId: number = 2; + const mock = getMock(); + const wrapper: ReactWrapper<*> = mount( + {mock}, + ); + const marshal: StyleMarshal = getMarshal(mock); marshal.dropping('CANCEL'); - const active: string = getStyleFromTag(marshal.styleContext); + const active: string = getDynamicStyleFromTag(uniqueId); + expect(active).toEqual(getStyles(`${uniqueId}`).userCancel); - expect(active).toEqual(styles.userCancel); + wrapper.unmount(); }); it('should remove the style tag from the head when unmounting', () => { - marshal.mount(); - const selector1: string = getDynamicStyleTagSelector(marshal.styleContext); - const selector2: string = getAlwaysStyleTagSelector(marshal.styleContext); + const uniqueId: number = 2; + const wrapper: ReactWrapper<*> = mount( + {getMock()}, + ); + const selector1: string = getDynamicStyleTagSelector(uniqueId); + const selector2: string = getAlwaysStyleTagSelector(uniqueId); // the style tag exists expect(document.querySelector(selector1)).toBeTruthy(); expect(document.querySelector(selector2)).toBeTruthy(); // now unmounted - marshal.unmount(); + wrapper.unmount(); expect(document.querySelector(selector1)).not.toBeTruthy(); expect(document.querySelector(selector2)).not.toBeTruthy(); }); -it('should log an error if attempting to apply styles after unmounted', () => { - marshal.mount(); - const selector: string = getDynamicStyleTagSelector(marshal.styleContext); - // grabbing the element before unmount - const el: HTMLElement = (document.querySelector(selector): any); - - // asserting it has the base styles - expect(el.innerHTML).toEqual(styles.resting); - - marshal.unmount(); - - expect(() => marshal.dragging()).toThrow(); -}); - it('should allow subsequent updates', () => { - marshal.mount(); + const uniqueId: number = 10; + const styles: Styles = getStyles(`${uniqueId}`); + const mock = getMock(); + const wrapper: ReactWrapper<*> = mount( + {mock}, + ); + const marshal: StyleMarshal = getMarshal(mock); Array.from({ length: 4 }).forEach(() => { marshal.resting(); - expect(getStyleFromTag(marshal.styleContext)).toEqual(styles.resting); + expect(getDynamicStyleFromTag(uniqueId)).toEqual(styles.resting); marshal.dragging(); - expect(getStyleFromTag(marshal.styleContext)).toEqual(styles.dragging); + expect(getDynamicStyleFromTag(uniqueId)).toEqual(styles.dragging); marshal.dropping('DROP'); - expect(getStyleFromTag(marshal.styleContext)).toEqual(styles.dropAnimating); + expect(getDynamicStyleFromTag(uniqueId)).toEqual(styles.dropAnimating); }); -}); -describe('resetStyleContext', () => { - it('should reset the style context counter for subsequent marshals', () => { - // initial marshal - marshal.mount(); - // initial style context - expect(marshal.styleContext).toBe('0'); - - // creating second marshal - const marshalBeforeReset = createStyleMarshal(); - expect(marshalBeforeReset.styleContext).toBe('1'); - - resetStyleContext(); - - // creating third marshal after reset - const marshalAfterReset = createStyleMarshal(); - expect(marshalAfterReset.styleContext).toBe('0'); - }); + wrapper.unmount(); }); diff --git a/test/utils/get-context-options.js b/test/utils/get-context-options.js deleted file mode 100644 index b6b6f4a464..0000000000 --- a/test/utils/get-context-options.js +++ /dev/null @@ -1,130 +0,0 @@ -// @flow -import PropTypes from 'prop-types'; -import { - storeKey, - droppableIdKey, - dimensionMarshalKey, - styleKey, - canLiftKey, - droppableTypeKey, - isMovementAllowedKey, -} from '../../src/view/context-keys'; -import createStore from '../../src/state/create-store'; -import { getMarshalStub } from './dimension-marshal'; -import type { DroppableId, TypeId } from '../../src/types'; -import type { DimensionMarshal } from '../../src/state/dimension-marshal/dimension-marshal-types'; -import type { StyleMarshal } from '../../src/view/style-marshal/style-marshal-types'; -import type { AutoScroller } from '../../src/state/auto-scroller/auto-scroller-types'; - -// Not using this store - just putting it on the context -// For any connected components that need it (eg DimensionPublisher) -export const withStore = () => ({ - context: { - // Each consumer will get their own store - [storeKey]: createStore({ - getDimensionMarshal: () => getMarshalStub(), - styleMarshal: { - dragging: jest.fn(), - dropping: jest.fn(), - resting: jest.fn(), - styleContext: 'fake-style-context', - unmount: jest.fn(), - mount: jest.fn(), - }, - getResponders: () => ({ - onDragEnd: () => {}, - }), - announce: () => {}, - getScroller: (): AutoScroller => ({ - start: jest.fn(), - stop: jest.fn(), - cancelPending: jest.fn(), - scroll: jest.fn(), - }), - }), - }, - childContextTypes: { - [storeKey]: PropTypes.shape({ - dispatch: PropTypes.func.isRequired, - subscribe: PropTypes.func.isRequired, - getState: PropTypes.func.isRequired, - }).isRequired, - }, -}); - -export const withDroppableId = (droppableId: DroppableId): Object => ({ - context: { - [droppableIdKey]: droppableId, - }, - childContextTypes: { - [droppableIdKey]: PropTypes.string.isRequired, - }, -}); - -export const withDroppableType = (type: TypeId): Object => ({ - context: { - [droppableTypeKey]: type, - }, - childContextTypes: { - [droppableTypeKey]: PropTypes.string.isRequired, - }, -}); - -export const withStyleContext = (marshal?: StyleMarshal): Object => ({ - context: { - [styleKey]: marshal ? marshal.styleContext : 'fake-style-context', - }, - childContextTypes: { - [styleKey]: PropTypes.string.isRequired, - }, -}); - -export const withCanLift = (): Object => ({ - context: { - [canLiftKey]: () => true, - }, - childContextTypes: { - [canLiftKey]: PropTypes.func.isRequired, - }, -}); - -export const withDimensionMarshal = (marshal?: DimensionMarshal): Object => ({ - context: { - [dimensionMarshalKey]: marshal || getMarshalStub(), - }, - childContextTypes: { - [dimensionMarshalKey]: PropTypes.object.isRequired, - }, -}); - -export const withIsMovementAllowed = ( - getIsMovementAllowed?: () => boolean = () => false, -) => ({ - context: { - [isMovementAllowedKey]: getIsMovementAllowed, - }, - childContextTypes: { - [isMovementAllowedKey]: PropTypes.func.isRequired, - }, -}); - -const base: Object = { - context: {}, - childContextTypes: {}, -}; - -// returning type Object because that is what enzyme wants -export const combine = (...args: Object[]): Object => - args.reduce( - (previous: Object, current: Object): Object => ({ - context: { - ...previous.context, - ...current.context, - }, - childContextTypes: { - ...previous.childContextTypes, - ...current.childContextTypes, - }, - }), - base, - ); From 0053f962fd9700cd3d6e379414798fa51aa082d9 Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Tue, 26 Mar 2019 13:10:08 +1100 Subject: [PATCH 065/117] tests for drag-drop-context --- src/view/use-style-marshal/index.js | 1 + test/unit/view/drag-drop-context/app.jsx | 21 --- .../clashing-with-consumers-redux.spec.js | 107 ++++++++++++ .../reset-server-context.spec.js | 42 ++++- .../store-management.spec.js | 154 ------------------ .../view/drag-drop-context/unmount.spec.js | 9 +- 6 files changed, 147 insertions(+), 187 deletions(-) delete mode 100644 test/unit/view/drag-drop-context/app.jsx create mode 100644 test/unit/view/drag-drop-context/clashing-with-consumers-redux.spec.js delete mode 100644 test/unit/view/drag-drop-context/store-management.spec.js diff --git a/src/view/use-style-marshal/index.js b/src/view/use-style-marshal/index.js index 361eefd61b..8eda849ff9 100644 --- a/src/view/use-style-marshal/index.js +++ b/src/view/use-style-marshal/index.js @@ -1 +1,2 @@ +// @flow export { default } from './use-style-marshal'; diff --git a/test/unit/view/drag-drop-context/app.jsx b/test/unit/view/drag-drop-context/app.jsx deleted file mode 100644 index 3eb9fa3b4a..0000000000 --- a/test/unit/view/drag-drop-context/app.jsx +++ /dev/null @@ -1,21 +0,0 @@ -// @flow -import React from 'react'; -import PropTypes from 'prop-types'; -import { storeKey, canLiftKey } from '../../../../src/view/context-keys'; - -export default class App extends React.Component<*> { - // Part of reacts api is to use flow types for this. - // Sadly cannot use flow - static contextTypes = { - [storeKey]: PropTypes.shape({ - dispatch: PropTypes.func.isRequired, - subscribe: PropTypes.func.isRequired, - getState: PropTypes.func.isRequired, - }).isRequired, - [canLiftKey]: PropTypes.func.isRequired, - }; - - render() { - return
Hi there
; - } -} diff --git a/test/unit/view/drag-drop-context/clashing-with-consumers-redux.spec.js b/test/unit/view/drag-drop-context/clashing-with-consumers-redux.spec.js new file mode 100644 index 0000000000..dc0b10c5f9 --- /dev/null +++ b/test/unit/view/drag-drop-context/clashing-with-consumers-redux.spec.js @@ -0,0 +1,107 @@ +// @flow +/* eslint-disable react/no-multi-comp */ +import React, { Component } from 'react'; +import { mount } from 'enzyme'; +import { Provider, connect } from 'react-redux'; +import { createStore } from 'redux'; +import { Droppable, Draggable, DragDropContext } from '../../../../src'; +import type { DraggableProvided, DroppableProvided } from '../../../../src'; +// Imported as wildcard so we can mock `resetStyleContext` using spyOn + +type AppState = {| + foo: string, +|}; +const original: AppState = { + foo: 'bar', +}; +// super boring reducer that always returns the same thing +const reducer = (state: AppState = original) => state; +const store = createStore(reducer); + +class Unconnected extends Component { + render() { + return
{this.props.foo}
; + } +} + +function mapStateToProps(state: AppState): AppState { + return state; +} + +const Connected = connect(mapStateToProps)(Unconnected); + +it('should avoid clashes with parent redux applications', () => { + class Container extends Component<*> { + render() { + return ( + + {}}> + + {(droppableProvided: DroppableProvided) => ( +
+ + {(draggableProvided: DraggableProvided) => ( +
+ {/* $FlowFixMe - not sure why this requires foo */} + +
+ )} +
+ {droppableProvided.placeholder} +
+ )} +
+
+
+ ); + } + } + const wrapper = mount(); + + expect(wrapper.find(Container).text()).toBe(original.foo); +}); + +it('should avoid clashes with child redux applications', () => { + class Container extends Component<*> { + render() { + return ( + {}}> + + {(droppableProvided: DroppableProvided) => ( +
+ + {(draggableProvided: DraggableProvided) => ( +
+ + {/* $FlowFixMe - not sure why this requires foo */} + + +
+ )} +
+ {droppableProvided.placeholder} +
+ )} +
+
+ ); + } + } + const wrapper = mount(); + + expect(wrapper.find(Container).text()).toBe(original.foo); +}); diff --git a/test/unit/view/drag-drop-context/reset-server-context.spec.js b/test/unit/view/drag-drop-context/reset-server-context.spec.js index 18f2f1b1f7..ecd21338c4 100644 --- a/test/unit/view/drag-drop-context/reset-server-context.spec.js +++ b/test/unit/view/drag-drop-context/reset-server-context.spec.js @@ -1,12 +1,44 @@ // @flow +import React from 'react'; +import { mount, type ReactWrapper } from 'enzyme'; +import DragDropContext from '../../../../src/view/drag-drop-context'; import { resetServerContext } from '../../../../src'; -import * as StyleMarshal from '../../../../src/view/style-marshal/style-marshal'; +import * as attributes from '../../../../src/view/data-attributes'; + +const doesStyleElementExist = (uniqueId: number): boolean => + Boolean( + document.querySelector(`[${attributes.prefix}-always="${uniqueId}"]`), + ); it('should reset the style marshal context', () => { - const spy = jest.spyOn(StyleMarshal, 'resetStyleContext'); - expect(spy).not.toHaveBeenCalled(); + expect(doesStyleElementExist(1)).toBe(false); + + const wrapper1: ReactWrapper<*> = mount( + {}}>{null}, + ); + expect(doesStyleElementExist(0)).toBe(true); + + const wrapper2: ReactWrapper<*> = mount( + {}}>{null}, + ); + expect(doesStyleElementExist(1)).toBe(true); + + // not created yet + expect(doesStyleElementExist(2)).toBe(false); + + // clearing away the old wrappers + wrapper1.unmount(); + wrapper2.unmount(); resetServerContext(); - expect(spy).toHaveBeenCalledTimes(1); - spy.mockRestore(); + // a new wrapper after the reset + const wrapper3: ReactWrapper<*> = mount( + {}}>{null}, + ); + + // now only '0' exists + expect(doesStyleElementExist(0)).toBe(true); + expect(doesStyleElementExist(1)).toBe(false); + + wrapper3.unmount(); }); diff --git a/test/unit/view/drag-drop-context/store-management.spec.js b/test/unit/view/drag-drop-context/store-management.spec.js deleted file mode 100644 index b841409e1a..0000000000 --- a/test/unit/view/drag-drop-context/store-management.spec.js +++ /dev/null @@ -1,154 +0,0 @@ -// @flow -/* eslint-disable react/no-multi-comp */ -import React, { Component } from 'react'; -import TestUtils from 'react-dom/test-utils'; -import { mount } from 'enzyme'; -import { Provider, connect } from 'react-redux'; -import { createStore } from 'redux'; -import { Droppable, Draggable, DragDropContext } from '../../../../src'; -import type { DraggableProvided, DroppableProvided } from '../../../../src'; -import { storeKey, canLiftKey } from '../../../../src/view/context-keys'; -import App from './app'; -// Imported as wildcard so we can mock `resetStyleContext` using spyOn - -it('should put a store on the context', () => { - // using react test utils to allow access to nested contexts - const tree = TestUtils.renderIntoDocument( - {}}> - - , - ); - - const app = TestUtils.findRenderedComponentWithType(tree, App); - - if (!app) { - throw new Error('Invalid test setup'); - } - - expect(app.context[storeKey]).toHaveProperty('dispatch'); - expect(app.context[storeKey].dispatch).toBeInstanceOf(Function); - expect(app.context[storeKey]).toHaveProperty('getState'); - expect(app.context[storeKey].getState).toBeInstanceOf(Function); - expect(app.context[storeKey]).toHaveProperty('subscribe'); - expect(app.context[storeKey].subscribe).toBeInstanceOf(Function); -}); - -describe('can start drag', () => { - // behavior of this function is tested in can-start-drag.spec.js - it('should put a can lift function on the context', () => { - // using react test utils to allow access to nested contexts - const tree = TestUtils.renderIntoDocument( - {}}> - - , - ); - - const app = TestUtils.findRenderedComponentWithType(tree, App); - - if (!app) { - throw new Error('Invalid test setup'); - } - - expect(app.context[canLiftKey]).toBeInstanceOf(Function); - }); -}); - -describe('Playing with other redux apps', () => { - type AppState = {| - foo: string, - |}; - const original: AppState = { - foo: 'bar', - }; - // super boring reducer that always returns the same thing - const reducer = (state: AppState = original) => state; - const store = createStore(reducer); - - class Unconnected extends Component { - render() { - return
{this.props.foo}
; - } - } - - function mapStateToProps(state: AppState): AppState { - return state; - } - - const Connected = connect(mapStateToProps)(Unconnected); - - it('should avoid clashes with parent redux applications', () => { - class Container extends Component<*> { - render() { - return ( - - {}}> - - {(droppableProvided: DroppableProvided) => ( -
- - {(draggableProvided: DraggableProvided) => ( -
- {/* $FlowFixMe - not sure why this requires foo */} - -
- )} -
- {droppableProvided.placeholder} -
- )} -
-
-
- ); - } - } - const wrapper = mount(); - - expect(wrapper.find(Container).text()).toBe(original.foo); - }); - - it('should avoid clashes with child redux applications', () => { - class Container extends Component<*> { - render() { - return ( - {}}> - - {(droppableProvided: DroppableProvided) => ( -
- - {(draggableProvided: DraggableProvided) => ( -
- - {/* $FlowFixMe - not sure why this requires foo */} - - -
- )} -
- {droppableProvided.placeholder} -
- )} -
-
- ); - } - } - const wrapper = mount(); - - expect(wrapper.find(Container).text()).toBe(original.foo); - }); -}); diff --git a/test/unit/view/drag-drop-context/unmount.spec.js b/test/unit/view/drag-drop-context/unmount.spec.js index 9da074c207..a7304567d5 100644 --- a/test/unit/view/drag-drop-context/unmount.spec.js +++ b/test/unit/view/drag-drop-context/unmount.spec.js @@ -1,14 +1,11 @@ // @flow import React from 'react'; import { mount } from 'enzyme'; -import App from './app'; import DragDropContext from '../../../../src/view/drag-drop-context'; it('should not throw when unmounting', () => { const wrapper = mount( - {}}> - - , + {}}>{null}, ); expect(() => wrapper.unmount()).not.toThrow(); @@ -19,9 +16,7 @@ it('should clean up any window event handlers', () => { jest.spyOn(window, 'removeEventListener'); const wrapper = mount( - {}}> - - , + {}}>{null}, ); wrapper.unmount(); From 076734ae222e67e02950cf42cdf894dfbc779dfc Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Tue, 26 Mar 2019 13:42:24 +1100 Subject: [PATCH 066/117] wip --- .../use-animate-in-out/use-animate-in-out.js | 30 +++++--- .../animate-in-out/animate-in-out.spec.js | 75 +++++++++++-------- 2 files changed, 62 insertions(+), 43 deletions(-) diff --git a/src/view/use-animate-in-out/use-animate-in-out.js b/src/view/use-animate-in-out/use-animate-in-out.js index 9f8eb0bb70..97036d1af0 100644 --- a/src/view/use-animate-in-out/use-animate-in-out.js +++ b/src/view/use-animate-in-out/use-animate-in-out.js @@ -1,5 +1,6 @@ // @flow import { useMemo, useCallback, useLayoutEffect, useState } from 'react'; +import { unstable_batchedUpdates as batch } from 'react-dom'; import type { InOutAnimationMode } from '../../types'; export type AnimateProvided = {| @@ -8,7 +9,7 @@ export type AnimateProvided = {| data: mixed, |}; -type Args = {| +export type Args = {| on: mixed, shouldAnimate: boolean, |}; @@ -22,6 +23,7 @@ export default function useAnimateInOut(args: Args): ?AnimateProvided { useLayoutEffect(() => { if (!args.shouldAnimate) { + console.log('settings state'); setIsVisible(Boolean(args.on)); setData(args.on); setAnimate('none'); @@ -30,9 +32,12 @@ export default function useAnimateInOut(args: Args): ?AnimateProvided { // need to animate in if (args.on) { - setIsVisible(true); - setData(args.on); - setAnimate('open'); + console.log('lets do this'); + batch(() => { + setIsVisible(true); + setData(args.on); + setAnimate('open'); + }); return; } @@ -43,10 +48,15 @@ export default function useAnimateInOut(args: Args): ?AnimateProvided { return; } - // close animation no longer visible - + // content is no longer visible + // 1. animate closed if there was previous data + // 2. instantly close if there was no previous data setIsVisible(false); - setAnimate('close'); + if (data) { + setAnimate('close'); + } else { + setAnimate('none'); + } setData(null); }); @@ -67,9 +77,5 @@ export default function useAnimateInOut(args: Args): ?AnimateProvided { [animate, data, onClose], ); - if (!isVisible) { - return null; - } - - return provided; + return isVisible ? provided : null; } diff --git a/test/unit/view/animate-in-out/animate-in-out.spec.js b/test/unit/view/animate-in-out/animate-in-out.spec.js index 9f5b26604c..9304f13013 100644 --- a/test/unit/view/animate-in-out/animate-in-out.spec.js +++ b/test/unit/view/animate-in-out/animate-in-out.spec.js @@ -1,44 +1,56 @@ // @flow -import React from 'react'; +import React, { type Node } from 'react'; import { mount, type ReactWrapper } from 'enzyme'; -import AnimateInOut, { +import useAnimateInOut, { type AnimateProvided, -} from '../../../../src/view/animate-in-out/animate-in-out'; + type Args, +} from '../../../../src/view/use-animate-in-out/use-animate-in-out'; + +type WithUseAnimateInOutProps = {| + ...Args, + children: (provided: ?AnimateProvided) => Node, +|}; + +function WithUseAnimateInOut(props: WithUseAnimateInOutProps) { + const { children, ...rest } = props; + const provided: ?AnimateProvided = useAnimateInOut(rest); + return props.children(provided); +} it('should allow children not to be rendered (no animation allowed)', () => { const child = jest.fn().mockReturnValue(
hi
); - const wrapper: ReactWrapper<*> = mount( - + mount( + {child} - , + , ); - expect(wrapper.children()).toHaveLength(0); - expect(child).not.toHaveBeenCalled(); + expect(child).toHaveBeenCalledWith(null); + expect(child).toHaveBeenCalledTimes(1); }); it('should allow children not to be rendered (even when animation is allowed)', () => { const child = jest.fn().mockReturnValue(
hi
); - const wrapper: ReactWrapper<*> = mount( - + mount( + {child} - , + , ); - expect(wrapper.children()).toHaveLength(0); - expect(child).not.toHaveBeenCalled(); + expect(child).toHaveBeenCalledWith(null); + expect(child).toHaveBeenCalledTimes(1); }); it('should pass data through to children', () => { const child = jest.fn().mockReturnValue(
hi
); const data = { hello: 'world' }; - const wrapper: ReactWrapper<*> = mount( - + mount( + {child} - , + , ); const expected: AnimateProvided = { @@ -47,9 +59,9 @@ it('should pass data through to children', () => { // $ExpectError - wrong type onClose: expect.any(Function), }; - expect(child).toHaveBeenCalledWith(expected); - wrapper.unmount(); + expect(child).toHaveBeenCalledWith(expected); + expect(child).toHaveBeenCalledTimes(1); }); it('should open instantly if required', () => { @@ -57,9 +69,9 @@ it('should open instantly if required', () => { const data = { hello: 'world' }; const wrapper: ReactWrapper<*> = mount( - + {child} - , + , ); const expected: AnimateProvided = { @@ -78,9 +90,9 @@ it('should animate open if requested', () => { const data = { hello: 'world' }; const wrapper: ReactWrapper<*> = mount( - + {child} - , + , ); const expected: AnimateProvided = { @@ -94,14 +106,14 @@ it('should animate open if requested', () => { wrapper.unmount(); }); -it('should close instantly if required', () => { +it.only('should close instantly if required', () => { const child = jest.fn().mockReturnValue(
hi
); const data = { hello: 'world' }; const wrapper: ReactWrapper<*> = mount( - + {child} - , + , ); const initial: AnimateProvided = { @@ -111,6 +123,7 @@ it('should close instantly if required', () => { onClose: expect.any(Function), }; expect(child).toHaveBeenCalledWith(initial); + expect(child).toHaveBeenCalledTimes(1); child.mockClear(); // start closing @@ -118,8 +131,8 @@ it('should close instantly if required', () => { // data is gone! this should trigger a close on: null, }); - expect(wrapper.children()).toHaveLength(0); - expect(child).not.toHaveBeenCalled(); + expect(child).toHaveBeenCalledWith(null); + expect(child).toHaveBeenCalledTimes(1); }); it('should animate closed if required', () => { @@ -127,9 +140,9 @@ it('should animate closed if required', () => { const data = { hello: 'world' }; const wrapper: ReactWrapper<*> = mount( - + {child} - , + , ); const initial: AnimateProvided = { @@ -166,6 +179,6 @@ it('should animate closed if required', () => { // tell enzyme to reconcile the react tree due to the setState wrapper.update(); - expect(wrapper.children()).toHaveLength(0); - expect(child).not.toHaveBeenCalled(); + expect(child).toHaveBeenCalledWith(null); + expect(child).toHaveBeenCalledTimes(1); }); From 187f88c665e0d668b4fd2c8737fa44582265bd51 Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Tue, 26 Mar 2019 13:59:24 +1100 Subject: [PATCH 067/117] wip --- src/view/use-animate-in-out/use-animate-in-out.js | 13 +++++-------- .../unit/view/animate-in-out/animate-in-out.spec.js | 10 +++++++--- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/src/view/use-animate-in-out/use-animate-in-out.js b/src/view/use-animate-in-out/use-animate-in-out.js index 97036d1af0..473536a19b 100644 --- a/src/view/use-animate-in-out/use-animate-in-out.js +++ b/src/view/use-animate-in-out/use-animate-in-out.js @@ -1,6 +1,5 @@ // @flow import { useMemo, useCallback, useLayoutEffect, useState } from 'react'; -import { unstable_batchedUpdates as batch } from 'react-dom'; import type { InOutAnimationMode } from '../../types'; export type AnimateProvided = {| @@ -23,7 +22,6 @@ export default function useAnimateInOut(args: Args): ?AnimateProvided { useLayoutEffect(() => { if (!args.shouldAnimate) { - console.log('settings state'); setIsVisible(Boolean(args.on)); setData(args.on); setAnimate('none'); @@ -32,12 +30,9 @@ export default function useAnimateInOut(args: Args): ?AnimateProvided { // need to animate in if (args.on) { - console.log('lets do this'); - batch(() => { - setIsVisible(true); - setData(args.on); - setAnimate('open'); - }); + setIsVisible(true); + setData(args.on); + setAnimate('open'); return; } @@ -62,9 +57,11 @@ export default function useAnimateInOut(args: Args): ?AnimateProvided { const onClose = useCallback(() => { if (animate !== 'close') { + console.log('no need to close'); return; } + console.log('setting is visible to false'); setIsVisible(false); }, [animate]); diff --git a/test/unit/view/animate-in-out/animate-in-out.spec.js b/test/unit/view/animate-in-out/animate-in-out.spec.js index 9304f13013..e2750649d2 100644 --- a/test/unit/view/animate-in-out/animate-in-out.spec.js +++ b/test/unit/view/animate-in-out/animate-in-out.spec.js @@ -106,7 +106,7 @@ it('should animate open if requested', () => { wrapper.unmount(); }); -it.only('should close instantly if required', () => { +it('should close instantly if required', () => { const child = jest.fn().mockReturnValue(
hi
); const data = { hello: 'world' }; @@ -132,7 +132,8 @@ it.only('should close instantly if required', () => { on: null, }); expect(child).toHaveBeenCalledWith(null); - expect(child).toHaveBeenCalledTimes(1); + // Currently does a x3 render for the x3 state + expect(child).toHaveBeenCalledTimes(3); }); it('should animate closed if required', () => { @@ -170,15 +171,18 @@ it('should animate closed if required', () => { onClose: expect.any(Function), }; expect(child).toHaveBeenCalledWith(second); + expect(child).toHaveBeenCalledTimes(1); // telling AnimateInOut that the animation is finished const provided: AnimateProvided = child.mock.calls[0][0]; child.mockClear(); // this will trigger a setState that will stop rendering the child + console.log('calling on close'); provided.onClose(); // tell enzyme to reconcile the react tree due to the setState wrapper.update(); expect(child).toHaveBeenCalledWith(null); - expect(child).toHaveBeenCalledTimes(1); + // Currently does a x3 render for the x3 state + expect(child).toHaveBeenCalledTimes(3); }); From 96760e0e5a01bff72276c7bfd044973577d1e600 Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Tue, 26 Mar 2019 16:20:00 +1100 Subject: [PATCH 068/117] wip --- .../use-animate-in-out/use-animate-in-out.js | 62 ++++++----- .../animate-in-out/child-rendering.spec.js | 78 -------------- .../child-rendering.spec.js | 102 ++++++++++++++++++ .../use-animate-in-out.spec.js} | 19 ++-- 4 files changed, 146 insertions(+), 115 deletions(-) delete mode 100644 test/unit/view/animate-in-out/child-rendering.spec.js create mode 100644 test/unit/view/use-animate-in-out/child-rendering.spec.js rename test/unit/view/{animate-in-out/animate-in-out.spec.js => use-animate-in-out/use-animate-in-out.spec.js} (92%) diff --git a/src/view/use-animate-in-out/use-animate-in-out.js b/src/view/use-animate-in-out/use-animate-in-out.js index 473536a19b..9b1b176024 100644 --- a/src/view/use-animate-in-out/use-animate-in-out.js +++ b/src/view/use-animate-in-out/use-animate-in-out.js @@ -14,54 +14,58 @@ export type Args = {| |}; export default function useAnimateInOut(args: Args): ?AnimateProvided { - const [isVisible, setIsVisible] = useState(Boolean(args.on)); - const [data, setData] = useState(args.on); - const [animate, setAnimate] = useState( + console.log('args.on', args.on); + const [isVisible, setIsVisible] = useState(() => Boolean(args.on)); + const [data, setData] = useState(() => args.on); + const [animate, setAnimate] = useState(() => args.shouldAnimate && args.on ? 'open' : 'none', ); + console.log('render', { isVisible, data, animate }); + // Instant changes useLayoutEffect(() => { - if (!args.shouldAnimate) { - setIsVisible(Boolean(args.on)); - setData(args.on); - setAnimate('none'); + const shouldChangeInstantly: boolean = !args.shouldAnimate; + if (!shouldChangeInstantly) { return; } + setIsVisible(Boolean(args.on)); + setData(args.on); + setAnimate('none'); + }, [args.on, args.shouldAnimate]); - // need to animate in - if (args.on) { - setIsVisible(true); - setData(args.on); - setAnimate('open'); + // We have data and need to either animate in or not + useLayoutEffect(() => { + const shouldShowWithAnimation: boolean = + Boolean(args.on) && args.shouldAnimate; + + if (!shouldShowWithAnimation) { return; } + // first swap over to having data + // if we previously had data, we can just use the last value + if (!data) { + console.log('opening'); + setAnimate('open'); + } - // need to animate out if there was data + setIsVisible(true); + setData(args.on); + }, [animate, args.on, args.shouldAnimate, data]); - if (isVisible) { - setAnimate('close'); - return; - } + // Animating out + useLayoutEffect(() => { + const shouldAnimateOut: boolean = + args.shouldAnimate && !args.on && isVisible; - // content is no longer visible - // 1. animate closed if there was previous data - // 2. instantly close if there was no previous data - setIsVisible(false); - if (data) { + if (shouldAnimateOut) { setAnimate('close'); - } else { - setAnimate('none'); } - setData(null); - }); + }, [args.on, args.shouldAnimate, isVisible]); const onClose = useCallback(() => { if (animate !== 'close') { - console.log('no need to close'); return; } - - console.log('setting is visible to false'); setIsVisible(false); }, [animate]); diff --git a/test/unit/view/animate-in-out/child-rendering.spec.js b/test/unit/view/animate-in-out/child-rendering.spec.js deleted file mode 100644 index 1865b35d40..0000000000 --- a/test/unit/view/animate-in-out/child-rendering.spec.js +++ /dev/null @@ -1,78 +0,0 @@ -// @flow -import React from 'react'; -import { mount, type ReactWrapper } from 'enzyme'; -import AnimateInOut, { - type AnimateProvided, -} from '../../../../src/view/animate-in-out/animate-in-out'; - -type ChildProps = {| - provided: AnimateProvided, -|}; - -class Child extends React.Component { - render() { - return
{this.props.provided.animate}
; - } -} - -it('should render children', () => { - const wrapper: ReactWrapper<*> = mount( - - {(provided: AnimateProvided) => } - , - ); - - expect(wrapper.find(Child)).toHaveLength(1); - wrapper.unmount(); -}); - -it('should allow children not to be rendered', () => { - { - const wrapper: ReactWrapper<*> = mount( - - {(provided: AnimateProvided) => } - , - ); - - expect(wrapper.find(Child)).toHaveLength(0); - wrapper.unmount(); - } - // initial animation set to true - { - const wrapper: ReactWrapper<*> = mount( - - {(provided: AnimateProvided) => } - , - ); - - expect(wrapper.find(Child)).toHaveLength(0); - wrapper.unmount(); - } -}); - -it('should allow children not to be rendered after a close animation', () => { - const wrapper: ReactWrapper<*> = mount( - - {(provided: AnimateProvided) => } - , - ); - expect(wrapper.find(Child)).toHaveLength(1); - - // data is gone - will animate closed - wrapper.setProps({ - on: null, - }); - expect(wrapper.find(Child)).toHaveLength(1); - - // letting animate-in-out know that the animation is finished - wrapper - .find(Child) - .props() - .provided.onClose(); - - // let enzyme know that the react tree has changed - wrapper.update(); - - expect(wrapper.find(Child)).toHaveLength(0); - wrapper.unmount(); -}); diff --git a/test/unit/view/use-animate-in-out/child-rendering.spec.js b/test/unit/view/use-animate-in-out/child-rendering.spec.js new file mode 100644 index 0000000000..21cb01d275 --- /dev/null +++ b/test/unit/view/use-animate-in-out/child-rendering.spec.js @@ -0,0 +1,102 @@ +// @flow +import React, { type Node } from 'react'; +import { act } from 'react-dom/test-utils'; +import { mount, type ReactWrapper } from 'enzyme'; +import useAnimateInOut, { + type AnimateProvided, + type Args, +} from '../../../../src/view/use-animate-in-out/use-animate-in-out'; + +type WithUseAnimateInOutProps = {| + ...Args, + children: (provided: ?AnimateProvided) => Node, +|}; + +function WithUseAnimateInOut(props: WithUseAnimateInOutProps) { + const { children, ...rest } = props; + const provided: ?AnimateProvided = useAnimateInOut(rest); + return props.children(provided); +} + +type ChildProps = {| + provided: AnimateProvided, +|}; + +class Child extends React.Component { + render() { + return
{this.props.provided.animate}
; + } +} + +it('should render children', () => { + const wrapper: ReactWrapper<*> = mount( + + {(provided: ?AnimateProvided) => + provided ? : null + } + , + ); + + expect(wrapper.find(Child)).toHaveLength(1); + wrapper.unmount(); +}); + +it('should allow children not to be rendered', () => { + { + const wrapper: ReactWrapper<*> = mount( + + {(provided: ?AnimateProvided) => + provided ? : null + } + , + ); + + expect(wrapper.find(Child)).toHaveLength(0); + wrapper.unmount(); + } + // initial animation set to true + { + const wrapper: ReactWrapper<*> = mount( + + {(provided: ?AnimateProvided) => + provided ? : null + } + , + ); + + expect(wrapper.find(Child)).toHaveLength(0); + wrapper.unmount(); + } +}); + +it('should allow children not to be rendered after a close animation', () => { + const wrapper: ReactWrapper<*> = mount( + + {(provided: ?AnimateProvided) => + provided ? : null + } + , + ); + expect(wrapper.find(Child)).toHaveLength(1); + + // data is gone - will animate closed + wrapper.setProps({ + on: null, + }); + // let enzyme know that the react tree has changed + wrapper.update(); + expect(wrapper.find(Child)).toHaveLength(1); + + // letting animate-in-out know that the animation is finished + act(() => { + wrapper + .find(Child) + .props() + .provided.onClose(); + }); + + // let enzyme know that the react tree has changed + wrapper.update(); + expect(wrapper.find(Child)).toHaveLength(0); + wrapper.unmount(); +}); diff --git a/test/unit/view/animate-in-out/animate-in-out.spec.js b/test/unit/view/use-animate-in-out/use-animate-in-out.spec.js similarity index 92% rename from test/unit/view/animate-in-out/animate-in-out.spec.js rename to test/unit/view/use-animate-in-out/use-animate-in-out.spec.js index e2750649d2..635d86cccd 100644 --- a/test/unit/view/animate-in-out/animate-in-out.spec.js +++ b/test/unit/view/use-animate-in-out/use-animate-in-out.spec.js @@ -1,5 +1,6 @@ // @flow import React, { type Node } from 'react'; +import { act } from 'react-dom/test-utils'; import { mount, type ReactWrapper } from 'enzyme'; import useAnimateInOut, { type AnimateProvided, @@ -132,8 +133,8 @@ it('should close instantly if required', () => { on: null, }); expect(child).toHaveBeenCalledWith(null); - // Currently does a x3 render for the x3 state - expect(child).toHaveBeenCalledTimes(3); + // Currently does a x2 render: once with old state, and a layout effect for new state + expect(child).toHaveBeenCalledTimes(2); }); it('should animate closed if required', () => { @@ -171,18 +172,20 @@ it('should animate closed if required', () => { onClose: expect.any(Function), }; expect(child).toHaveBeenCalledWith(second); - expect(child).toHaveBeenCalledTimes(1); + expect(child).toHaveBeenCalledTimes(2); // telling AnimateInOut that the animation is finished - const provided: AnimateProvided = child.mock.calls[0][0]; + // $FlowFixMe - untyped mock + const provided: AnimateProvided = + child.mock.calls[child.mock.calls.length - 1][0]; child.mockClear(); // this will trigger a setState that will stop rendering the child - console.log('calling on close'); - provided.onClose(); + act(() => { + provided.onClose(); + }); // tell enzyme to reconcile the react tree due to the setState wrapper.update(); expect(child).toHaveBeenCalledWith(null); - // Currently does a x3 render for the x3 state - expect(child).toHaveBeenCalledTimes(3); + expect(child).toHaveBeenCalledTimes(1); }); From 817d4428e7194009cfa3087fc57c24d65243d9e0 Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Tue, 26 Mar 2019 17:05:43 +1100 Subject: [PATCH 069/117] going back to getDerrivedStateFromProps --- src/view/animate-in-out/animate-in-out.jsx | 91 +++++++++++++++++++ src/view/animate-in-out/index.js | 2 + src/view/droppable/droppable.jsx | 55 +++++++---- src/view/placeholder/placeholder.jsx | 25 ++++- .../use-animate-in-out/use-animate-in-out.js | 2 +- src/view/use-style-marshal/get-styles.js | 9 +- 6 files changed, 162 insertions(+), 22 deletions(-) create mode 100644 src/view/animate-in-out/animate-in-out.jsx create mode 100644 src/view/animate-in-out/index.js diff --git a/src/view/animate-in-out/animate-in-out.jsx b/src/view/animate-in-out/animate-in-out.jsx new file mode 100644 index 0000000000..8c60a4a218 --- /dev/null +++ b/src/view/animate-in-out/animate-in-out.jsx @@ -0,0 +1,91 @@ +// @flow +import React, { type Node } from 'react'; +import type { InOutAnimationMode } from '../../types'; + +export type AnimateProvided = {| + onClose: () => void, + animate: InOutAnimationMode, + data: mixed, +|}; + +type Props = {| + on: mixed, + shouldAnimate: boolean, + children: (provided: AnimateProvided) => Node, +|}; + +type State = {| + data: mixed, + isVisible: boolean, + animate: InOutAnimationMode, +|}; + +export default class AnimateInOut extends React.PureComponent { + state: State = { + isVisible: Boolean(this.props.on), + data: this.props.on, + // not allowing to animate close on mount + animate: this.props.shouldAnimate && this.props.on ? 'open' : 'none', + }; + + static getDerivedStateFromProps(props: Props, state: State): State { + if (!props.shouldAnimate) { + return { + isVisible: Boolean(props.on), + data: props.on, + animate: 'none', + }; + } + + // need to animate in + if (props.on) { + return { + isVisible: true, + // have new data to animate in with + data: props.on, + animate: 'open', + }; + } + + // need to animate out if there was data + + if (state.isVisible) { + return { + isVisible: true, + // use old data for animating out + data: state.data, + animate: 'close', + }; + } + + // close animation no longer visible + return { + isVisible: false, + animate: 'close', + data: null, + }; + } + + onClose = () => { + if (this.state.animate !== 'close') { + return; + } + + this.setState({ + isVisible: false, + }); + }; + + render() { + if (!this.state.isVisible) { + return null; + } + + const provided: AnimateProvided = { + onClose: this.onClose, + data: this.state.data, + animate: this.state.animate, + }; + return this.props.children(provided); + } +} diff --git a/src/view/animate-in-out/index.js b/src/view/animate-in-out/index.js new file mode 100644 index 0000000000..5869168b0f --- /dev/null +++ b/src/view/animate-in-out/index.js @@ -0,0 +1,2 @@ +// @flow +export { default } from './animate-in-out'; diff --git a/src/view/droppable/droppable.jsx b/src/view/droppable/droppable.jsx index ec8cd9cc04..590886802c 100644 --- a/src/view/droppable/droppable.jsx +++ b/src/view/droppable/droppable.jsx @@ -14,11 +14,12 @@ import AppContext, { type AppContextValue } from '../context/app-context'; import DroppableContext, { type DroppableContextValue, } from '../context/droppable-context'; -import useAnimateInOut, { - type AnimateProvided, -} from '../use-animate-in-out/use-animate-in-out'; +// import useAnimateInOut from '../use-animate-in-out/use-animate-in-out'; import getMaxWindowScroll from '../window/get-max-window-scroll'; import useValidation from './use-validation'; +import AnimateInOut, { + type AnimateProvided, +} from '../animate-in-out/animate-in-out'; export default function Droppable(props: Props) { const appContext: ?AppContextValue = useContext(AppContext); @@ -77,21 +78,39 @@ export default function Droppable(props: Props) { getPlaceholderRef, }); - const instruction: ?AnimateProvided = useAnimateInOut({ - on: props.placeholder, - shouldAnimate: props.shouldAnimatePlaceholder, - }); + // const instruction: ?AnimateProvided = useAnimateInOut({ + // on: props.placeholder, + // shouldAnimate: props.shouldAnimatePlaceholder, + // }); + + const placeholder: Node = ( + + {({ onClose, data, animate }: AnimateProvided) => ( + + )} + + ); - const placeholder: Node | null = instruction ? ( - - ) : null; + // const placeholder: Node | null = instruction ? ( + // + // ) : null; const provided: Provided = useMemo( (): Provided => ({ @@ -117,7 +136,7 @@ export default function Droppable(props: Props) { getDroppableRef: () => droppableRef.current, // Not checking on the first placement :( // The droppable placeholder is not set yet as the useLayoutEffect + setState has not finished - shouldCheckPlaceholder: Boolean(instruction), + shouldCheckPlaceholder: true, getPlaceholderRef: () => placeholderRef.current, }); diff --git a/src/view/placeholder/placeholder.jsx b/src/view/placeholder/placeholder.jsx index a739f04be1..cccfa843ce 100644 --- a/src/view/placeholder/placeholder.jsx +++ b/src/view/placeholder/placeholder.jsx @@ -8,6 +8,8 @@ import type { import { transitions } from '../../animation'; import { noSpacing } from '../../state/spacing'; +function noop() {} + export type PlaceholderStyle = {| display: string, boxSizing: 'border-box', @@ -56,6 +58,7 @@ const getSize = ({ animate, }: HelperArgs): Size => { if (isAnimatingOpenOnMount) { + console.log('returning empty style'); return empty; } @@ -63,6 +66,7 @@ const getSize = ({ return empty; } + console.log('returning full style'); return { height: placeholder.client.borderBox.height, width: placeholder.client.borderBox.width, @@ -118,9 +122,24 @@ function Placeholder(props: Props): Node { // will run after a render is flushed useEffect(() => { - if (isAnimatingOpenOnMount) { - setIsAnimatingOpenOnMount(false); + if (!isAnimatingOpenOnMount) { + return noop; } + let timerId: ?TimeoutID = setTimeout(() => { + timerId = null; + if (!isAnimatingOpenOnMount) { + return; + } + setIsAnimatingOpenOnMount(false); + }); + + // clear the timer if needed + return () => { + if (timerId) { + clearTimeout(timerId); + timerId = null; + } + }; }, [isAnimatingOpenOnMount]); const onSizeChangeEnd = useCallback( @@ -148,6 +167,8 @@ function Placeholder(props: Props): Node { placeholder: props.placeholder, }); + console.log('style', style); + return React.createElement(props.placeholder.tagName, { style, 'data-react-beautiful-dnd-placeholder': styleContext, diff --git a/src/view/use-animate-in-out/use-animate-in-out.js b/src/view/use-animate-in-out/use-animate-in-out.js index 9b1b176024..38f3b57637 100644 --- a/src/view/use-animate-in-out/use-animate-in-out.js +++ b/src/view/use-animate-in-out/use-animate-in-out.js @@ -25,7 +25,7 @@ export default function useAnimateInOut(args: Args): ?AnimateProvided { // Instant changes useLayoutEffect(() => { const shouldChangeInstantly: boolean = !args.shouldAnimate; - if (!shouldChangeInstantly) { + if (shouldChangeInstantly) { return; } setIsVisible(Boolean(args.on)); diff --git a/src/view/use-style-marshal/get-styles.js b/src/view/use-style-marshal/get-styles.js index c7157125d2..4756f755a3 100644 --- a/src/view/use-style-marshal/get-styles.js +++ b/src/view/use-style-marshal/get-styles.js @@ -115,6 +115,13 @@ export default (uniqueContext: string): Styles => { }; })(); + const placeholder: Rule = { + selector: getSelector(attributes.placeholder), + styles: { + always: noPointerEvents, + }, + }; + // ## Droppable styles // overflow-anchor: none; @@ -159,7 +166,7 @@ export default (uniqueContext: string): Styles => { }, }; - const rules: Rule[] = [draggable, dragHandle, droppable, body]; + const rules: Rule[] = [draggable, dragHandle, droppable, body, placeholder]; return { always: getStyles(rules, 'always'), From fe0be1fc4ba678825677008db929c921d6caa2eb Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Tue, 26 Mar 2019 17:17:10 +1100 Subject: [PATCH 070/117] correctly aborting mouse-sensor --- src/view/animate-in-out/animate-in-out.jsx | 2 ++ src/view/placeholder/placeholder.jsx | 13 +++++++------ src/view/use-drag-handle/sensor/use-mouse-sensor.js | 1 + src/view/use-drag-handle/use-drag-handle.js | 12 ++++++++---- 4 files changed, 18 insertions(+), 10 deletions(-) diff --git a/src/view/animate-in-out/animate-in-out.jsx b/src/view/animate-in-out/animate-in-out.jsx index 8c60a4a218..5c3b2cefb8 100644 --- a/src/view/animate-in-out/animate-in-out.jsx +++ b/src/view/animate-in-out/animate-in-out.jsx @@ -20,6 +20,8 @@ type State = {| animate: InOutAnimationMode, |}; +// Using a class here rather than hooks because getDerivedStateFromProps is boss + export default class AnimateInOut extends React.PureComponent { state: State = { isVisible: Boolean(this.props.on), diff --git a/src/view/placeholder/placeholder.jsx b/src/view/placeholder/placeholder.jsx index cccfa843ce..37ae616557 100644 --- a/src/view/placeholder/placeholder.jsx +++ b/src/view/placeholder/placeholder.jsx @@ -120,17 +120,20 @@ function Placeholder(props: Props): Node { props.animate === 'open', ); - // will run after a render is flushed + // Will run after a render is flushed + // Still need to wait a timeout to ensure that the + // update is completely applied to the DOM useEffect(() => { + // No need to do anything if (!isAnimatingOpenOnMount) { return noop; } + let timerId: ?TimeoutID = setTimeout(() => { timerId = null; - if (!isAnimatingOpenOnMount) { - return; + if (isAnimatingOpenOnMount) { + setIsAnimatingOpenOnMount(false); } - setIsAnimatingOpenOnMount(false); }); // clear the timer if needed @@ -167,8 +170,6 @@ function Placeholder(props: Props): Node { placeholder: props.placeholder, }); - console.log('style', style); - return React.createElement(props.placeholder.tagName, { style, 'data-react-beautiful-dnd-placeholder': styleContext, diff --git a/src/view/use-drag-handle/sensor/use-mouse-sensor.js b/src/view/use-drag-handle/sensor/use-mouse-sensor.js index 62c52f0b9b..83352c4774 100644 --- a/src/view/use-drag-handle/sensor/use-mouse-sensor.js +++ b/src/view/use-drag-handle/sensor/use-mouse-sensor.js @@ -313,6 +313,7 @@ export default function useMouseSensor(args: Args): OnMouseDown { pendingRef.current = point; onCaptureStart(stop); bindWindowEvents(); + console.log('starting pending drag'); }, [bindWindowEvents, onCaptureStart, stop], ); diff --git a/src/view/use-drag-handle/use-drag-handle.js b/src/view/use-drag-handle/use-drag-handle.js index 679141fad9..c0e00ba776 100644 --- a/src/view/use-drag-handle/use-drag-handle.js +++ b/src/view/use-drag-handle/use-drag-handle.js @@ -196,11 +196,15 @@ export default function useDragHandle(args: Args): ?DragHandleProps { } } - // handle aborting + // Handle aborting // No longer dragging but still capturing: need to abort - if (!isDragging && capturingRef.current) { - abortCapture(); - } + // Using a layout effect to ensure that there is a flip from isDragging => !isDragging + // When there is a pending drag !isDragging will always be true + useLayoutEffect(() => { + if (!isDragging && capturingRef.current) { + abortCapture(); + } + }, [abortCapture, isDragging]); const props: ?DragHandleProps = useMemo(() => { if (!isEnabled) { From 8be2863534ce139d4adf5d0bae11f2b89a2d1032 Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Tue, 26 Mar 2019 17:19:58 +1100 Subject: [PATCH 071/117] moving old animate-in-out tests back --- src/view/use-animate-in-out/index.js | 2 - .../use-animate-in-out/use-animate-in-out.js | 82 -------------- .../animate-in-out.spec.js} | 84 ++++++--------- .../animate-in-out/child-rendering.spec.js | 78 ++++++++++++++ .../child-rendering.spec.js | 102 ------------------ 5 files changed, 110 insertions(+), 238 deletions(-) delete mode 100644 src/view/use-animate-in-out/index.js delete mode 100644 src/view/use-animate-in-out/use-animate-in-out.js rename test/unit/view/{use-animate-in-out/use-animate-in-out.spec.js => animate-in-out/animate-in-out.spec.js} (64%) create mode 100644 test/unit/view/animate-in-out/child-rendering.spec.js delete mode 100644 test/unit/view/use-animate-in-out/child-rendering.spec.js diff --git a/src/view/use-animate-in-out/index.js b/src/view/use-animate-in-out/index.js deleted file mode 100644 index 122e867330..0000000000 --- a/src/view/use-animate-in-out/index.js +++ /dev/null @@ -1,2 +0,0 @@ -// @flow -export { default } from './use-animate-in-out'; diff --git a/src/view/use-animate-in-out/use-animate-in-out.js b/src/view/use-animate-in-out/use-animate-in-out.js deleted file mode 100644 index 38f3b57637..0000000000 --- a/src/view/use-animate-in-out/use-animate-in-out.js +++ /dev/null @@ -1,82 +0,0 @@ -// @flow -import { useMemo, useCallback, useLayoutEffect, useState } from 'react'; -import type { InOutAnimationMode } from '../../types'; - -export type AnimateProvided = {| - onClose: () => void, - animate: InOutAnimationMode, - data: mixed, -|}; - -export type Args = {| - on: mixed, - shouldAnimate: boolean, -|}; - -export default function useAnimateInOut(args: Args): ?AnimateProvided { - console.log('args.on', args.on); - const [isVisible, setIsVisible] = useState(() => Boolean(args.on)); - const [data, setData] = useState(() => args.on); - const [animate, setAnimate] = useState(() => - args.shouldAnimate && args.on ? 'open' : 'none', - ); - console.log('render', { isVisible, data, animate }); - - // Instant changes - useLayoutEffect(() => { - const shouldChangeInstantly: boolean = !args.shouldAnimate; - if (shouldChangeInstantly) { - return; - } - setIsVisible(Boolean(args.on)); - setData(args.on); - setAnimate('none'); - }, [args.on, args.shouldAnimate]); - - // We have data and need to either animate in or not - useLayoutEffect(() => { - const shouldShowWithAnimation: boolean = - Boolean(args.on) && args.shouldAnimate; - - if (!shouldShowWithAnimation) { - return; - } - // first swap over to having data - // if we previously had data, we can just use the last value - if (!data) { - console.log('opening'); - setAnimate('open'); - } - - setIsVisible(true); - setData(args.on); - }, [animate, args.on, args.shouldAnimate, data]); - - // Animating out - useLayoutEffect(() => { - const shouldAnimateOut: boolean = - args.shouldAnimate && !args.on && isVisible; - - if (shouldAnimateOut) { - setAnimate('close'); - } - }, [args.on, args.shouldAnimate, isVisible]); - - const onClose = useCallback(() => { - if (animate !== 'close') { - return; - } - setIsVisible(false); - }, [animate]); - - const provided: AnimateProvided = useMemo( - () => ({ - onClose, - data, - animate, - }), - [animate, data, onClose], - ); - - return isVisible ? provided : null; -} diff --git a/test/unit/view/use-animate-in-out/use-animate-in-out.spec.js b/test/unit/view/animate-in-out/animate-in-out.spec.js similarity index 64% rename from test/unit/view/use-animate-in-out/use-animate-in-out.spec.js rename to test/unit/view/animate-in-out/animate-in-out.spec.js index 635d86cccd..9f5b26604c 100644 --- a/test/unit/view/use-animate-in-out/use-animate-in-out.spec.js +++ b/test/unit/view/animate-in-out/animate-in-out.spec.js @@ -1,57 +1,44 @@ // @flow -import React, { type Node } from 'react'; -import { act } from 'react-dom/test-utils'; +import React from 'react'; import { mount, type ReactWrapper } from 'enzyme'; -import useAnimateInOut, { +import AnimateInOut, { type AnimateProvided, - type Args, -} from '../../../../src/view/use-animate-in-out/use-animate-in-out'; - -type WithUseAnimateInOutProps = {| - ...Args, - children: (provided: ?AnimateProvided) => Node, -|}; - -function WithUseAnimateInOut(props: WithUseAnimateInOutProps) { - const { children, ...rest } = props; - const provided: ?AnimateProvided = useAnimateInOut(rest); - return props.children(provided); -} +} from '../../../../src/view/animate-in-out/animate-in-out'; it('should allow children not to be rendered (no animation allowed)', () => { const child = jest.fn().mockReturnValue(
hi
); - mount( - + const wrapper: ReactWrapper<*> = mount( + {child} - , + , ); - expect(child).toHaveBeenCalledWith(null); - expect(child).toHaveBeenCalledTimes(1); + expect(wrapper.children()).toHaveLength(0); + expect(child).not.toHaveBeenCalled(); }); it('should allow children not to be rendered (even when animation is allowed)', () => { const child = jest.fn().mockReturnValue(
hi
); - mount( - + const wrapper: ReactWrapper<*> = mount( + {child} - , + , ); - expect(child).toHaveBeenCalledWith(null); - expect(child).toHaveBeenCalledTimes(1); + expect(wrapper.children()).toHaveLength(0); + expect(child).not.toHaveBeenCalled(); }); it('should pass data through to children', () => { const child = jest.fn().mockReturnValue(
hi
); const data = { hello: 'world' }; - mount( - + const wrapper: ReactWrapper<*> = mount( + {child} - , + , ); const expected: AnimateProvided = { @@ -60,9 +47,9 @@ it('should pass data through to children', () => { // $ExpectError - wrong type onClose: expect.any(Function), }; - expect(child).toHaveBeenCalledWith(expected); - expect(child).toHaveBeenCalledTimes(1); + + wrapper.unmount(); }); it('should open instantly if required', () => { @@ -70,9 +57,9 @@ it('should open instantly if required', () => { const data = { hello: 'world' }; const wrapper: ReactWrapper<*> = mount( - + {child} - , + , ); const expected: AnimateProvided = { @@ -91,9 +78,9 @@ it('should animate open if requested', () => { const data = { hello: 'world' }; const wrapper: ReactWrapper<*> = mount( - + {child} - , + , ); const expected: AnimateProvided = { @@ -112,9 +99,9 @@ it('should close instantly if required', () => { const data = { hello: 'world' }; const wrapper: ReactWrapper<*> = mount( - + {child} - , + , ); const initial: AnimateProvided = { @@ -124,7 +111,6 @@ it('should close instantly if required', () => { onClose: expect.any(Function), }; expect(child).toHaveBeenCalledWith(initial); - expect(child).toHaveBeenCalledTimes(1); child.mockClear(); // start closing @@ -132,9 +118,8 @@ it('should close instantly if required', () => { // data is gone! this should trigger a close on: null, }); - expect(child).toHaveBeenCalledWith(null); - // Currently does a x2 render: once with old state, and a layout effect for new state - expect(child).toHaveBeenCalledTimes(2); + expect(wrapper.children()).toHaveLength(0); + expect(child).not.toHaveBeenCalled(); }); it('should animate closed if required', () => { @@ -142,9 +127,9 @@ it('should animate closed if required', () => { const data = { hello: 'world' }; const wrapper: ReactWrapper<*> = mount( - + {child} - , + , ); const initial: AnimateProvided = { @@ -172,20 +157,15 @@ it('should animate closed if required', () => { onClose: expect.any(Function), }; expect(child).toHaveBeenCalledWith(second); - expect(child).toHaveBeenCalledTimes(2); // telling AnimateInOut that the animation is finished - // $FlowFixMe - untyped mock - const provided: AnimateProvided = - child.mock.calls[child.mock.calls.length - 1][0]; + const provided: AnimateProvided = child.mock.calls[0][0]; child.mockClear(); // this will trigger a setState that will stop rendering the child - act(() => { - provided.onClose(); - }); + provided.onClose(); // tell enzyme to reconcile the react tree due to the setState wrapper.update(); - expect(child).toHaveBeenCalledWith(null); - expect(child).toHaveBeenCalledTimes(1); + expect(wrapper.children()).toHaveLength(0); + expect(child).not.toHaveBeenCalled(); }); diff --git a/test/unit/view/animate-in-out/child-rendering.spec.js b/test/unit/view/animate-in-out/child-rendering.spec.js new file mode 100644 index 0000000000..1865b35d40 --- /dev/null +++ b/test/unit/view/animate-in-out/child-rendering.spec.js @@ -0,0 +1,78 @@ +// @flow +import React from 'react'; +import { mount, type ReactWrapper } from 'enzyme'; +import AnimateInOut, { + type AnimateProvided, +} from '../../../../src/view/animate-in-out/animate-in-out'; + +type ChildProps = {| + provided: AnimateProvided, +|}; + +class Child extends React.Component { + render() { + return
{this.props.provided.animate}
; + } +} + +it('should render children', () => { + const wrapper: ReactWrapper<*> = mount( + + {(provided: AnimateProvided) => } + , + ); + + expect(wrapper.find(Child)).toHaveLength(1); + wrapper.unmount(); +}); + +it('should allow children not to be rendered', () => { + { + const wrapper: ReactWrapper<*> = mount( + + {(provided: AnimateProvided) => } + , + ); + + expect(wrapper.find(Child)).toHaveLength(0); + wrapper.unmount(); + } + // initial animation set to true + { + const wrapper: ReactWrapper<*> = mount( + + {(provided: AnimateProvided) => } + , + ); + + expect(wrapper.find(Child)).toHaveLength(0); + wrapper.unmount(); + } +}); + +it('should allow children not to be rendered after a close animation', () => { + const wrapper: ReactWrapper<*> = mount( + + {(provided: AnimateProvided) => } + , + ); + expect(wrapper.find(Child)).toHaveLength(1); + + // data is gone - will animate closed + wrapper.setProps({ + on: null, + }); + expect(wrapper.find(Child)).toHaveLength(1); + + // letting animate-in-out know that the animation is finished + wrapper + .find(Child) + .props() + .provided.onClose(); + + // let enzyme know that the react tree has changed + wrapper.update(); + + expect(wrapper.find(Child)).toHaveLength(0); + wrapper.unmount(); +}); diff --git a/test/unit/view/use-animate-in-out/child-rendering.spec.js b/test/unit/view/use-animate-in-out/child-rendering.spec.js deleted file mode 100644 index 21cb01d275..0000000000 --- a/test/unit/view/use-animate-in-out/child-rendering.spec.js +++ /dev/null @@ -1,102 +0,0 @@ -// @flow -import React, { type Node } from 'react'; -import { act } from 'react-dom/test-utils'; -import { mount, type ReactWrapper } from 'enzyme'; -import useAnimateInOut, { - type AnimateProvided, - type Args, -} from '../../../../src/view/use-animate-in-out/use-animate-in-out'; - -type WithUseAnimateInOutProps = {| - ...Args, - children: (provided: ?AnimateProvided) => Node, -|}; - -function WithUseAnimateInOut(props: WithUseAnimateInOutProps) { - const { children, ...rest } = props; - const provided: ?AnimateProvided = useAnimateInOut(rest); - return props.children(provided); -} - -type ChildProps = {| - provided: AnimateProvided, -|}; - -class Child extends React.Component { - render() { - return
{this.props.provided.animate}
; - } -} - -it('should render children', () => { - const wrapper: ReactWrapper<*> = mount( - - {(provided: ?AnimateProvided) => - provided ? : null - } - , - ); - - expect(wrapper.find(Child)).toHaveLength(1); - wrapper.unmount(); -}); - -it('should allow children not to be rendered', () => { - { - const wrapper: ReactWrapper<*> = mount( - - {(provided: ?AnimateProvided) => - provided ? : null - } - , - ); - - expect(wrapper.find(Child)).toHaveLength(0); - wrapper.unmount(); - } - // initial animation set to true - { - const wrapper: ReactWrapper<*> = mount( - - {(provided: ?AnimateProvided) => - provided ? : null - } - , - ); - - expect(wrapper.find(Child)).toHaveLength(0); - wrapper.unmount(); - } -}); - -it('should allow children not to be rendered after a close animation', () => { - const wrapper: ReactWrapper<*> = mount( - - {(provided: ?AnimateProvided) => - provided ? : null - } - , - ); - expect(wrapper.find(Child)).toHaveLength(1); - - // data is gone - will animate closed - wrapper.setProps({ - on: null, - }); - // let enzyme know that the react tree has changed - wrapper.update(); - expect(wrapper.find(Child)).toHaveLength(1); - - // letting animate-in-out know that the animation is finished - act(() => { - wrapper - .find(Child) - .props() - .provided.onClose(); - }); - - // let enzyme know that the react tree has changed - wrapper.update(); - expect(wrapper.find(Child)).toHaveLength(0); - wrapper.unmount(); -}); From c192147478dae9d659a90bada1715f0d510979c1 Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Tue, 26 Mar 2019 17:34:22 +1100 Subject: [PATCH 072/117] adding fix for aborted drags --- src/view/use-drag-handle/use-drag-handle.js | 1 + .../view/drag-handle/mouse-sensor.spec.js | 17 +++++++++++ .../view/drag-handle/touch-sensor.spec.js | 28 +++++++++++++++++++ 3 files changed, 46 insertions(+) diff --git a/src/view/use-drag-handle/use-drag-handle.js b/src/view/use-drag-handle/use-drag-handle.js index c0e00ba776..e32db83657 100644 --- a/src/view/use-drag-handle/use-drag-handle.js +++ b/src/view/use-drag-handle/use-drag-handle.js @@ -31,6 +31,7 @@ export default function useDragHandle(args: Args): ?DragHandleProps { // Capturing const capturingRef = useRef(null); const onCaptureStart = useCallback((abort: () => void) => { + console.log('capture starting'); invariant( !capturingRef.current, 'Cannot start capturing while something else is', diff --git a/test/unit/view/drag-handle/mouse-sensor.spec.js b/test/unit/view/drag-handle/mouse-sensor.spec.js index f0f91281f8..0c86556386 100644 --- a/test/unit/view/drag-handle/mouse-sensor.spec.js +++ b/test/unit/view/drag-handle/mouse-sensor.spec.js @@ -39,6 +39,7 @@ import { getWrapper } from './util/wrappers'; import type { Callbacks } from '../../../../src/view/use-drag-handle/drag-handle-types'; import type { AppContextValue } from '../../../../src/view/context/app-context'; import basicContext from './util/app-context'; +import forceUpdate from '../../../utils/force-update'; const origin: Position = { x: 0, y: 0 }; @@ -1090,6 +1091,22 @@ describe('disabled mid drag', () => { }); describe('cancelled elsewhere in the app mid drag', () => { + it('should not abort a drag if a render occurs during a pending drag', () => { + // lift + mouseDown(wrapper); + forceUpdate(wrapper); + + windowMouseMove({ x: 0, y: sloppyClickThreshold }); + + expect( + callbacksCalled(callbacks)({ + onLift: 1, + onMove: 0, + onCancel: 0, + }), + ).toBe(true); + }); + it('should end a current drag without firing the onCancel callback', () => { // lift mouseDown(wrapper); diff --git a/test/unit/view/drag-handle/touch-sensor.spec.js b/test/unit/view/drag-handle/touch-sensor.spec.js index 47cba37e48..b8e3db2ff5 100644 --- a/test/unit/view/drag-handle/touch-sensor.spec.js +++ b/test/unit/view/drag-handle/touch-sensor.spec.js @@ -29,6 +29,7 @@ import { getWrapper } from './util/wrappers'; import type { Callbacks } from '../../../../src/view/use-drag-handle/drag-handle-types'; import type { AppContextValue } from '../../../../src/view/context/app-context'; import basicContext from './util/app-context'; +import forceUpdate from '../../../utils/force-update'; const origin: Position = { x: 0, y: 0 }; let callbacks: Callbacks; @@ -465,6 +466,33 @@ describe('disabling a draggable during a drag', () => { }); describe('cancelled elsewhere in the app', () => { + it('should not abort a drag if a render occurs during a pending drag', () => { + // killing other wrapper + wrapper.unmount(); + + // lift + const customCallbacks = getStubCallbacks(); + const customWrapper = getWrapper(customCallbacks); + // pending drag started + touchStart(customWrapper, origin); + + // render should not kill a drag start + forceUpdate(customWrapper); + + // should still start a drag + jest.runTimersToTime(timeForLongPress); + + expect( + callbacksCalled(customCallbacks)({ + onLift: 1, + onMove: 0, + onCancel: 0, + }), + ).toBe(true); + + customWrapper.unmount(); + }); + it('should end the drag without firing the onCancel callback', () => { wrapper.setProps({ isDragging: true, From 64cdd2bba46ab8ccc0e54455a8b6cab04ec78718 Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Wed, 27 Mar 2019 08:46:29 +1100 Subject: [PATCH 073/117] adjusting placeholder tests for mount timer --- src/view/placeholder/placeholder.jsx | 49 ++++++---- .../sensor/use-mouse-sensor.js | 1 - src/view/use-drag-handle/use-drag-handle.js | 1 - .../view/placeholder/animated-mount.spec.js | 90 +++++++++++++++++-- .../placeholder/on-transition-end.spec.js | 7 ++ 5 files changed, 125 insertions(+), 23 deletions(-) diff --git a/src/view/placeholder/placeholder.jsx b/src/view/placeholder/placeholder.jsx index 37ae616557..43778e2300 100644 --- a/src/view/placeholder/placeholder.jsx +++ b/src/view/placeholder/placeholder.jsx @@ -1,5 +1,11 @@ // @flow -import React, { useState, useCallback, useEffect, type Node } from 'react'; +import React, { + useState, + useRef, + useCallback, + useEffect, + type Node, +} from 'react'; import type { Spacing } from 'css-box-model'; import type { Placeholder as PlaceholderType, @@ -58,7 +64,6 @@ const getSize = ({ animate, }: HelperArgs): Size => { if (isAnimatingOpenOnMount) { - console.log('returning empty style'); return empty; } @@ -66,7 +71,6 @@ const getSize = ({ return empty; } - console.log('returning full style'); return { height: placeholder.client.borderBox.height, width: placeholder.client.borderBox.width, @@ -115,6 +119,16 @@ const getStyle = ({ }; function Placeholder(props: Props): Node { + const animateOpenTimerRef = useRef(null); + + const tryClearAnimateOpenTimer = useCallback(() => { + if (!animateOpenTimerRef.current) { + return; + } + clearTimeout(animateOpenTimerRef.current); + animateOpenTimerRef.current = null; + }, []); + const { animate, onTransitionEnd, onClose, styleContext } = props; const [isAnimatingOpenOnMount, setIsAnimatingOpenOnMount] = useState( props.animate === 'open', @@ -129,21 +143,26 @@ function Placeholder(props: Props): Node { return noop; } - let timerId: ?TimeoutID = setTimeout(() => { - timerId = null; - if (isAnimatingOpenOnMount) { - setIsAnimatingOpenOnMount(false); - } + // might need to clear the timer + if (animate !== 'open') { + tryClearAnimateOpenTimer(); + setIsAnimatingOpenOnMount(false); + return noop; + } + + // timer already pending + if (animateOpenTimerRef.current) { + return noop; + } + + animateOpenTimerRef.current = setTimeout(() => { + animateOpenTimerRef.current = null; + setIsAnimatingOpenOnMount(false); }); // clear the timer if needed - return () => { - if (timerId) { - clearTimeout(timerId); - timerId = null; - } - }; - }, [isAnimatingOpenOnMount]); + return tryClearAnimateOpenTimer; + }, [animate, isAnimatingOpenOnMount, tryClearAnimateOpenTimer]); const onSizeChangeEnd = useCallback( (event: TransitionEvent) => { diff --git a/src/view/use-drag-handle/sensor/use-mouse-sensor.js b/src/view/use-drag-handle/sensor/use-mouse-sensor.js index 83352c4774..62c52f0b9b 100644 --- a/src/view/use-drag-handle/sensor/use-mouse-sensor.js +++ b/src/view/use-drag-handle/sensor/use-mouse-sensor.js @@ -313,7 +313,6 @@ export default function useMouseSensor(args: Args): OnMouseDown { pendingRef.current = point; onCaptureStart(stop); bindWindowEvents(); - console.log('starting pending drag'); }, [bindWindowEvents, onCaptureStart, stop], ); diff --git a/src/view/use-drag-handle/use-drag-handle.js b/src/view/use-drag-handle/use-drag-handle.js index e32db83657..c0e00ba776 100644 --- a/src/view/use-drag-handle/use-drag-handle.js +++ b/src/view/use-drag-handle/use-drag-handle.js @@ -31,7 +31,6 @@ export default function useDragHandle(args: Args): ?DragHandleProps { // Capturing const capturingRef = useRef(null); const onCaptureStart = useCallback((abort: () => void) => { - console.log('capture starting'); invariant( !capturingRef.current, 'Cannot start capturing while something else is', diff --git a/test/unit/view/placeholder/animated-mount.spec.js b/test/unit/view/placeholder/animated-mount.spec.js index bb991afe26..ed766388ad 100644 --- a/test/unit/view/placeholder/animated-mount.spec.js +++ b/test/unit/view/placeholder/animated-mount.spec.js @@ -1,6 +1,7 @@ // @flow import React from 'react'; import { mount, type ReactWrapper } from 'enzyme'; +import { act } from 'react-dom/test-utils'; import Placeholder from './util/placeholder-with-class'; import type { PlaceholderStyle } from '../../../../src/view/placeholder/placeholder-types'; import { expectIsEmpty, expectIsFull } from './util/expect'; @@ -11,15 +12,23 @@ import * as attributes from '../../../../src/view/data-attributes'; jest.useFakeTimers(); const styleContext: string = 'hello-there'; -const getCreatePlaceholderCalls = spy => { +let spy; + +beforeEach(() => { + spy = jest.spyOn(React, 'createElement'); +}); + +afterEach(() => { + spy.mockRestore(); +}); + +const getCreatePlaceholderCalls = () => { return spy.mock.calls.filter(call => { return call[1] && call[1][attributes.placeholder] === styleContext; }); }; it('should animate a mount', () => { - const spy = jest.spyOn(React, 'createElement'); - const wrapper: ReactWrapper<*> = mount( { />, ); - const calls = getCreatePlaceholderCalls(spy); - expect(calls.length).toBe(2); + expect(getCreatePlaceholderCalls().length).toBe(1); // first call had an empty size - const onMount: PlaceholderStyle = calls[0][1].style; + const onMount: PlaceholderStyle = getPlaceholderStyle(wrapper); expectIsEmpty(onMount); + // Will trigger a .setState + act(() => { + jest.runOnlyPendingTimers(); + }); + + // tell enzyme that something has changed + wrapper.update(); + const postMount: PlaceholderStyle = getPlaceholderStyle(wrapper); expectIsFull(postMount); }); + +it('should not animate a mount if interrupted', () => { + const wrapper: ReactWrapper<*> = mount( + , + ); + const onMount: PlaceholderStyle = getPlaceholderStyle(wrapper); + expectIsEmpty(onMount); + + expect(getCreatePlaceholderCalls()).toHaveLength(1); + + // interrupting animation + wrapper.setProps({ + animate: 'none', + }); + // render 1: normal + // render 2: useEffect calling setState + // render 3: result of setState + expect(getCreatePlaceholderCalls()).toHaveLength(3); + + // no timers are run + // let enzyme know that the react tree has changed due to the set state + wrapper.update(); + + const postMount: PlaceholderStyle = getPlaceholderStyle(wrapper); + expectIsFull(postMount); + + // validation - no further updates + spy.mockClear(); + jest.runOnlyPendingTimers(); + wrapper.update(); + expectIsFull(getPlaceholderStyle(wrapper)); + expect(getCreatePlaceholderCalls()).toHaveLength(0); +}); + +it('should not animate in if unmounted', () => { + jest.spyOn(console, 'error'); + + const wrapper: ReactWrapper<*> = mount( + , + ); + expectIsEmpty(getPlaceholderStyle(wrapper)); + + wrapper.unmount(); + jest.runOnlyPendingTimers(); + + // an internal setState would be triggered the timer was + // not cleared when unmounting + expect(console.error).not.toHaveBeenCalled(); + console.error.mockRestore(); +}); diff --git a/test/unit/view/placeholder/on-transition-end.spec.js b/test/unit/view/placeholder/on-transition-end.spec.js index 5e0c567a16..3135ec114f 100644 --- a/test/unit/view/placeholder/on-transition-end.spec.js +++ b/test/unit/view/placeholder/on-transition-end.spec.js @@ -1,11 +1,14 @@ // @flow import React from 'react'; import { mount, type ReactWrapper } from 'enzyme'; +import { act } from 'react-dom/test-utils'; import Placeholder from './util/placeholder-with-class'; import { expectIsFull } from './util/expect'; import getPlaceholderStyle from './util/get-placeholder-style'; import { placeholder } from './util/data'; +jest.useFakeTimers(); + it('should only fire a single transitionend event a single time when transitioning multiple properties', () => { const onTransitionEnd = jest.fn(); const onClose = jest.fn(); @@ -19,6 +22,10 @@ it('should only fire a single transitionend event a single time when transitioni styleContext="hey" />, ); + // finish the animate open timer + act(() => { + jest.runOnlyPendingTimers(); + }); // let enzyme know that the react tree has changed due to the set state wrapper.update(); expectIsFull(getPlaceholderStyle(wrapper)); From a4bcb5416216f96ebcf21daa158d79347d05e1c6 Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Wed, 27 Mar 2019 16:59:47 +1100 Subject: [PATCH 074/117] toggling is enabled for droppable --- .../use-droppable-dimension-publisher.js | 89 +++++++++---------- 1 file changed, 41 insertions(+), 48 deletions(-) diff --git a/src/view/use-droppable-dimension-publisher/use-droppable-dimension-publisher.js b/src/view/use-droppable-dimension-publisher/use-droppable-dimension-publisher.js index e78a085c85..9a58f78fe9 100644 --- a/src/view/use-droppable-dimension-publisher/use-droppable-dimension-publisher.js +++ b/src/view/use-droppable-dimension-publisher/use-droppable-dimension-publisher.js @@ -26,6 +26,7 @@ import withoutPlaceholder from './without-placeholder'; import { warning } from '../../dev-warning'; import getListenerOptions from './get-listener-options'; import useRequiredContext from '../use-required-context'; +import usePreviousRef from '../use-previous-ref'; type Props = {| droppableId: DroppableId, @@ -52,25 +53,13 @@ export default function useDroppableDimensionPublisher(args: Props) { const whileDraggingRef = useRef(null); const appContext: AppContextValue = useRequiredContext(AppContext); const marshal: DimensionMarshal = appContext.marshal; - - const { - direction, - droppableId, - type, - isDropDisabled, - isCombineEnabled, - ignoreContainerClipping, - getDroppableRef, - getPlaceholderRef, - } = args; - - const descriptor: DroppableDescriptor = useMemo( - (): DroppableDescriptor => ({ - id: droppableId, - type, - }), - [droppableId, type], - ); + const previousRef: { current: Props } = usePreviousRef(args); + const descriptor: DroppableDescriptor = useMemo((): DroppableDescriptor => { + return { + id: args.droppableId, + type: args.type, + }; + }, [args.droppableId, args.type]); const memoizedUpdateScroll = useCallback( (x: number, y: number) => { @@ -98,9 +87,9 @@ export default function useDroppableDimensionPublisher(args: Props) { memoizedUpdateScroll(scroll.x, scroll.y); }, [getClosestScroll, memoizedUpdateScroll]); - const scheduleScrollUpdate = useCallback(() => { - rafSchedule(updateScroll); - }, [updateScroll]); + const scheduleScrollUpdate = useMemo(() => rafSchedule(updateScroll), [ + updateScroll, + ]); const onClosestScroll = useCallback(() => { const dragging: ?WhileDragging = whileDraggingRef.current; @@ -124,7 +113,8 @@ export default function useDroppableDimensionPublisher(args: Props) { !whileDraggingRef.current, 'Cannot collect a droppable while a drag is occurring', ); - const ref: ?HTMLElement = getDroppableRef(); + const previous: Props = previousRef.current; + const ref: ?HTMLElement = previous.getDroppableRef(); invariant(ref, 'Cannot collect without a droppable ref'); const env: Env = getEnv(ref); @@ -142,10 +132,10 @@ export default function useDroppableDimensionPublisher(args: Props) { descriptor, env, windowScroll, - direction, - isDropDisabled, - isCombineEnabled, - shouldClipSubject: !ignoreContainerClipping, + direction: previous.direction, + isDropDisabled: previous.isDropDisabled, + isCombineEnabled: previous.isCombineEnabled, + shouldClipSubject: !previous.ignoreContainerClipping, }); if (env.closestScrollable) { @@ -164,18 +154,11 @@ export default function useDroppableDimensionPublisher(args: Props) { return dimension; }, - [ - descriptor, - direction, - getDroppableRef, - ignoreContainerClipping, - isCombineEnabled, - isDropDisabled, - onClosestScroll, - ], + [descriptor, onClosestScroll, previousRef], ); const recollect = useCallback( (options: RecollectDroppableOptions): DroppableDimension => { + console.log('creating recollect'); const dragging: ?WhileDragging = whileDraggingRef.current; const closest: ?Element = getClosestScrollableFromDrag(dragging); invariant( @@ -183,31 +166,27 @@ export default function useDroppableDimensionPublisher(args: Props) { 'Can only recollect Droppable client for Droppables that have a scroll container', ); + const previous: Props = previousRef.current; + const execute = (): DroppableDimension => getDimension({ ref: dragging.ref, descriptor: dragging.descriptor, env: dragging.env, windowScroll: origin, - direction, - isDropDisabled, - isCombineEnabled, - shouldClipSubject: !ignoreContainerClipping, + direction: previous.direction, + isDropDisabled: previous.isDropDisabled, + isCombineEnabled: previous.isCombineEnabled, + shouldClipSubject: !previous.ignoreContainerClipping, }); if (!options.withoutPlaceholder) { return execute(); } - return withoutPlaceholder(getPlaceholderRef(), execute); + return withoutPlaceholder(previous.getPlaceholderRef(), execute); }, - [ - direction, - getPlaceholderRef, - ignoreContainerClipping, - isCombineEnabled, - isDropDisabled, - ], + [previousRef], ); const dragStopped = useCallback(() => { const dragging: ?WhileDragging = whileDraggingRef.current; @@ -243,6 +222,7 @@ export default function useDroppableDimensionPublisher(args: Props) { }, []); const callbacks: DroppableCallbacks = useMemo(() => { + console.log('recreating callbacks'); return { getDimensionAndWatchScroll, recollect, @@ -266,4 +246,17 @@ export default function useDroppableDimensionPublisher(args: Props) { marshal.unregisterDroppable(descriptor); }; }, [callbacks, descriptor, dragStopped, marshal]); + + // update is enabled with the marshal + useLayoutEffect(() => { + marshal.updateDroppableIsEnabled(descriptor.id, !args.isDropDisabled); + }, [args.isDropDisabled, descriptor.id, marshal]); + + // update is combine enabled with the marshal + useLayoutEffect(() => { + marshal.updateDroppableIsCombineEnabled( + descriptor.id, + args.isCombineEnabled, + ); + }, [args.isCombineEnabled, descriptor.id, marshal]); } From 4eee1e3e1acd52aa8a8e684c28b3b01d124ae8b6 Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Wed, 27 Mar 2019 17:05:10 +1100 Subject: [PATCH 075/117] create ref --- src/state/middleware/lift.js | 1 + test/unit/view/drag-handle/contenteditable.spec.js | 3 ++- .../view/drag-handle/nested-drag-handles.spec.js | 3 ++- test/unit/view/drag-handle/throw-if-svg.spec.js | 3 ++- test/unit/view/drag-handle/util/wrappers.js | 12 ------------ .../registration.spec.js | 3 +-- .../droppable-dimension-publisher/util/shared.js | 9 ++++++++- test/utils/create-ref.js | 12 ++++++++++++ 8 files changed, 28 insertions(+), 18 deletions(-) create mode 100644 test/utils/create-ref.js diff --git a/src/state/middleware/lift.js b/src/state/middleware/lift.js index 129f579810..ab4c36d002 100644 --- a/src/state/middleware/lift.js +++ b/src/state/middleware/lift.js @@ -13,6 +13,7 @@ export default (marshal: DimensionMarshal) => ({ next(action); return; } + console.log('LIFT action'); const { id, clientSelection, movementMode } = action.payload; const initial: State = getState(); diff --git a/test/unit/view/drag-handle/contenteditable.spec.js b/test/unit/view/drag-handle/contenteditable.spec.js index a9e3e67168..fddeaa2104 100644 --- a/test/unit/view/drag-handle/contenteditable.spec.js +++ b/test/unit/view/drag-handle/contenteditable.spec.js @@ -3,7 +3,8 @@ import React from 'react'; import { mount } from 'enzyme'; import { forEach, type Control } from './util/controls'; import type { DragHandleProps } from '../../../../src/view/use-drag-handle/drag-handle-types'; -import { createRef, WithDragHandle } from './util/wrappers'; +import { WithDragHandle } from './util/wrappers'; +import createRef from '../../../utils/create-ref'; import { getStubCallbacks, callbacksCalled, diff --git a/test/unit/view/drag-handle/nested-drag-handles.spec.js b/test/unit/view/drag-handle/nested-drag-handles.spec.js index 8e8874936f..cf801f1695 100644 --- a/test/unit/view/drag-handle/nested-drag-handles.spec.js +++ b/test/unit/view/drag-handle/nested-drag-handles.spec.js @@ -8,7 +8,8 @@ import type { Callbacks, DragHandleProps, } from '../../../../src/view/use-drag-handle/drag-handle-types'; -import { createRef, Child, WithDragHandle } from './util/wrappers'; +import { Child, WithDragHandle } from './util/wrappers'; +import createRef from '../../../utils/create-ref'; import AppContext from '../../../../src/view/context/app-context'; const getNestedWrapper = ( diff --git a/test/unit/view/drag-handle/throw-if-svg.spec.js b/test/unit/view/drag-handle/throw-if-svg.spec.js index f39695fe54..6e32876f18 100644 --- a/test/unit/view/drag-handle/throw-if-svg.spec.js +++ b/test/unit/view/drag-handle/throw-if-svg.spec.js @@ -3,7 +3,8 @@ import React from 'react'; import { mount } from 'enzyme'; import type { DragHandleProps } from '../../../../src/view/use-drag-handle/drag-handle-types'; import { getStubCallbacks } from './util/callbacks'; -import { WithDragHandle, createRef } from './util/wrappers'; +import { WithDragHandle } from './util/wrappers'; +import createRef from '../../../utils/create-ref'; import AppContext from '../../../../src/view/context/app-context'; import basicContext from './util/app-context'; diff --git a/test/unit/view/drag-handle/util/wrappers.js b/test/unit/view/drag-handle/util/wrappers.js index d98692a628..32cdf17b04 100644 --- a/test/unit/view/drag-handle/util/wrappers.js +++ b/test/unit/view/drag-handle/util/wrappers.js @@ -34,18 +34,6 @@ export class Child extends React.Component { } } -export const createRef = () => { - let ref: ?HTMLElement = null; - - const setRef = (supplied: ?HTMLElement) => { - ref = supplied; - }; - - const getRef = (): ?HTMLElement => ref; - - return { ref, setRef, getRef }; -}; - type WithDragHandleProps = {| ...Args, children: (value: ?DragHandleProps) => Node | null, diff --git a/test/unit/view/droppable-dimension-publisher/registration.spec.js b/test/unit/view/droppable-dimension-publisher/registration.spec.js index cb396a747f..6b426f4a54 100644 --- a/test/unit/view/droppable-dimension-publisher/registration.spec.js +++ b/test/unit/view/droppable-dimension-publisher/registration.spec.js @@ -10,7 +10,6 @@ import type { DroppableDimension } from '../../../../src/types'; import getWindowScroll from '../../../../src/view/window/get-window-scroll'; import { getMarshalStub } from '../../../utils/dimension-marshal'; import forceUpdate from '../../../utils/force-update'; -import { withDimensionMarshal } from '../../../utils/get-context-options'; import { preset, scheduled, ScrollableItem } from './util/shared'; import { setViewport } from '../../../utils/viewport'; @@ -19,7 +18,7 @@ setViewport(preset.viewport); it('should register itself when mounting', () => { const marshal: DimensionMarshal = getMarshalStub(); - mount(, withDimensionMarshal(marshal)); + mount(); expect(marshal.registerDroppable).toHaveBeenCalledTimes(1); expect(marshal.registerDroppable.mock.calls[0][0]).toEqual( diff --git a/test/unit/view/droppable-dimension-publisher/util/shared.js b/test/unit/view/droppable-dimension-publisher/util/shared.js index 546fbcba3e..7a87e2ffdf 100644 --- a/test/unit/view/droppable-dimension-publisher/util/shared.js +++ b/test/unit/view/droppable-dimension-publisher/util/shared.js @@ -2,8 +2,9 @@ /* eslint-disable react/no-multi-comp */ import { createBox, type Spacing, type BoxModel } from 'css-box-model'; import React, { Component } from 'react'; -import DroppableDimensionPublisher from '../../../../../src/view/droppable-dimension-publisher/droppable-dimension-publisher'; +import useDroppableDimensionPublisher from '../../../../../src/view/use-droppable-dimension-publisher/use-droppable-dimension-publisher'; import { getComputedSpacing, getPreset } from '../../../../utils/dimension'; +import { type DimensionMarshal } from '../../../../../src/state/dimension-marshal/dimension-marshal-types'; import type { ScrollOptions, DroppableId, @@ -73,8 +74,14 @@ type ScrollableItemProps = {| isCombineEnabled: boolean, droppableId: DroppableId, type: TypeId, + marshal: DimensionMarshal, |}; +function ScrollableItem(props: ScrollableItemProps) { + const +} + + export class ScrollableItem extends React.Component { static defaultProps = { isScrollable: true, diff --git a/test/utils/create-ref.js b/test/utils/create-ref.js new file mode 100644 index 0000000000..95e0c6ff0c --- /dev/null +++ b/test/utils/create-ref.js @@ -0,0 +1,12 @@ +// @flow +export default function createRef() { + let ref: ?HTMLElement = null; + + const setRef = (supplied: ?HTMLElement) => { + ref = supplied; + }; + + const getRef = (): ?HTMLElement => ref; + + return { ref, setRef, getRef }; +} From 703303d698f9ad78a2391f9ba5d9dab888263391 Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Wed, 27 Mar 2019 17:05:40 +1100 Subject: [PATCH 076/117] adding missing import --- test/unit/view/drag-handle/util/wrappers.js | 1 + 1 file changed, 1 insertion(+) diff --git a/test/unit/view/drag-handle/util/wrappers.js b/test/unit/view/drag-handle/util/wrappers.js index 32cdf17b04..0b65f6775d 100644 --- a/test/unit/view/drag-handle/util/wrappers.js +++ b/test/unit/view/drag-handle/util/wrappers.js @@ -11,6 +11,7 @@ import basicContext from './app-context'; import AppContext, { type AppContextValue, } from '../../../../../src/view/context/app-context'; +import createRef from '../../../../utils/create-ref'; type ChildProps = {| dragHandleProps: ?DragHandleProps, From a55951909de982193a40a54e6561f63926886583 Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Wed, 27 Mar 2019 21:56:22 +1100 Subject: [PATCH 077/117] starting to fix droppable dimension tests --- .../dimension-marshal-types.js | 10 +- .../dimension-marshal/dimension-marshal.js | 50 ++--- .../use-droppable-dimension-publisher.js | 9 +- .../registration.spec.js | 76 +++++--- .../util/shared.js | 182 ++++++++++++------ test/utils/pass-through-props.jsx | 12 ++ 6 files changed, 217 insertions(+), 122 deletions(-) create mode 100644 test/utils/pass-through-props.jsx diff --git a/src/state/dimension-marshal/dimension-marshal-types.js b/src/state/dimension-marshal/dimension-marshal-types.js index 4e08767d22..6101dd027f 100644 --- a/src/state/dimension-marshal/dimension-marshal-types.js +++ b/src/state/dimension-marshal/dimension-marshal-types.js @@ -90,11 +90,11 @@ export type DimensionMarshal = {| descriptor: DroppableDescriptor, callbacks: DroppableCallbacks, ) => void, - updateDroppable: ( - previous: DroppableDescriptor, - descriptor: DroppableDescriptor, - callbacks: DroppableCallbacks, - ) => void, + // updateDroppable: ( + // previous: DroppableDescriptor, + // descriptor: DroppableDescriptor, + // callbacks: DroppableCallbacks, + // ) => void, // it is possible for a droppable to change whether it is enabled during a drag updateDroppableIsEnabled: (id: DroppableId, isEnabled: boolean) => void, // it is also possible to update whether combining is enabled diff --git a/src/state/dimension-marshal/dimension-marshal.js b/src/state/dimension-marshal/dimension-marshal.js index 85e47d0ce7..62d6963bf2 100644 --- a/src/state/dimension-marshal/dimension-marshal.js +++ b/src/state/dimension-marshal/dimension-marshal.js @@ -164,30 +164,30 @@ export default (callbacks: Callbacks) => { invariant(!collection, 'Cannot add a Droppable during a drag'); }; - const updateDroppable = ( - previous: DroppableDescriptor, - descriptor: DroppableDescriptor, - droppableCallbacks: DroppableCallbacks, - ) => { - invariant( - entries.droppables[previous.id], - 'Cannot update droppable registration as no previous registration was found', - ); - - // The id might have changed, so we are removing the old entry - delete entries.droppables[previous.id]; - - const entry: DroppableEntry = { - descriptor, - callbacks: droppableCallbacks, - }; - entries.droppables[descriptor.id] = entry; - - invariant( - !collection, - 'You are not able to update the id or type of a droppable during a drag', - ); - }; + // const updateDroppable = ( + // previous: DroppableDescriptor, + // descriptor: DroppableDescriptor, + // droppableCallbacks: DroppableCallbacks, + // ) => { + // invariant( + // entries.droppables[previous.id], + // 'Cannot update droppable registration as no previous registration was found', + // ); + + // // The id might have changed, so we are removing the old entry + // delete entries.droppables[previous.id]; + + // const entry: DroppableEntry = { + // descriptor, + // callbacks: droppableCallbacks, + // }; + // entries.droppables[descriptor.id] = entry; + + // invariant( + // !collection, + // 'You are not able to update the id or type of a droppable during a drag', + // ); + // }; const unregisterDroppable = (descriptor: DroppableDescriptor) => { const entry: ?DroppableEntry = entries.droppables[descriptor.id]; @@ -328,7 +328,7 @@ export default (callbacks: Callbacks) => { updateDraggable, unregisterDraggable, registerDroppable, - updateDroppable, + // updateDroppable, unregisterDroppable, // droppable changes diff --git a/src/view/use-droppable-dimension-publisher/use-droppable-dimension-publisher.js b/src/view/use-droppable-dimension-publisher/use-droppable-dimension-publisher.js index 9a58f78fe9..5acdb634b1 100644 --- a/src/view/use-droppable-dimension-publisher/use-droppable-dimension-publisher.js +++ b/src/view/use-droppable-dimension-publisher/use-droppable-dimension-publisher.js @@ -60,6 +60,8 @@ export default function useDroppableDimensionPublisher(args: Props) { type: args.type, }; }, [args.droppableId, args.type]); + // const lastDescriptorRef = usePreviousRef(descriptor); + // console.log('lastDescriptorREF.CURRENT', lastDescriptorRef.current); const memoizedUpdateScroll = useCallback( (x: number, y: number) => { @@ -158,7 +160,6 @@ export default function useDroppableDimensionPublisher(args: Props) { ); const recollect = useCallback( (options: RecollectDroppableOptions): DroppableDimension => { - console.log('creating recollect'); const dragging: ?WhileDragging = whileDraggingRef.current; const closest: ?Element = getClosestScrollableFromDrag(dragging); invariant( @@ -235,13 +236,17 @@ export default function useDroppableDimensionPublisher(args: Props) { // - any descriptor changes // - when it unmounts useLayoutEffect(() => { + console.log('registering', descriptor); marshal.registerDroppable(descriptor, callbacks); return () => { if (whileDraggingRef.current) { - warning('Unmounting Droppable while a drag is occurring'); + warning( + 'Unsupported: changing the droppableId or type of a Droppable during a drag', + ); dragStopped(); } + console.log('goodbye droppable', descriptor); marshal.unregisterDroppable(descriptor); }; diff --git a/test/unit/view/droppable-dimension-publisher/registration.spec.js b/test/unit/view/droppable-dimension-publisher/registration.spec.js index 6b426f4a54..b2f8dafced 100644 --- a/test/unit/view/droppable-dimension-publisher/registration.spec.js +++ b/test/unit/view/droppable-dimension-publisher/registration.spec.js @@ -6,19 +6,29 @@ import type { DimensionMarshal, DroppableCallbacks, } from '../../../../src/state/dimension-marshal/dimension-marshal-types'; -import type { DroppableDimension } from '../../../../src/types'; +import type { DroppableDescriptor } from '../../../../src/types'; import getWindowScroll from '../../../../src/view/window/get-window-scroll'; import { getMarshalStub } from '../../../utils/dimension-marshal'; import forceUpdate from '../../../utils/force-update'; -import { preset, scheduled, ScrollableItem } from './util/shared'; +import { + preset, + scheduled, + ScrollableItem, + WithAppContext, +} from './util/shared'; import { setViewport } from '../../../utils/viewport'; +import PassThroughProps from '../../../utils/pass-through-props'; setViewport(preset.viewport); it('should register itself when mounting', () => { const marshal: DimensionMarshal = getMarshalStub(); - mount(); + mount( + + + , + ); expect(marshal.registerDroppable).toHaveBeenCalledTimes(1); expect(marshal.registerDroppable.mock.calls[0][0]).toEqual( @@ -29,7 +39,11 @@ it('should register itself when mounting', () => { it('should unregister itself when unmounting', () => { const marshal: DimensionMarshal = getMarshalStub(); - const wrapper = mount(, withDimensionMarshal(marshal)); + const wrapper = mount( + + + , + ); expect(marshal.registerDroppable).toHaveBeenCalled(); expect(marshal.unregisterDroppable).not.toHaveBeenCalled(); @@ -43,49 +57,53 @@ it('should unregister itself when unmounting', () => { it('should update its registration when a descriptor property changes', () => { const marshal: DimensionMarshal = getMarshalStub(); - const wrapper = mount(, withDimensionMarshal(marshal)); + const wrapper = mount( + + {extra => ( + + + + )} + , + ); // asserting shape of original publish expect(marshal.registerDroppable.mock.calls[0][0]).toEqual( preset.home.descriptor, ); - const original: DroppableDimension = marshal.registerDroppable.mock.calls[0][1].getDimensionAndWatchScroll( - getWindowScroll(), - scheduled, - ); + marshal.registerDroppable.mockClear(); // updating the index wrapper.setProps({ droppableId: 'some-new-id', }); - const updated: DroppableDimension = { - ...original, - descriptor: { - ...original.descriptor, - id: 'some-new-id', - }, + const updated: DroppableDescriptor = { + ...preset.home.descriptor, + id: 'some-new-id', }; - expect(marshal.updateDroppable).toHaveBeenCalledTimes(1); - expect(marshal.updateDroppable).toHaveBeenCalledWith( + + // old descriptor removed + expect(marshal.unregisterDroppable).toHaveBeenCalledTimes(1); + expect(marshal.unregisterDroppable).toHaveBeenCalledWith( preset.home.descriptor, - updated.descriptor, - // Droppable callbacks - expect.any(Object), ); - // should now return a dimension with the correct descriptor - const callbacks: DroppableCallbacks = - marshal.updateDroppable.mock.calls[0][2]; - callbacks.dragStopped(); - expect( - callbacks.getDimensionAndWatchScroll(preset.windowScroll, scheduled), - ).toEqual(updated); + + // new descriptor added + expect(marshal.registerDroppable.mock.calls[0][0]).toEqual(updated); }); it('should not update its registration when a descriptor property does not change on an update', () => { const marshal: DimensionMarshal = getMarshalStub(); - const wrapper = mount(, withDimensionMarshal(marshal)); + const wrapper = mount( + + + , + ); expect(marshal.registerDroppable).toHaveBeenCalledTimes(1); + expect(marshal.unregisterDroppable).not.toHaveBeenCalled(); + marshal.registerDroppable.mockClear(); forceUpdate(wrapper); - expect(marshal.updateDroppable).not.toHaveBeenCalled(); + expect(marshal.unregisterDroppable).not.toHaveBeenCalled(); + expect(marshal.registerDroppable).not.toHaveBeenCalled(); }); diff --git a/test/unit/view/droppable-dimension-publisher/util/shared.js b/test/unit/view/droppable-dimension-publisher/util/shared.js index 7a87e2ffdf..1b4ce3e87e 100644 --- a/test/unit/view/droppable-dimension-publisher/util/shared.js +++ b/test/unit/view/droppable-dimension-publisher/util/shared.js @@ -1,7 +1,7 @@ // @flow /* eslint-disable react/no-multi-comp */ import { createBox, type Spacing, type BoxModel } from 'css-box-model'; -import React, { Component } from 'react'; +import React, { useMemo, type Node } from 'react'; import useDroppableDimensionPublisher from '../../../../../src/view/use-droppable-dimension-publisher/use-droppable-dimension-publisher'; import { getComputedSpacing, getPreset } from '../../../../utils/dimension'; import { type DimensionMarshal } from '../../../../../src/state/dimension-marshal/dimension-marshal-types'; @@ -11,6 +11,10 @@ import type { DroppableDescriptor, TypeId, } from '../../../../../src/types'; +import createRef from '../../../../utils/create-ref'; +import AppContext, { + type AppContextValue, +} from '../../../../../src/view/context/app-context'; export const scheduled: ScrollOptions = { shouldPublishImmediately: false, @@ -67,75 +71,131 @@ const withSpacing = getComputedSpacing({ padding, margin, border }); export const descriptor: DroppableDescriptor = preset.home.descriptor; -type ScrollableItemProps = {| - // scrollable item prop (default: false) - isScrollable: boolean, - isDropDisabled: boolean, - isCombineEnabled: boolean, - droppableId: DroppableId, - type: TypeId, +// static defaultProps = { +// isScrollable: true, +// type: descriptor.type, +// droppableId: descriptor.id, +// isDropDisabled: false, +// isCombineEnabled: false, +// }; + +type WithAppContextProps = {| marshal: DimensionMarshal, + children: Node, |}; -function ScrollableItem(props: ScrollableItemProps) { - const +export function WithAppContext(props: WithAppContextProps) { + const context: AppContextValue = useMemo( + () => ({ + marshal: props.marshal, + style: 'fake', + canLift: () => true, + isMovementAllowed: () => true, + }), + [props.marshal], + ); + + return ( + {props.children} + ); } +type ScrollableItemProps = {| + type?: TypeId, + isScrollable?: boolean, + isDropDisabled?: boolean, + isCombineEnabled?: boolean, + droppableId?: DroppableId, +|}; -export class ScrollableItem extends React.Component { - static defaultProps = { - isScrollable: true, - type: descriptor.type, - droppableId: descriptor.id, - isDropDisabled: false, - isCombineEnabled: false, - }; - /* eslint-disable react/sort-comp */ - ref: ?HTMLElement; - placeholderRef: ?HTMLElement; +export function ScrollableItem(props: ScrollableItemProps) { + const droppableRef = createRef(); + const placeholderRef = createRef(); - setRef = (ref: ?HTMLElement) => { - this.ref = ref; - }; + useDroppableDimensionPublisher({ + droppableId: props.droppableId || descriptor.id, + type: props.type || descriptor.type, + direction: preset.home.axis.direction, + isDropDisabled: props.isDropDisabled || false, + ignoreContainerClipping: false, + getDroppableRef: droppableRef.getRef, + getPlaceholderRef: placeholderRef.getRef, + isCombineEnabled: props.isCombineEnabled || false, + }); - setPlaceholderRef = (ref: ?HTMLElement) => { - this.placeholderRef = ref; - }; + return ( +
+ hi +
+
+ ); +} - getRef = (): ?HTMLElement => this.ref; - getPlaceholderRef = (): ?HTMLElement => this.placeholderRef; +// export class ScrollableItem extends React.Component { +// static defaultProps = { +// isScrollable: true, +// type: descriptor.type, +// droppableId: descriptor.id, +// isDropDisabled: false, +// isCombineEnabled: false, +// }; +// /* eslint-disable react/sort-comp */ +// ref: ?HTMLElement; +// placeholderRef: ?HTMLElement; - render() { - return ( - -
- hi -
-
- - ); - } -} +// setRef = (ref: ?HTMLElement) => { +// this.ref = ref; +// }; + +// setPlaceholderRef = (ref: ?HTMLElement) => { +// this.placeholderRef = ref; +// }; + +// getRef = (): ?HTMLElement => this.ref; +// getPlaceholderRef = (): ?HTMLElement => this.placeholderRef; + +// render() { +// return ( +// +//
+// hi +//
+//
+// +// ); +// } +// } type AppProps = {| droppableIsScrollable: boolean, @@ -144,7 +204,7 @@ type AppProps = {| showPlaceholder: boolean, |}; -export class App extends Component { +export class App extends React.Component { ref: ?HTMLElement; placeholderRef: ?HTMLElement; diff --git a/test/utils/pass-through-props.jsx b/test/utils/pass-through-props.jsx new file mode 100644 index 0000000000..b05c86b05c --- /dev/null +++ b/test/utils/pass-through-props.jsx @@ -0,0 +1,12 @@ +// @flow +import { type Node } from 'react'; + +type Props = {| + ...T, + children: (value: T) => Node, +|}; + +export default function PassThroughProps(props: Props<*>) { + const { children, ...rest } = props; + return children(rest); +} From bd505401bccbc4e08bc0aabaf1e7b76b68d0db00 Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Thu, 28 Mar 2019 08:53:23 +1100 Subject: [PATCH 078/117] handling updates for use-draggable-dimension-publisher --- .../use-draggable-dimension-publisher.js | 29 ++++++++++++++----- .../use-droppable-dimension-publisher.js | 16 +++++----- .../registration.spec.js | 13 ++------- .../use-draggable-dimension-publisher.spec.js | 14 +++++---- 4 files changed, 41 insertions(+), 31 deletions(-) diff --git a/src/view/use-draggable-dimension-publisher/use-draggable-dimension-publisher.js b/src/view/use-draggable-dimension-publisher/use-draggable-dimension-publisher.js index 5d941ca4e2..9f5e8a3ba0 100644 --- a/src/view/use-draggable-dimension-publisher/use-draggable-dimension-publisher.js +++ b/src/view/use-draggable-dimension-publisher/use-draggable-dimension-publisher.js @@ -1,5 +1,5 @@ // @flow -import { useMemo, useCallback, useLayoutEffect } from 'react'; +import { useMemo, useRef, useCallback, useLayoutEffect } from 'react'; import { type Position } from 'css-box-model'; import invariant from 'tiny-invariant'; import type { @@ -43,19 +43,34 @@ export default function useDraggableDimensionPublisher(args: Args) { return result; }, [draggableId, droppableId, index, type]); + const publishedDescriptorRef = useRef(descriptor); + const makeDimension = useCallback( (windowScroll?: Position): DraggableDimension => { + const latest: DraggableDescriptor = publishedDescriptorRef.current; const el: ?HTMLElement = getDraggableRef(); invariant(el, 'Cannot get dimension when no ref is set'); - return getDimension(descriptor, el, windowScroll); + return getDimension(latest, el, windowScroll); }, - [descriptor, getDraggableRef], + [getDraggableRef], ); - // Communicate with the marshal - // TODO: should it be an "update"? + // handle mounting / unmounting + useLayoutEffect(() => { + marshal.registerDraggable(publishedDescriptorRef.current, makeDimension); + return () => marshal.unregisterDraggable(publishedDescriptorRef.current); + }, [makeDimension, marshal]); + + // handle updates to descriptor useLayoutEffect(() => { - marshal.registerDraggable(descriptor, makeDimension); - return () => marshal.unregisterDraggable(descriptor); + // this will happen when mounting + if (publishedDescriptorRef.current === descriptor) { + return; + } + + const previous: DraggableDescriptor = publishedDescriptorRef.current; + publishedDescriptorRef.current = descriptor; + + marshal.updateDraggable(previous, descriptor, makeDimension); }, [descriptor, makeDimension, marshal]); } diff --git a/src/view/use-droppable-dimension-publisher/use-droppable-dimension-publisher.js b/src/view/use-droppable-dimension-publisher/use-droppable-dimension-publisher.js index 5acdb634b1..53ea5d6121 100644 --- a/src/view/use-droppable-dimension-publisher/use-droppable-dimension-publisher.js +++ b/src/view/use-droppable-dimension-publisher/use-droppable-dimension-publisher.js @@ -60,8 +60,7 @@ export default function useDroppableDimensionPublisher(args: Props) { type: args.type, }; }, [args.droppableId, args.type]); - // const lastDescriptorRef = usePreviousRef(descriptor); - // console.log('lastDescriptorREF.CURRENT', lastDescriptorRef.current); + const publishedDescriptorRef = useRef(descriptor); const memoizedUpdateScroll = useCallback( (x: number, y: number) => { @@ -236,7 +235,7 @@ export default function useDroppableDimensionPublisher(args: Props) { // - any descriptor changes // - when it unmounts useLayoutEffect(() => { - console.log('registering', descriptor); + publishedDescriptorRef.current = descriptor; marshal.registerDroppable(descriptor, callbacks); return () => { @@ -254,14 +253,17 @@ export default function useDroppableDimensionPublisher(args: Props) { // update is enabled with the marshal useLayoutEffect(() => { - marshal.updateDroppableIsEnabled(descriptor.id, !args.isDropDisabled); - }, [args.isDropDisabled, descriptor.id, marshal]); + marshal.updateDroppableIsEnabled( + publishedDescriptorRef.current.id, + !args.isDropDisabled, + ); + }, [args.isDropDisabled, marshal]); // update is combine enabled with the marshal useLayoutEffect(() => { marshal.updateDroppableIsCombineEnabled( - descriptor.id, + publishedDescriptorRef.current.id, args.isCombineEnabled, ); - }, [args.isCombineEnabled, descriptor.id, marshal]); + }, [args.isCombineEnabled, marshal]); } diff --git a/test/unit/view/droppable-dimension-publisher/registration.spec.js b/test/unit/view/droppable-dimension-publisher/registration.spec.js index b2f8dafced..a863146d80 100644 --- a/test/unit/view/droppable-dimension-publisher/registration.spec.js +++ b/test/unit/view/droppable-dimension-publisher/registration.spec.js @@ -2,20 +2,11 @@ /* eslint-disable react/no-multi-comp */ import { mount } from 'enzyme'; import React from 'react'; -import type { - DimensionMarshal, - DroppableCallbacks, -} from '../../../../src/state/dimension-marshal/dimension-marshal-types'; +import type { DimensionMarshal } from '../../../../src/state/dimension-marshal/dimension-marshal-types'; import type { DroppableDescriptor } from '../../../../src/types'; -import getWindowScroll from '../../../../src/view/window/get-window-scroll'; import { getMarshalStub } from '../../../utils/dimension-marshal'; import forceUpdate from '../../../utils/force-update'; -import { - preset, - scheduled, - ScrollableItem, - WithAppContext, -} from './util/shared'; +import { preset, ScrollableItem, WithAppContext } from './util/shared'; import { setViewport } from '../../../utils/viewport'; import PassThroughProps from '../../../utils/pass-through-props'; diff --git a/test/unit/view/use-draggable-dimension-publisher.spec.js b/test/unit/view/use-draggable-dimension-publisher.spec.js index 5f0a57d579..7fcfcc62bc 100644 --- a/test/unit/view/use-draggable-dimension-publisher.spec.js +++ b/test/unit/view/use-draggable-dimension-publisher.spec.js @@ -1,5 +1,5 @@ // @flow -import React, { useRef, useCallback, type Node } from 'react'; +import React, { useRef, useCallback } from 'react'; import invariant from 'tiny-invariant'; import { type Spacing, type Rect } from 'css-box-model'; import { mount, type ReactWrapper } from 'enzyme'; @@ -135,6 +135,8 @@ describe('dimension registration', () => { expect(marshal.registerDraggable.mock.calls[0][0]).toEqual( preset.inHome1.descriptor, ); + marshal.registerDraggable.mockClear(); + marshal.registerDroppable.mockClear(); // updating the index wrapper.setProps({ @@ -145,15 +147,15 @@ describe('dimension registration', () => { index: 1000, }; - // Old descriptor unregistered - expect(marshal.unregisterDraggable).toHaveBeenCalledWith( + // Descriptor updated + expect(marshal.updateDraggable).toHaveBeenCalledWith( preset.inHome1.descriptor, - ); - // New descriptor registered - expect(marshal.registerDraggable).toHaveBeenCalledWith( newDescriptor, expect.any(Function), ); + // Nothing else changed + expect(marshal.registerDraggable).not.toHaveBeenCalled(); + expect(marshal.unregisterDraggable).not.toHaveBeenCalled(); }); it('should not update its registration when a descriptor property does not change on an update', () => { From 711a4c2b0e2c66022a65bbc8866cc66fb055ab00 Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Thu, 28 Mar 2019 10:10:13 +1100 Subject: [PATCH 079/117] fixing droppable tests --- .../publishing.spec.js | 46 ++++++++++--------- 1 file changed, 25 insertions(+), 21 deletions(-) diff --git a/test/unit/view/droppable-dimension-publisher/publishing.spec.js b/test/unit/view/droppable-dimension-publisher/publishing.spec.js index 2040a8f812..9edef89fcb 100644 --- a/test/unit/view/droppable-dimension-publisher/publishing.spec.js +++ b/test/unit/view/droppable-dimension-publisher/publishing.spec.js @@ -12,11 +12,11 @@ import { negate } from '../../../../src/state/position'; import { offsetByPosition } from '../../../../src/state/spacing'; import { getDroppableDimension } from '../../../utils/dimension'; import { getMarshalStub } from '../../../utils/dimension-marshal'; -import { withDimensionMarshal } from '../../../utils/get-context-options'; import setWindowScroll from '../../../utils/set-window-scroll'; import { App, ScrollableItem, + WithAppContext, scheduled, immediate, preset, @@ -29,6 +29,7 @@ import { } from './util/shared'; import { setViewport } from '../../../utils/viewport'; import tryCleanPrototypeStubs from '../../../utils/try-clean-prototype-stubs'; +import PassThroughProps from '../../../utils/pass-through-props'; beforeEach(() => { setViewport(preset.viewport); @@ -38,7 +39,7 @@ afterEach(() => { tryCleanPrototypeStubs(); }); -it('should publish the dimensions of the target', () => { +it.only('should publish the dimensions of the target', () => { const marshal: DimensionMarshal = getMarshalStub(); const expected: DroppableDimension = getDroppableDimension({ descriptor: { @@ -52,14 +53,15 @@ it('should publish the dimensions of the target', () => { windowScroll: { x: 0, y: 0 }, }); const wrapper: ReactWrapper<*> = mount( - , - withDimensionMarshal(marshal), + + + , ); - const el: ?HTMLElement = wrapper.instance().getRef(); + const el: ?HTMLElement = wrapper.getDOMNode(); invariant(el); jest .spyOn(el, 'getBoundingClientRect') @@ -81,7 +83,7 @@ it('should publish the dimensions of the target', () => { expect(result.client.padding).toEqual(padding); }); -it('should consider the window scroll when calculating dimensions', () => { +it.only('should consider the window scroll when calculating dimensions', () => { const marshal: DimensionMarshal = getMarshalStub(); const windowScroll: Position = { x: 500, @@ -101,14 +103,15 @@ it('should consider the window scroll when calculating dimensions', () => { }); const wrapper: ReactWrapper<*> = mount( - , - withDimensionMarshal(marshal), + + + , ); - const el: ?HTMLElement = wrapper.instance().getRef(); + const el: ?HTMLElement = wrapper.getDOMNode(); invariant(el); jest .spyOn(el, 'getBoundingClientRect') @@ -127,7 +130,7 @@ it('should consider the window scroll when calculating dimensions', () => { }); describe('no closest scrollable', () => { - it('should return null for the closest scrollable if there is no scroll container', () => { + it.only('should return null for the closest scrollable if there is no scroll container', () => { const expected: DroppableDimension = getDroppableDimension({ descriptor, borderBox: bigClient.borderBox, @@ -138,10 +141,11 @@ describe('no closest scrollable', () => { }); const marshal: DimensionMarshal = getMarshalStub(); const wrapper = mount( - , - withDimensionMarshal(marshal), + + + , ); - const el: ?HTMLElement = wrapper.instance().getRef(); + const el: ?HTMLElement = wrapper.getDOMNode(); invariant(el); jest .spyOn(el, 'getBoundingClientRect') From e557764c26526f16e9b0f3d20c1eac0d49ab4126 Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Thu, 28 Mar 2019 10:21:30 +1100 Subject: [PATCH 080/117] test push forward --- .../util/shared.js | 185 ++++++------------ 1 file changed, 56 insertions(+), 129 deletions(-) diff --git a/test/unit/view/droppable-dimension-publisher/util/shared.js b/test/unit/view/droppable-dimension-publisher/util/shared.js index 1b4ce3e87e..ff3c739ef5 100644 --- a/test/unit/view/droppable-dimension-publisher/util/shared.js +++ b/test/unit/view/droppable-dimension-publisher/util/shared.js @@ -142,141 +142,68 @@ export function ScrollableItem(props: ScrollableItemProps) { ); } -// export class ScrollableItem extends React.Component { -// static defaultProps = { -// isScrollable: true, -// type: descriptor.type, -// droppableId: descriptor.id, -// isDropDisabled: false, -// isCombineEnabled: false, -// }; -// /* eslint-disable react/sort-comp */ -// ref: ?HTMLElement; -// placeholderRef: ?HTMLElement; - -// setRef = (ref: ?HTMLElement) => { -// this.ref = ref; -// }; - -// setPlaceholderRef = (ref: ?HTMLElement) => { -// this.placeholderRef = ref; -// }; - -// getRef = (): ?HTMLElement => this.ref; -// getPlaceholderRef = (): ?HTMLElement => this.placeholderRef; - -// render() { -// return ( -// -//
-// hi -//
-//
-// -// ); -// } -// } - type AppProps = {| - droppableIsScrollable: boolean, - parentIsScrollable: boolean, - ignoreContainerClipping: boolean, - showPlaceholder: boolean, + droppableIsScrollable?: boolean, + parentIsScrollable?: boolean, + ignoreContainerClipping?: boolean, + showPlaceholder?: boolean, |}; -export class App extends React.Component { - ref: ?HTMLElement; - placeholderRef: ?HTMLElement; - - static defaultProps = { - ignoreContainerClipping: false, - droppableIsScrollable: false, - parentIsScrollable: false, - showPlaceholder: false, - }; - - setRef = (ref: ?HTMLElement) => { - this.ref = ref; - }; +export function App(props: AppProps) { + const droppableRef = createRef(); + const placeholderRef = createRef(); - setPlaceholderRef = (ref: ?HTMLElement) => { - this.placeholderRef = ref; - }; + const { + droppableIsScrollable = false, + parentIsScrollable = false, + ignoreContainerClipping = false, + showPlaceholder = false, + } = props; - getRef = (): ?HTMLElement => this.ref; - getPlaceholderRef = (): ?HTMLElement => this.placeholderRef; + useDroppableDimensionPublisher({ + droppableId: descriptor.id, + direction: 'vertical', + isDropDisabled: false, + isCombineEnabled: false, + type: descriptor.type, + ignoreContainerClipping, + getDroppableRef: droppableRef.getRef, + getPlaceholderRef: placeholderRef.getRef, + }); - render() { - const { - droppableIsScrollable, - parentIsScrollable, - ignoreContainerClipping, - } = this.props; - return ( -
-
-
- -
hello world
- {this.props.showPlaceholder ? ( -
- ) : null} - -
+ return ( +
+
+
+
hello world
+ {showPlaceholder ? ( +
+ ) : null}
- ); - } +
+ ); } From 45419374a83e7b2e5166f330f62361febef59e66 Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Thu, 28 Mar 2019 10:50:31 +1100 Subject: [PATCH 081/117] refactoring error boundary --- src/view/drag-drop-context/app.jsx | 164 ++++++++++++++++++ .../drag-drop-context/drag-drop-context.jsx | 154 ++-------------- src/view/error-boundary/error-boundary.jsx | 21 ++- 3 files changed, 190 insertions(+), 149 deletions(-) create mode 100644 src/view/drag-drop-context/app.jsx diff --git a/src/view/drag-drop-context/app.jsx b/src/view/drag-drop-context/app.jsx new file mode 100644 index 0000000000..278c6fb72b --- /dev/null +++ b/src/view/drag-drop-context/app.jsx @@ -0,0 +1,164 @@ +// @flow +import React, { useEffect, useLayoutEffect, useRef, type Node } from 'react'; +import { bindActionCreators } from 'redux'; +import { Provider } from 'react-redux'; +import createStore from '../../state/create-store'; +import createDimensionMarshal from '../../state/dimension-marshal/dimension-marshal'; +import canStartDrag from '../../state/can-start-drag'; +import scrollWindow from '../window/scroll-window'; +import createAutoScroller from '../../state/auto-scroller'; +import useStyleMarshal from '../use-style-marshal/use-style-marshal'; +import type { AutoScroller } from '../../state/auto-scroller/auto-scroller-types'; +import type { StyleMarshal } from '../use-style-marshal/style-marshal-types'; +import type { + DimensionMarshal, + Callbacks as DimensionMarshalCallbacks, +} from '../../state/dimension-marshal/dimension-marshal-types'; +import type { DraggableId, State, Responders, Announce } from '../../types'; +import type { Store, Action } from '../../state/store-types'; +import StoreContext from '../context/store-context'; +import { + clean, + move, + publishWhileDragging, + updateDroppableScroll, + updateDroppableIsEnabled, + updateDroppableIsCombineEnabled, + collectionStarting, +} from '../../state/action-creators'; +import isMovementAllowed from '../../state/is-movement-allowed'; +import useAnnouncer from '../use-announcer'; +import AppContext, { type AppContextValue } from '../context/app-context'; +import useStartupValidation from './use-startup-validation'; +import useMemoOne from '../use-custom-memo/use-memo-one'; +import useCallbackOne from '../use-custom-memo/use-callback-one'; +import { useConstant, useConstantFn } from '../use-constant'; + +type Props = {| + ...Responders, + uniqueId: number, + setOnError: (onError: Function) => void, + // we do not technically need any children for this component + children: Node | null, +|}; + +const createResponders = (props: Props): Responders => ({ + onBeforeDragStart: props.onBeforeDragStart, + onDragStart: props.onDragStart, + onDragEnd: props.onDragEnd, + onDragUpdate: props.onDragUpdate, +}); + +export default function App(props: Props) { + const { uniqueId, setOnError } = props; + // flow does not support MutableRefObject + // let storeRef: MutableRefObject; + let storeRef; + + useStartupValidation(); + + // lazy collection of responders using a ref - update on ever render + const lastPropsRef = useRef(props); + useEffect(() => { + lastPropsRef.current = props; + }); + + const getResponders: () => Responders = useConstantFn(() => { + return createResponders(lastPropsRef.current); + }); + + const announce: Announce = useAnnouncer(uniqueId); + const styleMarshal: StyleMarshal = useStyleMarshal(uniqueId); + + const lazyDispatch: Action => void = useConstantFn( + (action: Action): void => { + storeRef.current.dispatch(action); + }, + ); + + const callbacks: DimensionMarshalCallbacks = useConstant(() => + bindActionCreators( + { + publishWhileDragging, + updateDroppableScroll, + updateDroppableIsEnabled, + updateDroppableIsCombineEnabled, + collectionStarting, + }, + // $FlowFixMe - not sure why this is wrong + lazyDispatch, + ), + ); + const dimensionMarshal: DimensionMarshal = useConstant(() => + createDimensionMarshal(callbacks), + ); + + const autoScroller: AutoScroller = useConstant(() => + createAutoScroller({ + scrollWindow, + scrollDroppable: dimensionMarshal.scrollDroppable, + ...bindActionCreators( + { + move, + }, + // $FlowFixMe - not sure why this is wrong + lazyDispatch, + ), + }), + ); + + const store: Store = useConstant(() => + createStore({ + dimensionMarshal, + styleMarshal, + announce, + autoScroller, + getResponders, + }), + ); + + storeRef = useRef(store); + + const getCanLift = useCallbackOne((id: DraggableId) => + canStartDrag(storeRef.current.getState(), id), + ); + + const getIsMovementAllowed = useCallbackOne(() => + isMovementAllowed(storeRef.current.getState()), + ); + + const appContext: AppContextValue = useMemoOne(() => ({ + marshal: dimensionMarshal, + style: styleMarshal.styleContext, + canLift: getCanLift, + isMovementAllowed: getIsMovementAllowed, + })); + + const tryResetStore = useCallbackOne(() => { + const state: State = storeRef.current.getState(); + if (state.phase !== 'IDLE') { + store.dispatch(clean({ shouldFlush: true })); + } + }); + + useLayoutEffect(() => { + setOnError(tryResetStore); + }, [setOnError, tryResetStore]); + + useLayoutEffect(() => { + tryResetStore(); + }, [tryResetStore]); + + // Clean store when unmounting + useEffect(() => { + return tryResetStore; + }, [tryResetStore]); + + return ( + + + {props.children} + + + ); +} diff --git a/src/view/drag-drop-context/drag-drop-context.jsx b/src/view/drag-drop-context/drag-drop-context.jsx index 9c1d6c3ecf..4db7749c1d 100644 --- a/src/view/drag-drop-context/drag-drop-context.jsx +++ b/src/view/drag-drop-context/drag-drop-context.jsx @@ -1,39 +1,9 @@ // @flow -import React, { useEffect, useRef, type Node } from 'react'; -import { bindActionCreators } from 'redux'; -import { Provider } from 'react-redux'; -import createStore from '../../state/create-store'; -import createDimensionMarshal from '../../state/dimension-marshal/dimension-marshal'; -import canStartDrag from '../../state/can-start-drag'; -import scrollWindow from '../window/scroll-window'; -import createAutoScroller from '../../state/auto-scroller'; -import useStyleMarshal from '../use-style-marshal/use-style-marshal'; -import type { AutoScroller } from '../../state/auto-scroller/auto-scroller-types'; -import type { StyleMarshal } from '../use-style-marshal/style-marshal-types'; -import type { - DimensionMarshal, - Callbacks as DimensionMarshalCallbacks, -} from '../../state/dimension-marshal/dimension-marshal-types'; -import type { DraggableId, State, Responders, Announce } from '../../types'; -import type { Store, Action } from '../../state/store-types'; -import StoreContext from '../context/store-context'; -import { - clean, - move, - publishWhileDragging, - updateDroppableScroll, - updateDroppableIsEnabled, - updateDroppableIsCombineEnabled, - collectionStarting, -} from '../../state/action-creators'; -import isMovementAllowed from '../../state/is-movement-allowed'; -import useAnnouncer from '../use-announcer'; -import AppContext, { type AppContextValue } from '../context/app-context'; -import useStartupValidation from './use-startup-validation'; +import React, { type Node } from 'react'; +import type { Responders } from '../../types'; import ErrorBoundary from '../error-boundary'; +import App from './app'; import useMemoOne from '../use-custom-memo/use-memo-one'; -import useCallbackOne from '../use-custom-memo/use-callback-one'; -import { useConstant, useConstantFn } from '../use-constant'; type Props = {| ...Responders, @@ -41,13 +11,6 @@ type Props = {| children: Node | null, |}; -const createResponders = (props: Props): Responders => ({ - onBeforeDragStart: props.onBeforeDragStart, - onDragStart: props.onDragStart, - onDragEnd: props.onDragEnd, - onDragUpdate: props.onDragUpdate, -}); - let instanceCount: number = 0; // Reset any context that gets persisted across server side renders @@ -56,111 +19,14 @@ export function resetServerContext() { } export default function DragDropContext(props: Props) { - // We do not want this to change - const uniqueId: number = useConstant((): number => instanceCount++); - - // flow does not support MutableRefObject - // let storeRef: MutableRefObject; - let storeRef; - - useStartupValidation(); - - // lazy collection of responders using a ref - update on ever render - const lastPropsRef = useRef(props); - useEffect(() => { - lastPropsRef.current = props; - }); - - const getResponders: () => Responders = useConstantFn(() => { - return createResponders(lastPropsRef.current); - }); - - const announce: Announce = useAnnouncer(uniqueId); - const styleMarshal: StyleMarshal = useStyleMarshal(uniqueId); - - const lazyDispatch: Action => void = useConstantFn( - (action: Action): void => { - storeRef.current.dispatch(action); - }, - ); - - const callbacks: DimensionMarshalCallbacks = useConstant(() => - bindActionCreators( - { - publishWhileDragging, - updateDroppableScroll, - updateDroppableIsEnabled, - updateDroppableIsCombineEnabled, - collectionStarting, - }, - // $FlowFixMe - not sure why this is wrong - lazyDispatch, - ), - ); - const dimensionMarshal: DimensionMarshal = useConstant(() => - createDimensionMarshal(callbacks), - ); - - const autoScroller: AutoScroller = useConstant(() => - createAutoScroller({ - scrollWindow, - scrollDroppable: dimensionMarshal.scrollDroppable, - ...bindActionCreators( - { - move, - }, - // $FlowFixMe - not sure why this is wrong - lazyDispatch, - ), - }), - ); - - const store: Store = useConstant(() => - createStore({ - dimensionMarshal, - styleMarshal, - announce, - autoScroller, - getResponders, - }), - ); - - storeRef = useRef(store); - - const getCanLift = useCallbackOne((id: DraggableId) => - canStartDrag(storeRef.current.getState(), id), - ); - - const getIsMovementAllowed = useCallbackOne(() => - isMovementAllowed(storeRef.current.getState()), - ); - - const appContext: AppContextValue = useMemoOne(() => ({ - marshal: dimensionMarshal, - style: styleMarshal.styleContext, - canLift: getCanLift, - isMovementAllowed: getIsMovementAllowed, - })); - - const tryResetStore = useCallbackOne(() => { - const state: State = storeRef.current.getState(); - if (state.phase !== 'IDLE') { - store.dispatch(clean({ shouldFlush: true })); - } - }); - - // Clean store when unmounting - useEffect(() => { - return tryResetStore; - }, [tryResetStore]); - + const uniqueId: number = useMemoOne(() => instanceCount++, []); return ( - - - + + {setOnError => ( + {props.children} - - - + + )} + ); } diff --git a/src/view/error-boundary/error-boundary.jsx b/src/view/error-boundary/error-boundary.jsx index 92c42b52ef..07eaf38812 100644 --- a/src/view/error-boundary/error-boundary.jsx +++ b/src/view/error-boundary/error-boundary.jsx @@ -1,10 +1,9 @@ // @flow import React, { type Node } from 'react'; -import { getFormattedMessage } from '../../dev-warning'; +import { getFormattedMessage, warning } from '../../dev-warning'; type Props = {| - onError: () => void, - children: Node | null, + children: (setOnError: Function) => Node, |}; function printFatalError(error: Error) { @@ -27,6 +26,9 @@ function printFatalError(error: Error) { } export default class ErrorBoundary extends React.Component { + // eslint-disable-next-line react/sort-comp + recover: ?() => void; + componentDidMount() { window.addEventListener('error', this.onFatalError); } @@ -34,9 +36,18 @@ export default class ErrorBoundary extends React.Component { window.removeEventListener('error', this.onFatalError); } + setOnError = (onError: () => void) => { + this.recover = onError; + }; + onFatalError = (error: Error) => { printFatalError(error); - this.props.onError(); + + if (this.recover) { + this.recover(); + } else { + warning('Could not find recovering function'); + } // If the failure was due to an invariant failure - then we handle the error if (error.message.indexOf('Invariant failed') !== -1) { @@ -53,6 +64,6 @@ export default class ErrorBoundary extends React.Component { } render() { - return this.props.children; + return this.props.children(this.setOnError); } } From 14357d5c018d5cc166152732685724fc925382a7 Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Thu, 28 Mar 2019 11:20:20 +1100 Subject: [PATCH 082/117] publishing tests --- .../use-droppable-dimension-publisher.js | 2 +- .../publishing.spec.js | 68 ++++++++++--------- 2 files changed, 38 insertions(+), 32 deletions(-) diff --git a/src/view/use-droppable-dimension-publisher/use-droppable-dimension-publisher.js b/src/view/use-droppable-dimension-publisher/use-droppable-dimension-publisher.js index 53ea5d6121..37f481ce58 100644 --- a/src/view/use-droppable-dimension-publisher/use-droppable-dimension-publisher.js +++ b/src/view/use-droppable-dimension-publisher/use-droppable-dimension-publisher.js @@ -222,7 +222,7 @@ export default function useDroppableDimensionPublisher(args: Props) { }, []); const callbacks: DroppableCallbacks = useMemo(() => { - console.log('recreating callbacks'); + console.log('creating callbacks'); return { getDimensionAndWatchScroll, recollect, diff --git a/test/unit/view/droppable-dimension-publisher/publishing.spec.js b/test/unit/view/droppable-dimension-publisher/publishing.spec.js index 9edef89fcb..f86a611b52 100644 --- a/test/unit/view/droppable-dimension-publisher/publishing.spec.js +++ b/test/unit/view/droppable-dimension-publisher/publishing.spec.js @@ -29,7 +29,6 @@ import { } from './util/shared'; import { setViewport } from '../../../utils/viewport'; import tryCleanPrototypeStubs from '../../../utils/try-clean-prototype-stubs'; -import PassThroughProps from '../../../utils/pass-through-props'; beforeEach(() => { setViewport(preset.viewport); @@ -39,7 +38,7 @@ afterEach(() => { tryCleanPrototypeStubs(); }); -it.only('should publish the dimensions of the target', () => { +it('should publish the dimensions of the target', () => { const marshal: DimensionMarshal = getMarshalStub(); const expected: DroppableDimension = getDroppableDimension({ descriptor: { @@ -83,7 +82,7 @@ it.only('should publish the dimensions of the target', () => { expect(result.client.padding).toEqual(padding); }); -it.only('should consider the window scroll when calculating dimensions', () => { +it('should consider the window scroll when calculating dimensions', () => { const marshal: DimensionMarshal = getMarshalStub(); const windowScroll: Position = { x: 500, @@ -130,7 +129,7 @@ it.only('should consider the window scroll when calculating dimensions', () => { }); describe('no closest scrollable', () => { - it.only('should return null for the closest scrollable if there is no scroll container', () => { + it('should return null for the closest scrollable if there is no scroll container', () => { const expected: DroppableDimension = getDroppableDimension({ descriptor, borderBox: bigClient.borderBox, @@ -145,7 +144,7 @@ describe('no closest scrollable', () => { , ); - const el: ?HTMLElement = wrapper.getDOMNode(); + const el: ?HTMLElement = wrapper.find('.droppable').getDOMNode(); invariant(el); jest .spyOn(el, 'getBoundingClientRect') @@ -198,10 +197,11 @@ describe('droppable is scrollable', () => { const marshal: DimensionMarshal = getMarshalStub(); // both the droppable and the parent are scrollable const wrapper = mount( - , - withDimensionMarshal(marshal), + + + , ); - const el: ?HTMLElement = wrapper.instance().getRef(); + const el: ?HTMLElement = wrapper.find('.droppable').getDOMNode(); invariant(el); // returning smaller border box as this is what occurs when the element is scrollable jest @@ -261,10 +261,11 @@ describe('droppable is scrollable', () => { const marshal: DimensionMarshal = getMarshalStub(); // both the droppable and the parent are scrollable const wrapper = mount( - , - withDimensionMarshal(marshal), + + + , ); - const el: ?HTMLElement = wrapper.instance().getRef(); + const el: ?HTMLElement = wrapper.find('.droppable').getDOMNode(); invariant(el); // returning smaller border box as this is what occurs when the element is scrollable jest @@ -318,15 +319,16 @@ describe('parent of droppable is scrollable', () => { }); const marshal: DimensionMarshal = getMarshalStub(); const wrapper = mount( - , - withDimensionMarshal(marshal), + + + , ); - const droppable: ?HTMLElement = wrapper.instance().getRef(); + const droppable: ?HTMLElement = wrapper.find('.droppable').getDOMNode(); invariant(droppable); jest .spyOn(droppable, 'getBoundingClientRect') .mockImplementation(() => bigClient.borderBox); - const parent: HTMLElement = wrapper.getDOMNode(); + const parent: HTMLElement = wrapper.find('.scroll-parent').getDOMNode(); jest .spyOn(parent, 'getBoundingClientRect') .mockImplementation(() => smallFrameClient.borderBox); @@ -374,12 +376,13 @@ describe('both droppable and parent is scrollable', () => { }); const marshal: DimensionMarshal = getMarshalStub(); const wrapper = mount( - , - withDimensionMarshal(marshal), + + , + , ); - const droppable: ?HTMLElement = wrapper.instance().getRef(); + const droppable: ?HTMLElement = wrapper.find('.droppable').getDOMNode(); invariant(droppable); - const parent: HTMLElement = wrapper.getDOMNode(); + const parent: HTMLElement = wrapper.find('.scroll-parent').getDOMNode(); jest .spyOn(droppable, 'getBoundingClientRect') .mockImplementation(() => smallFrameClient.borderBox); @@ -417,12 +420,14 @@ it('should capture the initial scroll of the closest scrollable', () => { const frameScroll: Position = { x: 10, y: 20 }; const marshal: DimensionMarshal = getMarshalStub(); const wrapper = mount( - , - withDimensionMarshal(marshal), + + , + , ); - const droppable: ?HTMLElement = wrapper.instance().getRef(); + const droppable: ?HTMLElement = wrapper.find('.droppable').getDOMNode(); invariant(droppable); - const parent: HTMLElement = wrapper.getDOMNode(); + const parent: HTMLElement = wrapper.find('.scroll-parent').getDOMNode(); + invariant(parent); // manually setting the scroll of the parent node parent.scrollTop = frameScroll.y; parent.scrollLeft = frameScroll.x; @@ -475,16 +480,17 @@ it('should indicate if subject clipping is permitted based on the ignoreContaine // in this case the parent of the droppable is the closest scrollable const marshal: DimensionMarshal = getMarshalStub(); const wrapper = mount( - , - withDimensionMarshal(marshal), + + + , ); - const droppable: ?HTMLElement = wrapper.instance().getRef(); + const droppable: ?HTMLElement = wrapper.find('.droppable').getDOMNode(); invariant(droppable); - const parent: HTMLElement = wrapper.getDOMNode(); + const parent: HTMLElement = wrapper.find('.scroll-parent').getDOMNode(); const scrollSize: ScrollSize = { scrollWidth: bigClient.paddingBox.width, scrollHeight: bigClient.paddingBox.height, From abf5c217fb2945d064122021c05ca44746244f1d Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Thu, 28 Mar 2019 16:58:24 +1100 Subject: [PATCH 083/117] scrollable droppable tests --- .../use-droppable-dimension-publisher.js | 22 ++-- .../scroll-watching.spec.js | 123 +++++++++++++----- .../util/shared.js | 14 +- 3 files changed, 109 insertions(+), 50 deletions(-) diff --git a/src/view/use-droppable-dimension-publisher/use-droppable-dimension-publisher.js b/src/view/use-droppable-dimension-publisher/use-droppable-dimension-publisher.js index 37f481ce58..402ac05720 100644 --- a/src/view/use-droppable-dimension-publisher/use-droppable-dimension-publisher.js +++ b/src/view/use-droppable-dimension-publisher/use-droppable-dimension-publisher.js @@ -3,6 +3,7 @@ import { useCallback, useMemo, useLayoutEffect, useRef } from 'react'; import invariant from 'tiny-invariant'; import { type Position } from 'css-box-model'; import rafSchedule from 'raf-schd'; +import memoizeOne from 'memoize-one'; import checkForNestedScrollContainers from './check-for-nested-scroll-container'; import { origin } from '../../state/position'; import getScroll from './get-scroll'; @@ -62,15 +63,16 @@ export default function useDroppableDimensionPublisher(args: Props) { }, [args.droppableId, args.type]); const publishedDescriptorRef = useRef(descriptor); - const memoizedUpdateScroll = useCallback( - (x: number, y: number) => { - invariant( - whileDraggingRef.current, - 'Can only update scroll when dragging', - ); - const scroll: Position = { x, y }; - marshal.updateDroppableScroll(descriptor.id, scroll); - }, + const memoizedUpdateScroll = useMemo( + () => + memoizeOne((x: number, y: number) => { + invariant( + whileDraggingRef.current, + 'Can only update scroll when dragging', + ); + const scroll: Position = { x, y }; + marshal.updateDroppableScroll(descriptor.id, scroll); + }), [descriptor.id, marshal], ); @@ -222,7 +224,6 @@ export default function useDroppableDimensionPublisher(args: Props) { }, []); const callbacks: DroppableCallbacks = useMemo(() => { - console.log('creating callbacks'); return { getDimensionAndWatchScroll, recollect, @@ -245,7 +246,6 @@ export default function useDroppableDimensionPublisher(args: Props) { ); dragStopped(); } - console.log('goodbye droppable', descriptor); marshal.unregisterDroppable(descriptor); }; diff --git a/test/unit/view/droppable-dimension-publisher/scroll-watching.spec.js b/test/unit/view/droppable-dimension-publisher/scroll-watching.spec.js index 4b4e7e5fa7..ddf99ceb05 100644 --- a/test/unit/view/droppable-dimension-publisher/scroll-watching.spec.js +++ b/test/unit/view/droppable-dimension-publisher/scroll-watching.spec.js @@ -1,5 +1,6 @@ // @flow import { mount } from 'enzyme'; +import invariant from 'tiny-invariant'; import React from 'react'; import { type Position } from 'css-box-model'; import type { @@ -7,9 +8,14 @@ import type { DroppableCallbacks, } from '../../../../src/state/dimension-marshal/dimension-marshal-types'; import { getMarshalStub } from '../../../utils/dimension-marshal'; -import { withDimensionMarshal } from '../../../utils/get-context-options'; import { setViewport } from '../../../utils/viewport'; -import { immediate, preset, scheduled, ScrollableItem } from './util/shared'; +import { + immediate, + preset, + scheduled, + ScrollableItem, + WithAppContext, +} from './util/shared'; const scroll = (el: HTMLElement, target: Position) => { el.scrollTop = target.y; @@ -22,12 +28,15 @@ setViewport(preset.viewport); describe('should immediately publish updates', () => { it('should immediately publish the scroll offset of the closest scrollable', () => { const marshal: DimensionMarshal = getMarshalStub(); - const wrapper = mount(, withDimensionMarshal(marshal)); - const container: HTMLElement = wrapper.getDOMNode(); - - if (!container.classList.contains('scroll-container')) { - throw new Error('incorrect dom node collected'); - } + const wrapper = mount( + + + , + ); + const container: ?HTMLElement = wrapper + .find('.scroll-container') + .getDOMNode(); + invariant(container); // tell the droppable to watch for scrolling const callbacks: DroppableCallbacks = @@ -46,8 +55,15 @@ describe('should immediately publish updates', () => { it('should not fire a scroll if the value has not changed since the previous call', () => { // this can happen if you scroll backward and forward super quick const marshal: DimensionMarshal = getMarshalStub(); - const wrapper = mount(, withDimensionMarshal(marshal)); - const container: HTMLElement = wrapper.getDOMNode(); + const wrapper = mount( + + + , + ); + const container: ?HTMLElement = wrapper + .find('.scroll-container') + .getDOMNode(); + invariant(container); // tell the droppable to watch for scrolling const callbacks: DroppableCallbacks = marshal.registerDroppable.mock.calls[0][1]; @@ -80,12 +96,15 @@ describe('should immediately publish updates', () => { describe('should schedule publish updates', () => { it('should publish the scroll offset of the closest scrollable', () => { const marshal: DimensionMarshal = getMarshalStub(); - const wrapper = mount(, withDimensionMarshal(marshal)); - const container: HTMLElement = wrapper.getDOMNode(); - - if (!container.classList.contains('scroll-container')) { - throw new Error('incorrect dom node collected'); - } + const wrapper = mount( + + + , + ); + const container: ?HTMLElement = wrapper + .find('.scroll-container') + .getDOMNode(); + invariant(container); // tell the droppable to watch for scrolling const callbacks: DroppableCallbacks = @@ -105,8 +124,15 @@ describe('should schedule publish updates', () => { it('should throttle multiple scrolls into a animation frame', () => { const marshal: DimensionMarshal = getMarshalStub(); - const wrapper = mount(, withDimensionMarshal(marshal)); - const container: HTMLElement = wrapper.getDOMNode(); + const wrapper = mount( + + + , + ); + const container: ?HTMLElement = wrapper + .find('.scroll-container') + .getDOMNode(); + invariant(container); // tell the droppable to watch for scrolling const callbacks: DroppableCallbacks = marshal.registerDroppable.mock.calls[0][1]; @@ -136,8 +162,15 @@ describe('should schedule publish updates', () => { it('should not fire a scroll if the value has not changed since the previous frame', () => { // this can happen if you scroll backward and forward super quick const marshal: DimensionMarshal = getMarshalStub(); - const wrapper = mount(, withDimensionMarshal(marshal)); - const container: HTMLElement = wrapper.getDOMNode(); + const wrapper = mount( + + + , + ); + const container: ?HTMLElement = wrapper + .find('.scroll-container') + .getDOMNode(); + invariant(container); // tell the droppable to watch for scrolling const callbacks: DroppableCallbacks = marshal.registerDroppable.mock.calls[0][1]; @@ -169,8 +202,15 @@ describe('should schedule publish updates', () => { it('should not publish a scroll update after requested not to update while an animation frame is occurring', () => { const marshal: DimensionMarshal = getMarshalStub(); - const wrapper = mount(, withDimensionMarshal(marshal)); - const container: HTMLElement = wrapper.getDOMNode(); + const wrapper = mount( + + + , + ); + const container: ?HTMLElement = wrapper + .find('.scroll-container') + .getDOMNode(); + invariant(container); // tell the droppable to watch for scrolling const callbacks: DroppableCallbacks = marshal.registerDroppable.mock.calls[0][1]; @@ -200,8 +240,15 @@ describe('should schedule publish updates', () => { it('should stop watching scroll when no longer required to publish', () => { // this can happen if you scroll backward and forward super quick const marshal: DimensionMarshal = getMarshalStub(); - const wrapper = mount(, withDimensionMarshal(marshal)); - const container: HTMLElement = wrapper.getDOMNode(); + const wrapper = mount( + + + , + ); + const container: ?HTMLElement = wrapper + .find('.scroll-container') + .getDOMNode(); + invariant(container); // tell the droppable to watch for scrolling const callbacks: DroppableCallbacks = marshal.registerDroppable.mock.calls[0][1]; @@ -224,8 +271,15 @@ it('should stop watching scroll when no longer required to publish', () => { it('should stop watching for scroll events when the component is unmounted', () => { jest.spyOn(console, 'warn').mockImplementation(() => {}); const marshal: DimensionMarshal = getMarshalStub(); - const wrapper = mount(, withDimensionMarshal(marshal)); - const container: HTMLElement = wrapper.getDOMNode(); + const wrapper = mount( + + + , + ); + const container: ?HTMLElement = wrapper + .find('.scroll-container') + .getDOMNode(); + invariant(container); // tell the droppable to watch for scrolling const callbacks: DroppableCallbacks = marshal.registerDroppable.mock.calls[0][1]; @@ -247,7 +301,11 @@ it('should stop watching for scroll events when the component is unmounted', () it('should throw an error if asked to watch a scroll when already listening for scroll changes', () => { const marshal: DimensionMarshal = getMarshalStub(); - const wrapper = mount(, withDimensionMarshal(marshal)); + const wrapper = mount( + + + , + ); // tell the droppable to watch for scrolling const callbacks: DroppableCallbacks = marshal.registerDroppable.mock.calls[0][1]; @@ -266,8 +324,15 @@ it('should throw an error if asked to watch a scroll when already listening for // if this is not the case then it will break in IE11 it('should add and remove events with the same event options', () => { const marshal: DimensionMarshal = getMarshalStub(); - const wrapper = mount(, withDimensionMarshal(marshal)); - const container: HTMLElement = wrapper.getDOMNode(); + const wrapper = mount( + + + , + ); + const container: ?HTMLElement = wrapper + .find('.scroll-container') + .getDOMNode(); + invariant(container); jest.spyOn(container, 'addEventListener'); jest.spyOn(container, 'removeEventListener'); diff --git a/test/unit/view/droppable-dimension-publisher/util/shared.js b/test/unit/view/droppable-dimension-publisher/util/shared.js index ff3c739ef5..e98f64384e 100644 --- a/test/unit/view/droppable-dimension-publisher/util/shared.js +++ b/test/unit/view/droppable-dimension-publisher/util/shared.js @@ -71,14 +71,6 @@ const withSpacing = getComputedSpacing({ padding, margin, border }); export const descriptor: DroppableDescriptor = preset.home.descriptor; -// static defaultProps = { -// isScrollable: true, -// type: descriptor.type, -// droppableId: descriptor.id, -// isDropDisabled: false, -// isCombineEnabled: false, -// }; - type WithAppContextProps = {| marshal: DimensionMarshal, children: Node, @@ -111,6 +103,8 @@ type ScrollableItemProps = {| export function ScrollableItem(props: ScrollableItemProps) { const droppableRef = createRef(); const placeholderRef = createRef(); + // originally tests where made with this as the default + const isScrollable: boolean = props.isScrollable !== false; useDroppableDimensionPublisher({ droppableId: props.droppableId || descriptor.id, @@ -131,8 +125,8 @@ export function ScrollableItem(props: ScrollableItemProps) { height: bigClient.borderBox.height, width: bigClient.borderBox.width, ...withSpacing, - overflowX: props.isScrollable ? 'scroll' : 'visible', - overflowY: props.isScrollable ? 'scroll' : 'visible', + overflowX: isScrollable ? 'scroll' : 'visible', + overflowY: isScrollable ? 'scroll' : 'visible', }} ref={droppableRef.setRef} > From 26682532bca5ab82dab044ff05df71050abb35e3 Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Thu, 28 Mar 2019 17:08:38 +1100 Subject: [PATCH 084/117] is enabled change test --- .../use-droppable-dimension-publisher.js | 8 +++++ .../is-enabled-change.spec.js | 36 ++++++++++++++----- 2 files changed, 36 insertions(+), 8 deletions(-) diff --git a/src/view/use-droppable-dimension-publisher/use-droppable-dimension-publisher.js b/src/view/use-droppable-dimension-publisher/use-droppable-dimension-publisher.js index 402ac05720..24cc1c5780 100644 --- a/src/view/use-droppable-dimension-publisher/use-droppable-dimension-publisher.js +++ b/src/view/use-droppable-dimension-publisher/use-droppable-dimension-publisher.js @@ -252,7 +252,11 @@ export default function useDroppableDimensionPublisher(args: Props) { }, [callbacks, descriptor, dragStopped, marshal]); // update is enabled with the marshal + // only need to update when there is a drag useLayoutEffect(() => { + if (!whileDraggingRef.current) { + return; + } marshal.updateDroppableIsEnabled( publishedDescriptorRef.current.id, !args.isDropDisabled, @@ -260,7 +264,11 @@ export default function useDroppableDimensionPublisher(args: Props) { }, [args.isDropDisabled, marshal]); // update is combine enabled with the marshal + // only need to update when there is a drag useLayoutEffect(() => { + if (!whileDraggingRef.current) { + return; + } marshal.updateDroppableIsCombineEnabled( publishedDescriptorRef.current.id, args.isCombineEnabled, diff --git a/test/unit/view/droppable-dimension-publisher/is-enabled-change.spec.js b/test/unit/view/droppable-dimension-publisher/is-enabled-change.spec.js index 4757419e5c..0d6298368f 100644 --- a/test/unit/view/droppable-dimension-publisher/is-enabled-change.spec.js +++ b/test/unit/view/droppable-dimension-publisher/is-enabled-change.spec.js @@ -6,18 +6,28 @@ import type { DroppableCallbacks, } from '../../../../src/state/dimension-marshal/dimension-marshal-types'; import { getMarshalStub } from '../../../utils/dimension-marshal'; -import { withDimensionMarshal } from '../../../utils/get-context-options'; import { setViewport } from '../../../utils/viewport'; -import { preset, scheduled, ScrollableItem } from './util/shared'; +import { + preset, + scheduled, + ScrollableItem, + WithAppContext, +} from './util/shared'; import forceUpdate from '../../../utils/force-update'; +import PassThroughProps from '../../../utils/pass-through-props'; setViewport(preset.viewport); it('should publish updates to the enabled state when dragging', () => { const marshal: DimensionMarshal = getMarshalStub(); const wrapper = mount( - , - withDimensionMarshal(marshal), + + {extra => ( + + + + )} + , ); // not called yet expect(marshal.updateDroppableIsEnabled).not.toHaveBeenCalled(); @@ -41,8 +51,13 @@ it('should publish updates to the enabled state when dragging', () => { it('should not publish updates to the enabled state when there is no drag', () => { const marshal: DimensionMarshal = getMarshalStub(); const wrapper = mount( - , - withDimensionMarshal(marshal), + + {extra => ( + + + + )} + , ); // not called yet @@ -60,8 +75,13 @@ it('should not publish updates to the enabled state when there is no drag', () = it('should not publish updates when there is no change', () => { const marshal: DimensionMarshal = getMarshalStub(); const wrapper = mount( - , - withDimensionMarshal(marshal), + + {extra => ( + + + + )} + , ); // not called yet From cf45963d2cd5f277dd01a183e7f53bf028f8b2fb Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Thu, 28 Mar 2019 17:19:06 +1100 Subject: [PATCH 085/117] droppable tests --- .../forced-scroll.spec.js | 45 +++++++++++-------- .../is-combined-enabled-change.spec.js | 36 +++++++++++---- .../is-element-scrollable.spec.js | 2 +- 3 files changed, 55 insertions(+), 28 deletions(-) diff --git a/test/unit/view/droppable-dimension-publisher/forced-scroll.spec.js b/test/unit/view/droppable-dimension-publisher/forced-scroll.spec.js index d05c64ae07..dd61532b31 100644 --- a/test/unit/view/droppable-dimension-publisher/forced-scroll.spec.js +++ b/test/unit/view/droppable-dimension-publisher/forced-scroll.spec.js @@ -7,7 +7,6 @@ import type { DroppableCallbacks, } from '../../../../src/state/dimension-marshal/dimension-marshal-types'; import { getMarshalStub } from '../../../utils/dimension-marshal'; -import { withDimensionMarshal } from '../../../utils/get-context-options'; import { setViewport } from '../../../utils/viewport'; import { App, @@ -17,6 +16,7 @@ import { preset, scheduled, ScrollableItem, + WithAppContext, } from './util/shared'; import tryCleanPrototypeStubs from '../../../utils/try-clean-prototype-stubs'; @@ -30,12 +30,14 @@ it('should throw if the droppable has no closest scrollable', () => { const marshal: DimensionMarshal = getMarshalStub(); // no scroll parent const wrapper = mount( - , - withDimensionMarshal(marshal), + + , + , ); - const droppable: ?HTMLElement = wrapper.instance().getRef(); + const droppable: ?HTMLElement = wrapper.find('.droppable').getDOMNode(); invariant(droppable); - const parent: HTMLElement = wrapper.getDOMNode(); + const parent: ?HTMLElement = wrapper.find('.scroll-parent').getDOMNode(); + invariant(parent); jest .spyOn(droppable, 'getBoundingClientRect') .mockImplementation(() => smallFrameClient.borderBox); @@ -67,12 +69,15 @@ it('should throw if the droppable has no closest scrollable', () => { describe('there is a closest scrollable', () => { it('should update the scroll of the closest scrollable', () => { const marshal: DimensionMarshal = getMarshalStub(); - const wrapper = mount(, withDimensionMarshal(marshal)); - const container: HTMLElement = wrapper.getDOMNode(); - - if (!container.classList.contains('scroll-container')) { - throw new Error('incorrect dom node collected'); - } + const wrapper = mount( + + + , + ); + const container: ?HTMLElement = wrapper + .find('.scroll-container') + .getDOMNode(); + invariant(container); expect(container.scrollTop).toBe(0); expect(container.scrollLeft).toBe(0); @@ -91,14 +96,16 @@ describe('there is a closest scrollable', () => { it('should throw if asked to scoll while scroll is not currently being watched', () => { const marshal: DimensionMarshal = getMarshalStub(); - const wrapper = mount(, withDimensionMarshal(marshal)); - - const container: HTMLElement = wrapper.getDOMNode(); - - if (!container.classList.contains('scroll-container')) { - throw new Error('incorrect dom node collected'); - } - + const wrapper = mount( + + + , + ); + + const container: ?HTMLElement = wrapper + .find('.scroll-container') + .getDOMNode(); + invariant(container); expect(container.scrollTop).toBe(0); expect(container.scrollLeft).toBe(0); diff --git a/test/unit/view/droppable-dimension-publisher/is-combined-enabled-change.spec.js b/test/unit/view/droppable-dimension-publisher/is-combined-enabled-change.spec.js index 5ac40cb3c6..2ea67e33f1 100644 --- a/test/unit/view/droppable-dimension-publisher/is-combined-enabled-change.spec.js +++ b/test/unit/view/droppable-dimension-publisher/is-combined-enabled-change.spec.js @@ -6,18 +6,28 @@ import type { DroppableCallbacks, } from '../../../../src/state/dimension-marshal/dimension-marshal-types'; import { getMarshalStub } from '../../../utils/dimension-marshal'; -import { withDimensionMarshal } from '../../../utils/get-context-options'; import { setViewport } from '../../../utils/viewport'; -import { preset, scheduled, ScrollableItem } from './util/shared'; +import { + preset, + scheduled, + ScrollableItem, + WithAppContext, +} from './util/shared'; import forceUpdate from '../../../utils/force-update'; +import PassThroughProps from '../../../utils/pass-through-props'; setViewport(preset.viewport); it('should publish updates to the enabled state when dragging', () => { const marshal: DimensionMarshal = getMarshalStub(); const wrapper = mount( - , - withDimensionMarshal(marshal), + + {extra => ( + + + + )} + , ); // not called yet expect(marshal.updateDroppableIsCombineEnabled).not.toHaveBeenCalled(); @@ -51,8 +61,13 @@ it('should publish updates to the enabled state when dragging', () => { it('should not publish updates to the enabled state when there is no drag', () => { const marshal: DimensionMarshal = getMarshalStub(); const wrapper = mount( - , - withDimensionMarshal(marshal), + + {extra => ( + + , + + )} + , ); // not called yet @@ -70,8 +85,13 @@ it('should not publish updates to the enabled state when there is no drag', () = it('should not publish updates when there is no change', () => { const marshal: DimensionMarshal = getMarshalStub(); const wrapper = mount( - , - withDimensionMarshal(marshal), + + {extra => ( + + , + + )} + , ); // not called yet diff --git a/test/unit/view/droppable-dimension-publisher/is-element-scrollable.spec.js b/test/unit/view/droppable-dimension-publisher/is-element-scrollable.spec.js index a4d1ef79f7..f45225cd9c 100644 --- a/test/unit/view/droppable-dimension-publisher/is-element-scrollable.spec.js +++ b/test/unit/view/droppable-dimension-publisher/is-element-scrollable.spec.js @@ -1,6 +1,6 @@ // @flow import invariant from 'tiny-invariant'; -import getClosestScrollable from '../../../../src/view/droppable-dimension-publisher/get-closest-scrollable'; +import getClosestScrollable from '../../../../src/view/use-droppable-dimension-publisher/get-closest-scrollable'; it('should return true if an element has overflow:auto or overflow:scroll', () => { ['overflowY', 'overflowX'].forEach((overflow: string) => { From a076db016c8dc5194323689e45258d2a27c9c685 Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Thu, 28 Mar 2019 17:29:07 +1100 Subject: [PATCH 086/117] droppable recollection tests --- .../recollection.spec.js | 38 +++++++++++-------- 1 file changed, 23 insertions(+), 15 deletions(-) diff --git a/test/unit/view/droppable-dimension-publisher/recollection.spec.js b/test/unit/view/droppable-dimension-publisher/recollection.spec.js index f0a7473694..0c5cb193f9 100644 --- a/test/unit/view/droppable-dimension-publisher/recollection.spec.js +++ b/test/unit/view/droppable-dimension-publisher/recollection.spec.js @@ -9,7 +9,6 @@ import type { import type { DroppableDimension } from '../../../../src/types'; import { getDroppableDimension } from '../../../utils/dimension'; import { getMarshalStub } from '../../../utils/dimension-marshal'; -import { withDimensionMarshal } from '../../../utils/get-context-options'; import tryCleanPrototypeStubs from '../../../utils/try-clean-prototype-stubs'; import { setViewport } from '../../../utils/viewport'; import { @@ -22,6 +21,7 @@ import { padding, preset, smallFrameClient, + WithAppContext, } from './util/shared'; beforeEach(() => { @@ -61,10 +61,11 @@ it('should recollect a dimension if requested', () => { const marshal: DimensionMarshal = getMarshalStub(); // both the droppable and the parent are scrollable const wrapper = mount( - , - withDimensionMarshal(marshal), + + + , ); - const el: ?HTMLElement = wrapper.instance().getRef(); + const el: ?HTMLElement = wrapper.find('.droppable').getDOMNode(); invariant(el); // returning smaller border box as this is what occurs when the element is scrollable jest @@ -102,12 +103,13 @@ it('should hide any placeholder when recollecting dimensions if requested', () = const marshal: DimensionMarshal = getMarshalStub(); // both the droppable and the parent are scrollable const wrapper = mount( - , - withDimensionMarshal(marshal), + + + , ); - const el: ?HTMLElement = wrapper.instance().getRef(); - const placeholderEl: ?HTMLElement = wrapper.instance().getPlaceholderRef(); + const el: ?HTMLElement = wrapper.find('.droppable').getDOMNode(); invariant(el); + const placeholderEl: ?HTMLElement = wrapper.find('.placeholder').getDOMNode(); invariant(placeholderEl); // returning smaller border box as this is what occurs when the element is scrollable jest @@ -142,12 +144,13 @@ it('should not hide any placeholder when recollecting dimensions if requested', const marshal: DimensionMarshal = getMarshalStub(); // both the droppable and the parent are scrollable const wrapper = mount( - , - withDimensionMarshal(marshal), + + + , ); - const el: ?HTMLElement = wrapper.instance().getRef(); - const placeholderEl: ?HTMLElement = wrapper.instance().getPlaceholderRef(); + const el: ?HTMLElement = wrapper.find('.droppable').getDOMNode(); invariant(el); + const placeholderEl: ?HTMLElement = wrapper.find('.placeholder').getDOMNode(); invariant(placeholderEl); // returning smaller border box as this is what occurs when the element is scrollable jest @@ -179,8 +182,9 @@ it('should throw if there is no drag occurring when a recollection is requested' const marshal: DimensionMarshal = getMarshalStub(); // both the droppable and the parent are scrollable mount( - , - withDimensionMarshal(marshal), + + + , ); const callbacks: DroppableCallbacks = @@ -192,7 +196,11 @@ it('should throw if there is no drag occurring when a recollection is requested' it('should throw if there if recollecting from droppable that is not a scroll container', () => { const marshal: DimensionMarshal = getMarshalStub(); // both the droppable and the parent are scrollable - mount(, withDimensionMarshal(marshal)); + mount( + + + , + ); const callbacks: DroppableCallbacks = marshal.registerDroppable.mock.calls[0][1]; From 296c4ec9012ecda15e6e206f77cd4bf76b166660 Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Fri, 29 Mar 2019 08:21:04 +1100 Subject: [PATCH 087/117] draggable child render behaviour --- .../child-render-behaviour.spec.js | 46 +++++++++---------- 1 file changed, 21 insertions(+), 25 deletions(-) diff --git a/test/unit/view/connected-draggable/child-render-behaviour.spec.js b/test/unit/view/connected-draggable/child-render-behaviour.spec.js index f6ff79e94b..a92df60c5e 100644 --- a/test/unit/view/connected-draggable/child-render-behaviour.spec.js +++ b/test/unit/view/connected-draggable/child-render-behaviour.spec.js @@ -1,37 +1,29 @@ // @flow import React, { Component } from 'react'; import { mount } from 'enzyme'; -import { - combine, - withStore, - withDroppableId, - withDroppableType, - withDimensionMarshal, - withStyleContext, - withCanLift, -} from '../../../utils/get-context-options'; import type { DimensionMarshal } from '../../../../src/state/dimension-marshal/dimension-marshal-types'; import { getMarshalStub, getDroppableCallbacks, } from '../../../utils/dimension-marshal'; +import { DragDropContext } from '../../../../src'; import { getPreset } from '../../../utils/dimension'; import forceUpdate from '../../../utils/force-update'; import Draggable from '../../../../src/view/draggable/connected-draggable'; import type { Provided } from '../../../../src/view/draggable/draggable-types'; +import DroppableContext, { + type DroppableContextValue, +} from '../../../../src/view/context/droppable-context'; const preset = getPreset(); // creating our own marshal so we can publish a droppable // so that the draggable can publish itself const marshal: DimensionMarshal = getMarshalStub(); -const options: Object = combine( - withStore(), - withDroppableId(preset.home.descriptor.id), - withDroppableType(preset.home.descriptor.type), - withDimensionMarshal(marshal), - withStyleContext(), - withCanLift(), -); + +const droppableContext: DroppableContextValue = { + type: preset.home.descriptor.type, + droppableId: preset.home.descriptor.id, +}; // registering a fake droppable so that when a draggable // registers itself the marshal can find its parent @@ -58,11 +50,15 @@ class Person extends Component<{ name: string, provided: Provided }> { class App extends Component<{ currentUser: string }> { render() { return ( - - {(dragProvided: Provided) => ( - - )} - + {}}> + + + {(dragProvided: Provided) => ( + + )} + + + ); } } @@ -76,7 +72,7 @@ afterEach(() => { }); it('should render the child function when the parent renders', () => { - const wrapper = mount(, options); + const wrapper = mount(); expect(Person.prototype.render).toHaveBeenCalledTimes(1); expect(wrapper.find(Person).props().name).toBe('Jake'); @@ -85,7 +81,7 @@ it('should render the child function when the parent renders', () => { }); it('should render the child function when the parent re-renders', () => { - const wrapper = mount(, options); + const wrapper = mount(); forceUpdate(wrapper); @@ -96,7 +92,7 @@ it('should render the child function when the parent re-renders', () => { }); it('should render the child function when the parents props changes that cause a re-render', () => { - const wrapper = mount(, options); + const wrapper = mount(); wrapper.setProps({ currentUser: 'Finn', From 4d6784bcfef1ebd7d0ce8ed67ab35370c879255f Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Fri, 29 Mar 2019 08:24:00 +1100 Subject: [PATCH 088/117] connected droppable child render behaviour --- .../child-render-behaviour.spec.js | 35 +++++++------------ 1 file changed, 12 insertions(+), 23 deletions(-) diff --git a/test/unit/view/connected-droppable/child-render-behaviour.spec.js b/test/unit/view/connected-droppable/child-render-behaviour.spec.js index 6a59637d6c..ac79b63f9e 100644 --- a/test/unit/view/connected-droppable/child-render-behaviour.spec.js +++ b/test/unit/view/connected-droppable/child-render-behaviour.spec.js @@ -1,16 +1,10 @@ // @flow import React, { Component } from 'react'; import { mount } from 'enzyme'; -import { - withStore, - combine, - withDimensionMarshal, - withStyleContext, - withIsMovementAllowed, -} from '../../../utils/get-context-options'; +import type { Provided } from '../../../../src/view/droppable/droppable-types'; import Droppable from '../../../../src/view/droppable/connected-droppable'; import forceUpdate from '../../../utils/force-update'; -import type { Provided } from '../../../../src/view/droppable/droppable-types'; +import { DragDropContext } from '../../../../src'; class Person extends Component<{ name: string, provided: Provided }> { render() { @@ -26,22 +20,17 @@ class Person extends Component<{ name: string, provided: Provided }> { class App extends Component<{ currentUser: string }> { render() { return ( - - {(provided: Provided) => ( - - )} - + {}}> + + {(provided: Provided) => ( + + )} + + ); } } -const contextOptions = combine( - withStore(), - withDimensionMarshal(), - withStyleContext(), - withIsMovementAllowed(), -); - beforeEach(() => { jest.spyOn(Person.prototype, 'render'); }); @@ -51,7 +40,7 @@ afterEach(() => { }); it('should render the child function when the parent renders', () => { - const wrapper = mount(, contextOptions); + const wrapper = mount(); expect(Person.prototype.render).toHaveBeenCalledTimes(1); expect(wrapper.find(Person).props().name).toBe('Jake'); @@ -60,7 +49,7 @@ it('should render the child function when the parent renders', () => { }); it('should render the child function when the parent re-renders', () => { - const wrapper = mount(, contextOptions); + const wrapper = mount(); forceUpdate(wrapper); @@ -71,7 +60,7 @@ it('should render the child function when the parent re-renders', () => { }); it('should render the child function when the parents props changes that cause a re-render', () => { - const wrapper = mount(, contextOptions); + const wrapper = mount(); wrapper.setProps({ currentUser: 'Finn', From a233fc09f374bb0960d8f41d95a68a4c83230a38 Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Fri, 29 Mar 2019 08:32:04 +1100 Subject: [PATCH 089/117] more tests --- .../dimension-marshal/dimension-marshal.js | 25 ------------------- src/state/middleware/lift.js | 2 -- .../dimension-marshal/initial-publish.spec.js | 4 +-- test/utils/dimension-marshal.js | 1 - 4 files changed, 2 insertions(+), 30 deletions(-) diff --git a/src/state/dimension-marshal/dimension-marshal.js b/src/state/dimension-marshal/dimension-marshal.js index 62d6963bf2..b61f0c71ad 100644 --- a/src/state/dimension-marshal/dimension-marshal.js +++ b/src/state/dimension-marshal/dimension-marshal.js @@ -164,31 +164,6 @@ export default (callbacks: Callbacks) => { invariant(!collection, 'Cannot add a Droppable during a drag'); }; - // const updateDroppable = ( - // previous: DroppableDescriptor, - // descriptor: DroppableDescriptor, - // droppableCallbacks: DroppableCallbacks, - // ) => { - // invariant( - // entries.droppables[previous.id], - // 'Cannot update droppable registration as no previous registration was found', - // ); - - // // The id might have changed, so we are removing the old entry - // delete entries.droppables[previous.id]; - - // const entry: DroppableEntry = { - // descriptor, - // callbacks: droppableCallbacks, - // }; - // entries.droppables[descriptor.id] = entry; - - // invariant( - // !collection, - // 'You are not able to update the id or type of a droppable during a drag', - // ); - // }; - const unregisterDroppable = (descriptor: DroppableDescriptor) => { const entry: ?DroppableEntry = entries.droppables[descriptor.id]; diff --git a/src/state/middleware/lift.js b/src/state/middleware/lift.js index ab4c36d002..d9f3909036 100644 --- a/src/state/middleware/lift.js +++ b/src/state/middleware/lift.js @@ -13,8 +13,6 @@ export default (marshal: DimensionMarshal) => ({ next(action); return; } - console.log('LIFT action'); - const { id, clientSelection, movementMode } = action.payload; const initial: State = getState(); diff --git a/test/unit/view/dimension-marshal/initial-publish.spec.js b/test/unit/view/dimension-marshal/initial-publish.spec.js index 4c16ec10cb..b7842b22ed 100644 --- a/test/unit/view/dimension-marshal/initial-publish.spec.js +++ b/test/unit/view/dimension-marshal/initial-publish.spec.js @@ -186,8 +186,8 @@ it('should publish droppables that have been updated (id change)', () => { id: 'some new id', }, }; - marshal.updateDroppable( - preset.home.descriptor, + marshal.unregisterDroppable(preset.home.descriptor); + marshal.registerDroppable( updatedHome.descriptor, getDroppableCallbacks(updatedHome), ); diff --git a/test/utils/dimension-marshal.js b/test/utils/dimension-marshal.js index afb4a2334e..8f50f0bef2 100644 --- a/test/utils/dimension-marshal.js +++ b/test/utils/dimension-marshal.js @@ -48,7 +48,6 @@ export const getMarshalStub = (): DimensionMarshal => ({ updateDraggable: jest.fn(), unregisterDraggable: jest.fn(), registerDroppable: jest.fn(), - updateDroppable: jest.fn(), unregisterDroppable: jest.fn(), updateDroppableScroll: jest.fn(), updateDroppableIsEnabled: jest.fn(), From a37b858d68614a298e0a20dcde529243f830b725 Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Fri, 29 Mar 2019 08:44:40 +1100 Subject: [PATCH 090/117] adding action payload to logging --- src/debug/middleware/log.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/debug/middleware/log.js b/src/debug/middleware/log.js index 2c9aa69118..b187df0fbf 100644 --- a/src/debug/middleware/log.js +++ b/src/debug/middleware/log.js @@ -6,6 +6,7 @@ export default (store: Store) => (next: Action => mixed) => ( action: Action, ): any => { console.group(`action: ${action.type}`); + console.log('action payload', action.payload); console.log('state before', store.getState()); const result: mixed = next(action); From 225c814874c5f2c54884ce55aae5bd6a64c147b6 Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Fri, 29 Mar 2019 10:16:25 +1100 Subject: [PATCH 091/117] fixing flow --- test/unit/state/middleware/style.spec.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/test/unit/state/middleware/style.spec.js b/test/unit/state/middleware/style.spec.js index b435ccbbee..42e0b2a301 100644 --- a/test/unit/state/middleware/style.spec.js +++ b/test/unit/state/middleware/style.spec.js @@ -20,8 +20,6 @@ const getMarshalStub = (): StyleMarshal => ({ dragging: jest.fn(), dropping: jest.fn(), resting: jest.fn(), - mount: jest.fn(), - unmount: jest.fn(), styleContext: 'why hello there', }); From 639c4f6fd93cac845b7de3f014a56e12d8a44f0b Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Fri, 29 Mar 2019 16:57:50 +1100 Subject: [PATCH 092/117] focus management --- jest.config.js | 1 + src/view/use-drag-handle/use-drag-handle.js | 14 +- .../use-drag-handle/use-focus-retainer.js | 92 +++++++++ stories/11-portal.stories.js | 2 +- stories/src/portal/portal-app.jsx | 2 + .../server-rendering.spec.js.snap | 2 +- .../view/drag-handle/focus-management.spec.js | 181 ++++++++---------- 7 files changed, 186 insertions(+), 108 deletions(-) create mode 100644 src/view/use-drag-handle/use-focus-retainer.js diff --git a/jest.config.js b/jest.config.js index d1fe8423c8..33eeb2a32a 100644 --- a/jest.config.js +++ b/jest.config.js @@ -14,4 +14,5 @@ module.exports = { 'jest-watch-typeahead/filename', 'jest-watch-typeahead/testname', ], + verbose: true, }; diff --git a/src/view/use-drag-handle/use-drag-handle.js b/src/view/use-drag-handle/use-drag-handle.js index c0e00ba776..06882242d6 100644 --- a/src/view/use-drag-handle/use-drag-handle.js +++ b/src/view/use-drag-handle/use-drag-handle.js @@ -18,6 +18,7 @@ import useTouchSensor, { import usePreviousRef from '../use-previous-ref'; import { warning } from '../../dev-warning'; import useValidation from './use-validation'; +import useFocusRetainer from './use-focus-retainer'; function preventHtml5Dnd(event: DragEvent) { event.preventDefault(); @@ -63,6 +64,7 @@ export default function useDragHandle(args: Args): ?DragHandleProps { getShouldRespectForceTouch, canDragInteractiveElements, } = args; + const lastArgsRef = usePreviousRef(args); useValidation(getDraggableRef); @@ -71,14 +73,6 @@ export default function useDragHandle(args: Args): ?DragHandleProps { [getDraggableRef], ); - const isFocusedRef = useRef(false); - const onFocus = useCallback(() => { - isFocusedRef.current = true; - }, []); - const onBlur = useCallback(() => { - isFocusedRef.current = false; - }, []); - const canStartCapturing = useCallback( (event: Event) => { // Cannot lift when disabled @@ -103,6 +97,8 @@ export default function useDragHandle(args: Args): ?DragHandleProps { [canDragInteractiveElements, canLift, draggableId, isEnabled], ); + const { onBlur, onFocus } = useFocusRetainer(args); + const mouseArgs: MouseSensorArgs = useMemo( () => ({ callbacks, @@ -168,7 +164,7 @@ export default function useDragHandle(args: Args): ?DragHandleProps { const onTouchStart = useTouchSensor(touchArgs); // aborting on unmount - const lastArgsRef = usePreviousRef(args); + useLayoutEffect(() => { // only when unmounting return () => { diff --git a/src/view/use-drag-handle/use-focus-retainer.js b/src/view/use-drag-handle/use-focus-retainer.js new file mode 100644 index 0000000000..c0d74f5e9f --- /dev/null +++ b/src/view/use-drag-handle/use-focus-retainer.js @@ -0,0 +1,92 @@ +// @flow +import invariant from 'tiny-invariant'; +import { useRef, useCallback, useLayoutEffect } from 'react'; +import type { Args } from './drag-handle-types'; +import usePrevious from '../use-previous-ref'; +import focusRetainer from './util/focus-retainer'; +import getDragHandleRef from './util/get-drag-handle-ref'; + +export type Result = {| + onBlur: () => void, + onFocus: () => void, +|}; + +function noop() {} + +export default function useFocusRetainer(args: Args): Result { + const isFocusedRef = useRef(false); + const lastArgsRef = usePrevious(args); + const { getDraggableRef } = args; + + const onFocus = useCallback(() => { + isFocusedRef.current = true; + }, []); + const onBlur = useCallback(() => { + isFocusedRef.current = false; + }, []); + + useLayoutEffect(() => { + // mounting: try to restore focus + const first: Args = lastArgsRef.current; + if (!first.isEnabled) { + return noop; + } + const draggable: ?HTMLElement = getDraggableRef(); + invariant(draggable, 'Drag handle could not obtain draggable ref'); + + const dragHandle: HTMLElement = getDragHandleRef(draggable); + + focusRetainer.tryRestoreFocus(first.draggableId, dragHandle); + + // unmounting: try to retain focus + return () => { + const last: Args = lastArgsRef.current; + const shouldRetainFocus = ((): boolean => { + // will not restore if not enabled + if (!last.isEnabled) { + return false; + } + // not focused + if (!isFocusedRef.current) { + return false; + } + + // a drag is finishing + return last.isDragging || last.isDropAnimating; + })(); + + if (shouldRetainFocus) { + focusRetainer.retain(last.draggableId); + } + }; + }, [getDraggableRef, lastArgsRef]); + + const lastDraggableRef = useRef(getDraggableRef()); + + useLayoutEffect(() => { + const draggableRef: ?HTMLElement = getDraggableRef(); + + // Cannot focus on nothing + if (!draggableRef) { + return; + } + + // no change in ref + if (draggableRef === lastArgsRef.current.draggableId) { + return; + } + + // ref has changed - let's do this + if (isFocusedRef.current && lastArgsRef.current.isEnabled) { + getDragHandleRef(draggableRef).focus(); + } + + // Doing our own should run check + }); + + useLayoutEffect(() => { + lastDraggableRef.current = getDraggableRef(); + }); + + return { onBlur, onFocus }; +} diff --git a/stories/11-portal.stories.js b/stories/11-portal.stories.js index a76b4e62ec..a3bc17f108 100644 --- a/stories/11-portal.stories.js +++ b/stories/11-portal.stories.js @@ -5,5 +5,5 @@ import PortalApp from './src/portal/portal-app'; import { quotes } from './src/data'; storiesOf('Portals', module).add('Using your own portal', () => ( - + )); diff --git a/stories/src/portal/portal-app.jsx b/stories/src/portal/portal-app.jsx index 77e52bab2c..3b8b5e8480 100644 --- a/stories/src/portal/portal-app.jsx +++ b/stories/src/portal/portal-app.jsx @@ -75,6 +75,7 @@ class PortalAwareItem extends Component { ); if (!usePortal) { + console.warn('rendering out of portal'); return child; } @@ -149,6 +150,7 @@ export default class PortalApp extends Component { )} ))} + {droppableProvided.placeholder} )} diff --git a/test/unit/integration/server-side-rendering/__snapshots__/server-rendering.spec.js.snap b/test/unit/integration/server-side-rendering/__snapshots__/server-rendering.spec.js.snap index dca7fb9796..87c17c0604 100644 --- a/test/unit/integration/server-side-rendering/__snapshots__/server-rendering.spec.js.snap +++ b/test/unit/integration/server-side-rendering/__snapshots__/server-rendering.spec.js.snap @@ -1,5 +1,5 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`should support rendering to a string 1`] = `"
Drag me!
"`; +exports[`should support rendering to a string 1`] = `"
Drag me!
"`; exports[`should support rendering to static markup 1`] = `"
Drag me!
"`; diff --git a/test/unit/view/drag-handle/focus-management.spec.js b/test/unit/view/drag-handle/focus-management.spec.js index 26d09028d1..0c003cae2a 100644 --- a/test/unit/view/drag-handle/focus-management.spec.js +++ b/test/unit/view/drag-handle/focus-management.spec.js @@ -1,30 +1,26 @@ // @flow import React, { type Node } from 'react'; +import invariant from 'tiny-invariant'; import ReactDOM from 'react-dom'; -import PropTypes from 'prop-types'; import { mount } from 'enzyme'; import type { ReactWrapper } from 'enzyme'; -import DragHandle from '../../../../src/view/drag-handle'; -import { styleKey, canLiftKey } from '../../../../src/view/context-keys'; -import type { DragHandleProps } from '../../../../src/view/drag-handle/drag-handle-types'; +import type { DragHandleProps } from '../../../../src/view/use-drag-handle/drag-handle-types'; import { getStubCallbacks } from './util/callbacks'; - -const options = { - context: { - [styleKey]: 'hello', - [canLiftKey]: () => true, - }, - childContextTypes: { - [styleKey]: PropTypes.string.isRequired, - [canLiftKey]: PropTypes.func.isRequired, - }, -}; +import { WithDragHandle } from './util/wrappers'; +import { getMarshalStub } from '../../../utils/dimension-marshal'; +import AppContext, { + type AppContextValue, +} from '../../../../src/view/context/app-context'; const body: ?HTMLElement = document.body; +invariant(body, 'Cannot find body'); -if (!body) { - throw new Error('document.body not found'); -} +const appContext: AppContextValue = { + marshal: getMarshalStub(), + style: 'fake style context', + canLift: () => true, + isMovementAllowed: () => true, +}; describe('Portal usage (ref changing while mounted)', () => { type ChildProps = {| @@ -51,7 +47,11 @@ describe('Portal usage (ref changing while mounted)', () => { render() { const child: Node = ( -
+
Drag me!
); @@ -75,35 +75,34 @@ describe('Portal usage (ref changing while mounted)', () => { render() { return ( - this.ref} - canDragInteractiveElements={false} - getShouldRespectForceTouch={() => true} - > - {(dragHandleProps: ?DragHandleProps) => ( - - )} - + + this.ref} + canDragInteractiveElements={false} + getShouldRespectForceTouch={() => true} + > + {(dragHandleProps: ?DragHandleProps) => ( + + )} + + ); } } it('should retain focus if draggable ref is changing and had focus', () => { - const wrapper = mount( - , - options, - ); + const wrapper = mount(); - const original: HTMLElement = wrapper.getDOMNode(); + const original: HTMLElement = wrapper.find('.drag-handle').getDOMNode(); expect(original).not.toBe(document.activeElement); // giving it focus @@ -116,17 +115,14 @@ describe('Portal usage (ref changing while mounted)', () => { usePortal: true, }); - const inPortal: HTMLElement = wrapper.getDOMNode(); + const inPortal: HTMLElement = wrapper.find('.drag-handle').getDOMNode(); expect(inPortal).toBe(document.activeElement); expect(inPortal).not.toBe(original); expect(original).not.toBe(document.activeElement); }); it('should not retain focus if draggable ref is changing and did not have focus', () => { - const wrapper = mount( - , - options, - ); + const wrapper = mount(); const original: HTMLElement = wrapper.getDOMNode(); expect(original).not.toBe(document.activeElement); @@ -165,39 +161,45 @@ describe('Focus retention moving between lists (focus retention between mounts)' render() { return ( - this.ref} - canDragInteractiveElements={false} - getShouldRespectForceTouch={() => true} - > - {(dragHandleProps: ?DragHandleProps) => ( -
- Drag me! -
- )} -
+ + this.ref} + canDragInteractiveElements={false} + getShouldRespectForceTouch={() => true} + > + {(dragHandleProps: ?DragHandleProps) => ( +
+ Drag me! +
+ )} +
+
); } } it('should maintain focus if unmounting while dragging', () => { - const first: ReactWrapper<*> = mount(, options); + const first: ReactWrapper<*> = mount(); const original: HTMLElement = first.getDOMNode(); expect(original).not.toBe(document.activeElement); // get focus original.focus(); - first.find(DragHandle).simulate('focus'); + first.find('.drag-handle').simulate('focus'); expect(original).toBe(document.activeElement); first.unmount(); - const second: ReactWrapper<*> = mount(, options); + const second: ReactWrapper<*> = mount(); const latest: HTMLElement = second.getDOMNode(); expect(latest).toBe(document.activeElement); // validation @@ -209,21 +211,18 @@ describe('Focus retention moving between lists (focus retention between mounts)' }); it('should maintain focus if unmounting while drop animating', () => { - const first: ReactWrapper<*> = mount( - , - options, - ); + const first: ReactWrapper<*> = mount(); const original: HTMLElement = first.getDOMNode(); expect(original).not.toBe(document.activeElement); // get focus original.focus(); - first.find(DragHandle).simulate('focus'); + first.find('.drag-handle').simulate('focus'); expect(original).toBe(document.activeElement); first.unmount(); - const second: ReactWrapper<*> = mount(, options); + const second: ReactWrapper<*> = mount(); const latest: HTMLElement = second.getDOMNode(); expect(latest).toBe(document.activeElement); // validation @@ -238,20 +237,19 @@ describe('Focus retention moving between lists (focus retention between mounts)' it('should not maintain focus if the item was not dragging or drop animating', () => { const first: ReactWrapper<*> = mount( , - options, ); const original: HTMLElement = first.getDOMNode(); expect(original).not.toBe(document.activeElement); // get focus original.focus(); - first.find(DragHandle).simulate('focus'); + first.find('.drag-handle').simulate('focus'); expect(original).toBe(document.activeElement); first.unmount(); // will not get focus as it was not previously dragging or drop animating - const second: ReactWrapper<*> = mount(, options); + const second: ReactWrapper<*> = mount(); const latest: HTMLElement = second.getDOMNode(); expect(latest).not.toBe(document.activeElement); // validation @@ -260,13 +258,13 @@ describe('Focus retention moving between lists (focus retention between mounts)' }); it('should not give focus to something that was not previously focused', () => { - const first: ReactWrapper<*> = mount(, options); + const first: ReactWrapper<*> = mount(); const original: HTMLElement = first.getDOMNode(); expect(original).not.toBe(document.activeElement); first.unmount(); - const second: ReactWrapper<*> = mount(, options); + const second: ReactWrapper<*> = mount(); const latest: HTMLElement = second.getDOMNode(); expect(latest).not.toBe(document.activeElement); // validation @@ -280,30 +278,25 @@ describe('Focus retention moving between lists (focus retention between mounts)' it('should maintain focus if another component is mounted before the focused component', () => { const first: ReactWrapper<*> = mount( , - options, ); const original: HTMLElement = first.getDOMNode(); expect(original).not.toBe(document.activeElement); // get focus original.focus(); - first.find(DragHandle).simulate('focus'); + first.find('.drag-handle').simulate('focus'); expect(original).toBe(document.activeElement); // unmounting the first first.unmount(); // mounting something with a different id - const other: ReactWrapper<*> = mount( - , - options, - ); + const other: ReactWrapper<*> = mount(); expect(other.getDOMNode()).not.toBe(document.activeElement); // mounting something with the same id as the first const second: ReactWrapper<*> = mount( , - options, ); const latest: HTMLElement = second.getDOMNode(); expect(latest).toBe(document.activeElement); @@ -314,22 +307,19 @@ describe('Focus retention moving between lists (focus retention between mounts)' }); it('should only maintain focus once', () => { - const first: ReactWrapper<*> = mount( - , - options, - ); + const first: ReactWrapper<*> = mount(); const original: HTMLElement = first.getDOMNode(); expect(original).not.toBe(document.activeElement); // get focus original.focus(); - first.find(DragHandle).simulate('focus'); + first.find('.drag-handle').simulate('focus'); expect(original).toBe(document.activeElement); first.unmount(); // obtaining focus on first remount - const second: ReactWrapper<*> = mount(, options); + const second: ReactWrapper<*> = mount(); const latest: HTMLElement = second.getDOMNode(); expect(latest).toBe(document.activeElement); // validation @@ -339,7 +329,7 @@ describe('Focus retention moving between lists (focus retention between mounts)' second.unmount(); // should not obtain focus on the second remount - const third: ReactWrapper<*> = mount(, options); + const third: ReactWrapper<*> = mount(); expect(third.getDOMNode()).not.toBe(document.activeElement); // cleanup @@ -350,16 +340,13 @@ describe('Focus retention moving between lists (focus retention between mounts)' // eslint-disable-next-line react/button-has-type const button: HTMLElement = document.createElement('button'); body.appendChild(button); - const first: ReactWrapper<*> = mount( - , - options, - ); + const first: ReactWrapper<*> = mount(); const original: HTMLElement = first.getDOMNode(); expect(original).not.toBe(document.activeElement); // get focus original.focus(); - first.find(DragHandle).simulate('focus'); + first.find('.drag-handle').simulate('focus'); expect(original).toBe(document.activeElement); first.unmount(); @@ -369,7 +356,7 @@ describe('Focus retention moving between lists (focus retention between mounts)' expect(button).toBe(document.activeElement); // remount should now not claim focus - const second: ReactWrapper<*> = mount(, options); + const second: ReactWrapper<*> = mount(); expect(second.getDOMNode()).not.toBe(document.activeElement); // focus maintained on button expect(button).toBe(document.activeElement); From dcf8ac0f4bc75bdb5b2c10adea9c3daf9810533f Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Fri, 29 Mar 2019 17:07:34 +1100 Subject: [PATCH 093/117] fixing rollup --- .size-snapshot.json | 24 ++++++++++++------------ rollup.config.js | 4 ++-- src/animation.js | 28 ++++++++++++++-------------- 3 files changed, 28 insertions(+), 28 deletions(-) diff --git a/.size-snapshot.json b/.size-snapshot.json index 227fc07505..cb965f0120 100644 --- a/.size-snapshot.json +++ b/.size-snapshot.json @@ -1,25 +1,25 @@ { "dist/react-beautiful-dnd.js": { - "bundled": 357308, - "minified": 138768, - "gzipped": 40712 + "bundled": 393282, + "minified": 148165, + "gzipped": 41353 }, "dist/react-beautiful-dnd.min.js": { - "bundled": 303784, - "minified": 113686, - "gzipped": 32910 + "bundled": 327153, + "minified": 117840, + "gzipped": 33592 }, "dist/react-beautiful-dnd.esm.js": { - "bundled": 237952, - "minified": 125718, - "gzipped": 31356, + "bundled": 239396, + "minified": 123800, + "gzipped": 31505, "treeshaked": { "rollup": { - "code": 85891, - "import_statements": 832 + "code": 30300, + "import_statements": 774 }, "webpack": { - "code": 88596 + "code": 34399 } } } diff --git a/rollup.config.js b/rollup.config.js index b86d918c75..56e215e822 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -33,10 +33,10 @@ const snapshotArgs = const commonjsArgs = { include: 'node_modules/**', - // needed for react-is via react-redux v5.1 + // needed for react-is via react-redux // https://stackoverflow.com/questions/50080893/rollup-error-isvalidelementtype-is-not-exported-by-node-modules-react-is-inde/50098540 namedExports: { - 'node_modules/react-is/index.js': [ + 'node_modules/react-redux/node_modules/react-is/index.js': [ 'isValidElementType', 'isContextConsumer', ], diff --git a/src/animation.js b/src/animation.js index c734b418db..00967732dc 100644 --- a/src/animation.js +++ b/src/animation.js @@ -19,26 +19,26 @@ export const combine = { }, }; -// export const timings = { -// outOfTheWay: 0.2, -// // greater than the out of the way time -// // so that when the drop ends everything will -// // have to be out of the way -// minDropTime: 0.33, -// maxDropTime: 0.55, -// }; - -// slow timings -// uncomment to use export const timings = { - outOfTheWay: 2, + outOfTheWay: 0.2, // greater than the out of the way time // so that when the drop ends everything will // have to be out of the way - minDropTime: 3, - maxDropTime: 4, + minDropTime: 0.33, + maxDropTime: 0.55, }; +// slow timings +// uncomment to use +// export const timings = { +// outOfTheWay: 2, +// // greater than the out of the way time +// // so that when the drop ends everything will +// // have to be out of the way +// minDropTime: 3, +// maxDropTime: 4, +// }; + const outOfTheWayTiming: string = `${timings.outOfTheWay}s ${ curves.outOfTheWay }`; From 5938f16155d7d0ad224281154bde7cf1aca16479 Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Fri, 29 Mar 2019 17:18:14 +1100 Subject: [PATCH 094/117] adding ssr console test --- .../drop-dev-warnings-for-prod.spec.js | 6 ++++- .../server-rendering.spec.js | 23 ++++++++++++++++++- 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/test/unit/integration/drop-dev-warnings-for-prod.spec.js b/test/unit/integration/drop-dev-warnings-for-prod.spec.js index f7c61793ae..11e9d28832 100644 --- a/test/unit/integration/drop-dev-warnings-for-prod.spec.js +++ b/test/unit/integration/drop-dev-warnings-for-prod.spec.js @@ -28,7 +28,11 @@ const getCode = async ({ mode }): Promise => { // needed for react-is via react-redux v5.1 // https://stackoverflow.com/questions/50080893/rollup-error-isvalidelementtype-is-not-exported-by-node-modules-react-is-inde/50098540 namedExports: { - 'node_modules/react-is/index.js': ['isValidElementType'], + 'node_modules/react-redux/node_modules/react-is/index.js': [ + 'isValidElementType', + 'isContextConsumer', + ], + 'node_modules/react-dom/index.js': ['unstable_batchedUpdates'], }, }), ]; diff --git a/test/unit/integration/server-side-rendering/server-rendering.spec.js b/test/unit/integration/server-side-rendering/server-rendering.spec.js index 6781864653..5dff5b52b1 100644 --- a/test/unit/integration/server-side-rendering/server-rendering.spec.js +++ b/test/unit/integration/server-side-rendering/server-rendering.spec.js @@ -1,18 +1,36 @@ +// @flow /** * @jest-environment node */ -// @flow +/* eslint-disable no-console */ import React from 'react'; import { renderToString, renderToStaticMarkup } from 'react-dom/server'; import invariant from 'tiny-invariant'; import { resetServerContext } from '../../../../src'; import App from './app'; +const consoleFunctions: string[] = ['warn', 'error', 'log']; + beforeEach(() => { // Reset server context between tests to prevent state being shared between them resetServerContext(); + consoleFunctions.forEach((name: string) => { + jest.spyOn(console, name); + }); }); +afterEach(() => { + consoleFunctions.forEach((name: string) => { + console[name].mockRestore(); + }); +}); + +const expectConsoleNotCalled = () => { + consoleFunctions.forEach((name: string) => { + console[name].not.toHaveBeenCalled(); + }); +}; + // Checking that the browser globals are not available in this test file invariant( typeof window === 'undefined' && typeof document === 'undefined', @@ -24,6 +42,7 @@ it('should support rendering to a string', () => { expect(result).toEqual(expect.any(String)); expect(result).toMatchSnapshot(); + expectConsoleNotCalled(); }); it('should support rendering to static markup', () => { @@ -31,6 +50,7 @@ it('should support rendering to static markup', () => { expect(result).toEqual(expect.any(String)); expect(result).toMatchSnapshot(); + expectConsoleNotCalled(); }); it('should render identical content when resetting context between renders', () => { @@ -41,4 +61,5 @@ it('should render identical content when resetting context between renders', () resetServerContext(); const nextRenderAfterReset = renderToString(); expect(firstRender).toEqual(nextRenderAfterReset); + expectConsoleNotCalled(); }); From 8e9e6aeb9727c40aaef68fe8e7623167858f3b13 Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Fri, 29 Mar 2019 17:26:25 +1100 Subject: [PATCH 095/117] useIsomorphicLayoutEffect --- src/view/drag-drop-context/app.jsx | 7 ++++--- src/view/droppable/droppable.jsx | 3 --- src/view/droppable/use-validation.js | 6 +----- src/view/use-drag-handle/sensor/use-mouse-sensor.js | 7 +------ src/view/use-drag-handle/sensor/use-touch-sensor.js | 2 +- src/view/use-drag-handle/use-drag-handle.js | 7 ++++--- src/view/use-drag-handle/use-focus-retainer.js | 9 +++++---- .../use-draggable-dimension-publisher.js | 7 ++++--- .../use-droppable-dimension-publisher.js | 9 +++++---- src/view/use-isomorphic-layout-effect.js | 13 +++++++++++++ .../server-side-rendering/server-rendering.spec.js | 7 ++++--- 11 files changed, 42 insertions(+), 35 deletions(-) create mode 100644 src/view/use-isomorphic-layout-effect.js diff --git a/src/view/drag-drop-context/app.jsx b/src/view/drag-drop-context/app.jsx index 278c6fb72b..4d42aa80d8 100644 --- a/src/view/drag-drop-context/app.jsx +++ b/src/view/drag-drop-context/app.jsx @@ -1,5 +1,5 @@ // @flow -import React, { useEffect, useLayoutEffect, useRef, type Node } from 'react'; +import React, { useEffect, useRef, type Node } from 'react'; import { bindActionCreators } from 'redux'; import { Provider } from 'react-redux'; import createStore from '../../state/create-store'; @@ -33,6 +33,7 @@ import useStartupValidation from './use-startup-validation'; import useMemoOne from '../use-custom-memo/use-memo-one'; import useCallbackOne from '../use-custom-memo/use-callback-one'; import { useConstant, useConstantFn } from '../use-constant'; +import useIsomorphicLayoutEffect from '../use-isomorphic-layout-effect'; type Props = {| ...Responders, @@ -141,11 +142,11 @@ export default function App(props: Props) { } }); - useLayoutEffect(() => { + useIsomorphicLayoutEffect(() => { setOnError(tryResetStore); }, [setOnError, tryResetStore]); - useLayoutEffect(() => { + useIsomorphicLayoutEffect(() => { tryResetStore(); }, [tryResetStore]); diff --git a/src/view/droppable/droppable.jsx b/src/view/droppable/droppable.jsx index 590886802c..e2199cbe36 100644 --- a/src/view/droppable/droppable.jsx +++ b/src/view/droppable/droppable.jsx @@ -134,9 +134,6 @@ export default function Droppable(props: Props) { useValidation({ props, getDroppableRef: () => droppableRef.current, - // Not checking on the first placement :( - // The droppable placeholder is not set yet as the useLayoutEffect + setState has not finished - shouldCheckPlaceholder: true, getPlaceholderRef: () => placeholderRef.current, }); diff --git a/src/view/droppable/use-validation.js b/src/view/droppable/use-validation.js index 489044a596..5b30e6914e 100644 --- a/src/view/droppable/use-validation.js +++ b/src/view/droppable/use-validation.js @@ -42,14 +42,12 @@ function checkPlaceholderRef(props: Props, placeholderEl: ?HTMLElement) { type Args = {| props: Props, getDroppableRef: () => ?HTMLElement, - shouldCheckPlaceholder: boolean, getPlaceholderRef: () => ?HTMLElement, |}; export default function useValidation({ props, getDroppableRef, - shouldCheckPlaceholder, getPlaceholderRef, }: Args) { // Running on every update @@ -60,8 +58,6 @@ export default function useValidation({ checkOwnProps(props); checkIsValidInnerRef(getDroppableRef()); - if (shouldCheckPlaceholder) { - checkPlaceholderRef(props, getPlaceholderRef()); - } + checkPlaceholderRef(props, getPlaceholderRef()); }); } diff --git a/src/view/use-drag-handle/sensor/use-mouse-sensor.js b/src/view/use-drag-handle/sensor/use-mouse-sensor.js index 62c52f0b9b..01c06d1e2c 100644 --- a/src/view/use-drag-handle/sensor/use-mouse-sensor.js +++ b/src/view/use-drag-handle/sensor/use-mouse-sensor.js @@ -1,6 +1,6 @@ // @flow import type { Position } from 'css-box-model'; -import { useRef, useLayoutEffect, useCallback, useMemo } from 'react'; +import { useRef, useCallback, useMemo } from 'react'; import invariant from 'tiny-invariant'; import type { EventBinding } from '../util/event-types'; import createEventMarshal, { @@ -368,10 +368,5 @@ export default function useMouseSensor(args: Args): OnMouseDown { [canStartCapturing, getIsCapturing, startPendingDrag], ); - // When unmounting - cancel - // useLayoutEffect(() => { - // return cancel; - // }, [cancel]); - return onMouseDown; } diff --git a/src/view/use-drag-handle/sensor/use-touch-sensor.js b/src/view/use-drag-handle/sensor/use-touch-sensor.js index 6a6e3f5d99..f4b9c057f7 100644 --- a/src/view/use-drag-handle/sensor/use-touch-sensor.js +++ b/src/view/use-drag-handle/sensor/use-touch-sensor.js @@ -1,6 +1,6 @@ // @flow import type { Position } from 'css-box-model'; -import { useRef, useCallback, useMemo, useLayoutEffect } from 'react'; +import { useRef, useCallback, useMemo } from 'react'; import invariant from 'tiny-invariant'; import type { EventBinding } from '../util/event-types'; import createEventMarshal, { diff --git a/src/view/use-drag-handle/use-drag-handle.js b/src/view/use-drag-handle/use-drag-handle.js index 06882242d6..22ff8d3b45 100644 --- a/src/view/use-drag-handle/use-drag-handle.js +++ b/src/view/use-drag-handle/use-drag-handle.js @@ -1,6 +1,6 @@ // @flow import invariant from 'tiny-invariant'; -import { useLayoutEffect, useRef, useMemo, useCallback } from 'react'; +import { useRef, useMemo, useCallback } from 'react'; import type { Args, DragHandleProps } from './drag-handle-types'; import getWindowFromEl from '../window/get-window-from-el'; import useRequiredContext from '../use-required-context'; @@ -19,6 +19,7 @@ import usePreviousRef from '../use-previous-ref'; import { warning } from '../../dev-warning'; import useValidation from './use-validation'; import useFocusRetainer from './use-focus-retainer'; +import useIsomorphicLayoutEffect from '../use-isomorphic-layout-effect'; function preventHtml5Dnd(event: DragEvent) { event.preventDefault(); @@ -165,7 +166,7 @@ export default function useDragHandle(args: Args): ?DragHandleProps { // aborting on unmount - useLayoutEffect(() => { + useIsomorphicLayoutEffect(() => { // only when unmounting return () => { if (!capturingRef.current) { @@ -196,7 +197,7 @@ export default function useDragHandle(args: Args): ?DragHandleProps { // No longer dragging but still capturing: need to abort // Using a layout effect to ensure that there is a flip from isDragging => !isDragging // When there is a pending drag !isDragging will always be true - useLayoutEffect(() => { + useIsomorphicLayoutEffect(() => { if (!isDragging && capturingRef.current) { abortCapture(); } diff --git a/src/view/use-drag-handle/use-focus-retainer.js b/src/view/use-drag-handle/use-focus-retainer.js index c0d74f5e9f..a9c869fa30 100644 --- a/src/view/use-drag-handle/use-focus-retainer.js +++ b/src/view/use-drag-handle/use-focus-retainer.js @@ -1,10 +1,11 @@ // @flow import invariant from 'tiny-invariant'; -import { useRef, useCallback, useLayoutEffect } from 'react'; +import { useRef, useCallback } from 'react'; import type { Args } from './drag-handle-types'; import usePrevious from '../use-previous-ref'; import focusRetainer from './util/focus-retainer'; import getDragHandleRef from './util/get-drag-handle-ref'; +import useIsomorphicLayoutEffect from '../use-isomorphic-layout-effect'; export type Result = {| onBlur: () => void, @@ -25,7 +26,7 @@ export default function useFocusRetainer(args: Args): Result { isFocusedRef.current = false; }, []); - useLayoutEffect(() => { + useIsomorphicLayoutEffect(() => { // mounting: try to restore focus const first: Args = lastArgsRef.current; if (!first.isEnabled) { @@ -63,7 +64,7 @@ export default function useFocusRetainer(args: Args): Result { const lastDraggableRef = useRef(getDraggableRef()); - useLayoutEffect(() => { + useIsomorphicLayoutEffect(() => { const draggableRef: ?HTMLElement = getDraggableRef(); // Cannot focus on nothing @@ -84,7 +85,7 @@ export default function useFocusRetainer(args: Args): Result { // Doing our own should run check }); - useLayoutEffect(() => { + useIsomorphicLayoutEffect(() => { lastDraggableRef.current = getDraggableRef(); }); diff --git a/src/view/use-draggable-dimension-publisher/use-draggable-dimension-publisher.js b/src/view/use-draggable-dimension-publisher/use-draggable-dimension-publisher.js index 9f5e8a3ba0..10f1c5f20e 100644 --- a/src/view/use-draggable-dimension-publisher/use-draggable-dimension-publisher.js +++ b/src/view/use-draggable-dimension-publisher/use-draggable-dimension-publisher.js @@ -1,5 +1,5 @@ // @flow -import { useMemo, useRef, useCallback, useLayoutEffect } from 'react'; +import { useMemo, useRef, useCallback } from 'react'; import { type Position } from 'css-box-model'; import invariant from 'tiny-invariant'; import type { @@ -14,6 +14,7 @@ import getDimension from './get-dimension'; import DroppableContext, { type DroppableContextValue, } from '../context/droppable-context'; +import useIsomorphicLayoutEffect from '../use-isomorphic-layout-effect'; export type Args = {| draggableId: DraggableId, @@ -56,13 +57,13 @@ export default function useDraggableDimensionPublisher(args: Args) { ); // handle mounting / unmounting - useLayoutEffect(() => { + useIsomorphicLayoutEffect(() => { marshal.registerDraggable(publishedDescriptorRef.current, makeDimension); return () => marshal.unregisterDraggable(publishedDescriptorRef.current); }, [makeDimension, marshal]); // handle updates to descriptor - useLayoutEffect(() => { + useIsomorphicLayoutEffect(() => { // this will happen when mounting if (publishedDescriptorRef.current === descriptor) { return; diff --git a/src/view/use-droppable-dimension-publisher/use-droppable-dimension-publisher.js b/src/view/use-droppable-dimension-publisher/use-droppable-dimension-publisher.js index 24cc1c5780..cc61b140cd 100644 --- a/src/view/use-droppable-dimension-publisher/use-droppable-dimension-publisher.js +++ b/src/view/use-droppable-dimension-publisher/use-droppable-dimension-publisher.js @@ -1,5 +1,5 @@ // @flow -import { useCallback, useMemo, useLayoutEffect, useRef } from 'react'; +import { useCallback, useMemo, useRef } from 'react'; import invariant from 'tiny-invariant'; import { type Position } from 'css-box-model'; import rafSchedule from 'raf-schd'; @@ -28,6 +28,7 @@ import { warning } from '../../dev-warning'; import getListenerOptions from './get-listener-options'; import useRequiredContext from '../use-required-context'; import usePreviousRef from '../use-previous-ref'; +import useIsomorphicLayoutEffect from '../use-isomorphic-layout-effect'; type Props = {| droppableId: DroppableId, @@ -235,7 +236,7 @@ export default function useDroppableDimensionPublisher(args: Props) { // Register with the marshal and let it know of: // - any descriptor changes // - when it unmounts - useLayoutEffect(() => { + useIsomorphicLayoutEffect(() => { publishedDescriptorRef.current = descriptor; marshal.registerDroppable(descriptor, callbacks); @@ -253,7 +254,7 @@ export default function useDroppableDimensionPublisher(args: Props) { // update is enabled with the marshal // only need to update when there is a drag - useLayoutEffect(() => { + useIsomorphicLayoutEffect(() => { if (!whileDraggingRef.current) { return; } @@ -265,7 +266,7 @@ export default function useDroppableDimensionPublisher(args: Props) { // update is combine enabled with the marshal // only need to update when there is a drag - useLayoutEffect(() => { + useIsomorphicLayoutEffect(() => { if (!whileDraggingRef.current) { return; } diff --git a/src/view/use-isomorphic-layout-effect.js b/src/view/use-isomorphic-layout-effect.js new file mode 100644 index 0000000000..ef32b241ec --- /dev/null +++ b/src/view/use-isomorphic-layout-effect.js @@ -0,0 +1,13 @@ +// @flow +import { useLayoutEffect, useEffect } from 'react'; + +// https://github.com/reduxjs/react-redux/blob/v7-beta/src/components/connectAdvanced.js#L35 +// React currently throws a warning when using useLayoutEffect on the server. +// To get around it, we can conditionally useEffect on the server (no-op) and +// useLayoutEffect in the browser. We need useLayoutEffect because we want +// `connect` to perform sync updates to a ref to save the latest props after +// a render is actually committed to the DOM. +const useIsomorphicLayoutEffect = + typeof window !== 'undefined' ? useLayoutEffect : useEffect; + +export default useIsomorphicLayoutEffect; diff --git a/test/unit/integration/server-side-rendering/server-rendering.spec.js b/test/unit/integration/server-side-rendering/server-rendering.spec.js index 5dff5b52b1..fd66e1935b 100644 --- a/test/unit/integration/server-side-rendering/server-rendering.spec.js +++ b/test/unit/integration/server-side-rendering/server-rendering.spec.js @@ -1,8 +1,7 @@ -// @flow /** * @jest-environment node */ -/* eslint-disable no-console */ +// @flow import React from 'react'; import { renderToString, renderToStaticMarkup } from 'react-dom/server'; import invariant from 'tiny-invariant'; @@ -21,13 +20,15 @@ beforeEach(() => { afterEach(() => { consoleFunctions.forEach((name: string) => { + // eslint-disable-next-line no-console console[name].mockRestore(); }); }); const expectConsoleNotCalled = () => { consoleFunctions.forEach((name: string) => { - console[name].not.toHaveBeenCalled(); + // eslint-disable-next-line no-console + expect(console[name]).not.toHaveBeenCalled(); }); }; From a4c7d7097ddaac7199e7227deeecbc021802493b Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Mon, 1 Apr 2019 08:47:33 +1100 Subject: [PATCH 096/117] updating comment --- test/unit/integration/drop-dev-warnings-for-prod.spec.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/unit/integration/drop-dev-warnings-for-prod.spec.js b/test/unit/integration/drop-dev-warnings-for-prod.spec.js index 11e9d28832..168b78e76d 100644 --- a/test/unit/integration/drop-dev-warnings-for-prod.spec.js +++ b/test/unit/integration/drop-dev-warnings-for-prod.spec.js @@ -25,7 +25,7 @@ const getCode = async ({ mode }): Promise => { resolve({ extensions }), commonjs({ include: 'node_modules/**', - // needed for react-is via react-redux v5.1 + // needed for react-is via react-redux // https://stackoverflow.com/questions/50080893/rollup-error-isvalidelementtype-is-not-exported-by-node-modules-react-is-inde/50098540 namedExports: { 'node_modules/react-redux/node_modules/react-is/index.js': [ From c40adab7f789b091a319bd5bc602d1640d81d44f Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Mon, 1 Apr 2019 10:21:38 +1100 Subject: [PATCH 097/117] updating snapshot and type --- .size-snapshot.json | 22 +++++++++++----------- src/view/use-custom-memo/use-memo-one.js | 2 +- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/.size-snapshot.json b/.size-snapshot.json index cb965f0120..3a1cdefde0 100644 --- a/.size-snapshot.json +++ b/.size-snapshot.json @@ -1,25 +1,25 @@ { "dist/react-beautiful-dnd.js": { - "bundled": 393282, - "minified": 148165, - "gzipped": 41353 + "bundled": 393322, + "minified": 147989, + "gzipped": 41340 }, "dist/react-beautiful-dnd.min.js": { - "bundled": 327153, - "minified": 117840, - "gzipped": 33592 + "bundled": 327193, + "minified": 117669, + "gzipped": 33593 }, "dist/react-beautiful-dnd.esm.js": { - "bundled": 239396, - "minified": 123800, - "gzipped": 31505, + "bundled": 239473, + "minified": 123943, + "gzipped": 31513, "treeshaked": { "rollup": { - "code": 30300, + "code": 30284, "import_statements": 774 }, "webpack": { - "code": 34399 + "code": 34182 } } } diff --git a/src/view/use-custom-memo/use-memo-one.js b/src/view/use-custom-memo/use-memo-one.js index 04daeced92..94ee4bfb10 100644 --- a/src/view/use-custom-memo/use-memo-one.js +++ b/src/view/use-custom-memo/use-memo-one.js @@ -9,7 +9,7 @@ export default function useMemoOne( inputs?: mixed[] = [], ): T { const isFirstCallRef = useRef(true); - const lastInputsRef = useRef(inputs); + const lastInputsRef = useRef(inputs); // Cannot lazy create a ref value, so setting to null // $ExpectError - T is not null const resultRef = useRef(null); From 47f7840a1ca2bb4d97b9082c1c90d9699dda70aa Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Mon, 1 Apr 2019 10:24:06 +1100 Subject: [PATCH 098/117] changing order in use-memo-one --- src/view/use-custom-memo/use-memo-one.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/view/use-custom-memo/use-memo-one.js b/src/view/use-custom-memo/use-memo-one.js index 94ee4bfb10..59d351c5a2 100644 --- a/src/view/use-custom-memo/use-memo-one.js +++ b/src/view/use-custom-memo/use-memo-one.js @@ -26,7 +26,8 @@ export default function useMemoOne( return resultRef.current; } - lastInputsRef.current = inputs; + // try to generate result first in case it throws resultRef.current = getResult(); + lastInputsRef.current = inputs; return resultRef.current; } From adbd7c8027185e9449cb5fbc21ffa0ac16a6eb16 Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Mon, 1 Apr 2019 10:38:43 +1100 Subject: [PATCH 099/117] fixing brwoser test --- stories/1-single-vertical-list.stories.js | 2 +- stories/src/data.js | 4 ++-- stories/src/portal/portal-app.jsx | 1 - test/unit/view/draggable/util/mount.js | 1 - 4 files changed, 3 insertions(+), 5 deletions(-) diff --git a/stories/1-single-vertical-list.stories.js b/stories/1-single-vertical-list.stories.js index d0e1a2735f..764070b7d6 100644 --- a/stories/1-single-vertical-list.stories.js +++ b/stories/1-single-vertical-list.stories.js @@ -7,7 +7,7 @@ import { quotes, getQuotes } from './src/data'; import { grid } from './src/constants'; const data = { - small: quotes.slice(0, 2), + small: quotes, medium: getQuotes(40), large: getQuotes(500), }; diff --git a/stories/src/data.js b/stories/src/data.js index efd6512a94..4e466b5b60 100644 --- a/stories/src/data.js +++ b/stories/src/data.js @@ -54,12 +54,12 @@ export const authors: Author[] = [jake, BMO, finn, princess]; export const quotes: Quote[] = [ { - id: 'item:0', + id: '0', content: 'Sometimes life is scary and dark', author: BMO, }, { - id: 'item:1', + id: '1', content: 'Sucking at something is the first step towards being sorta good at something.', author: jake, diff --git a/stories/src/portal/portal-app.jsx b/stories/src/portal/portal-app.jsx index 3b8b5e8480..4521da46ed 100644 --- a/stories/src/portal/portal-app.jsx +++ b/stories/src/portal/portal-app.jsx @@ -75,7 +75,6 @@ class PortalAwareItem extends Component { ); if (!usePortal) { - console.warn('rendering out of portal'); return child; } diff --git a/test/unit/view/draggable/util/mount.js b/test/unit/view/draggable/util/mount.js index 6abe278b0f..1dfc1f3412 100644 --- a/test/unit/view/draggable/util/mount.js +++ b/test/unit/view/draggable/util/mount.js @@ -9,7 +9,6 @@ import type { Provided, StateSnapshot, } from '../../../../../src/view/draggable/draggable-types'; -import type { StyleMarshal } from '../../../../../src/view/use-style-marshal/style-marshal-types'; import { atRestMapProps, getDispatchPropsStub, From d51f9692d271729456bd24837bd88df73850b7ad Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Mon, 1 Apr 2019 10:56:32 +1100 Subject: [PATCH 100/117] trying to again fix browser tests --- cypress/integration/move-between-lists.spec.js | 2 +- cypress/integration/reorder-lists.spec.js | 2 +- cypress/integration/reorder.spec.js | 4 +--- src/view/use-style-marshal/use-style-marshal.js | 12 +++++++----- stories/src/data.js | 4 ++-- 5 files changed, 12 insertions(+), 12 deletions(-) diff --git a/cypress/integration/move-between-lists.spec.js b/cypress/integration/move-between-lists.spec.js index b5971bb034..9571355359 100644 --- a/cypress/integration/move-between-lists.spec.js +++ b/cypress/integration/move-between-lists.spec.js @@ -3,7 +3,7 @@ import * as keyCodes from '../../src/view/key-codes'; import { timings } from '../../src/animation'; beforeEach(() => { - cy.visit('/iframe.html?selectedKind=board&selectedStory=simple'); + cy.visit('/iframe.html?id=board--simple'); }); it('should move between lists', () => { diff --git a/cypress/integration/reorder-lists.spec.js b/cypress/integration/reorder-lists.spec.js index 7e27edd128..bfe2017fd0 100644 --- a/cypress/integration/reorder-lists.spec.js +++ b/cypress/integration/reorder-lists.spec.js @@ -3,7 +3,7 @@ import * as keyCodes from '../../src/view/key-codes'; import { timings } from '../../src/animation'; beforeEach(() => { - cy.visit('/iframe.html?selectedKind=board&selectedStory=simple'); + cy.visit('/iframe.html?id=board--simple'); }); it('should reorder lists', () => { diff --git a/cypress/integration/reorder.spec.js b/cypress/integration/reorder.spec.js index 372b02b282..6e3f7dba4d 100644 --- a/cypress/integration/reorder.spec.js +++ b/cypress/integration/reorder.spec.js @@ -3,9 +3,7 @@ import * as keyCodes from '../../src/view/key-codes'; import { timings } from '../../src/animation'; beforeEach(() => { - cy.visit( - '/iframe.html?selectedKind=single%20vertical%20list&selectedStory=basic', - ); + cy.visit('/iframe.html?id=single-vertical-list--basic'); }); it('should reorder a list', () => { diff --git a/src/view/use-style-marshal/use-style-marshal.js b/src/view/use-style-marshal/use-style-marshal.js index ba3d6e78f7..2f6cfdf62d 100644 --- a/src/view/use-style-marshal/use-style-marshal.js +++ b/src/view/use-style-marshal/use-style-marshal.js @@ -1,11 +1,12 @@ // @flow -import { useRef, useCallback, useEffect, useMemo } from 'react'; -import invariant from 'tiny-invariant'; +import { useRef, useCallback, useMemo } from 'react'; import memoizeOne from 'memoize-one'; -import getStyles, { type Styles } from './get-styles'; -import { prefix } from '../data-attributes'; +import invariant from 'tiny-invariant'; import type { StyleMarshal } from './style-marshal-types'; import type { DropReason } from '../../types'; +import getStyles, { type Styles } from './get-styles'; +import { prefix } from '../data-attributes'; +import useIsomorphicLayoutEffect from '../use-isomorphic-layout-effect'; const getHead = (): HTMLHeadElement => { const head: ?HTMLHeadElement = document.querySelector('head'); @@ -43,7 +44,8 @@ export default function useStyleMarshal(uniqueId: number) { el.textContent = proposed; }, []); - useEffect(() => { + // using layout effect as programatic dragging might start straight away (such as for cypress) + useIsomorphicLayoutEffect(() => { invariant( !alwaysRef.current && !dynamicRef.current, 'style elements already mounted', diff --git a/stories/src/data.js b/stories/src/data.js index 4e466b5b60..f167807b55 100644 --- a/stories/src/data.js +++ b/stories/src/data.js @@ -54,12 +54,12 @@ export const authors: Author[] = [jake, BMO, finn, princess]; export const quotes: Quote[] = [ { - id: '0', + id: '1', content: 'Sometimes life is scary and dark', author: BMO, }, { - id: '1', + id: '2', content: 'Sucking at something is the first step towards being sorta good at something.', author: jake, From 3d77a43f8c0e9825193dbeeaaabe657859256e4d Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Mon, 1 Apr 2019 11:08:02 +1100 Subject: [PATCH 101/117] fixing tests --- .../integration/drop-dev-warnings-for-prod.spec.js | 4 ++-- .../server-side-rendering/client-hydration.spec.js | 12 ++++++++++++ 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/test/unit/integration/drop-dev-warnings-for-prod.spec.js b/test/unit/integration/drop-dev-warnings-for-prod.spec.js index 168b78e76d..b494e77d3b 100644 --- a/test/unit/integration/drop-dev-warnings-for-prod.spec.js +++ b/test/unit/integration/drop-dev-warnings-for-prod.spec.js @@ -43,13 +43,13 @@ const getCode = async ({ mode }): Promise => { const inputOptions = { input: './src/index.js', - external: ['react'], + external: ['react', 'react-dom'], plugins, }; const outputOptions = { format: 'umd', name: 'ReactBeautifulDnd', - globals: { react: 'React' }, + globals: { react: 'React', 'react-dom': 'ReactDOM' }, }; const bundle = await rollup(inputOptions); const result = await bundle.generate(outputOptions); diff --git a/test/unit/integration/server-side-rendering/client-hydration.spec.js b/test/unit/integration/server-side-rendering/client-hydration.spec.js index 9eb97f6812..94d0ea37ac 100644 --- a/test/unit/integration/server-side-rendering/client-hydration.spec.js +++ b/test/unit/integration/server-side-rendering/client-hydration.spec.js @@ -19,8 +19,20 @@ invariant( it('should support hydrating a server side rendered application', () => { // would be done server side + // we need to mock out the warnings caused by useLayoutEffect + // This will not happen on the client as the string is rendered + // on the server + jest.spyOn(console, 'error').mockImplementation(() => {}); + const serverHTML: string = ReactDOMServer.renderToString(); + console.error.mock.calls.forEach(call => { + expect( + call[0].includes('Warning: useLayoutEffect does nothing on the server'), + ).toBe(true); + }); + console.error.mockRestore(); + // would be done client side // would have a fresh server context on the client resetServerContext(); From 972669d91af09b8d08b9c6f176058c4d00c2c6b1 Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Mon, 1 Apr 2019 13:51:45 +1100 Subject: [PATCH 102/117] removing unused files --- .../dimension-marshal-types.js | 5 - .../dimension-marshal/dimension-marshal.js | 1 - src/view/animate-in-out/animate-in-out.jsx | 4 +- src/view/drag-drop-context/app.jsx | 19 +- src/view/droppable/droppable.jsx | 11 - src/view/use-constant.js | 29 -- .../sensor/create-keyboard-sensor.js | 233 ---------- .../sensor/create-mouse-sensor.js | 343 -------------- .../sensor/create-touch-sensor.js | 432 ------------------ 9 files changed, 11 insertions(+), 1066 deletions(-) delete mode 100644 src/view/use-constant.js delete mode 100644 src/view/use-drag-handle/sensor/create-keyboard-sensor.js delete mode 100644 src/view/use-drag-handle/sensor/create-mouse-sensor.js delete mode 100644 src/view/use-drag-handle/sensor/create-touch-sensor.js diff --git a/src/state/dimension-marshal/dimension-marshal-types.js b/src/state/dimension-marshal/dimension-marshal-types.js index 6101dd027f..2df5ce7471 100644 --- a/src/state/dimension-marshal/dimension-marshal-types.js +++ b/src/state/dimension-marshal/dimension-marshal-types.js @@ -90,11 +90,6 @@ export type DimensionMarshal = {| descriptor: DroppableDescriptor, callbacks: DroppableCallbacks, ) => void, - // updateDroppable: ( - // previous: DroppableDescriptor, - // descriptor: DroppableDescriptor, - // callbacks: DroppableCallbacks, - // ) => void, // it is possible for a droppable to change whether it is enabled during a drag updateDroppableIsEnabled: (id: DroppableId, isEnabled: boolean) => void, // it is also possible to update whether combining is enabled diff --git a/src/state/dimension-marshal/dimension-marshal.js b/src/state/dimension-marshal/dimension-marshal.js index b61f0c71ad..e749000684 100644 --- a/src/state/dimension-marshal/dimension-marshal.js +++ b/src/state/dimension-marshal/dimension-marshal.js @@ -303,7 +303,6 @@ export default (callbacks: Callbacks) => { updateDraggable, unregisterDraggable, registerDroppable, - // updateDroppable, unregisterDroppable, // droppable changes diff --git a/src/view/animate-in-out/animate-in-out.jsx b/src/view/animate-in-out/animate-in-out.jsx index 5c3b2cefb8..cb05e2eab1 100644 --- a/src/view/animate-in-out/animate-in-out.jsx +++ b/src/view/animate-in-out/animate-in-out.jsx @@ -20,7 +20,9 @@ type State = {| animate: InOutAnimationMode, |}; -// Using a class here rather than hooks because getDerivedStateFromProps is boss +// Using a class here rather than hooks because +// getDerivedStateFromProps results in far less renders. +// Using hooks to implement this was quite messy and resulted in lots of additional renders export default class AnimateInOut extends React.PureComponent { state: State = { diff --git a/src/view/drag-drop-context/app.jsx b/src/view/drag-drop-context/app.jsx index 4d42aa80d8..28bda10032 100644 --- a/src/view/drag-drop-context/app.jsx +++ b/src/view/drag-drop-context/app.jsx @@ -32,8 +32,8 @@ import AppContext, { type AppContextValue } from '../context/app-context'; import useStartupValidation from './use-startup-validation'; import useMemoOne from '../use-custom-memo/use-memo-one'; import useCallbackOne from '../use-custom-memo/use-callback-one'; -import { useConstant, useConstantFn } from '../use-constant'; import useIsomorphicLayoutEffect from '../use-isomorphic-layout-effect'; +import usePrevious from '../use-previous-ref'; type Props = {| ...Responders, @@ -59,25 +59,22 @@ export default function App(props: Props) { useStartupValidation(); // lazy collection of responders using a ref - update on ever render - const lastPropsRef = useRef(props); - useEffect(() => { - lastPropsRef.current = props; - }); + const lastPropsRef = usePrevious(props); - const getResponders: () => Responders = useConstantFn(() => { + const getResponders: () => Responders = useCallbackOne(() => { return createResponders(lastPropsRef.current); }); const announce: Announce = useAnnouncer(uniqueId); const styleMarshal: StyleMarshal = useStyleMarshal(uniqueId); - const lazyDispatch: Action => void = useConstantFn( + const lazyDispatch: Action => void = useCallbackOne( (action: Action): void => { storeRef.current.dispatch(action); }, ); - const callbacks: DimensionMarshalCallbacks = useConstant(() => + const callbacks: DimensionMarshalCallbacks = useMemoOne(() => bindActionCreators( { publishWhileDragging, @@ -90,11 +87,11 @@ export default function App(props: Props) { lazyDispatch, ), ); - const dimensionMarshal: DimensionMarshal = useConstant(() => + const dimensionMarshal: DimensionMarshal = useMemoOne(() => createDimensionMarshal(callbacks), ); - const autoScroller: AutoScroller = useConstant(() => + const autoScroller: AutoScroller = useMemoOne(() => createAutoScroller({ scrollWindow, scrollDroppable: dimensionMarshal.scrollDroppable, @@ -108,7 +105,7 @@ export default function App(props: Props) { }), ); - const store: Store = useConstant(() => + const store: Store = useMemoOne(() => createStore({ dimensionMarshal, styleMarshal, diff --git a/src/view/droppable/droppable.jsx b/src/view/droppable/droppable.jsx index e2199cbe36..bc4fc863ae 100644 --- a/src/view/droppable/droppable.jsx +++ b/src/view/droppable/droppable.jsx @@ -101,17 +101,6 @@ export default function Droppable(props: Props) { ); - // const placeholder: Node | null = instruction ? ( - // - // ) : null; - const provided: Provided = useMemo( (): Provided => ({ innerRef: setDroppableRef, diff --git a/src/view/use-constant.js b/src/view/use-constant.js deleted file mode 100644 index 7f22eed25d..0000000000 --- a/src/view/use-constant.js +++ /dev/null @@ -1,29 +0,0 @@ -// @flow -import { useRef } from 'react'; - -// Using an object to hold the result incase the result is falsy -type Result = { - value: T, -}; - -// Similar to useMemo(() => T, []), but not subject to memoization purging -export function useConstant(fn: () => T): T { - const bucket = useRef>(null); - - if (bucket.current) { - return bucket.current.value; - } - - bucket.current = { - value: fn(), - }; - - return bucket.current.value; -} - -export function useConstantFn(getter: GetterFn): GetterFn { - // set bucket with original getter - const bucket = useRef(getter); - // never override the original, just keep returning it - return bucket.current; -} diff --git a/src/view/use-drag-handle/sensor/create-keyboard-sensor.js b/src/view/use-drag-handle/sensor/create-keyboard-sensor.js deleted file mode 100644 index 477f8815a5..0000000000 --- a/src/view/use-drag-handle/sensor/create-keyboard-sensor.js +++ /dev/null @@ -1,233 +0,0 @@ -// @flow -/* eslint-disable no-use-before-define */ -import invariant from 'tiny-invariant'; -import { type Position } from 'css-box-model'; -import createScheduler from '../util/create-scheduler'; -import preventStandardKeyEvents from '../util/prevent-standard-key-events'; -import * as keyCodes from '../../key-codes'; -import getBorderBoxCenterPosition from '../../get-border-box-center-position'; -import { bindEvents, unbindEvents } from '../util/bind-events'; -import supportedPageVisibilityEventName from '../util/supported-page-visibility-event-name'; -import type { EventBinding } from '../util/event-types'; -import type { KeyboardSensor, CreateSensorArgs } from './sensor-types'; - -type State = {| - isDragging: boolean, -|}; - -type KeyMap = { - [key: number]: true, -}; - -const scrollJumpKeys: KeyMap = { - [keyCodes.pageDown]: true, - [keyCodes.pageUp]: true, - [keyCodes.home]: true, - [keyCodes.end]: true, -}; - -const noop = () => {}; - -export default ({ - getCallbacks, - getWindow, - getDraggableRef, - canStartCapturing, -}: CreateSensorArgs): KeyboardSensor => { - let state: State = { - isDragging: false, - }; - const setState = (newState: State): void => { - state = newState; - }; - const startDragging = (fn?: Function = noop) => { - setState({ - isDragging: true, - }); - bindWindowEvents(); - fn(); - }; - const stopDragging = (postDragFn?: Function = noop) => { - schedule.cancel(); - unbindWindowEvents(); - setState({ isDragging: false }); - postDragFn(); - }; - const kill = () => { - if (state.isDragging) { - stopDragging(); - } - }; - const cancel = () => { - stopDragging(getCallbacks().onCancel); - }; - const isDragging = (): boolean => state.isDragging; - // TODO: we assume here that the callbacks can never change - const schedule = createScheduler(getCallbacks()); - - const onKeyDown = (event: KeyboardEvent) => { - // not yet dragging - if (!isDragging()) { - // We may already be lifting on a child draggable. - // We do not need to use an EventMarshal here as - // we always call preventDefault on the first input - if (event.defaultPrevented) { - return; - } - - // Cannot lift at this time - if (!canStartCapturing(event)) { - return; - } - - if (event.keyCode !== keyCodes.space) { - return; - } - - const ref: ?HTMLElement = getDraggableRef(); - - invariant(ref, 'Cannot start a keyboard drag without a draggable ref'); - - // using center position as selection - const center: Position = getBorderBoxCenterPosition(ref); - - // we are using this event for part of the drag - event.preventDefault(); - startDragging(() => - getCallbacks().onLift({ - clientSelection: center, - movementMode: 'SNAP', - }), - ); - return; - } - - // Cancelling - if (event.keyCode === keyCodes.escape) { - event.preventDefault(); - cancel(); - return; - } - - // Dropping - if (event.keyCode === keyCodes.space) { - // need to stop parent Draggable's thinking this is a lift - event.preventDefault(); - stopDragging(getCallbacks().onDrop); - return; - } - - // Movement - - if (event.keyCode === keyCodes.arrowDown) { - event.preventDefault(); - schedule.moveDown(); - return; - } - - if (event.keyCode === keyCodes.arrowUp) { - event.preventDefault(); - schedule.moveUp(); - return; - } - - if (event.keyCode === keyCodes.arrowRight) { - event.preventDefault(); - schedule.moveRight(); - return; - } - - if (event.keyCode === keyCodes.arrowLeft) { - event.preventDefault(); - schedule.moveLeft(); - return; - } - - // preventing scroll jumping at this time - if (scrollJumpKeys[event.keyCode]) { - event.preventDefault(); - return; - } - - preventStandardKeyEvents(event); - }; - - const windowBindings: EventBinding[] = [ - // any mouse actions kills a drag - { - eventName: 'mousedown', - fn: cancel, - }, - { - eventName: 'mouseup', - fn: cancel, - }, - { - eventName: 'click', - fn: cancel, - }, - { - eventName: 'touchstart', - fn: cancel, - }, - // resizing the browser kills a drag - { - eventName: 'resize', - fn: cancel, - }, - // kill if the user is using the mouse wheel - // We are not supporting wheel / trackpad scrolling with keyboard dragging - { - eventName: 'wheel', - fn: cancel, - // chrome says it is a violation for this to not be passive - // it is fine for it to be passive as we just cancel as soon as we get - // any event - options: { passive: true }, - }, - // Need to respond instantly to a jump scroll request - // Not using the scheduler - { - eventName: 'scroll', - // Scroll events on elements do not bubble, but they go through the capture phase - // https://twitter.com/alexandereardon/status/985994224867819520 - // Using capture: false here as we want to avoid intercepting droppable scroll requests - options: { capture: false }, - fn: (event: UIEvent) => { - // IE11 fix: - // Scrollable events still bubble up and are caught by this handler in ie11. - // We can ignore this event - if (event.currentTarget !== getWindow()) { - return; - } - - getCallbacks().onWindowScroll(); - }, - }, - // Cancel on page visibility change - { - eventName: supportedPageVisibilityEventName, - fn: cancel, - }, - ]; - - const bindWindowEvents = () => { - bindEvents(getWindow(), windowBindings, { capture: true }); - }; - - const unbindWindowEvents = () => { - unbindEvents(getWindow(), windowBindings, { capture: true }); - }; - - const sensor: KeyboardSensor = { - onKeyDown, - kill, - isDragging, - // a drag starts instantly so capturing is the same as dragging - isCapturing: isDragging, - // no additional cleanup needed other then what it is kill - unmount: kill, - }; - - return sensor; -}; diff --git a/src/view/use-drag-handle/sensor/create-mouse-sensor.js b/src/view/use-drag-handle/sensor/create-mouse-sensor.js deleted file mode 100644 index d8b91ea9e6..0000000000 --- a/src/view/use-drag-handle/sensor/create-mouse-sensor.js +++ /dev/null @@ -1,343 +0,0 @@ -// @flow -/* eslint-disable no-use-before-define */ -import invariant from 'tiny-invariant'; -import { type Position } from 'css-box-model'; -import createScheduler from '../util/create-scheduler'; -import isSloppyClickThresholdExceeded from '../util/is-sloppy-click-threshold-exceeded'; -import * as keyCodes from '../../key-codes'; -import preventStandardKeyEvents from '../util/prevent-standard-key-events'; -import createPostDragEventPreventer, { - type EventPreventer, -} from '../util/create-post-drag-event-preventer'; -import { bindEvents, unbindEvents } from '../util/bind-events'; -import createEventMarshal, { - type EventMarshal, -} from '../util/create-event-marshal'; -import supportedPageVisibilityEventName from '../util/supported-page-visibility-event-name'; -import type { EventBinding } from '../util/event-types'; -import type { MouseSensor, CreateSensorArgs } from './sensor-types'; -import { warning } from '../../../dev-warning'; - -// Custom event format for force press inputs -type MouseForceChangedEvent = MouseEvent & { - webkitForce?: number, -}; - -type State = {| - isDragging: boolean, - pending: ?Position, -|}; - -// https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/button -const primaryButton: number = 0; -const noop = () => {}; - -// shared management of mousedown without needing to call preventDefault() -const mouseDownMarshal: EventMarshal = createEventMarshal(); - -export default ({ - getCallbacks, - getWindow, - canStartCapturing, - getShouldRespectForceTouch, -}: CreateSensorArgs): MouseSensor => { - let state: State = { - isDragging: false, - pending: null, - }; - const setState = (newState: State): void => { - state = newState; - }; - const isDragging = (): boolean => state.isDragging; - const isCapturing = (): boolean => Boolean(state.pending || state.isDragging); - // TODO: we assume here that the callbacks can never change - const schedule = createScheduler(getCallbacks()); - const postDragEventPreventer: EventPreventer = createPostDragEventPreventer( - getWindow, - ); - - const startDragging = (fn?: Function = noop) => { - setState({ - pending: null, - isDragging: true, - }); - fn(); - }; - const stopDragging = ( - fn?: Function = noop, - shouldBlockClick?: boolean = true, - ) => { - schedule.cancel(); - unbindWindowEvents(); - mouseDownMarshal.reset(); - if (shouldBlockClick) { - postDragEventPreventer.preventNext(); - } - setState({ - isDragging: false, - pending: null, - }); - fn(); - }; - const startPendingDrag = (point: Position) => { - setState({ pending: point, isDragging: false }); - bindWindowEvents(); - }; - const stopPendingDrag = () => { - stopDragging(noop, false); - }; - - const kill = (fn?: Function = noop) => { - if (state.pending) { - stopPendingDrag(); - return; - } - if (state.isDragging) { - stopDragging(fn); - } - }; - - const unmount = (): void => { - kill(); - postDragEventPreventer.abort(); - }; - - const cancel = () => { - kill(getCallbacks().onCancel); - }; - - const windowBindings: EventBinding[] = [ - { - eventName: 'mousemove', - fn: (event: MouseEvent) => { - const { button, clientX, clientY } = event; - if (button !== primaryButton) { - return; - } - - const point: Position = { - x: clientX, - y: clientY, - }; - - // Already dragging - if (state.isDragging) { - // preventing default as we are using this event - event.preventDefault(); - schedule.move(point); - return; - } - - // There should be a pending drag at this point - - if (!state.pending) { - // this should be an impossible state - // we cannot use kill directly as it checks if there is a pending drag - stopPendingDrag(); - invariant( - false, - 'Expected there to be an active or pending drag when window mousemove event is received', - ); - } - - // threshold not yet exceeded - if (!isSloppyClickThresholdExceeded(state.pending, point)) { - return; - } - - // preventing default as we are using this event - event.preventDefault(); - startDragging(() => - getCallbacks().onLift({ - clientSelection: point, - movementMode: 'FLUID', - }), - ); - }, - }, - { - eventName: 'mouseup', - fn: (event: MouseEvent) => { - if (state.pending) { - stopPendingDrag(); - return; - } - - // preventing default as we are using this event - event.preventDefault(); - stopDragging(getCallbacks().onDrop); - }, - }, - { - eventName: 'mousedown', - fn: (event: MouseEvent) => { - // this can happen during a drag when the user clicks a button - // other than the primary mouse button - if (state.isDragging) { - event.preventDefault(); - } - - stopDragging(getCallbacks().onCancel); - }, - }, - { - eventName: 'keydown', - fn: (event: KeyboardEvent) => { - // firing a keyboard event before the drag has started - // treat this as an indirect cancel - if (!state.isDragging) { - cancel(); - return; - } - - // cancelling a drag - if (event.keyCode === keyCodes.escape) { - event.preventDefault(); - cancel(); - return; - } - - preventStandardKeyEvents(event); - }, - }, - { - eventName: 'resize', - fn: cancel, - }, - { - eventName: 'scroll', - // ## Passive: true - // Eventual consistency is fine because we use position: fixed on the item - // ## Capture: false - // Scroll events on elements do not bubble, but they go through the capture phase - // https://twitter.com/alexandereardon/status/985994224867819520 - // Using capture: false here as we want to avoid intercepting droppable scroll requests - // TODO: can result in awkward drop position - options: { passive: true, capture: false }, - fn: (event: UIEvent) => { - // IE11 fix: - // Scrollable events still bubble up and are caught by this handler in ie11. - // We can ignore this event - if (event.currentTarget !== getWindow()) { - return; - } - - // stop a pending drag - if (state.pending) { - stopPendingDrag(); - return; - } - // getCallbacks().onWindowScroll(); - schedule.windowScrollMove(); - }, - }, - // Need to opt out of dragging if the user is a force press - // Only for safari which has decided to introduce its own custom way of doing things - // https://developer.apple.com/library/content/documentation/AppleApplications/Conceptual/SafariJSProgTopics/RespondingtoForceTouchEventsfromJavaScript.html - { - eventName: 'webkitmouseforcechanged', - fn: (event: MouseForceChangedEvent) => { - if ( - event.webkitForce == null || - (MouseEvent: any).WEBKIT_FORCE_AT_FORCE_MOUSE_DOWN == null - ) { - warning( - 'handling a mouse force changed event when it is not supported', - ); - return; - } - - const forcePressThreshold: number = (MouseEvent: any) - .WEBKIT_FORCE_AT_FORCE_MOUSE_DOWN; - const isForcePressing: boolean = - event.webkitForce >= forcePressThreshold; - - // force press is not being respected - // opt out of default browser behaviour and continue the drag - if (!getShouldRespectForceTouch()) { - event.preventDefault(); - return; - } - - if (isForcePressing) { - // it is considered a indirect cancel so we do not - // prevent default in any situation. - cancel(); - } - }, - }, - // Cancel on page visibility change - { - eventName: supportedPageVisibilityEventName, - fn: cancel, - }, - ]; - - const bindWindowEvents = () => { - const win: HTMLElement = getWindow(); - bindEvents(win, windowBindings, { capture: true }); - }; - - const unbindWindowEvents = () => { - const win: HTMLElement = getWindow(); - unbindEvents(win, windowBindings, { capture: true }); - }; - - const onMouseDown = (event: MouseEvent): void => { - if (mouseDownMarshal.isHandled()) { - return; - } - - invariant( - !isCapturing(), - 'Should not be able to perform a mouse down while a drag or pending drag is occurring', - ); - - // We do not need to prevent the event on a dropping draggable as - // the mouse down event will not fire due to pointer-events: none - // https://codesandbox.io/s/oxo0o775rz - if (!canStartCapturing(event)) { - return; - } - - // only starting a drag if dragging with the primary mouse button - if (event.button !== primaryButton) { - return; - } - - // Do not start a drag if any modifier key is pressed - if (event.ctrlKey || event.metaKey || event.shiftKey || event.altKey) { - return; - } - - // Registering that this event has been handled. - // This is to prevent parent draggables using this event - // to start also. - // Ideally we would not use preventDefault() as we are not sure - // if this mouse down is part of a drag interaction - // Unfortunately we do to prevent the element obtaining focus (see below). - mouseDownMarshal.handle(); - - // Unfortunately we do need to prevent the drag handle from getting focus on mousedown. - // This goes against our policy on not blocking events before a drag has started. - // See [How we use dom events](/docs/guides/how-we-use-dom-events.md). - event.preventDefault(); - - const point: Position = { - x: event.clientX, - y: event.clientY, - }; - - startPendingDrag(point); - }; - - const sensor: MouseSensor = { - onMouseDown, - kill, - isCapturing, - isDragging, - unmount, - }; - - return sensor; -}; diff --git a/src/view/use-drag-handle/sensor/create-touch-sensor.js b/src/view/use-drag-handle/sensor/create-touch-sensor.js deleted file mode 100644 index 29148e4411..0000000000 --- a/src/view/use-drag-handle/sensor/create-touch-sensor.js +++ /dev/null @@ -1,432 +0,0 @@ -// @flow -/* eslint-disable no-use-before-define */ -import invariant from 'tiny-invariant'; -import { type Position } from 'css-box-model'; -import type { EventBinding } from '../util/event-types'; -import type { TouchSensor, CreateSensorArgs } from './sensor-types'; -import createScheduler from '../util/create-scheduler'; -import createPostDragEventPreventer, { - type EventPreventer, -} from '../util/create-post-drag-event-preventer'; -import createEventMarshal, { - type EventMarshal, -} from '../util/create-event-marshal'; -import { bindEvents, unbindEvents } from '../util/bind-events'; -import * as keyCodes from '../../key-codes'; -import supportedPageVisibilityEventName from '../util/supported-page-visibility-event-name'; - -type State = { - isDragging: boolean, - hasMoved: boolean, - longPressTimerId: ?TimeoutID, - pending: ?Position, -}; - -type TouchWithForce = Touch & { - force: number, -}; - -type WebkitHack = {| - preventTouchMove: () => void, - releaseTouchMove: () => void, -|}; - -export const timeForLongPress: number = 150; -export const forcePressThreshold: number = 0.15; -const touchStartMarshal: EventMarshal = createEventMarshal(); -const noop = (): void => {}; - -// Webkit does not allow event.preventDefault() in dynamically added handlers -// So we add an always listening event handler to get around this :( -// webkit bug: https://bugs.webkit.org/show_bug.cgi?id=184250 -const webkitHack: WebkitHack = (() => { - const stub: WebkitHack = { - preventTouchMove: noop, - releaseTouchMove: noop, - }; - - // Do nothing when server side rendering - if (typeof window === 'undefined') { - return stub; - } - - // Device has no touch support - no point adding the touch listener - if (!('ontouchstart' in window)) { - return stub; - } - - // Not adding any user agent testing as everything pretends to be webkit - - let isBlocking: boolean = false; - - // Adding a persistent event handler - window.addEventListener( - 'touchmove', - (event: TouchEvent) => { - // We let the event go through as normal as nothing - // is blocking the touchmove - if (!isBlocking) { - return; - } - - // Our event handler would have worked correctly if the browser - // was not webkit based, or an older version of webkit. - if (event.defaultPrevented) { - return; - } - - // Okay, now we need to step in and fix things - event.preventDefault(); - - // Forcing this to be non-passive so we can get every touchmove - // Not activating in the capture phase like the dynamic touchmove we add. - // Technically it would not matter if we did this in the capture phase - }, - { passive: false, capture: false }, - ); - - const preventTouchMove = () => { - isBlocking = true; - }; - const releaseTouchMove = () => { - isBlocking = false; - }; - - return { preventTouchMove, releaseTouchMove }; -})(); - -const initial: State = { - isDragging: false, - pending: null, - hasMoved: false, - longPressTimerId: null, -}; - -export default ({ - getCallbacks, - getWindow, - canStartCapturing, - getShouldRespectForceTouch, -}: CreateSensorArgs): TouchSensor => { - let state: State = initial; - - const setState = (partial: Object): void => { - state = { - ...state, - ...partial, - }; - }; - const isDragging = (): boolean => state.isDragging; - const isCapturing = (): boolean => - Boolean(state.pending || state.isDragging || state.longPressTimerId); - // TODO: assuming they do not change reference... - const schedule = createScheduler(getCallbacks()); - const postDragEventPreventer: EventPreventer = createPostDragEventPreventer( - getWindow, - ); - - const startDragging = () => { - const pending: ?Position = state.pending; - - if (!pending) { - // this should be an impossible state - // cannot use kill() as it will not unbind when there is no pending - stopPendingDrag(); - invariant(false, 'cannot start a touch drag without a pending position'); - } - - setState({ - isDragging: true, - // has not moved from original position yet - hasMoved: false, - // no longer relevant - pending: null, - longPressTimerId: null, - }); - - getCallbacks().onLift({ - clientSelection: pending, - movementMode: 'FLUID', - }); - }; - const stopDragging = (fn?: Function = noop) => { - schedule.cancel(); - touchStartMarshal.reset(); - webkitHack.releaseTouchMove(); - unbindWindowEvents(); - postDragEventPreventer.preventNext(); - setState(initial); - fn(); - }; - - const startPendingDrag = (event: TouchEvent) => { - const touch: Touch = event.touches[0]; - const { clientX, clientY } = touch; - const point: Position = { - x: clientX, - y: clientY, - }; - - const longPressTimerId: TimeoutID = setTimeout( - startDragging, - timeForLongPress, - ); - - setState({ - longPressTimerId, - pending: point, - isDragging: false, - hasMoved: false, - }); - bindWindowEvents(); - }; - - const stopPendingDrag = () => { - if (state.longPressTimerId) { - clearTimeout(state.longPressTimerId); - } - schedule.cancel(); - touchStartMarshal.reset(); - webkitHack.releaseTouchMove(); - unbindWindowEvents(); - - setState(initial); - }; - - const kill = (fn?: Function = noop) => { - if (state.pending) { - stopPendingDrag(); - return; - } - if (state.isDragging) { - stopDragging(fn); - } - }; - - const unmount = () => { - kill(); - postDragEventPreventer.abort(); - }; - - const cancel = () => { - kill(getCallbacks().onCancel); - }; - - const windowBindings: EventBinding[] = [ - { - eventName: 'touchmove', - // Opting out of passive touchmove (default) so as to prevent scrolling while moving - // Not worried about performance as effect of move is throttled in requestAnimationFrame - options: { passive: false }, - fn: (event: TouchEvent) => { - // Drag has not yet started and we are waiting for a long press. - if (!state.isDragging) { - stopPendingDrag(); - return; - } - - // At this point we are dragging - - if (!state.hasMoved) { - setState({ - hasMoved: true, - }); - } - - const { clientX, clientY } = event.touches[0]; - - const point: Position = { - x: clientX, - y: clientY, - }; - - // We need to prevent the default event in order to block native scrolling - // Also because we are using it as part of a drag we prevent the default action - // as a sign that we are using the event - event.preventDefault(); - schedule.move(point); - }, - }, - { - eventName: 'touchend', - fn: (event: TouchEvent) => { - // drag had not started yet - do not prevent the default action - if (!state.isDragging) { - stopPendingDrag(); - return; - } - - // already dragging - this event is directly ending a drag - event.preventDefault(); - stopDragging(getCallbacks().onDrop); - }, - }, - { - eventName: 'touchcancel', - fn: (event: TouchEvent) => { - // drag had not started yet - do not prevent the default action - if (!state.isDragging) { - stopPendingDrag(); - return; - } - - // already dragging - this event is directly ending a drag - event.preventDefault(); - stopDragging(getCallbacks().onCancel); - }, - }, - // another touch start should not happen without a - // touchend or touchcancel. However, just being super safe - { - eventName: 'touchstart', - fn: cancel, - }, - // If the orientation of the device changes - kill the drag - // https://davidwalsh.name/orientation-change - { - eventName: 'orientationchange', - fn: cancel, - }, - // some devices fire resize if the orientation changes - { - eventName: 'resize', - fn: cancel, - }, - // ## Passive: true - // For scroll events we are okay with eventual consistency. - // Passive scroll listeners is the default behavior for mobile - // but we are being really clear here - // ## Capture: false - // Scroll events on elements do not bubble, but they go through the capture phase - // https://twitter.com/alexandereardon/status/985994224867819520 - // Using capture: false here as we want to avoid intercepting droppable scroll requests - { - eventName: 'scroll', - options: { passive: true, capture: false }, - fn: () => { - // stop a pending drag - if (state.pending) { - stopPendingDrag(); - return; - } - schedule.windowScrollMove(); - }, - }, - // Long press can bring up a context menu - // need to opt out of this behavior - { - eventName: 'contextmenu', - fn: (event: Event) => { - // always opting out of context menu events - event.preventDefault(); - }, - }, - // On some devices it is possible to have a touch interface with a keyboard. - // On any keyboard event we cancel a touch drag - { - eventName: 'keydown', - fn: (event: KeyboardEvent) => { - if (!state.isDragging) { - cancel(); - return; - } - - // direct cancel: we are preventing the default action - // indirect cancel: we are not preventing the default action - - // escape is a direct cancel - if (event.keyCode === keyCodes.escape) { - event.preventDefault(); - } - cancel(); - }, - }, - // Need to opt out of dragging if the user is a force press - // Only for webkit which has decided to introduce its own custom way of doing things - // https://developer.apple.com/library/content/documentation/AppleApplications/Conceptual/SafariJSProgTopics/RespondingtoForceTouchEventsfromJavaScript.html - { - eventName: 'touchforcechange', - fn: (event: TouchEvent) => { - if (!state.isDragging && !state.pending) { - return; - } - - // A force push action will no longer fire after a touchmove - if (state.hasMoved) { - // This is being super safe. While this situation should not occur we - // are still expressing that we want to opt out of force pressing - event.preventDefault(); - return; - } - - // A drag could be pending or has already started but no movement has occurred - - // Not respecting force press - prevent the event - if (!getShouldRespectForceTouch()) { - event.preventDefault(); - return; - } - - const touch: TouchWithForce = (event.touches[0]: any); - - if (touch.force >= forcePressThreshold) { - // this is an indirect cancel so we do not preventDefault - // we also want to allow the force press to occur - cancel(); - } - }, - }, - // Cancel on page visibility change - { - eventName: supportedPageVisibilityEventName, - fn: cancel, - }, - ]; - - const bindWindowEvents = () => { - bindEvents(getWindow(), windowBindings, { capture: true }); - }; - - const unbindWindowEvents = () => { - unbindEvents(getWindow(), windowBindings, { capture: true }); - }; - - // entry point - const onTouchStart = (event: TouchEvent) => { - if (touchStartMarshal.isHandled()) { - return; - } - - invariant( - !isCapturing(), - 'Should not be able to perform a touch start while a drag or pending drag is occurring', - ); - - // We do not need to prevent the event on a dropping draggable as - // the touchstart event will not fire due to pointer-events: none - // https://codesandbox.io/s/oxo0o775rz - if (!canStartCapturing(event)) { - return; - } - - // We need to stop parents from responding to this event - which may cause a double lift - // We also need to NOT call event.preventDefault() so as to maintain as much standard - // browser interactions as possible. - // This includes navigation on anchors which we want to preserve - touchStartMarshal.handle(); - - // A webkit only hack to prevent touch move events - webkitHack.preventTouchMove(); - startPendingDrag(event); - }; - - const sensor: TouchSensor = { - onTouchStart, - kill, - isCapturing, - isDragging, - unmount, - }; - - return sensor; -}; From 1bb8eaa7c0470d704dfe48e307fc9a8c50336c33 Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Mon, 1 Apr 2019 13:58:16 +1100 Subject: [PATCH 103/117] pointing tests and the right file --- test/unit/view/drag-handle/touch-sensor.spec.js | 2 +- test/unit/view/drag-handle/util/controls.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/test/unit/view/drag-handle/touch-sensor.spec.js b/test/unit/view/drag-handle/touch-sensor.spec.js index b8e3db2ff5..2175987e5f 100644 --- a/test/unit/view/drag-handle/touch-sensor.spec.js +++ b/test/unit/view/drag-handle/touch-sensor.spec.js @@ -7,7 +7,7 @@ import setWindowScroll from '../../../utils/set-window-scroll'; import { timeForLongPress, forcePressThreshold, -} from '../../../../src/view/use-drag-handle/sensor/create-touch-sensor'; +} from '../../../../src/view/use-drag-handle/sensor/use-touch-sensor'; import { dispatchWindowEvent, dispatchWindowKeyDownEvent, diff --git a/test/unit/view/drag-handle/util/controls.js b/test/unit/view/drag-handle/util/controls.js index 15b7745ef0..d86c7e4a3a 100644 --- a/test/unit/view/drag-handle/util/controls.js +++ b/test/unit/view/drag-handle/util/controls.js @@ -1,7 +1,7 @@ // @flow import type { ReactWrapper } from 'enzyme'; import { sloppyClickThreshold } from '../../../../../src/view/use-drag-handle/util/is-sloppy-click-threshold-exceeded'; -import { timeForLongPress } from '../../../../../src/view/use-drag-handle/sensor/create-touch-sensor'; +import { timeForLongPress } from '../../../../../src/view/use-drag-handle/sensor/use-touch-sensor'; import { primaryButton, touchStart, From 6152b4b2399da0b17dd6d1c89d1634fa4cf7cdd2 Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Mon, 1 Apr 2019 14:09:58 +1100 Subject: [PATCH 104/117] removing redundant style --- src/view/use-style-marshal/get-styles.js | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/src/view/use-style-marshal/get-styles.js b/src/view/use-style-marshal/get-styles.js index 4756f755a3..c7157125d2 100644 --- a/src/view/use-style-marshal/get-styles.js +++ b/src/view/use-style-marshal/get-styles.js @@ -115,13 +115,6 @@ export default (uniqueContext: string): Styles => { }; })(); - const placeholder: Rule = { - selector: getSelector(attributes.placeholder), - styles: { - always: noPointerEvents, - }, - }; - // ## Droppable styles // overflow-anchor: none; @@ -166,7 +159,7 @@ export default (uniqueContext: string): Styles => { }, }; - const rules: Rule[] = [draggable, dragHandle, droppable, body, placeholder]; + const rules: Rule[] = [draggable, dragHandle, droppable, body]; return { always: getStyles(rules, 'always'), From 7b2b2d8c1f24c3580953a21ee94901e779991e6b Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Mon, 1 Apr 2019 16:31:17 +1100 Subject: [PATCH 105/117] safer guarding --- src/view/draggable/use-validation.js | 8 ++++---- src/view/droppable/use-validation.js | 11 +++++------ src/view/use-drag-handle/use-validation.js | 11 +++++------ 3 files changed, 14 insertions(+), 16 deletions(-) diff --git a/src/view/draggable/use-validation.js b/src/view/draggable/use-validation.js index 9abc5d52a1..06cfcf321a 100644 --- a/src/view/draggable/use-validation.js +++ b/src/view/draggable/use-validation.js @@ -23,10 +23,10 @@ export default function useValidation( ) { // running after every update in development useEffect(() => { - if (process.env.NODE_ENV === 'production') { - return; + // wrapping entire block for better minification + if (process.env.NODE_ENV !== 'production') { + checkOwnProps(props); + checkIsValidInnerRef(getRef()); } - checkOwnProps(props); - checkIsValidInnerRef(getRef()); }); } diff --git a/src/view/droppable/use-validation.js b/src/view/droppable/use-validation.js index 5b30e6914e..63299971ea 100644 --- a/src/view/droppable/use-validation.js +++ b/src/view/droppable/use-validation.js @@ -52,12 +52,11 @@ export default function useValidation({ }: Args) { // Running on every update useEffect(() => { - if (process.env.NODE_ENV === 'production') { - return; + // wrapping entire block for better minification + if (process.env.NODE_ENV !== 'production') { + checkOwnProps(props); + checkIsValidInnerRef(getDroppableRef()); + checkPlaceholderRef(props, getPlaceholderRef()); } - - checkOwnProps(props); - checkIsValidInnerRef(getDroppableRef()); - checkPlaceholderRef(props, getPlaceholderRef()); }); } diff --git a/src/view/use-drag-handle/use-validation.js b/src/view/use-drag-handle/use-validation.js index ed72349534..0454eb45d6 100644 --- a/src/view/use-drag-handle/use-validation.js +++ b/src/view/use-drag-handle/use-validation.js @@ -6,12 +6,11 @@ import getDragHandleRef from './util/get-drag-handle-ref'; export default function useValidation(getDraggableRef: () => ?HTMLElement) { // validate ref on mount useEffect(() => { - if (process.env.NODE_ENV === 'production') { - return; + // wrapping entire block for better minification + if (process.env.NODE_ENV !== 'production') { + const draggableRef: ?HTMLElement = getDraggableRef(); + invariant(draggableRef, 'Drag handle was unable to find draggable ref'); + getDragHandleRef(draggableRef); } - const draggableRef: ?HTMLElement = getDraggableRef(); - invariant(draggableRef, 'Drag handle was unable to find draggable ref'); - - getDragHandleRef(draggableRef); }, [getDraggableRef]); } From 14862d95082988bddf93fd10384e0df63312955e Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Tue, 2 Apr 2019 07:21:03 +1100 Subject: [PATCH 106/117] trying to be concurrent mode safe --- src/view/use-custom-memo/use-callback-one.js | 44 +++++++++++-------- src/view/use-custom-memo/use-memo-one.js | 45 +++++++++++--------- 2 files changed, 52 insertions(+), 37 deletions(-) diff --git a/src/view/use-custom-memo/use-callback-one.js b/src/view/use-custom-memo/use-callback-one.js index 5f3f43bb22..09afd1350f 100644 --- a/src/view/use-custom-memo/use-callback-one.js +++ b/src/view/use-custom-memo/use-callback-one.js @@ -1,32 +1,40 @@ // @flow -import { useRef } from 'react'; +import { useRef, useState, useEffect } from 'react'; import areInputsEqual from './are-inputs-equal'; +type Result = {| + inputs: mixed[], + callback: T, +|}; + export default function useCallbackOne( // getResult changes on every call, callback: T, // the inputs array changes on every call inputs?: mixed[] = [], ): T { - const isFirstCallRef = useRef(true); - const lastInputsRef = useRef(inputs); - // Cannot lazy create a ref value, so setting to null - // $ExpectError - T is not null - const resultRef = useRef(null); + // using useState to generate initial value as it is lazy + const initial: Result = useState(() => ({ + inputs, + callback, + }))[0]; - // on first call return the initial result - if (isFirstCallRef.current) { - isFirstCallRef.current = false; - resultRef.current = callback; - return resultRef.current; - } + const uncommitted = useRef>(initial); + const committed = useRef>(initial); - // Don't recalculate result if the inputs have not changed - if (areInputsEqual(inputs, lastInputsRef.current)) { - return resultRef.current; + // persist any uncommitted changes after they have been committed + useEffect(() => { + committed.current = uncommitted.current; + }); + + if (areInputsEqual(inputs, committed.current.inputs)) { + return committed.current.callback; } - lastInputsRef.current = inputs; - resultRef.current = callback; - return resultRef.current; + uncommitted.current = { + inputs, + callback, + }; + + return uncommitted.current.callback; } diff --git a/src/view/use-custom-memo/use-memo-one.js b/src/view/use-custom-memo/use-memo-one.js index 59d351c5a2..44f4c7596d 100644 --- a/src/view/use-custom-memo/use-memo-one.js +++ b/src/view/use-custom-memo/use-memo-one.js @@ -1,33 +1,40 @@ // @flow -import { useRef } from 'react'; +import { useRef, useState, useEffect } from 'react'; import areInputsEqual from './are-inputs-equal'; +type Result = {| + inputs: mixed[], + result: T, +|}; + export default function useMemoOne( // getResult changes on every call, getResult: () => T, // the inputs array changes on every call inputs?: mixed[] = [], ): T { - const isFirstCallRef = useRef(true); - const lastInputsRef = useRef(inputs); - // Cannot lazy create a ref value, so setting to null - // $ExpectError - T is not null - const resultRef = useRef(null); + // using useState to generate initial value as it is lazy + const initial: Result = useState(() => ({ + inputs, + result: getResult(), + }))[0]; - // on first call return the initial result - if (isFirstCallRef.current) { - isFirstCallRef.current = false; - resultRef.current = getResult(); - return resultRef.current; - } + const uncommitted = useRef>(initial); + const committed = useRef>(initial); - // Don't recalculate result if the inputs have not changed - if (areInputsEqual(inputs, lastInputsRef.current)) { - return resultRef.current; + // persist any uncommitted changes after they have been committed + useEffect(() => { + committed.current = uncommitted.current; + }); + + if (areInputsEqual(inputs, committed.current.inputs)) { + return committed.current.result; } - // try to generate result first in case it throws - resultRef.current = getResult(); - lastInputsRef.current = inputs; - return resultRef.current; + uncommitted.current = { + inputs, + result: getResult(), + }; + + return uncommitted.current.result; } From 864b372de7513e56f4470c7ac47272e73144c0c4 Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Tue, 2 Apr 2019 07:24:45 +1100 Subject: [PATCH 107/117] useCallbackOne now using useMemoOne --- src/view/use-custom-memo/use-callback-one.js | 33 ++------------------ 1 file changed, 2 insertions(+), 31 deletions(-) diff --git a/src/view/use-custom-memo/use-callback-one.js b/src/view/use-custom-memo/use-callback-one.js index 09afd1350f..63cc73e53f 100644 --- a/src/view/use-custom-memo/use-callback-one.js +++ b/src/view/use-custom-memo/use-callback-one.js @@ -1,11 +1,5 @@ // @flow -import { useRef, useState, useEffect } from 'react'; -import areInputsEqual from './are-inputs-equal'; - -type Result = {| - inputs: mixed[], - callback: T, -|}; +import useMemoOne from './use-memo-one'; export default function useCallbackOne( // getResult changes on every call, @@ -13,28 +7,5 @@ export default function useCallbackOne( // the inputs array changes on every call inputs?: mixed[] = [], ): T { - // using useState to generate initial value as it is lazy - const initial: Result = useState(() => ({ - inputs, - callback, - }))[0]; - - const uncommitted = useRef>(initial); - const committed = useRef>(initial); - - // persist any uncommitted changes after they have been committed - useEffect(() => { - committed.current = uncommitted.current; - }); - - if (areInputsEqual(inputs, committed.current.inputs)) { - return committed.current.callback; - } - - uncommitted.current = { - inputs, - callback, - }; - - return uncommitted.current.callback; + return useMemoOne(() => callback, inputs); } From fa4fc56fcb644efa49cb5ea4c7ceea9ef8a3e920 Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Tue, 2 Apr 2019 07:46:01 +1100 Subject: [PATCH 108/117] moving to useMemoOne --- .eslintrc.js | 14 ++++++++++ .../drag-drop-context/drag-drop-context.jsx | 3 +++ src/view/draggable/draggable.jsx | 22 ++++++++------- src/view/droppable/droppable.jsx | 24 +++++++---------- src/view/placeholder/placeholder.jsx | 13 +++------ src/view/use-announcer/use-announcer.js | 8 +++--- .../sensor/use-keyboard-sensor.js | 20 +++++++------- .../sensor/use-mouse-sensor.js | 27 ++++++++++--------- .../sensor/use-touch-sensor.js | 24 +++++++++-------- src/view/use-drag-handle/use-drag-handle.js | 22 ++++++++------- .../use-drag-handle/use-focus-retainer.js | 7 ++--- .../use-draggable-dimension-publisher.js | 8 +++--- .../use-droppable-dimension-publisher.js | 26 +++++++++--------- .../use-style-marshal/use-style-marshal.js | 20 +++++++------- test/.eslintrc.js | 3 +++ 15 files changed, 135 insertions(+), 106 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index 092fc9541c..9e1296c79d 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -64,6 +64,20 @@ module.exports = { }, ], + 'no-restricted-imports': [ + 'error', + { + paths: [ + { + name: 'react', + importNames: ['useMemo', 'useCallback'], + message: + 'useMemo and useCallback are subject to cache busting. Please use useMemoOne and useCallbackOne', + }, + ], + }, + ], + // Allowing jsx in files with any file extension (old components have jsx but not the extension) 'react/jsx-filename-extension': 'off', diff --git a/src/view/drag-drop-context/drag-drop-context.jsx b/src/view/drag-drop-context/drag-drop-context.jsx index 4db7749c1d..6e66cda533 100644 --- a/src/view/drag-drop-context/drag-drop-context.jsx +++ b/src/view/drag-drop-context/drag-drop-context.jsx @@ -20,6 +20,9 @@ export function resetServerContext() { export default function DragDropContext(props: Props) { const uniqueId: number = useMemoOne(() => instanceCount++, []); + + // We need the error boundary to be on the outside of App + // so that it can catch any errors caused by App return ( {setOnError => ( diff --git a/src/view/draggable/draggable.jsx b/src/view/draggable/draggable.jsx index 9944d60c17..89ed2c8fb8 100644 --- a/src/view/draggable/draggable.jsx +++ b/src/view/draggable/draggable.jsx @@ -1,5 +1,5 @@ // @flow -import { useMemo, useRef, useCallback } from 'react'; +import { useRef } from 'react'; import { type Position } from 'css-box-model'; import invariant from 'tiny-invariant'; import getStyle from './get-style'; @@ -21,14 +21,16 @@ import getWindowScroll from '../window/get-window-scroll'; import AppContext, { type AppContextValue } from '../context/app-context'; import useRequiredContext from '../use-required-context'; import useValidation from './use-validation'; +import useCallbackOne from '../use-custom-memo/use-callback-one'; +import useMemoOne from '../use-custom-memo/use-memo-one'; export default function Draggable(props: Props) { // reference to DOM node const ref = useRef(null); - const setRef = useCallback((el: ?HTMLElement) => { + const setRef = useCallbackOne((el: ?HTMLElement) => { ref.current = el; }, []); - const getRef = useCallback((): ?HTMLElement => ref.current, []); + const getRef = useCallbackOne((): ?HTMLElement => ref.current, []); // context const appContext: AppContextValue = useRequiredContext(AppContext); @@ -62,7 +64,7 @@ export default function Draggable(props: Props) { } = props; // The dimension publisher: talks to the marshal - const forPublisher: DimensionPublisherArgs = useMemo( + const forPublisher: DimensionPublisherArgs = useMemoOne( () => ({ draggableId, index, @@ -74,7 +76,7 @@ export default function Draggable(props: Props) { // The Drag handle - const onLift = useCallback( + const onLift = useCallbackOne( (options: { clientSelection: Position, movementMode: MovementMode }) => { timings.start('LIFT'); const el: ?HTMLElement = ref.current; @@ -92,12 +94,12 @@ export default function Draggable(props: Props) { [draggableId, isDragDisabled, liftAction], ); - const getShouldRespectForceTouch = useCallback( + const getShouldRespectForceTouch = useCallbackOne( () => shouldRespectForceTouch, [shouldRespectForceTouch], ); - const callbacks: DragHandleCallbacks = useMemo( + const callbacks: DragHandleCallbacks = useMemoOne( () => ({ onLift, onMove: (clientSelection: Position) => @@ -129,7 +131,7 @@ export default function Draggable(props: Props) { const isDropAnimating: boolean = mapped.type === 'DRAGGING' && Boolean(mapped.dropping); - const dragHandleArgs: DragHandleArgs = useMemo( + const dragHandleArgs: DragHandleArgs = useMemoOne( () => ({ draggableId, isDragging, @@ -154,7 +156,7 @@ export default function Draggable(props: Props) { const dragHandleProps: ?DragHandleProps = useDragHandle(dragHandleArgs); - const onMoveEnd = useCallback( + const onMoveEnd = useCallbackOne( (event: TransitionEvent) => { if (mapped.type !== 'DRAGGING') { return; @@ -175,7 +177,7 @@ export default function Draggable(props: Props) { [dropAnimationFinishedAction, mapped], ); - const provided: Provided = useMemo(() => { + const provided: Provided = useMemoOne(() => { const style: DraggableStyle = getStyle(mapped); const onTransitionEnd = mapped.type === 'DRAGGING' && mapped.dropping ? onMoveEnd : null; diff --git a/src/view/droppable/droppable.jsx b/src/view/droppable/droppable.jsx index bc4fc863ae..49c16c798e 100644 --- a/src/view/droppable/droppable.jsx +++ b/src/view/droppable/droppable.jsx @@ -1,12 +1,6 @@ // @flow import invariant from 'tiny-invariant'; -import React, { - useMemo, - useRef, - useCallback, - useContext, - type Node, -} from 'react'; +import React, { useRef, useContext, type Node } from 'react'; import type { Props, Provided } from './droppable-types'; import useDroppableDimensionPublisher from '../use-droppable-dimension-publisher'; import Placeholder from '../placeholder'; @@ -20,6 +14,8 @@ import useValidation from './use-validation'; import AnimateInOut, { type AnimateProvided, } from '../animate-in-out/animate-in-out'; +import useCallbackOne from '../use-custom-memo/use-callback-one'; +import useMemoOne from '../use-custom-memo/use-memo-one'; export default function Droppable(props: Props) { const appContext: ?AppContextValue = useContext(AppContext); @@ -45,22 +41,22 @@ export default function Droppable(props: Props) { updateViewportMaxScroll, } = props; - const getDroppableRef = useCallback( + const getDroppableRef = useCallbackOne( (): ?HTMLElement => droppableRef.current, [], ); - const getPlaceholderRef = useCallback( + const getPlaceholderRef = useCallbackOne( (): ?HTMLElement => placeholderRef.current, [], ); - const setDroppableRef = useCallback((value: ?HTMLElement) => { + const setDroppableRef = useCallbackOne((value: ?HTMLElement) => { droppableRef.current = value; }, []); - const setPlaceholderRef = useCallback((value: ?HTMLElement) => { + const setPlaceholderRef = useCallbackOne((value: ?HTMLElement) => { placeholderRef.current = value; }, []); - const onPlaceholderTransitionEnd = useCallback(() => { + const onPlaceholderTransitionEnd = useCallbackOne(() => { // A placeholder change can impact the window's max scroll if (isMovementAllowed()) { updateViewportMaxScroll({ maxScroll: getMaxWindowScroll() }); @@ -101,7 +97,7 @@ export default function Droppable(props: Props) { ); - const provided: Provided = useMemo( + const provided: Provided = useMemoOne( (): Provided => ({ innerRef: setDroppableRef, placeholder, @@ -112,7 +108,7 @@ export default function Droppable(props: Props) { [placeholder, setDroppableRef, styleContext], ); - const droppableContext: ?DroppableContextValue = useMemo( + const droppableContext: ?DroppableContextValue = useMemoOne( () => ({ droppableId, type, diff --git a/src/view/placeholder/placeholder.jsx b/src/view/placeholder/placeholder.jsx index 43778e2300..6707149b1d 100644 --- a/src/view/placeholder/placeholder.jsx +++ b/src/view/placeholder/placeholder.jsx @@ -1,11 +1,5 @@ // @flow -import React, { - useState, - useRef, - useCallback, - useEffect, - type Node, -} from 'react'; +import React, { useState, useRef, useEffect, type Node } from 'react'; import type { Spacing } from 'css-box-model'; import type { Placeholder as PlaceholderType, @@ -13,6 +7,7 @@ import type { } from '../../types'; import { transitions } from '../../animation'; import { noSpacing } from '../../state/spacing'; +import useCallbackOne from '../use-custom-memo/use-callback-one'; function noop() {} @@ -121,7 +116,7 @@ const getStyle = ({ function Placeholder(props: Props): Node { const animateOpenTimerRef = useRef(null); - const tryClearAnimateOpenTimer = useCallback(() => { + const tryClearAnimateOpenTimer = useCallbackOne(() => { if (!animateOpenTimerRef.current) { return; } @@ -164,7 +159,7 @@ function Placeholder(props: Props): Node { return tryClearAnimateOpenTimer; }, [animate, isAnimatingOpenOnMount, tryClearAnimateOpenTimer]); - const onSizeChangeEnd = useCallback( + const onSizeChangeEnd = useCallbackOne( (event: TransitionEvent) => { // We transition height, width and margin // each of those transitions will independently call this callback diff --git a/src/view/use-announcer/use-announcer.js b/src/view/use-announcer/use-announcer.js index 5e749501d2..5344bbf2cd 100644 --- a/src/view/use-announcer/use-announcer.js +++ b/src/view/use-announcer/use-announcer.js @@ -1,9 +1,11 @@ // @flow -import { useRef, useMemo, useEffect, useCallback } from 'react'; +import { useRef, useEffect } from 'react'; import invariant from 'tiny-invariant'; import type { Announce } from '../../types'; import { warning } from '../../dev-warning'; import getBodyElement from '../get-body-element'; +import useMemoOne from '../use-custom-memo/use-memo-one'; +import useCallbackOne from '../use-custom-memo/use-callback-one'; // https://allyjs.io/tutorials/hiding-elements.html // Element is visually hidden but is readable by screen readers @@ -24,7 +26,7 @@ export const getId = (uniqueId: number): string => `react-beautiful-dnd-announcement-${uniqueId}`; export default function useAnnouncer(uniqueId: number): Announce { - const id: string = useMemo(() => getId(uniqueId), [uniqueId]); + const id: string = useMemoOne(() => getId(uniqueId), [uniqueId]); const ref = useRef(null); useEffect(() => { @@ -60,7 +62,7 @@ export default function useAnnouncer(uniqueId: number): Announce { }; }, [id]); - const announce: Announce = useCallback((message: string): void => { + const announce: Announce = useCallbackOne((message: string): void => { const el: ?HTMLElement = ref.current; if (el) { el.textContent = message; diff --git a/src/view/use-drag-handle/sensor/use-keyboard-sensor.js b/src/view/use-drag-handle/sensor/use-keyboard-sensor.js index c74893fe8e..5b36d4119f 100644 --- a/src/view/use-drag-handle/sensor/use-keyboard-sensor.js +++ b/src/view/use-drag-handle/sensor/use-keyboard-sensor.js @@ -1,6 +1,6 @@ // @flow import type { Position } from 'css-box-model'; -import { useRef, useCallback, useMemo } from 'react'; +import { useRef } from 'react'; import invariant from 'tiny-invariant'; import type { EventBinding } from '../util/event-types'; import { bindEvents, unbindEvents } from '../util/bind-events'; @@ -10,6 +10,8 @@ import supportedPageVisibilityEventName from '../util/supported-page-visibility- import preventStandardKeyEvents from '../util/prevent-standard-key-events'; import type { Callbacks } from '../drag-handle-types'; import getBorderBoxCenterPosition from '../../get-border-box-center-position'; +import useCallbackOne from '../../use-custom-memo/use-callback-one'; +import useMemoOne from '../../use-custom-memo/use-memo-one'; export type Args = {| callbacks: Callbacks, @@ -46,9 +48,9 @@ export default function useKeyboardSensor(args: Args): OnKeyDown { const isDraggingRef = useRef(false); const unbindWindowEventsRef = useRef<() => void>(noop); - const getIsDragging = useCallback(() => isDraggingRef.current, []); + const getIsDragging = useCallbackOne(() => isDraggingRef.current, []); - const schedule = useMemo(() => { + const schedule = useMemoOne(() => { invariant( !getIsDragging(), 'Should not recreate scheduler while capturing', @@ -56,7 +58,7 @@ export default function useKeyboardSensor(args: Args): OnKeyDown { return createScheduler(callbacks); }, [callbacks, getIsDragging]); - const stop = useCallback(() => { + const stop = useCallbackOne(() => { if (!getIsDragging()) { return; } @@ -67,7 +69,7 @@ export default function useKeyboardSensor(args: Args): OnKeyDown { onCaptureEnd(); }, [getIsDragging, onCaptureEnd, schedule]); - const cancel = useCallback(() => { + const cancel = useCallbackOne(() => { const wasDragging: boolean = isDraggingRef.current; stop(); @@ -76,7 +78,7 @@ export default function useKeyboardSensor(args: Args): OnKeyDown { } }, [callbacks, stop]); - const windowBindings: EventBinding[] = useMemo(() => { + const windowBindings: EventBinding[] = useMemoOne(() => { invariant( !getIsDragging(), 'Should not recreate window bindings when dragging', @@ -141,7 +143,7 @@ export default function useKeyboardSensor(args: Args): OnKeyDown { ]; }, [callbacks, cancel, getIsDragging, getWindow]); - const bindWindowEvents = useCallback(() => { + const bindWindowEvents = useCallbackOne(() => { const win: HTMLElement = getWindow(); const options = { capture: true }; @@ -152,7 +154,7 @@ export default function useKeyboardSensor(args: Args): OnKeyDown { bindEvents(win, windowBindings, options); }, [getWindow, windowBindings]); - const startDragging = useCallback(() => { + const startDragging = useCallbackOne(() => { invariant(!isDraggingRef.current, 'Cannot start a drag while dragging'); const ref: ?HTMLElement = getDraggableRef(); @@ -169,7 +171,7 @@ export default function useKeyboardSensor(args: Args): OnKeyDown { }); }, [bindWindowEvents, callbacks, getDraggableRef, onCaptureStart, stop]); - const onKeyDown: OnKeyDown = useCallback( + const onKeyDown: OnKeyDown = useCallbackOne( (event: KeyboardEvent) => { // not dragging yet if (!getIsDragging()) { diff --git a/src/view/use-drag-handle/sensor/use-mouse-sensor.js b/src/view/use-drag-handle/sensor/use-mouse-sensor.js index 01c06d1e2c..17bb4cf39c 100644 --- a/src/view/use-drag-handle/sensor/use-mouse-sensor.js +++ b/src/view/use-drag-handle/sensor/use-mouse-sensor.js @@ -1,11 +1,12 @@ // @flow import type { Position } from 'css-box-model'; -import { useRef, useCallback, useMemo } from 'react'; +import { useRef } from 'react'; import invariant from 'tiny-invariant'; import type { EventBinding } from '../util/event-types'; import createEventMarshal, { type EventMarshal, } from '../util/create-event-marshal'; +import type { Callbacks } from '../drag-handle-types'; import { bindEvents, unbindEvents } from '../util/bind-events'; import createScheduler from '../util/create-scheduler'; import { warning } from '../../../dev-warning'; @@ -16,8 +17,8 @@ import createPostDragEventPreventer, { } from '../util/create-post-drag-event-preventer'; import isSloppyClickThresholdExceeded from '../util/is-sloppy-click-threshold-exceeded'; import preventStandardKeyEvents from '../util/prevent-standard-key-events'; -import type { Callbacks } from '../drag-handle-types'; -// import useMemo from '../../use-custom-memo/use-memo-one'; +import useCallbackOne from '../../use-custom-memo/use-callback-one'; +import useMemoOne from '../../use-custom-memo/use-memo-one'; export type Args = {| callbacks: Callbacks, @@ -55,12 +56,12 @@ export default function useMouseSensor(args: Args): OnMouseDown { const pendingRef = useRef(null); const isDraggingRef = useRef(false); const unbindWindowEventsRef = useRef<() => void>(noop); - const getIsCapturing = useCallback( + const getIsCapturing = useCallbackOne( () => Boolean(pendingRef.current || isDraggingRef.current), [], ); - const schedule = useMemo(() => { + const schedule = useMemoOne(() => { invariant( !getIsCapturing(), 'Should not recreate scheduler while capturing', @@ -68,12 +69,12 @@ export default function useMouseSensor(args: Args): OnMouseDown { return createScheduler(callbacks); }, [callbacks, getIsCapturing]); - const postDragEventPreventer: EventPreventer = useMemo( + const postDragEventPreventer: EventPreventer = useMemoOne( () => createPostDragEventPreventer(getWindow), [getWindow], ); - const stop = useCallback(() => { + const stop = useCallbackOne(() => { if (!getIsCapturing()) { return; } @@ -95,7 +96,7 @@ export default function useMouseSensor(args: Args): OnMouseDown { onCaptureEnd(); }, [getIsCapturing, onCaptureEnd, postDragEventPreventer, schedule]); - const cancel = useCallback(() => { + const cancel = useCallbackOne(() => { const wasDragging: boolean = isDraggingRef.current; stop(); @@ -104,7 +105,7 @@ export default function useMouseSensor(args: Args): OnMouseDown { } }, [callbacks, stop]); - const startDragging = useCallback(() => { + const startDragging = useCallbackOne(() => { invariant(!isDraggingRef.current, 'Cannot start a drag while dragging'); const pending: ?Position = pendingRef.current; invariant(pending, 'Cannot start a drag without a pending drag'); @@ -118,7 +119,7 @@ export default function useMouseSensor(args: Args): OnMouseDown { }); }, [callbacks]); - const windowBindings: EventBinding[] = useMemo(() => { + const windowBindings: EventBinding[] = useMemoOne(() => { invariant( !getIsCapturing(), 'Should not recreate window bindings while capturing', @@ -296,7 +297,7 @@ export default function useMouseSensor(args: Args): OnMouseDown { getShouldRespectForceTouch, ]); - const bindWindowEvents = useCallback(() => { + const bindWindowEvents = useCallbackOne(() => { const win: HTMLElement = getWindow(); const options = { capture: true }; @@ -307,7 +308,7 @@ export default function useMouseSensor(args: Args): OnMouseDown { bindEvents(win, windowBindings, options); }, [getWindow, windowBindings]); - const startPendingDrag = useCallback( + const startPendingDrag = useCallbackOne( (point: Position) => { invariant(!pendingRef.current, 'Expected there to be no pending drag'); pendingRef.current = point; @@ -317,7 +318,7 @@ export default function useMouseSensor(args: Args): OnMouseDown { [bindWindowEvents, onCaptureStart, stop], ); - const onMouseDown = useCallback( + const onMouseDown = useCallbackOne( (event: MouseEvent) => { if (mouseDownMarshal.isHandled()) { return; diff --git a/src/view/use-drag-handle/sensor/use-touch-sensor.js b/src/view/use-drag-handle/sensor/use-touch-sensor.js index f4b9c057f7..0782f18cd7 100644 --- a/src/view/use-drag-handle/sensor/use-touch-sensor.js +++ b/src/view/use-drag-handle/sensor/use-touch-sensor.js @@ -1,11 +1,12 @@ // @flow import type { Position } from 'css-box-model'; -import { useRef, useCallback, useMemo } from 'react'; +import { useRef } from 'react'; import invariant from 'tiny-invariant'; import type { EventBinding } from '../util/event-types'; import createEventMarshal, { type EventMarshal, } from '../util/create-event-marshal'; +import type { Callbacks } from '../drag-handle-types'; import { bindEvents, unbindEvents } from '../util/bind-events'; import createScheduler from '../util/create-scheduler'; import * as keyCodes from '../../key-codes'; @@ -13,7 +14,8 @@ import supportedPageVisibilityEventName from '../util/supported-page-visibility- import createPostDragEventPreventer, { type EventPreventer, } from '../util/create-post-drag-event-preventer'; -import type { Callbacks } from '../drag-handle-types'; +import useCallbackOne from '../../use-custom-memo/use-callback-one'; +import useMemoOne from '../../use-custom-memo/use-memo-one'; export type Args = {| callbacks: Callbacks, @@ -117,16 +119,16 @@ export default function useTouchSensor(args: Args): OnTouchStart { const isDraggingRef = useRef(false); const hasMovedRef = useRef(false); const unbindWindowEventsRef = useRef<() => void>(noop); - const getIsCapturing = useCallback( + const getIsCapturing = useCallbackOne( () => Boolean(pendingRef.current || isDraggingRef.current), [], ); - const postDragClickPreventer: EventPreventer = useMemo( + const postDragClickPreventer: EventPreventer = useMemoOne( () => createPostDragEventPreventer(getWindow), [getWindow], ); - const schedule = useMemo(() => { + const schedule = useMemoOne(() => { invariant( !getIsCapturing(), 'Should not recreate scheduler while capturing', @@ -134,7 +136,7 @@ export default function useTouchSensor(args: Args): OnTouchStart { return createScheduler(callbacks); }, [callbacks, getIsCapturing]); - const stop = useCallback(() => { + const stop = useCallbackOne(() => { if (!getIsCapturing()) { return; } @@ -160,7 +162,7 @@ export default function useTouchSensor(args: Args): OnTouchStart { pendingRef.current = null; }, [getIsCapturing, onCaptureEnd, postDragClickPreventer, schedule]); - const cancel = useCallback(() => { + const cancel = useCallbackOne(() => { const wasDragging: boolean = isDraggingRef.current; stop(); @@ -169,7 +171,7 @@ export default function useTouchSensor(args: Args): OnTouchStart { } }, [callbacks, stop]); - const windowBindings: EventBinding[] = useMemo(() => { + const windowBindings: EventBinding[] = useMemoOne(() => { invariant( !getIsCapturing(), 'Should not recreate window bindings while capturing', @@ -350,7 +352,7 @@ export default function useTouchSensor(args: Args): OnTouchStart { stop, ]); - const bindWindowEvents = useCallback(() => { + const bindWindowEvents = useCallbackOne(() => { const win: HTMLElement = getWindow(); const options = { capture: true }; @@ -361,7 +363,7 @@ export default function useTouchSensor(args: Args): OnTouchStart { bindEvents(win, windowBindings, options); }, [getWindow, windowBindings]); - const startDragging = useCallback(() => { + const startDragging = useCallbackOne(() => { const pending: ?PendingDrag = pendingRef.current; invariant(pending, 'Cannot start a drag without a pending drag'); @@ -375,7 +377,7 @@ export default function useTouchSensor(args: Args): OnTouchStart { }); }, [callbacks]); - const startPendingDrag = useCallback( + const startPendingDrag = useCallbackOne( (event: TouchEvent) => { invariant(!pendingRef.current, 'Expected there to be no pending drag'); const touch: Touch = event.touches[0]; diff --git a/src/view/use-drag-handle/use-drag-handle.js b/src/view/use-drag-handle/use-drag-handle.js index 22ff8d3b45..0a567d95eb 100644 --- a/src/view/use-drag-handle/use-drag-handle.js +++ b/src/view/use-drag-handle/use-drag-handle.js @@ -1,6 +1,6 @@ // @flow import invariant from 'tiny-invariant'; -import { useRef, useMemo, useCallback } from 'react'; +import { useRef } from 'react'; import type { Args, DragHandleProps } from './drag-handle-types'; import getWindowFromEl from '../window/get-window-from-el'; import useRequiredContext from '../use-required-context'; @@ -20,6 +20,8 @@ import { warning } from '../../dev-warning'; import useValidation from './use-validation'; import useFocusRetainer from './use-focus-retainer'; import useIsomorphicLayoutEffect from '../use-isomorphic-layout-effect'; +import useCallbackOne from '../use-custom-memo/use-callback-one'; +import useMemoOne from '../use-custom-memo/use-memo-one'; function preventHtml5Dnd(event: DragEvent) { event.preventDefault(); @@ -32,7 +34,7 @@ type Capturing = {| export default function useDragHandle(args: Args): ?DragHandleProps { // Capturing const capturingRef = useRef(null); - const onCaptureStart = useCallback((abort: () => void) => { + const onCaptureStart = useCallbackOne((abort: () => void) => { invariant( !capturingRef.current, 'Cannot start capturing while something else is', @@ -41,14 +43,14 @@ export default function useDragHandle(args: Args): ?DragHandleProps { abort, }; }, []); - const onCaptureEnd = useCallback(() => { + const onCaptureEnd = useCallbackOne(() => { invariant( capturingRef.current, 'Cannot stop capturing while nothing is capturing', ); capturingRef.current = null; }, []); - const abortCapture = useCallback(() => { + const abortCapture = useCallbackOne(() => { invariant(capturingRef.current, 'Cannot abort capture when there is none'); capturingRef.current.abort(); }, []); @@ -69,12 +71,12 @@ export default function useDragHandle(args: Args): ?DragHandleProps { useValidation(getDraggableRef); - const getWindow = useCallback( + const getWindow = useCallbackOne( (): HTMLElement => getWindowFromEl(getDraggableRef()), [getDraggableRef], ); - const canStartCapturing = useCallback( + const canStartCapturing = useCallbackOne( (event: Event) => { // Cannot lift when disabled if (!isEnabled) { @@ -100,7 +102,7 @@ export default function useDragHandle(args: Args): ?DragHandleProps { const { onBlur, onFocus } = useFocusRetainer(args); - const mouseArgs: MouseSensorArgs = useMemo( + const mouseArgs: MouseSensorArgs = useMemoOne( () => ({ callbacks, getDraggableRef, @@ -122,7 +124,7 @@ export default function useDragHandle(args: Args): ?DragHandleProps { ); const onMouseDown = useMouseSensor(mouseArgs); - const keyboardArgs: KeyboardSensorArgs = useMemo( + const keyboardArgs: KeyboardSensorArgs = useMemoOne( () => ({ callbacks, getDraggableRef, @@ -142,7 +144,7 @@ export default function useDragHandle(args: Args): ?DragHandleProps { ); const onKeyDown = useKeyboardSensor(keyboardArgs); - const touchArgs: TouchSensorArgs = useMemo( + const touchArgs: TouchSensorArgs = useMemoOne( () => ({ callbacks, getDraggableRef, @@ -203,7 +205,7 @@ export default function useDragHandle(args: Args): ?DragHandleProps { } }, [abortCapture, isDragging]); - const props: ?DragHandleProps = useMemo(() => { + const props: ?DragHandleProps = useMemoOne(() => { if (!isEnabled) { return null; } diff --git a/src/view/use-drag-handle/use-focus-retainer.js b/src/view/use-drag-handle/use-focus-retainer.js index a9c869fa30..fea6c7112d 100644 --- a/src/view/use-drag-handle/use-focus-retainer.js +++ b/src/view/use-drag-handle/use-focus-retainer.js @@ -1,11 +1,12 @@ // @flow import invariant from 'tiny-invariant'; -import { useRef, useCallback } from 'react'; +import { useRef } from 'react'; import type { Args } from './drag-handle-types'; import usePrevious from '../use-previous-ref'; import focusRetainer from './util/focus-retainer'; import getDragHandleRef from './util/get-drag-handle-ref'; import useIsomorphicLayoutEffect from '../use-isomorphic-layout-effect'; +import useCallbackOne from '../use-custom-memo/use-callback-one'; export type Result = {| onBlur: () => void, @@ -19,10 +20,10 @@ export default function useFocusRetainer(args: Args): Result { const lastArgsRef = usePrevious(args); const { getDraggableRef } = args; - const onFocus = useCallback(() => { + const onFocus = useCallbackOne(() => { isFocusedRef.current = true; }, []); - const onBlur = useCallback(() => { + const onBlur = useCallbackOne(() => { isFocusedRef.current = false; }, []); diff --git a/src/view/use-draggable-dimension-publisher/use-draggable-dimension-publisher.js b/src/view/use-draggable-dimension-publisher/use-draggable-dimension-publisher.js index 10f1c5f20e..c4f2d0e816 100644 --- a/src/view/use-draggable-dimension-publisher/use-draggable-dimension-publisher.js +++ b/src/view/use-draggable-dimension-publisher/use-draggable-dimension-publisher.js @@ -1,5 +1,5 @@ // @flow -import { useMemo, useRef, useCallback } from 'react'; +import { useRef } from 'react'; import { type Position } from 'css-box-model'; import invariant from 'tiny-invariant'; import type { @@ -15,6 +15,8 @@ import DroppableContext, { type DroppableContextValue, } from '../context/droppable-context'; import useIsomorphicLayoutEffect from '../use-isomorphic-layout-effect'; +import useMemoOne from '../use-custom-memo/use-memo-one'; +import useCallbackOne from '../use-custom-memo/use-callback-one'; export type Args = {| draggableId: DraggableId, @@ -34,7 +36,7 @@ export default function useDraggableDimensionPublisher(args: Args) { ); const { droppableId, type } = droppableContext; - const descriptor: DraggableDescriptor = useMemo(() => { + const descriptor: DraggableDescriptor = useMemoOne(() => { const result = { id: draggableId, droppableId, @@ -46,7 +48,7 @@ export default function useDraggableDimensionPublisher(args: Args) { const publishedDescriptorRef = useRef(descriptor); - const makeDimension = useCallback( + const makeDimension = useCallbackOne( (windowScroll?: Position): DraggableDimension => { const latest: DraggableDescriptor = publishedDescriptorRef.current; const el: ?HTMLElement = getDraggableRef(); diff --git a/src/view/use-droppable-dimension-publisher/use-droppable-dimension-publisher.js b/src/view/use-droppable-dimension-publisher/use-droppable-dimension-publisher.js index cc61b140cd..0884b22eec 100644 --- a/src/view/use-droppable-dimension-publisher/use-droppable-dimension-publisher.js +++ b/src/view/use-droppable-dimension-publisher/use-droppable-dimension-publisher.js @@ -1,5 +1,5 @@ // @flow -import { useCallback, useMemo, useRef } from 'react'; +import { useRef } from 'react'; import invariant from 'tiny-invariant'; import { type Position } from 'css-box-model'; import rafSchedule from 'raf-schd'; @@ -29,6 +29,8 @@ import getListenerOptions from './get-listener-options'; import useRequiredContext from '../use-required-context'; import usePreviousRef from '../use-previous-ref'; import useIsomorphicLayoutEffect from '../use-isomorphic-layout-effect'; +import useMemoOne from '../use-custom-memo/use-memo-one'; +import useCallbackOne from '../use-custom-memo/use-callback-one'; type Props = {| droppableId: DroppableId, @@ -56,7 +58,7 @@ export default function useDroppableDimensionPublisher(args: Props) { const appContext: AppContextValue = useRequiredContext(AppContext); const marshal: DimensionMarshal = appContext.marshal; const previousRef: { current: Props } = usePreviousRef(args); - const descriptor: DroppableDescriptor = useMemo((): DroppableDescriptor => { + const descriptor: DroppableDescriptor = useMemoOne((): DroppableDescriptor => { return { id: args.droppableId, type: args.type, @@ -64,7 +66,7 @@ export default function useDroppableDimensionPublisher(args: Props) { }, [args.droppableId, args.type]); const publishedDescriptorRef = useRef(descriptor); - const memoizedUpdateScroll = useMemo( + const memoizedUpdateScroll = useMemoOne( () => memoizeOne((x: number, y: number) => { invariant( @@ -77,7 +79,7 @@ export default function useDroppableDimensionPublisher(args: Props) { [descriptor.id, marshal], ); - const getClosestScroll = useCallback((): Position => { + const getClosestScroll = useCallbackOne((): Position => { const dragging: ?WhileDragging = whileDraggingRef.current; if (!dragging || !dragging.env.closestScrollable) { return origin; @@ -86,16 +88,16 @@ export default function useDroppableDimensionPublisher(args: Props) { return getScroll(dragging.env.closestScrollable); }, []); - const updateScroll = useCallback(() => { + const updateScroll = useCallbackOne(() => { const scroll: Position = getClosestScroll(); memoizedUpdateScroll(scroll.x, scroll.y); }, [getClosestScroll, memoizedUpdateScroll]); - const scheduleScrollUpdate = useMemo(() => rafSchedule(updateScroll), [ + const scheduleScrollUpdate = useMemoOne(() => rafSchedule(updateScroll), [ updateScroll, ]); - const onClosestScroll = useCallback(() => { + const onClosestScroll = useCallbackOne(() => { const dragging: ?WhileDragging = whileDraggingRef.current; const closest: ?Element = getClosestScrollableFromDrag(dragging); @@ -111,7 +113,7 @@ export default function useDroppableDimensionPublisher(args: Props) { scheduleScrollUpdate(); }, [scheduleScrollUpdate, updateScroll]); - const getDimensionAndWatchScroll = useCallback( + const getDimensionAndWatchScroll = useCallbackOne( (windowScroll: Position, options: ScrollOptions) => { invariant( !whileDraggingRef.current, @@ -160,7 +162,7 @@ export default function useDroppableDimensionPublisher(args: Props) { }, [descriptor, onClosestScroll, previousRef], ); - const recollect = useCallback( + const recollect = useCallbackOne( (options: RecollectDroppableOptions): DroppableDimension => { const dragging: ?WhileDragging = whileDraggingRef.current; const closest: ?Element = getClosestScrollableFromDrag(dragging); @@ -191,7 +193,7 @@ export default function useDroppableDimensionPublisher(args: Props) { }, [previousRef], ); - const dragStopped = useCallback(() => { + const dragStopped = useCallbackOne(() => { const dragging: ?WhileDragging = whileDraggingRef.current; invariant(dragging, 'Cannot stop drag when no active drag'); const closest: ?Element = getClosestScrollableFromDrag(dragging); @@ -212,7 +214,7 @@ export default function useDroppableDimensionPublisher(args: Props) { ); }, [onClosestScroll, scheduleScrollUpdate]); - const scroll = useCallback((change: Position) => { + const scroll = useCallbackOne((change: Position) => { // arrange const dragging: ?WhileDragging = whileDraggingRef.current; invariant(dragging, 'Cannot scroll when there is no drag'); @@ -224,7 +226,7 @@ export default function useDroppableDimensionPublisher(args: Props) { closest.scrollLeft += change.x; }, []); - const callbacks: DroppableCallbacks = useMemo(() => { + const callbacks: DroppableCallbacks = useMemoOne(() => { return { getDimensionAndWatchScroll, recollect, diff --git a/src/view/use-style-marshal/use-style-marshal.js b/src/view/use-style-marshal/use-style-marshal.js index 2f6cfdf62d..4fe8d672dd 100644 --- a/src/view/use-style-marshal/use-style-marshal.js +++ b/src/view/use-style-marshal/use-style-marshal.js @@ -1,5 +1,5 @@ // @flow -import { useRef, useCallback, useMemo } from 'react'; +import { useRef } from 'react'; import memoizeOne from 'memoize-one'; import invariant from 'tiny-invariant'; import type { StyleMarshal } from './style-marshal-types'; @@ -7,6 +7,8 @@ import type { DropReason } from '../../types'; import getStyles, { type Styles } from './get-styles'; import { prefix } from '../data-attributes'; import useIsomorphicLayoutEffect from '../use-isomorphic-layout-effect'; +import useMemoOne from '../use-custom-memo/use-memo-one'; +import useCallbackOne from '../use-custom-memo/use-callback-one'; const getHead = (): HTMLHeadElement => { const head: ?HTMLHeadElement = document.querySelector('head'); @@ -21,14 +23,14 @@ const createStyleEl = (): HTMLStyleElement => { }; export default function useStyleMarshal(uniqueId: number) { - const uniqueContext: string = useMemo(() => `${uniqueId}`, [uniqueId]); - const styles: Styles = useMemo(() => getStyles(uniqueContext), [ + const uniqueContext: string = useMemoOne(() => `${uniqueId}`, [uniqueId]); + const styles: Styles = useMemoOne(() => getStyles(uniqueContext), [ uniqueContext, ]); const alwaysRef = useRef(null); const dynamicRef = useRef(null); - const setDynamicStyle = useCallback( + const setDynamicStyle = useCallbackOne( // Using memoizeOne to prevent frequent updates to textContext memoizeOne((proposed: string) => { const el: ?HTMLStyleElement = dynamicRef.current; @@ -38,7 +40,7 @@ export default function useStyleMarshal(uniqueId: number) { [], ); - const setAlwaysStyle = useCallback((proposed: string) => { + const setAlwaysStyle = useCallbackOne((proposed: string) => { const el: ?HTMLStyleElement = alwaysRef.current; invariant(el, 'Cannot set dynamic style element if it is not set'); el.textContent = proposed; @@ -89,11 +91,11 @@ export default function useStyleMarshal(uniqueId: number) { uniqueContext, ]); - const dragging = useCallback(() => setDynamicStyle(styles.dragging), [ + const dragging = useCallbackOne(() => setDynamicStyle(styles.dragging), [ setDynamicStyle, styles.dragging, ]); - const dropping = useCallback( + const dropping = useCallbackOne( (reason: DropReason) => { if (reason === 'DROP') { setDynamicStyle(styles.dropAnimating); @@ -103,7 +105,7 @@ export default function useStyleMarshal(uniqueId: number) { }, [setDynamicStyle, styles.dropAnimating, styles.userCancel], ); - const resting = useCallback(() => { + const resting = useCallbackOne(() => { // Can be called defensively if (!dynamicRef.current) { return; @@ -111,7 +113,7 @@ export default function useStyleMarshal(uniqueId: number) { setDynamicStyle(styles.resting); }, [setDynamicStyle, styles.resting]); - const marshal: StyleMarshal = useMemo( + const marshal: StyleMarshal = useMemoOne( () => ({ dragging, dropping, diff --git a/test/.eslintrc.js b/test/.eslintrc.js index 0d2ee38ba9..41c47472db 100644 --- a/test/.eslintrc.js +++ b/test/.eslintrc.js @@ -4,5 +4,8 @@ module.exports = { // this is because we often mock console.warn and console.error and adding this rul // avoids needing to constantly be opting out of the rule 'no-console': ['error', { allow: ['warn', 'error'] }], + + // allowing useMemo and useCallback in tests + 'no-restricted-imports': 'off', }, }; From 7d4f788b7287dee619071c657f23073733b5d59a Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Tue, 2 Apr 2019 08:23:46 +1100 Subject: [PATCH 109/117] adding tests for use-memo-one --- .../view/use-memo-one/use-memo-one.spec.js | 86 +++++++++++++++++++ 1 file changed, 86 insertions(+) create mode 100644 test/unit/view/use-memo-one/use-memo-one.spec.js diff --git a/test/unit/view/use-memo-one/use-memo-one.spec.js b/test/unit/view/use-memo-one/use-memo-one.spec.js new file mode 100644 index 0000000000..82e1a70c15 --- /dev/null +++ b/test/unit/view/use-memo-one/use-memo-one.spec.js @@ -0,0 +1,86 @@ +// @flow +import React, { type Node } from 'react'; +import { mount } from 'enzyme'; +import useMemoOne from '../../../../src/view/use-custom-memo/use-memo-one'; + +type WithMemoProps = {| + inputs: mixed[], + children: (value: mixed) => Node, + getResult: () => mixed, +|}; + +function WithMemo(props: WithMemoProps) { + const value: mixed = useMemoOne(props.getResult, props.inputs); + return props.children(value); +} + +it('should not break the cache on multiple calls', () => { + const mock = jest.fn().mockReturnValue(
hey
); + const wrapper = mount( + ({ hello: 'world' })}> + {mock} + , + ); + + // initial call + expect(mock).toHaveBeenCalledTimes(1); + expect(mock).toHaveBeenCalledWith({ hello: 'world' }); + const initial: mixed = mock.mock.calls[0][0]; + expect(initial).toEqual({ hello: 'world' }); + mock.mockClear(); + + wrapper.setProps({ inputs: [1, 2] }); + + expect(mock).toHaveBeenCalledWith(initial); + const second: mixed = mock.mock.calls[0][0]; + // same reference + expect(initial).toBe(second); +}); + +it('should break the cache when the inputs change', () => { + const mock = jest.fn().mockReturnValue(
hey
); + const wrapper = mount( + ({ hello: 'world' })}> + {mock} + , + ); + + expect(mock).toHaveBeenCalledTimes(1); + expect(mock).toHaveBeenCalledWith({ hello: 'world' }); + const initial: mixed = mock.mock.calls[0][0]; + expect(initial).toEqual({ hello: 'world' }); + mock.mockClear(); + + // inputs are different + wrapper.setProps({ inputs: [1, 2, 3] }); + + expect(mock).toHaveBeenCalledWith(initial); + expect(mock).toHaveBeenCalledTimes(1); + const second: mixed = mock.mock.calls[0][0]; + // different reference + expect(initial).not.toBe(second); +}); + +it('should use the latest get result function when the cache breaks', () => { + const mock = jest.fn().mockReturnValue(
hey
); + const wrapper = mount( + ({ hello: 'world' })}> + {mock} + , + ); + + expect(mock).toHaveBeenCalledTimes(1); + expect(mock).toHaveBeenCalledWith({ hello: 'world' }); + const initial: mixed = mock.mock.calls[0][0]; + expect(initial).toEqual({ hello: 'world' }); + mock.mockClear(); + + // inputs are different + wrapper.setProps({ + inputs: [1, 2, 3], + getResult: () => ({ different: 'value' }), + }); + + expect(mock).toHaveBeenCalledWith({ different: 'value' }); + expect(mock).toHaveBeenCalledTimes(1); +}); From 4f7893ace3e5a3e0b591a5526edd76f18a2c647c Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Tue, 2 Apr 2019 09:15:14 +1100 Subject: [PATCH 110/117] shaving a few bytes --- .size-snapshot.json | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/.size-snapshot.json b/.size-snapshot.json index 3a1cdefde0..82567add0c 100644 --- a/.size-snapshot.json +++ b/.size-snapshot.json @@ -1,25 +1,25 @@ { "dist/react-beautiful-dnd.js": { - "bundled": 393322, - "minified": 147989, - "gzipped": 41340 + "bundled": 392241, + "minified": 146845, + "gzipped": 41224 }, "dist/react-beautiful-dnd.min.js": { - "bundled": 327193, - "minified": 117669, - "gzipped": 33593 + "bundled": 324794, + "minified": 116476, + "gzipped": 33462 }, "dist/react-beautiful-dnd.esm.js": { - "bundled": 239473, - "minified": 123943, - "gzipped": 31513, + "bundled": 238832, + "minified": 123803, + "gzipped": 31439, "treeshaked": { "rollup": { - "code": 30284, - "import_statements": 774 + "code": 30609, + "import_statements": 737 }, "webpack": { - "code": 34182 + "code": 33382 } } } From 8fc21f232c99db93d95b37acb07ddf3b16f3ff38 Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Tue, 2 Apr 2019 11:23:39 +1100 Subject: [PATCH 111/117] moving to use-memo-one --- package.json | 3 +- src/view/drag-drop-context/app.jsx | 3 +- .../drag-drop-context/drag-drop-context.jsx | 2 +- src/view/draggable/draggable.jsx | 3 +- src/view/droppable/droppable.jsx | 3 +- src/view/placeholder/placeholder.jsx | 2 +- src/view/use-announcer/use-announcer.js | 3 +- src/view/use-custom-memo/are-inputs-equal.js | 16 ---- src/view/use-custom-memo/use-callback-one.js | 11 --- src/view/use-custom-memo/use-memo-one.js | 40 --------- .../sensor/use-keyboard-sensor.js | 3 +- .../sensor/use-mouse-sensor.js | 3 +- .../sensor/use-touch-sensor.js | 3 +- src/view/use-drag-handle/use-drag-handle.js | 3 +- .../use-drag-handle/use-focus-retainer.js | 2 +- .../use-draggable-dimension-publisher.js | 3 +- .../use-droppable-dimension-publisher.js | 3 +- .../use-style-marshal/use-style-marshal.js | 3 +- .../view/use-memo-one/use-memo-one.spec.js | 86 ------------------- yarn.lock | 5 ++ 20 files changed, 21 insertions(+), 179 deletions(-) delete mode 100644 src/view/use-custom-memo/are-inputs-equal.js delete mode 100644 src/view/use-custom-memo/use-callback-one.js delete mode 100644 src/view/use-custom-memo/use-memo-one.js delete mode 100644 test/unit/view/use-memo-one/use-memo-one.spec.js diff --git a/package.json b/package.json index ebaa10fc94..9eee477d5b 100644 --- a/package.json +++ b/package.json @@ -60,7 +60,8 @@ "raf-schd": "^4.0.0", "react-redux": "7.0.0-beta.0", "redux": "^4.0.1", - "tiny-invariant": "^1.0.4" + "tiny-invariant": "^1.0.4", + "use-memo-one": "^0.0.6" }, "devDependencies": { "@atlaskit/css-reset": "^3.0.6", diff --git a/src/view/drag-drop-context/app.jsx b/src/view/drag-drop-context/app.jsx index 28bda10032..74c6bfbb1f 100644 --- a/src/view/drag-drop-context/app.jsx +++ b/src/view/drag-drop-context/app.jsx @@ -2,6 +2,7 @@ import React, { useEffect, useRef, type Node } from 'react'; import { bindActionCreators } from 'redux'; import { Provider } from 'react-redux'; +import { useMemoOne, useCallbackOne } from 'use-memo-one'; import createStore from '../../state/create-store'; import createDimensionMarshal from '../../state/dimension-marshal/dimension-marshal'; import canStartDrag from '../../state/can-start-drag'; @@ -30,8 +31,6 @@ import isMovementAllowed from '../../state/is-movement-allowed'; import useAnnouncer from '../use-announcer'; import AppContext, { type AppContextValue } from '../context/app-context'; import useStartupValidation from './use-startup-validation'; -import useMemoOne from '../use-custom-memo/use-memo-one'; -import useCallbackOne from '../use-custom-memo/use-callback-one'; import useIsomorphicLayoutEffect from '../use-isomorphic-layout-effect'; import usePrevious from '../use-previous-ref'; diff --git a/src/view/drag-drop-context/drag-drop-context.jsx b/src/view/drag-drop-context/drag-drop-context.jsx index 6e66cda533..4957a08457 100644 --- a/src/view/drag-drop-context/drag-drop-context.jsx +++ b/src/view/drag-drop-context/drag-drop-context.jsx @@ -1,9 +1,9 @@ // @flow import React, { type Node } from 'react'; +import { useMemoOne } from 'use-memo-one'; import type { Responders } from '../../types'; import ErrorBoundary from '../error-boundary'; import App from './app'; -import useMemoOne from '../use-custom-memo/use-memo-one'; type Props = {| ...Responders, diff --git a/src/view/draggable/draggable.jsx b/src/view/draggable/draggable.jsx index 89ed2c8fb8..d8dfec1ca2 100644 --- a/src/view/draggable/draggable.jsx +++ b/src/view/draggable/draggable.jsx @@ -2,6 +2,7 @@ import { useRef } from 'react'; import { type Position } from 'css-box-model'; import invariant from 'tiny-invariant'; +import { useMemoOne, useCallbackOne } from 'use-memo-one'; import getStyle from './get-style'; import useDragHandle from '../use-drag-handle/use-drag-handle'; import type { @@ -21,8 +22,6 @@ import getWindowScroll from '../window/get-window-scroll'; import AppContext, { type AppContextValue } from '../context/app-context'; import useRequiredContext from '../use-required-context'; import useValidation from './use-validation'; -import useCallbackOne from '../use-custom-memo/use-callback-one'; -import useMemoOne from '../use-custom-memo/use-memo-one'; export default function Draggable(props: Props) { // reference to DOM node diff --git a/src/view/droppable/droppable.jsx b/src/view/droppable/droppable.jsx index 49c16c798e..a6e16db7fc 100644 --- a/src/view/droppable/droppable.jsx +++ b/src/view/droppable/droppable.jsx @@ -1,5 +1,6 @@ // @flow import invariant from 'tiny-invariant'; +import { useMemoOne, useCallbackOne } from 'use-memo-one'; import React, { useRef, useContext, type Node } from 'react'; import type { Props, Provided } from './droppable-types'; import useDroppableDimensionPublisher from '../use-droppable-dimension-publisher'; @@ -14,8 +15,6 @@ import useValidation from './use-validation'; import AnimateInOut, { type AnimateProvided, } from '../animate-in-out/animate-in-out'; -import useCallbackOne from '../use-custom-memo/use-callback-one'; -import useMemoOne from '../use-custom-memo/use-memo-one'; export default function Droppable(props: Props) { const appContext: ?AppContextValue = useContext(AppContext); diff --git a/src/view/placeholder/placeholder.jsx b/src/view/placeholder/placeholder.jsx index 6707149b1d..84889aac99 100644 --- a/src/view/placeholder/placeholder.jsx +++ b/src/view/placeholder/placeholder.jsx @@ -1,5 +1,6 @@ // @flow import React, { useState, useRef, useEffect, type Node } from 'react'; +import { useCallbackOne } from 'use-memo-one'; import type { Spacing } from 'css-box-model'; import type { Placeholder as PlaceholderType, @@ -7,7 +8,6 @@ import type { } from '../../types'; import { transitions } from '../../animation'; import { noSpacing } from '../../state/spacing'; -import useCallbackOne from '../use-custom-memo/use-callback-one'; function noop() {} diff --git a/src/view/use-announcer/use-announcer.js b/src/view/use-announcer/use-announcer.js index 5344bbf2cd..798b3e562b 100644 --- a/src/view/use-announcer/use-announcer.js +++ b/src/view/use-announcer/use-announcer.js @@ -1,11 +1,10 @@ // @flow import { useRef, useEffect } from 'react'; import invariant from 'tiny-invariant'; +import { useMemoOne, useCallbackOne } from 'use-memo-one'; import type { Announce } from '../../types'; import { warning } from '../../dev-warning'; import getBodyElement from '../get-body-element'; -import useMemoOne from '../use-custom-memo/use-memo-one'; -import useCallbackOne from '../use-custom-memo/use-callback-one'; // https://allyjs.io/tutorials/hiding-elements.html // Element is visually hidden but is readable by screen readers diff --git a/src/view/use-custom-memo/are-inputs-equal.js b/src/view/use-custom-memo/are-inputs-equal.js deleted file mode 100644 index dbf521be54..0000000000 --- a/src/view/use-custom-memo/are-inputs-equal.js +++ /dev/null @@ -1,16 +0,0 @@ -// @flow -const isShallowEqual = (newValue: mixed, oldValue: mixed): boolean => - newValue === oldValue; - -export default function areInputsEqual( - newInputs: mixed[], - lastInputs: mixed[], -) { - return ( - newInputs.length === lastInputs.length && - newInputs.every( - (newArg: mixed, index: number): boolean => - isShallowEqual(newArg, lastInputs[index]), - ) - ); -} diff --git a/src/view/use-custom-memo/use-callback-one.js b/src/view/use-custom-memo/use-callback-one.js deleted file mode 100644 index 63cc73e53f..0000000000 --- a/src/view/use-custom-memo/use-callback-one.js +++ /dev/null @@ -1,11 +0,0 @@ -// @flow -import useMemoOne from './use-memo-one'; - -export default function useCallbackOne( - // getResult changes on every call, - callback: T, - // the inputs array changes on every call - inputs?: mixed[] = [], -): T { - return useMemoOne(() => callback, inputs); -} diff --git a/src/view/use-custom-memo/use-memo-one.js b/src/view/use-custom-memo/use-memo-one.js deleted file mode 100644 index 44f4c7596d..0000000000 --- a/src/view/use-custom-memo/use-memo-one.js +++ /dev/null @@ -1,40 +0,0 @@ -// @flow -import { useRef, useState, useEffect } from 'react'; -import areInputsEqual from './are-inputs-equal'; - -type Result = {| - inputs: mixed[], - result: T, -|}; - -export default function useMemoOne( - // getResult changes on every call, - getResult: () => T, - // the inputs array changes on every call - inputs?: mixed[] = [], -): T { - // using useState to generate initial value as it is lazy - const initial: Result = useState(() => ({ - inputs, - result: getResult(), - }))[0]; - - const uncommitted = useRef>(initial); - const committed = useRef>(initial); - - // persist any uncommitted changes after they have been committed - useEffect(() => { - committed.current = uncommitted.current; - }); - - if (areInputsEqual(inputs, committed.current.inputs)) { - return committed.current.result; - } - - uncommitted.current = { - inputs, - result: getResult(), - }; - - return uncommitted.current.result; -} diff --git a/src/view/use-drag-handle/sensor/use-keyboard-sensor.js b/src/view/use-drag-handle/sensor/use-keyboard-sensor.js index 5b36d4119f..41f2c59786 100644 --- a/src/view/use-drag-handle/sensor/use-keyboard-sensor.js +++ b/src/view/use-drag-handle/sensor/use-keyboard-sensor.js @@ -1,6 +1,7 @@ // @flow import type { Position } from 'css-box-model'; import { useRef } from 'react'; +import { useMemoOne, useCallbackOne } from 'use-memo-one'; import invariant from 'tiny-invariant'; import type { EventBinding } from '../util/event-types'; import { bindEvents, unbindEvents } from '../util/bind-events'; @@ -10,8 +11,6 @@ import supportedPageVisibilityEventName from '../util/supported-page-visibility- import preventStandardKeyEvents from '../util/prevent-standard-key-events'; import type { Callbacks } from '../drag-handle-types'; import getBorderBoxCenterPosition from '../../get-border-box-center-position'; -import useCallbackOne from '../../use-custom-memo/use-callback-one'; -import useMemoOne from '../../use-custom-memo/use-memo-one'; export type Args = {| callbacks: Callbacks, diff --git a/src/view/use-drag-handle/sensor/use-mouse-sensor.js b/src/view/use-drag-handle/sensor/use-mouse-sensor.js index 17bb4cf39c..dfc8ecbc15 100644 --- a/src/view/use-drag-handle/sensor/use-mouse-sensor.js +++ b/src/view/use-drag-handle/sensor/use-mouse-sensor.js @@ -2,6 +2,7 @@ import type { Position } from 'css-box-model'; import { useRef } from 'react'; import invariant from 'tiny-invariant'; +import { useMemoOne, useCallbackOne } from 'use-memo-one'; import type { EventBinding } from '../util/event-types'; import createEventMarshal, { type EventMarshal, @@ -17,8 +18,6 @@ import createPostDragEventPreventer, { } from '../util/create-post-drag-event-preventer'; import isSloppyClickThresholdExceeded from '../util/is-sloppy-click-threshold-exceeded'; import preventStandardKeyEvents from '../util/prevent-standard-key-events'; -import useCallbackOne from '../../use-custom-memo/use-callback-one'; -import useMemoOne from '../../use-custom-memo/use-memo-one'; export type Args = {| callbacks: Callbacks, diff --git a/src/view/use-drag-handle/sensor/use-touch-sensor.js b/src/view/use-drag-handle/sensor/use-touch-sensor.js index 0782f18cd7..22306c442d 100644 --- a/src/view/use-drag-handle/sensor/use-touch-sensor.js +++ b/src/view/use-drag-handle/sensor/use-touch-sensor.js @@ -2,6 +2,7 @@ import type { Position } from 'css-box-model'; import { useRef } from 'react'; import invariant from 'tiny-invariant'; +import { useMemoOne, useCallbackOne } from 'use-memo-one'; import type { EventBinding } from '../util/event-types'; import createEventMarshal, { type EventMarshal, @@ -14,8 +15,6 @@ import supportedPageVisibilityEventName from '../util/supported-page-visibility- import createPostDragEventPreventer, { type EventPreventer, } from '../util/create-post-drag-event-preventer'; -import useCallbackOne from '../../use-custom-memo/use-callback-one'; -import useMemoOne from '../../use-custom-memo/use-memo-one'; export type Args = {| callbacks: Callbacks, diff --git a/src/view/use-drag-handle/use-drag-handle.js b/src/view/use-drag-handle/use-drag-handle.js index 0a567d95eb..37f05462ef 100644 --- a/src/view/use-drag-handle/use-drag-handle.js +++ b/src/view/use-drag-handle/use-drag-handle.js @@ -1,6 +1,7 @@ // @flow import invariant from 'tiny-invariant'; import { useRef } from 'react'; +import { useMemoOne, useCallbackOne } from 'use-memo-one'; import type { Args, DragHandleProps } from './drag-handle-types'; import getWindowFromEl from '../window/get-window-from-el'; import useRequiredContext from '../use-required-context'; @@ -20,8 +21,6 @@ import { warning } from '../../dev-warning'; import useValidation from './use-validation'; import useFocusRetainer from './use-focus-retainer'; import useIsomorphicLayoutEffect from '../use-isomorphic-layout-effect'; -import useCallbackOne from '../use-custom-memo/use-callback-one'; -import useMemoOne from '../use-custom-memo/use-memo-one'; function preventHtml5Dnd(event: DragEvent) { event.preventDefault(); diff --git a/src/view/use-drag-handle/use-focus-retainer.js b/src/view/use-drag-handle/use-focus-retainer.js index fea6c7112d..c40c5de944 100644 --- a/src/view/use-drag-handle/use-focus-retainer.js +++ b/src/view/use-drag-handle/use-focus-retainer.js @@ -1,12 +1,12 @@ // @flow import invariant from 'tiny-invariant'; import { useRef } from 'react'; +import { useCallbackOne } from 'use-memo-one'; import type { Args } from './drag-handle-types'; import usePrevious from '../use-previous-ref'; import focusRetainer from './util/focus-retainer'; import getDragHandleRef from './util/get-drag-handle-ref'; import useIsomorphicLayoutEffect from '../use-isomorphic-layout-effect'; -import useCallbackOne from '../use-custom-memo/use-callback-one'; export type Result = {| onBlur: () => void, diff --git a/src/view/use-draggable-dimension-publisher/use-draggable-dimension-publisher.js b/src/view/use-draggable-dimension-publisher/use-draggable-dimension-publisher.js index c4f2d0e816..9e56812429 100644 --- a/src/view/use-draggable-dimension-publisher/use-draggable-dimension-publisher.js +++ b/src/view/use-draggable-dimension-publisher/use-draggable-dimension-publisher.js @@ -2,6 +2,7 @@ import { useRef } from 'react'; import { type Position } from 'css-box-model'; import invariant from 'tiny-invariant'; +import { useMemoOne, useCallbackOne } from 'use-memo-one'; import type { DraggableDescriptor, DraggableDimension, @@ -15,8 +16,6 @@ import DroppableContext, { type DroppableContextValue, } from '../context/droppable-context'; import useIsomorphicLayoutEffect from '../use-isomorphic-layout-effect'; -import useMemoOne from '../use-custom-memo/use-memo-one'; -import useCallbackOne from '../use-custom-memo/use-callback-one'; export type Args = {| draggableId: DraggableId, diff --git a/src/view/use-droppable-dimension-publisher/use-droppable-dimension-publisher.js b/src/view/use-droppable-dimension-publisher/use-droppable-dimension-publisher.js index 0884b22eec..1b8d4c0bb6 100644 --- a/src/view/use-droppable-dimension-publisher/use-droppable-dimension-publisher.js +++ b/src/view/use-droppable-dimension-publisher/use-droppable-dimension-publisher.js @@ -3,6 +3,7 @@ import { useRef } from 'react'; import invariant from 'tiny-invariant'; import { type Position } from 'css-box-model'; import rafSchedule from 'raf-schd'; +import { useMemoOne, useCallbackOne } from 'use-memo-one'; import memoizeOne from 'memoize-one'; import checkForNestedScrollContainers from './check-for-nested-scroll-container'; import { origin } from '../../state/position'; @@ -29,8 +30,6 @@ import getListenerOptions from './get-listener-options'; import useRequiredContext from '../use-required-context'; import usePreviousRef from '../use-previous-ref'; import useIsomorphicLayoutEffect from '../use-isomorphic-layout-effect'; -import useMemoOne from '../use-custom-memo/use-memo-one'; -import useCallbackOne from '../use-custom-memo/use-callback-one'; type Props = {| droppableId: DroppableId, diff --git a/src/view/use-style-marshal/use-style-marshal.js b/src/view/use-style-marshal/use-style-marshal.js index 4fe8d672dd..068af5c098 100644 --- a/src/view/use-style-marshal/use-style-marshal.js +++ b/src/view/use-style-marshal/use-style-marshal.js @@ -1,14 +1,13 @@ // @flow import { useRef } from 'react'; import memoizeOne from 'memoize-one'; +import { useMemoOne, useCallbackOne } from 'use-memo-one'; import invariant from 'tiny-invariant'; import type { StyleMarshal } from './style-marshal-types'; import type { DropReason } from '../../types'; import getStyles, { type Styles } from './get-styles'; import { prefix } from '../data-attributes'; import useIsomorphicLayoutEffect from '../use-isomorphic-layout-effect'; -import useMemoOne from '../use-custom-memo/use-memo-one'; -import useCallbackOne from '../use-custom-memo/use-callback-one'; const getHead = (): HTMLHeadElement => { const head: ?HTMLHeadElement = document.querySelector('head'); diff --git a/test/unit/view/use-memo-one/use-memo-one.spec.js b/test/unit/view/use-memo-one/use-memo-one.spec.js deleted file mode 100644 index 82e1a70c15..0000000000 --- a/test/unit/view/use-memo-one/use-memo-one.spec.js +++ /dev/null @@ -1,86 +0,0 @@ -// @flow -import React, { type Node } from 'react'; -import { mount } from 'enzyme'; -import useMemoOne from '../../../../src/view/use-custom-memo/use-memo-one'; - -type WithMemoProps = {| - inputs: mixed[], - children: (value: mixed) => Node, - getResult: () => mixed, -|}; - -function WithMemo(props: WithMemoProps) { - const value: mixed = useMemoOne(props.getResult, props.inputs); - return props.children(value); -} - -it('should not break the cache on multiple calls', () => { - const mock = jest.fn().mockReturnValue(
hey
); - const wrapper = mount( - ({ hello: 'world' })}> - {mock} - , - ); - - // initial call - expect(mock).toHaveBeenCalledTimes(1); - expect(mock).toHaveBeenCalledWith({ hello: 'world' }); - const initial: mixed = mock.mock.calls[0][0]; - expect(initial).toEqual({ hello: 'world' }); - mock.mockClear(); - - wrapper.setProps({ inputs: [1, 2] }); - - expect(mock).toHaveBeenCalledWith(initial); - const second: mixed = mock.mock.calls[0][0]; - // same reference - expect(initial).toBe(second); -}); - -it('should break the cache when the inputs change', () => { - const mock = jest.fn().mockReturnValue(
hey
); - const wrapper = mount( - ({ hello: 'world' })}> - {mock} - , - ); - - expect(mock).toHaveBeenCalledTimes(1); - expect(mock).toHaveBeenCalledWith({ hello: 'world' }); - const initial: mixed = mock.mock.calls[0][0]; - expect(initial).toEqual({ hello: 'world' }); - mock.mockClear(); - - // inputs are different - wrapper.setProps({ inputs: [1, 2, 3] }); - - expect(mock).toHaveBeenCalledWith(initial); - expect(mock).toHaveBeenCalledTimes(1); - const second: mixed = mock.mock.calls[0][0]; - // different reference - expect(initial).not.toBe(second); -}); - -it('should use the latest get result function when the cache breaks', () => { - const mock = jest.fn().mockReturnValue(
hey
); - const wrapper = mount( - ({ hello: 'world' })}> - {mock} - , - ); - - expect(mock).toHaveBeenCalledTimes(1); - expect(mock).toHaveBeenCalledWith({ hello: 'world' }); - const initial: mixed = mock.mock.calls[0][0]; - expect(initial).toEqual({ hello: 'world' }); - mock.mockClear(); - - // inputs are different - wrapper.setProps({ - inputs: [1, 2, 3], - getResult: () => ({ different: 'value' }), - }); - - expect(mock).toHaveBeenCalledWith({ different: 'value' }); - expect(mock).toHaveBeenCalledTimes(1); -}); diff --git a/yarn.lock b/yarn.lock index ddc024a0cf..51887a0f1e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -11858,6 +11858,11 @@ url@0.11.0, url@^0.11.0: punycode "1.3.2" querystring "0.2.0" +use-memo-one@^0.0.6: + version "0.0.6" + resolved "https://registry.yarnpkg.com/use-memo-one/-/use-memo-one-0.0.6.tgz#354618b801ae6ec30f91fc71af4c532488126677" + integrity sha512-bRYftkJKaoLmtU4Ot7l6xUm+HdSQ8xCKuBxLKKFtHrHlcJIIUblmxp53cCfu+YlvM8NYHEhTYHEnd5nkB44Uog== + use@^3.1.0: version "3.1.1" resolved "https://registry.yarnpkg.com/use/-/use-3.1.1.tgz#d50c8cac79a19fbc20f2911f56eb973f4e10070f" From 539dcf13e31e4a726c260f76551043ebbc7bab12 Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Tue, 2 Apr 2019 13:13:01 +1100 Subject: [PATCH 112/117] adding array guards --- src/view/drag-drop-context/app.jsx | 101 ++++++++++++++++------------- 1 file changed, 57 insertions(+), 44 deletions(-) diff --git a/src/view/drag-drop-context/app.jsx b/src/view/drag-drop-context/app.jsx index 74c6bfbb1f..d7f62bc83d 100644 --- a/src/view/drag-drop-context/app.jsx +++ b/src/view/drag-drop-context/app.jsx @@ -62,7 +62,7 @@ export default function App(props: Props) { const getResponders: () => Responders = useCallbackOne(() => { return createResponders(lastPropsRef.current); - }); + }, []); const announce: Announce = useAnnouncer(uniqueId); const styleMarshal: StyleMarshal = useStyleMarshal(uniqueId); @@ -71,72 +71,85 @@ export default function App(props: Props) { (action: Action): void => { storeRef.current.dispatch(action); }, + [], ); - const callbacks: DimensionMarshalCallbacks = useMemoOne(() => - bindActionCreators( - { - publishWhileDragging, - updateDroppableScroll, - updateDroppableIsEnabled, - updateDroppableIsCombineEnabled, - collectionStarting, - }, - // $FlowFixMe - not sure why this is wrong - lazyDispatch, - ), - ); - const dimensionMarshal: DimensionMarshal = useMemoOne(() => - createDimensionMarshal(callbacks), - ); - - const autoScroller: AutoScroller = useMemoOne(() => - createAutoScroller({ - scrollWindow, - scrollDroppable: dimensionMarshal.scrollDroppable, - ...bindActionCreators( + const callbacks: DimensionMarshalCallbacks = useMemoOne( + () => + bindActionCreators( { - move, + publishWhileDragging, + updateDroppableScroll, + updateDroppableIsEnabled, + updateDroppableIsCombineEnabled, + collectionStarting, }, // $FlowFixMe - not sure why this is wrong lazyDispatch, ), - }), + [], + ); + const dimensionMarshal: DimensionMarshal = useMemoOne( + () => createDimensionMarshal(callbacks), + [], ); - const store: Store = useMemoOne(() => - createStore({ - dimensionMarshal, - styleMarshal, - announce, - autoScroller, - getResponders, - }), + const autoScroller: AutoScroller = useMemoOne( + () => + createAutoScroller({ + scrollWindow, + scrollDroppable: dimensionMarshal.scrollDroppable, + ...bindActionCreators( + { + move, + }, + // $FlowFixMe - not sure why this is wrong + lazyDispatch, + ), + }), + [], + ); + + const store: Store = useMemoOne( + () => + createStore({ + dimensionMarshal, + styleMarshal, + announce, + autoScroller, + getResponders, + }), + [], ); storeRef = useRef(store); - const getCanLift = useCallbackOne((id: DraggableId) => - canStartDrag(storeRef.current.getState(), id), + const getCanLift = useCallbackOne( + (id: DraggableId) => canStartDrag(storeRef.current.getState(), id), + [], ); - const getIsMovementAllowed = useCallbackOne(() => - isMovementAllowed(storeRef.current.getState()), + const getIsMovementAllowed = useCallbackOne( + () => isMovementAllowed(storeRef.current.getState()), + [], ); - const appContext: AppContextValue = useMemoOne(() => ({ - marshal: dimensionMarshal, - style: styleMarshal.styleContext, - canLift: getCanLift, - isMovementAllowed: getIsMovementAllowed, - })); + const appContext: AppContextValue = useMemoOne( + () => ({ + marshal: dimensionMarshal, + style: styleMarshal.styleContext, + canLift: getCanLift, + isMovementAllowed: getIsMovementAllowed, + }), + [], + ); const tryResetStore = useCallbackOne(() => { const state: State = storeRef.current.getState(); if (state.phase !== 'IDLE') { store.dispatch(clean({ shouldFlush: true })); } - }); + }, []); useIsomorphicLayoutEffect(() => { setOnError(tryResetStore); From 424abf3f9a3870b9cf49478ca06f3aa29f2c4ac1 Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Tue, 2 Apr 2019 13:19:48 +1100 Subject: [PATCH 113/117] bumping use-memo-one --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 9eee477d5b..2706c97fb1 100644 --- a/package.json +++ b/package.json @@ -61,7 +61,7 @@ "react-redux": "7.0.0-beta.0", "redux": "^4.0.1", "tiny-invariant": "^1.0.4", - "use-memo-one": "^0.0.6" + "use-memo-one": "^0.0.7" }, "devDependencies": { "@atlaskit/css-reset": "^3.0.6", diff --git a/yarn.lock b/yarn.lock index 51887a0f1e..8ba925841d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -11858,10 +11858,10 @@ url@0.11.0, url@^0.11.0: punycode "1.3.2" querystring "0.2.0" -use-memo-one@^0.0.6: - version "0.0.6" - resolved "https://registry.yarnpkg.com/use-memo-one/-/use-memo-one-0.0.6.tgz#354618b801ae6ec30f91fc71af4c532488126677" - integrity sha512-bRYftkJKaoLmtU4Ot7l6xUm+HdSQ8xCKuBxLKKFtHrHlcJIIUblmxp53cCfu+YlvM8NYHEhTYHEnd5nkB44Uog== +use-memo-one@^0.0.7: + version "0.0.7" + resolved "https://registry.yarnpkg.com/use-memo-one/-/use-memo-one-0.0.7.tgz#38d8eebd9e207f05b75253cc64ad6a89e57e3ded" + integrity sha512-6CD0xMfFY+ZC1VHO9yXbOXZtd10J56zvjuTcpwPl1dGaq66+jQ9UVQN1N2dBEyHCIgB2OB4kWePi6hQ68sl6qw== use@^3.1.0: version "3.1.1" From c41e1b32cb840c508928861bc8e9e33de2d64f1d Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Tue, 2 Apr 2019 14:28:45 +1100 Subject: [PATCH 114/117] bumping use-memo-one --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 2706c97fb1..6e02305d78 100644 --- a/package.json +++ b/package.json @@ -61,7 +61,7 @@ "react-redux": "7.0.0-beta.0", "redux": "^4.0.1", "tiny-invariant": "^1.0.4", - "use-memo-one": "^0.0.7" + "use-memo-one": "^0.0.8" }, "devDependencies": { "@atlaskit/css-reset": "^3.0.6", diff --git a/yarn.lock b/yarn.lock index 8ba925841d..6c76b92908 100644 --- a/yarn.lock +++ b/yarn.lock @@ -11858,10 +11858,10 @@ url@0.11.0, url@^0.11.0: punycode "1.3.2" querystring "0.2.0" -use-memo-one@^0.0.7: - version "0.0.7" - resolved "https://registry.yarnpkg.com/use-memo-one/-/use-memo-one-0.0.7.tgz#38d8eebd9e207f05b75253cc64ad6a89e57e3ded" - integrity sha512-6CD0xMfFY+ZC1VHO9yXbOXZtd10J56zvjuTcpwPl1dGaq66+jQ9UVQN1N2dBEyHCIgB2OB4kWePi6hQ68sl6qw== +use-memo-one@^0.0.8: + version "0.0.8" + resolved "https://registry.yarnpkg.com/use-memo-one/-/use-memo-one-0.0.8.tgz#dd083b97e30248f8ab426687e2efdfb8ac5f1cee" + integrity sha512-pYGvl1OO29bXKlFm1FUGUF0IGmK6+P5I0d3INSoDYIqFahBRSGn5m8tdqS8SA74Q9lhoPtyCNAGCibfwbvZLDw== use@^3.1.0: version "3.1.1" From c5f5d6e792cfff3997ad7abcfda24a8c805aeabb Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Tue, 2 Apr 2019 14:57:48 +1100 Subject: [PATCH 115/117] updating bundle size --- .size-snapshot.json | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/.size-snapshot.json b/.size-snapshot.json index 82567add0c..cb6057e8e2 100644 --- a/.size-snapshot.json +++ b/.size-snapshot.json @@ -1,25 +1,25 @@ { "dist/react-beautiful-dnd.js": { - "bundled": 392241, - "minified": 146845, - "gzipped": 41224 + "bundled": 392153, + "minified": 146738, + "gzipped": 41191 }, "dist/react-beautiful-dnd.min.js": { - "bundled": 324794, - "minified": 116476, - "gzipped": 33462 + "bundled": 324706, + "minified": 116369, + "gzipped": 33451 }, "dist/react-beautiful-dnd.esm.js": { - "bundled": 238832, - "minified": 123803, - "gzipped": 31439, + "bundled": 237886, + "minified": 123357, + "gzipped": 31310, "treeshaked": { "rollup": { - "code": 30609, - "import_statements": 737 + "code": 30267, + "import_statements": 799 }, "webpack": { - "code": 33382 + "code": 34399 } } } From c278db0b949bcb1eb98698d4c43b161469cfb7eb Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Tue, 2 Apr 2019 15:55:56 +1100 Subject: [PATCH 116/117] moving to 1.0 of useMemoOne --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 6e02305d78..212f56093a 100644 --- a/package.json +++ b/package.json @@ -61,7 +61,7 @@ "react-redux": "7.0.0-beta.0", "redux": "^4.0.1", "tiny-invariant": "^1.0.4", - "use-memo-one": "^0.0.8" + "use-memo-one": "^1.0.0" }, "devDependencies": { "@atlaskit/css-reset": "^3.0.6", diff --git a/yarn.lock b/yarn.lock index 6c76b92908..91cd8bde43 100644 --- a/yarn.lock +++ b/yarn.lock @@ -11858,10 +11858,10 @@ url@0.11.0, url@^0.11.0: punycode "1.3.2" querystring "0.2.0" -use-memo-one@^0.0.8: - version "0.0.8" - resolved "https://registry.yarnpkg.com/use-memo-one/-/use-memo-one-0.0.8.tgz#dd083b97e30248f8ab426687e2efdfb8ac5f1cee" - integrity sha512-pYGvl1OO29bXKlFm1FUGUF0IGmK6+P5I0d3INSoDYIqFahBRSGn5m8tdqS8SA74Q9lhoPtyCNAGCibfwbvZLDw== +use-memo-one@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/use-memo-one/-/use-memo-one-1.0.0.tgz#725a24878dfa3e87d0c99a755c06a2da0f6fa272" + integrity sha512-ITeeQU5bmuFASUiScPnQ55YRXJ3bLOvfLN6Q49ZvtEiTOToQj2hF8BzSPRN6YAoI4l3gbQhLd6iHYQl9jMyqEg== use@^3.1.0: version "3.1.1" From 0fc6aa4b6bd9cfd186d3325d29d95224a17a37d2 Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Wed, 3 Apr 2019 16:01:55 +1100 Subject: [PATCH 117/117] v11.0.0-beta --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 212f56093a..c99ae2ff67 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "react-beautiful-dnd", - "version": "10.1.1", + "version": "11.0.0-beta", "description": "Beautiful and accessible drag and drop for lists with React", "author": "Alex Reardon ", "keywords": [