diff --git a/CHANGELOG.md b/CHANGELOG.md index 531dd9e07c8..e8de795c100 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ ## [`master`](https://github.com/elastic/eui/tree/master) - **[Beta]** Added new `EuiSelectable` component ([#1699](https://github.com/elastic/eui/pull/1699)) +- **[Beta]** Added new drag and drop components: `EuiDragDropContext`, `EuiDraggable`, and `EuiDroppable` ([#1733](https://github.com/elastic/eui/pull/1733)) ## [`9.7.2`](https://github.com/elastic/eui/tree/v9.7.2) diff --git a/package.json b/package.json index f34dd723398..02dd222cc41 100644 --- a/package.json +++ b/package.json @@ -47,6 +47,7 @@ "dependencies": { "@types/lodash": "^4.14.116", "@types/numeral": "^0.0.25", + "@types/react-beautiful-dnd": "^10.1.0", "classnames": "^2.2.5", "core-js": "^2.5.1", "highlight.js": "^9.12.0", @@ -56,6 +57,7 @@ "numeral": "^2.0.6", "prop-types": "^15.6.0", "react-ace": "^5.5.0", + "react-beautiful-dnd": "^10.1.0", "react-color": "^2.13.8", "react-focus-lock": "^1.17.7", "react-input-autosize": "^2.2.1", diff --git a/src-docs/src/components/guide_section/guide_section.js b/src-docs/src/components/guide_section/guide_section.js index cd312f15609..b61815f4bf4 100644 --- a/src-docs/src/components/guide_section/guide_section.js +++ b/src-docs/src/components/guide_section/guide_section.js @@ -184,7 +184,7 @@ export class GuideSection extends Component { const rows = propNames.map(propName => { const { - description: propDescription, + description: propDescription = '', required, defaultValue, type, diff --git a/src-docs/src/routes.js b/src-docs/src/routes.js index ffaeb225c1e..66d1b11a379 100644 --- a/src-docs/src/routes.js +++ b/src-docs/src/routes.js @@ -108,6 +108,9 @@ import { DelayHideExample } import { DescriptionListExample } from './views/description_list/description_list_example'; +import { DragAndDropExample } + from './views/drag_and_drop/drag_and_drop_example'; + import { EmptyPromptExample } from './views/empty_prompt/empty_prompt_example'; @@ -388,6 +391,7 @@ const navigation = [{ CardExample, CodeExample, DescriptionListExample, + DragAndDropExample, EmptyPromptExample, HealthExample, IconExample, diff --git a/src-docs/src/views/drag_and_drop/drag_and_drop.js b/src-docs/src/views/drag_and_drop/drag_and_drop.js new file mode 100644 index 00000000000..172175cb9ee --- /dev/null +++ b/src-docs/src/views/drag_and_drop/drag_and_drop.js @@ -0,0 +1,41 @@ +import React, { useState } from 'react'; +import { + EuiDragDropContext, + EuiDraggable, + EuiDroppable, + EuiPanel +} from '../../../../src/components'; + +import { reorder } from '../../../../src/components/drag_and_drop'; + +import { makeList } from './helper'; + +export default () => { + const [list, setList] = useState(makeList(3)); + const onDragEnd = ({ source, destination }) => { + if (source && destination) { + const items = reorder( + list, + source.index, + destination.index + ); + + setList(items); + } + }; + return ( + + + {list.map(({ content, id }, idx) => ( + + {(provided, state) => ( + + {content}{state.isDragging && ' ✨'} + + )} + + ))} + + + ); +}; diff --git a/src-docs/src/views/drag_and_drop/drag_and_drop_bare.js b/src-docs/src/views/drag_and_drop/drag_and_drop_bare.js new file mode 100644 index 00000000000..858cf3ac3c2 --- /dev/null +++ b/src-docs/src/views/drag_and_drop/drag_and_drop_bare.js @@ -0,0 +1,30 @@ +import React, { useState } from 'react'; +import { + EuiDragDropContext, + EuiDraggable, + EuiDroppable +} from '../../../../src/components'; + +import { makeList } from './helper'; + +export default () => { + const [list] = useState(makeList(3)); + const onDragEnd = ({ source, destination }) => { + console.log(source, destination); + }; + return ( + + + {list.map(({ content, id }, idx) => ( + + {() => ( +
+ {content} +
+ )} +
+ ))} +
+
+ ); +}; diff --git a/src-docs/src/views/drag_and_drop/drag_and_drop_clone.js b/src-docs/src/views/drag_and_drop/drag_and_drop_clone.js new file mode 100644 index 00000000000..771b85c5c04 --- /dev/null +++ b/src-docs/src/views/drag_and_drop/drag_and_drop_clone.js @@ -0,0 +1,119 @@ +import React, { useState } from 'react'; +import { + EuiButtonIcon, + EuiDragDropContext, + EuiFlexGroup, + EuiFlexItem, + EuiDraggable, + EuiDroppable, + EuiIcon, + EuiPanel +} from '../../../../src/components'; + +import { + copy, + reorder +} from '../../../../src/components/drag_and_drop'; + +import { makeId, makeList } from './helper'; + +export default () => { + const [isItemRemovable, setIsItemRemovable] = useState(false); + const [list1, setList1] = useState(makeList(3)); + const [list2, setList2] = useState([]); + const lists = { DROPPABLE_AREA_COPY_1: list1, DROPPABLE_AREA_COPY_2: list2 }; + const actions = { DROPPABLE_AREA_COPY_1: setList1, DROPPABLE_AREA_COPY_2: setList2 }; + const remove = (droppableId, index) => { + const list = Array.from(lists[droppableId]); + list.splice(index, 1); + + actions[droppableId](list); + }; + const onDragUpdate = ({ source, destination }) => { + const shouldRemove = !destination && source.droppableId === 'DROPPABLE_AREA_COPY_2'; + setIsItemRemovable(shouldRemove); + }; + const onDragEnd = ({ source, destination }) => { + if (source && destination) { + if (source.droppableId === destination.droppableId) { + const items = reorder( + lists[destination.droppableId], + source.index, + destination.index + ); + + actions[destination.droppableId](items); + } else { + const sourceId = source.droppableId; + const destinationId = destination.droppableId; + const result = copy( + lists[sourceId], + lists[destinationId], + source, + destination, + { + property: 'id', + modifier: makeId + } + ); + + actions[sourceId](result[sourceId]); + actions[destinationId](result[destinationId]); + } + } else if (!destination && source.droppableId === 'DROPPABLE_AREA_COPY_2') { + remove(source.droppableId, source.index); + } + }; + return ( + + + + + + {list1.map(({ content, id }, idx) => ( + + + {content} + + + ))} + + + + + + + {list2.length ? + ( + list2.map(({ content, id }, idx) => ( + + + + {content} + + {isItemRemovable ? ( + + ) : ( + remove('DROPPABLE_AREA_COPY_2', idx)} + /> + )} + + + + + )) + ) : ( + + Drop Items Here + + )} + + + + + + ); +}; diff --git a/src-docs/src/views/drag_and_drop/drag_and_drop_complex.js b/src-docs/src/views/drag_and_drop/drag_and_drop_complex.js new file mode 100644 index 00000000000..5d1a409bf75 --- /dev/null +++ b/src-docs/src/views/drag_and_drop/drag_and_drop_complex.js @@ -0,0 +1,90 @@ +import React, { useState } from 'react'; +import { + EuiDragDropContext, + EuiDraggable, + EuiDroppable, + EuiButtonIcon, + EuiPanel +} from '../../../../src/components'; + +import { move, reorder } from '../../../../src/components/drag_and_drop'; + +import { makeList } from './helper'; + +export default () => { + const [list, setList] = useState([1, 2]); + const [list1, setList1] = useState(makeList(3)); + const [list2, setList2] = useState(makeList(3, 4)); + const lists = { COMPLEX_DROPPABLE_PARENT: list, COMPLEX_DROPPABLE_AREA_1: list1, COMPLEX_DROPPABLE_AREA_2: list2 }; + const actions = { COMPLEX_DROPPABLE_PARENT: setList, COMPLEX_DROPPABLE_AREA_1: setList1, COMPLEX_DROPPABLE_AREA_2: setList2 }; + const onDragEnd = ({ source, destination }) => { + if (source && destination) { + if (source.droppableId === destination.droppableId) { + const items = reorder( + lists[destination.droppableId], + source.index, + destination.index + ); + + actions[destination.droppableId](items); + } else { + const sourceId = source.droppableId; + const destinationId = destination.droppableId; + const result = move( + lists[sourceId], + lists[destinationId], + source, + destination + ); + + actions[sourceId](result[sourceId]); + actions[destinationId](result[destinationId]); + } + + } + }; + return ( + + + + {list.map((did, didx) => ( + + {(provided) => ( + + + + {lists[`COMPLEX_DROPPABLE_AREA_${did}`].map(({ content, id }, idx) => ( + + + {content} + + + ))} + + + )} + + ))} + + + + ); +}; diff --git a/src-docs/src/views/drag_and_drop/drag_and_drop_custom_handle.js b/src-docs/src/views/drag_and_drop/drag_and_drop_custom_handle.js new file mode 100644 index 00000000000..d0086cf55ad --- /dev/null +++ b/src-docs/src/views/drag_and_drop/drag_and_drop_custom_handle.js @@ -0,0 +1,53 @@ +import React, { useState } from 'react'; +import { + EuiDragDropContext, + EuiDraggable, + EuiDroppable, + EuiFlexGroup, + EuiFlexItem, + EuiIcon, + EuiPanel +} from '../../../../src/components'; + +import { reorder } from '../../../../src/components/drag_and_drop'; + +import { makeList } from './helper'; + +export default () => { + const [list, setList] = useState(makeList(3)); + const onDragEnd = ({ source, destination }) => { + if (source && destination) { + const items = reorder( + list, + source.index, + destination.index + ); + + setList(items); + } + }; + return ( + + + {list.map(({ content, id }, idx) => ( + + {(provided) => ( + + + +
+ +
+
+ + {content} + +
+
+ )} +
+ ))} +
+
+ ); +}; diff --git a/src-docs/src/views/drag_and_drop/drag_and_drop_disable_blocking.js b/src-docs/src/views/drag_and_drop/drag_and_drop_disable_blocking.js new file mode 100644 index 00000000000..0fd54c5ed7e --- /dev/null +++ b/src-docs/src/views/drag_and_drop/drag_and_drop_disable_blocking.js @@ -0,0 +1,39 @@ +import React, { useState } from 'react'; +import { + EuiButton, + EuiDragDropContext, + EuiDraggable, + EuiDroppable +} from '../../../../src/components'; + +import { reorder } from '../../../../src/components/drag_and_drop'; + +import { makeList } from './helper'; + +export default () => { + const [list, setList] = useState(makeList(3)); + const onDragEnd = ({ source, destination }) => { + if (source && destination) { + const items = reorder( + list, + source.index, + destination.index + ); + + setList(items); + } + }; + return ( + + + {list.map(({ content, id }, idx) => ( + + alert(`${content} clicked!`)}> + {content} + + + ))} + + + ); +}; diff --git a/src-docs/src/views/drag_and_drop/drag_and_drop_example.js b/src-docs/src/views/drag_and_drop/drag_and_drop_example.js new file mode 100644 index 00000000000..bef1d0ddb6e --- /dev/null +++ b/src-docs/src/views/drag_and_drop/drag_and_drop_example.js @@ -0,0 +1,332 @@ +import React from 'react'; +import { GuideSectionTypes } from '../../components'; +import { + EuiCallOut, + EuiCode, + EuiDragDropContext, + EuiDraggable, + EuiDroppable, + EuiLink, + EuiSpacer, + EuiText +} from '../../../../src/components'; +import { renderToHtml } from '../../services'; + +import DragAndDropBare from './drag_and_drop_bare'; +const dragAndDropBareSource = require('!!raw-loader!./drag_and_drop_bare'); +const dragAndDropBareHtml = renderToHtml(DragAndDropBare); + +import DragAndDrop from './drag_and_drop'; +const dragAndDropSource = require('!!raw-loader!./drag_and_drop'); +const dragAndDropHtml = renderToHtml(DragAndDrop); + +import DragAndDropCustomHandle from './drag_and_drop_custom_handle'; +const dragAndDropCustomHandleSource = require('!!raw-loader!./drag_and_drop_custom_handle'); +const dragAndDropCustomHandleHtml = renderToHtml(DragAndDropCustomHandle); + +import DragAndDropDisableBlocking from './drag_and_drop_disable_blocking'; +const dragAndDropDisableBlockingSource = require('!!raw-loader!./drag_and_drop_disable_blocking'); +const dragAndDropDisableBlockingHtml = renderToHtml(DragAndDropDisableBlocking); + +import DragAndDropMoveLists from './drag_and_drop_move_lists'; +const dragAndDropMoveListsSource = require('!!raw-loader!./drag_and_drop_move_lists'); +const dragAndDropMoveListsHtml = renderToHtml(DragAndDropMoveLists); + +import DragAndDropTypes from './drag_and_drop_types'; +const dragAndDropTypesSource = require('!!raw-loader!./drag_and_drop_types'); +const dragAndDropTypesHtml = renderToHtml(DragAndDropTypes); + +import DragAndDropClone from './drag_and_drop_clone'; +const dragAndDropCloneSource = require('!!raw-loader!./drag_and_drop_clone'); +const dragAndDropCloneHtml = renderToHtml(DragAndDropClone); + +import DragAndDropComplex from './drag_and_drop_complex'; +const dragAndDropComplexSource = require('!!raw-loader!./drag_and_drop_complex'); +const dragAndDropComplexHtml = renderToHtml(DragAndDropComplex); + +export const DragAndDropExample = { + title: 'Drag And Drop', + beta: true, + intro: ( + + +

+ An extension of react-beautiful-dnd with a compatible API and built-in style opinions. + Functionality results from 3 components working together: +

+ +
+ + + + +

+ Drag and drop interfaces are not well-adapted to many cases, and may be less suitable than other form types for data operations. + For instance, drag and drop interaction relies heavily on spatial orientation that may not be entirelty valid to all + users (e.g., screen readers as the sole source of information). Similarly, users navigating by keyboard may not be afforded + nuanced, dual-axis drag item manipulation. +

+

+ {`EUI (largely due to the great work already in react-beautiful-dnd) has and will continue to ensure accessibility where possible. + With that in mind, keep your users' working context in mind.`} +

+
+ + +
+ ), + sections: [ + { + title: 'Just the facts', + source: [ + { + type: GuideSectionTypes.JS, + code: dragAndDropBareSource + }, + { + type: GuideSectionTypes.HTML, + code: dragAndDropBareHtml + } + ], + text: ( + +

+ EuiDraggable makes very few assumptions about what content it contains. + To give affordance to draggable elements and to ensure a consistent experience, child elements + must be able to accept a border and drop shadow (automatically applied via CSS). No other style opinions are applied, however. +

+

+ Similarly, EuiDroppable must accept a background color overlay (automatically applied via CSS), + but has no other restrictions. +

+

+ All EuiDragDropContext elements are discrete and isolated; EuiDroppables and + EuiDraggables cannot be shared/transferred between instances. Also, EuiDragDropContexts + cannot be nested. It is recommended that a single, high-level EuiDragDropContext is used and + EuiDroppables account for categorical and functional separation (see later examples). +

+

+ EuiDragDropContext handles all eventing but makes no assumptions about the result of a drop event. + As such, the following event handlers are available: +

+ +

+ EUI also provides methods for helping to deal to common action types: +

+ +
+ ), + props: { EuiDragDropContext, EuiDraggable, EuiDroppable }, + demo: + }, + { + title: 'Simple item reorder', + source: [ + { + type: GuideSectionTypes.JS, + code: dragAndDropSource + }, + { + type: GuideSectionTypes.HTML, + code: dragAndDropHtml + } + ], + text: ( + +

+ The simplest case, demonstrating a single EuiDroppable with reorder behavior. +

+

+ Notice the ability to change rendered content based on dragging state. + EuiDraggable children is a render prop that mush return a ReactElement. + The snapshot parameter on that function has state data that can be used to alter appearance or behavior + (e.g., isDragging). +

+
+ ), + props: { EuiDragDropContext, EuiDraggable, EuiDroppable }, + demo: + }, + { + title: 'Custom drag handle', + source: [ + { + type: GuideSectionTypes.JS, + code: dragAndDropCustomHandleSource + }, + { + type: GuideSectionTypes.HTML, + code: dragAndDropCustomHandleHtml + } + ], + text: ( + +

+ By default the entire element surface can initiate a drag. + To specify a certain element within as the handle, set + customDragHandle=true on the EuiDraggable. +

+

+ The provided parameter on the EuiDraggable children render prop has all + data required for functionality. Along with the customDragHandle flag, + provided.dragHandleProps needs to be added to the intended handle element. +

+
+ ), + demo: + }, + { + title: 'Interactive elements', + source: [ + { + type: GuideSectionTypes.JS, + code: dragAndDropDisableBlockingSource + }, + { + type: GuideSectionTypes.HTML, + code: dragAndDropDisableBlockingHtml + } + ], + text: ( + +

+ EuiDraggable elements can contain interactive elements such as buttons and form fields by adding the + disableInteractiveElementBlocking prop. This will keep drag functionality while also enabling click, etc., + events on the interactive child elements. +

+
+ ), + demo: + }, + { + title: 'Move between lists', + source: [ + { + type: GuideSectionTypes.JS, + code: dragAndDropMoveListsSource + }, + { + type: GuideSectionTypes.HTML, + code: dragAndDropMoveListsHtml + } + ], + text: ( + +

+ By default, all EuiDroppable elements are of the same type and will + accept EuiDraggable elements from others in the same EuiDragDropContext. +

+

+ The EUI move method is demonstrated in this example. +

+
+ ), + demo: + }, + { + title: 'Distinguish droppable areas by type', + source: [ + { + type: GuideSectionTypes.JS, + code: dragAndDropTypesSource + }, + { + type: GuideSectionTypes.HTML, + code: dragAndDropTypesHtml + } + ], + text: ( + +

+ Setting the type prop on an EuiDroppable element will ensure that it will only accept + EuiDraggable elements from the same type of EuiDroppable. +

+

+ Notice that the enabled, compatible EuiDroppable elements have a visual change that indicates they can accept + the actively moving/focused EuiDraggable element. +

+
+ ), + demo: + }, + { + title: 'Copyable items', + source: [ + { + type: GuideSectionTypes.JS, + code: dragAndDropCloneSource + }, + { + type: GuideSectionTypes.HTML, + code: dragAndDropCloneHtml + } + ], + text: ( + +

+ For cases where collections of EuiDraggable elements are static or can be used in multiple places + set cloneDraggables=true on the parent EuiDroppable. The EuiDroppable + becomes disabled (does not accept new EuiDraggable elements) + in this scenario to avoid mixed content intentions. +

+

+ The EUI copy method is available and demonstrated in the example below. Note that the data point used as + draggableId in EuiDraggable must change to allow for real duplication. +

+

+ isRemovable is used in the example for cloned items. This API is likely to change, but currently provides + the visual changes with drop-to-remove interactions. +

+
+ ), + demo: + }, { + title: 'We have fun', + source: [ + { + type: GuideSectionTypes.JS, + code: dragAndDropComplexSource + }, + { + type: GuideSectionTypes.HTML, + code: dragAndDropComplexHtml + } + ], + text: ( + +

+ EuiDraggables in EuiDroppables, EuiDroppables in + EuiDraggables, custom drag handles, horizontal movement, vertical movement, flexbox, + EuiPanel Inception, you name it. +

+
+ ), + demo: + } + ] +}; diff --git a/src-docs/src/views/drag_and_drop/drag_and_drop_move_lists.js b/src-docs/src/views/drag_and_drop/drag_and_drop_move_lists.js new file mode 100644 index 00000000000..65f427501d3 --- /dev/null +++ b/src-docs/src/views/drag_and_drop/drag_and_drop_move_lists.js @@ -0,0 +1,98 @@ +import React, { useState } from 'react'; +import { + EuiDragDropContext, + EuiFlexGroup, + EuiFlexItem, + EuiDraggable, + EuiDroppable, + EuiIcon, + EuiPanel +} from '../../../../src/components'; + +import { + move, + reorder +} from '../../../../src/components/drag_and_drop'; + +import { makeList } from './helper'; + +export default () => { + const [list1, setList1] = useState(makeList(3)); + const [list2, setList2] = useState(makeList(3, 4)); + const onDragEnd = ({ source, destination }) => { + const lists = { DROPPABLE_AREA_1: list1, DROPPABLE_AREA_2: list2 }; + const actions = { DROPPABLE_AREA_1: setList1, DROPPABLE_AREA_2: setList2 }; + if (source && destination) { + if (source.droppableId === destination.droppableId) { + const items = reorder( + lists[destination.droppableId], + source.index, + destination.index + ); + + actions[destination.droppableId](items); + } else { + const sourceId = source.droppableId; + const destinationId = destination.droppableId; + const result = move( + lists[sourceId], + lists[destinationId], + source, + destination + ); + + actions[sourceId](result[sourceId]); + actions[destinationId](result[destinationId]); + } + + } + }; + return ( + + + + + + {list1.length > 0 ? ( + list1.map(({ content, id }, idx) => ( + + {(provided, state) => ( + + {content}{state.isDragging && ' ✨'} + + )} + + )) + ) : ( + + + + )} + + + + + + + {list2.length > 0 ? ( + list2.map(({ content, id }, idx) => ( + + {(provided, state) => ( + + {content}{state.isDragging && ' ✨'} + + )} + + )) + ) : ( + + + + )} + + + + + + ); +}; diff --git a/src-docs/src/views/drag_and_drop/drag_and_drop_types.js b/src-docs/src/views/drag_and_drop/drag_and_drop_types.js new file mode 100644 index 00000000000..ddbfedcd0e6 --- /dev/null +++ b/src-docs/src/views/drag_and_drop/drag_and_drop_types.js @@ -0,0 +1,101 @@ +import React, { useState } from 'react'; +import { + EuiDragDropContext, + EuiFlexGroup, + EuiFlexItem, + EuiDraggable, + EuiDroppable, + EuiPanel +} from '../../../../src/components'; + +import { + move, + reorder +} from '../../../../src/components/drag_and_drop'; + +import { makeList } from './helper'; + +export default () => { + const [list1, setList1] = useState(makeList(3)); + const [list2, setList2] = useState(makeList(3, 4)); + const [list3, setList3] = useState(makeList(3, 7)); + const onDragEnd = ({ source, destination }) => { + const lists = { DROPPABLE_AREA_TYPE_1: list1, DROPPABLE_AREA_TYPE_2: list2, DROPPABLE_AREA_TYPE_3: list3 }; + const actions = { DROPPABLE_AREA_TYPE_1: setList1, DROPPABLE_AREA_TYPE_2: setList2, DROPPABLE_AREA_TYPE_3: setList3 }; + if (source && destination) { + if (source.droppableId === destination.droppableId) { + const items = reorder( + lists[destination.droppableId], + source.index, + destination.index + ); + + actions[destination.droppableId](items); + } else { + const sourceId = source.droppableId; + const destinationId = destination.droppableId; + const result = move( + lists[sourceId], + lists[destinationId], + source, + destination + ); + + actions[sourceId](result[sourceId]); + actions[destinationId](result[destinationId]); + } + + } + }; + return ( + + + + + + {list1.map(({ content, id }, idx) => ( + + {(provided, state) => ( + + {content}{state.isDragging && ' ✨'} + + )} + + ))} + + + + + + + {list2.map(({ content, id }, idx) => ( + + {(provided, state) => ( + + {content}{state.isDragging && ' ✨'} + + )} + + ))} + + + + + + + {list3.map(({ content, id }, idx) => ( + + {(provided, state) => ( + + {content}{state.isDragging && ' ✨'} + + )} + + ))} + + + + + + ); +}; diff --git a/src-docs/src/views/drag_and_drop/helper.js b/src-docs/src/views/drag_and_drop/helper.js new file mode 100644 index 00000000000..2915968e3c8 --- /dev/null +++ b/src-docs/src/views/drag_and_drop/helper.js @@ -0,0 +1,10 @@ +import { htmlIdGenerator } from '../../../../src/services'; + +export const makeId = htmlIdGenerator(); + +export const makeList = (number, start = 1) => Array.from({ length: number }, (v, k) => k + start).map(el => { + return { + content: `Item ${el}`, + id: makeId() + }; +}); diff --git a/src/components/drag_and_drop/__snapshots__/drag_drop_context.test.tsx.snap b/src/components/drag_and_drop/__snapshots__/drag_drop_context.test.tsx.snap new file mode 100644 index 00000000000..0cebb4a6dc4 --- /dev/null +++ b/src/components/drag_and_drop/__snapshots__/drag_drop_context.test.tsx.snap @@ -0,0 +1,3 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`EuiDragDropContext is rendered 1`] = `
`; diff --git a/src/components/drag_and_drop/__snapshots__/draggable.test.tsx.snap b/src/components/drag_and_drop/__snapshots__/draggable.test.tsx.snap new file mode 100644 index 00000000000..6ca5b4cb5db --- /dev/null +++ b/src/components/drag_and_drop/__snapshots__/draggable.test.tsx.snap @@ -0,0 +1,55 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`EuiDraggable can be given ReactElement children 1`] = ` +
+
+
+ Hello +
+
+
+
+`; + +exports[`EuiDraggable is rendered 1`] = ` +
+
+
+ Hello +
+
+
+
+`; diff --git a/src/components/drag_and_drop/__snapshots__/droppable.test.tsx.snap b/src/components/drag_and_drop/__snapshots__/droppable.test.tsx.snap new file mode 100644 index 00000000000..daf52206633 --- /dev/null +++ b/src/components/drag_and_drop/__snapshots__/droppable.test.tsx.snap @@ -0,0 +1,27 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`EuiDroppable can be given ReactElement children 1`] = ` +
+
+
+
+`; + +exports[`EuiDroppable is rendered 1`] = ` +
+
+
+
+`; diff --git a/src/components/drag_and_drop/_draggable.scss b/src/components/drag_and_drop/_draggable.scss new file mode 100644 index 00000000000..6f7481654c6 --- /dev/null +++ b/src/components/drag_and_drop/_draggable.scss @@ -0,0 +1,43 @@ +// sass-lint:disable no-empty-rulesets + +.euiDraggable { + &.euiDraggable--isDragging { + // Overriding inline styles on JS-inserted HTML elements + z-index: $euiZLevel9 !important; // sass-lint:disable-line no-important + } + + &.euiDraggable--hasClone:not(.euiDraggable--isDragging) { + // Overriding inline styles on JS-inserted HTML elements + transform: none !important; // sass-lint:disable-line no-important + } + + &.euiDraggable--withoutDropAnimation { + // Overriding inline styles on JS-inserted HTML elements + transition-duration: .001s !important; // sass-lint:disable-line no-important + } + + &:focus > .euiDraggable__item, + &.euiDraggable--hasCustomDragHandle > .euiDraggable__item [data-react-beautiful-dnd-drag-handle]:focus { + @include euiFocusRing; + } + + .euiDraggable__item { + &.euiDraggable__item--isDisabled { + cursor: not-allowed; + } + + &.euiDraggable__item--isDragging { + // TODO: Resolve below + // Commenting this out for now, + // I'm thinking about adding an optional prop to auto-add these styles versus always having them + // @include euiBottomShadow; + // @include euiFocusRing; + } + } +} + +@each $size, $spacing in $euiDragAndDropSpacing { + .euiDraggable--#{$size} { + padding: $spacing; + } +} diff --git a/src/components/drag_and_drop/_droppable.scss b/src/components/drag_and_drop/_droppable.scss new file mode 100644 index 00000000000..48db048d5d6 --- /dev/null +++ b/src/components/drag_and_drop/_droppable.scss @@ -0,0 +1,37 @@ +@import '../panel/mixins'; + +.euiDroppable { + $euiDroppableColor: $euiColorSecondary; + transition: background-color $euiAnimSpeedExtraSlow ease; + + &.euiDroppable--isDraggingType:not(.euiDroppable--isDisabled) { + background-color: transparentize($euiDroppableColor, .9); + + &.euiDroppable--isDraggingOver { + background-color: transparentize($euiDroppableColor, .75); + } + } + + .euiDroppable__placeholder { + &.euiDroppable__placeholder--isHidden { + // Overriding inline styles on JS-inserted HTML elements + display: none !important; // sass-lint:disable-line no-important + } + } +} + +@include euiPanel('euiDroppable--withPanel'); + +.euiDroppable--noGrow { + flex-grow: 0; +} + +.euiDroppable--grow { + flex-grow: 1; +} + +@each $size, $spacing in $euiDragAndDropSpacing { + .euiDroppable--#{$size} { + padding: $spacing; + } +} diff --git a/src/components/drag_and_drop/_index.scss b/src/components/drag_and_drop/_index.scss new file mode 100644 index 00000000000..e3ae587b279 --- /dev/null +++ b/src/components/drag_and_drop/_index.scss @@ -0,0 +1,3 @@ +@import 'variables'; +@import 'draggable'; +@import 'droppable'; diff --git a/src/components/drag_and_drop/_variables.scss b/src/components/drag_and_drop/_variables.scss new file mode 100644 index 00000000000..65dc0d6a8b0 --- /dev/null +++ b/src/components/drag_and_drop/_variables.scss @@ -0,0 +1,5 @@ +$euiDragAndDropSpacing: ( + s: ($euiSizeXS / 2), + m: ($euiSizeS / 2), + l: ($euiSize / 2), +); diff --git a/src/components/drag_and_drop/drag_drop_context.test.tsx b/src/components/drag_and_drop/drag_drop_context.test.tsx new file mode 100644 index 00000000000..069a9005c6b --- /dev/null +++ b/src/components/drag_and_drop/drag_drop_context.test.tsx @@ -0,0 +1,42 @@ +import React from 'react'; +import { render, mount } from 'enzyme'; + +import { findTestSubject } from '../../test'; +import { requiredProps } from '../../test/required_props'; + +import { EuiDragDropContext } from './'; +import { EuiDragDropContextContext } from './drag_drop_context'; + +describe('EuiDragDropContext', () => { + test('is rendered', () => { + const handler = jest.fn(); + const component = render( + +
+ + ); + + expect(component).toMatchSnapshot(); + }); + + describe('custom behavior', () => { + describe('isDraggingType', () => { + test('is set on proprietary context', () => { + const handler = jest.fn(); + const component = mount( + + + {value => ( +
+ {value.hasOwnProperty('isDraggingType') ? 'true' : 'false'} +
+ )} +
+
+ ); + + expect(findTestSubject(component, 'child').text()).toBe('true'); + }); + }); + }); +}); diff --git a/src/components/drag_and_drop/drag_drop_context.tsx b/src/components/drag_and_drop/drag_drop_context.tsx new file mode 100644 index 00000000000..b76cb6d87e3 --- /dev/null +++ b/src/components/drag_and_drop/drag_drop_context.tsx @@ -0,0 +1,58 @@ +import React, { FunctionComponent, useState } from 'react'; +import { + DragDropContext, + DragDropContextProps, + DropResult, + DragStart, + ResponderProvided, +} from 'react-beautiful-dnd'; + +// export interface EuiDragDropContextProps extends DragDropContextProps {} + +export const EuiDragDropContextContext = React.createContext({ + isDraggingType: null, +}); + +export const EuiDragDropContext: FunctionComponent = ({ + onBeforeDragStart, + onDragStart, + onDragUpdate, + onDragEnd, + children, + ...rest +}) => { + const [isDraggingType, setIsDraggingType] = useState(); + const euiOnDragStart = ( + start: DragStart, + provided: ResponderProvided + ): void => { + setIsDraggingType(start.type); + if (onDragStart) { + onDragStart(start, provided); + } + }; + const euiOnDragEnd = ( + result: DropResult, + provided: ResponderProvided + ): void => { + setIsDraggingType(null); + if (onDragEnd) { + onDragEnd(result, provided); + } + }; + return ( + + + {children} + + + ); +}; diff --git a/src/components/drag_and_drop/draggable.test.tsx b/src/components/drag_and_drop/draggable.test.tsx new file mode 100644 index 00000000000..efc4e029657 --- /dev/null +++ b/src/components/drag_and_drop/draggable.test.tsx @@ -0,0 +1,39 @@ +import React from 'react'; +import { render } from 'enzyme'; + +// import { findTestSubject } from '../../test'; +import { requiredProps } from '../../test/required_props'; + +import { EuiDragDropContext, EuiDraggable, EuiDroppable } from './'; + +describe('EuiDraggable', () => { + test('is rendered', () => { + const handler = jest.fn(); + const component = render( + + + + {() =>
Hello
} +
+
+
+ ); + + expect(component).toMatchSnapshot(); + }); + + test('can be given ReactElement children', () => { + const handler = jest.fn(); + const component = render( + + + +
Hello
+
+
+
+ ); + + expect(component).toMatchSnapshot(); + }); +}); diff --git a/src/components/drag_and_drop/draggable.tsx b/src/components/drag_and_drop/draggable.tsx new file mode 100644 index 00000000000..4537368ac64 --- /dev/null +++ b/src/components/drag_and_drop/draggable.tsx @@ -0,0 +1,109 @@ +import React, { + CSSProperties, + Fragment, + FunctionComponent, + ReactElement, + cloneElement, + useContext, +} from 'react'; +import { Draggable, DraggableProps } from 'react-beautiful-dnd'; +import classNames from 'classnames'; +import { CommonProps, Omit, keysOf } from '../common'; +import { EuiDroppableContext } from './droppable'; + +const spacingToClassNameMap = { + none: null, + s: 'euiDraggable--s', + m: 'euiDraggable--m', + l: 'euiDraggable--l', +}; + +export const SPACING = keysOf(spacingToClassNameMap); +export type EuiDraggableSpacing = keyof typeof spacingToClassNameMap; + +export interface EuiDraggableProps + extends CommonProps, + Omit { + children: ReactElement | DraggableProps['children']; + className?: string; + /** + * Whether the `children` will provide and set up its own drag handle + */ + customDragHandle?: boolean; + /** + * Whether the item is currently in a position to be removed + */ + isRemovable?: boolean; + /** + * Adds padding to the draggable item + */ + spacing?: EuiDraggableSpacing; + style?: CSSProperties; +} + +export const EuiDraggable: FunctionComponent = ({ + customDragHandle = false, + draggableId, + isDragDisabled = false, + isRemovable = false, + index, + children, + className, + spacing = 'none', + style, + ...rest +}) => { + const { cloneItems } = useContext(EuiDroppableContext); + + return ( + + {(provided, snapshot) => { + const classes = classNames( + 'euiDraggable', + { + 'euiDraggable--hasClone': cloneItems, + 'euiDraggable--hasCustomDragHandle': customDragHandle, + 'euiDraggable--isDragging': snapshot.isDragging, + 'euiDraggable--withoutDropAnimation': isRemovable, + }, + spacingToClassNameMap[spacing], + className + ); + const childClasses = classNames('euiDraggable__item', { + 'euiDraggable__item--hasCustomDragHandle': customDragHandle, + 'euiDraggable__item--isDisabled': isDragDisabled, + 'euiDraggable__item--isDragging': snapshot.isDragging, + 'euiDraggable__item--isDropAnimating': snapshot.isDropAnimating, + }); + const DraggableElement = + typeof children === 'function' + ? children(provided, snapshot) + : (children as ReactElement); // as specified by `DraggableProps` + return ( + +
+ {cloneElement(DraggableElement, { + className: classNames( + DraggableElement.props.className, + childClasses + ), + })} +
+ {cloneItems && + (snapshot.isDragging && ( +
+ {DraggableElement} +
+ ))} +
+ ); + }} +
+ ); +}; diff --git a/src/components/drag_and_drop/droppable.test.tsx b/src/components/drag_and_drop/droppable.test.tsx new file mode 100644 index 00000000000..e47b5e35a42 --- /dev/null +++ b/src/components/drag_and_drop/droppable.test.tsx @@ -0,0 +1,61 @@ +import React from 'react'; +import { render, mount } from 'enzyme'; + +import { findTestSubject } from '../../test'; +import { requiredProps } from '../../test/required_props'; + +import { EuiDragDropContext, EuiDroppable } from './'; +import { EuiDroppableContext } from './droppable'; + +describe('EuiDroppable', () => { + test('is rendered', () => { + const handler = jest.fn(); + const component = render( + + {() =>
} + + ); + + expect(component).toMatchSnapshot(); + }); + + test('can be given ReactElement children', () => { + const handler = jest.fn(); + const component = render( + + +
+ + + ); + + expect(component).toMatchSnapshot(); + }); + + describe('custom behavior', () => { + describe('cloneDraggables', () => { + const handler = jest.fn(); + const component = mount( + + + + {({ cloneItems }) => ( +
+ {cloneItems ? 'true' : 'false'} +
+ )} +
+
+
+ ); + + test('sets `cloneItems` on proprietary context', () => { + expect(findTestSubject(component, 'child').text()).toBe('true'); + }); + + test('sets `isDropDisabled`', () => { + expect(component.find('.euiDroppable--isDisabled').length).toBe(1); + }); + }); + }); +}); diff --git a/src/components/drag_and_drop/droppable.tsx b/src/components/drag_and_drop/droppable.tsx new file mode 100644 index 00000000000..35fc0b101b3 --- /dev/null +++ b/src/components/drag_and_drop/droppable.tsx @@ -0,0 +1,113 @@ +import React, { + CSSProperties, + FunctionComponent, + ReactElement, + useContext, +} from 'react'; +import { Droppable, DroppableProps } from 'react-beautiful-dnd'; +import classNames from 'classnames'; +import { CommonProps, Omit, keysOf } from '../common'; +import { EuiDragDropContextContext } from './drag_drop_context'; + +const spacingToClassNameMap = { + none: null, + s: 'euiDroppable--s', + m: 'euiDroppable--m', + l: 'euiDroppable--l', +}; + +export const SPACING = keysOf(spacingToClassNameMap); +export type EuiDroppableSpacing = keyof typeof spacingToClassNameMap; + +export interface EuiDroppableProps + extends CommonProps, + Omit { + children: ReactElement | DroppableProps['children']; + className?: string; + /** + * Makes its items immutable. Dragging creates cloned items that can be dropped elsewhere. + */ + cloneDraggables?: boolean; + style?: CSSProperties; + /** + * Adds padding to the droppable area + */ + spacing?: EuiDroppableSpacing; + /** + * Adds an EuiPanel style to the droppable area + */ + withPanel?: boolean; + /** + * Allow the panel to flex-grow? + */ + grow?: boolean; +} + +export const EuiDroppableContext = React.createContext({ + cloneItems: false, +}); + +export const EuiDroppable: FunctionComponent = ({ + droppableId, + direction, + isDropDisabled = false, + children, + className, + cloneDraggables = false, + spacing = 'none', + style, + type = 'EUI_DEFAULT', + withPanel = false, + grow = false, + ...rest +}) => { + const { isDraggingType } = useContext(EuiDragDropContextContext); + const dropIsDisabled: boolean = cloneDraggables ? true : isDropDisabled; + return ( + + {(provided, snapshot) => { + const classes = classNames( + 'euiDroppable', + { + 'euiDroppable--isDisabled': dropIsDisabled, + 'euiDroppable--isDraggingOver': snapshot.isDraggingOver, + 'euiDroppable--isDraggingType': isDraggingType === type, + 'euiDroppable--withPanel': withPanel, + 'euiDroppable--grow': grow, + 'euiDroppable--noGrow': !grow, + }, + spacingToClassNameMap[spacing], + className + ); + const placeholderClasses = classNames('euiDroppable__placeholder', { + 'euiDroppable__placeholder--isHidden': cloneDraggables, + }); + const DroppableElement = + typeof children === 'function' + ? children(provided, snapshot) + : children; + return ( +
+ + {DroppableElement} + +
{provided.placeholder}
+
+ ); + }} +
+ ); +}; diff --git a/src/components/drag_and_drop/index.ts b/src/components/drag_and_drop/index.ts new file mode 100644 index 00000000000..8ca9763f8fb --- /dev/null +++ b/src/components/drag_and_drop/index.ts @@ -0,0 +1,4 @@ +export { EuiDragDropContext } from './drag_drop_context'; +export { EuiDraggable } from './draggable'; +export { EuiDroppable } from './droppable'; +export { copy, move, reorder } from './services'; diff --git a/src/components/drag_and_drop/services.ts b/src/components/drag_and_drop/services.ts new file mode 100644 index 00000000000..a726609e985 --- /dev/null +++ b/src/components/drag_and_drop/services.ts @@ -0,0 +1,63 @@ +import { DraggableLocation } from 'react-beautiful-dnd'; + +interface DropResult { + [droppableId: string]: any[]; +} + +export const reorder = ( + list: [], + startIndex: number, + endIndex: number +): Array<{}> => { + const result = Array.from(list); + const [removed] = result.splice(startIndex, 1); + result.splice(endIndex, 0, removed); + + return result; +}; + +export const move = ( + sourceList: any[], + destinationList: any[], + dropResultSource: DraggableLocation, + dropResultDestination: DraggableLocation +): DropResult => { + const sourceClone = [...sourceList]; + const destClone = [...destinationList]; + const [removed] = sourceClone.splice(dropResultSource.index, 1); + + destClone.splice(dropResultDestination.index, 0, removed); + + return { + [dropResultSource.droppableId]: sourceClone, + [dropResultDestination.droppableId]: destClone, + }; +}; + +export const copy = ( + sourceList: any[], + destinationList: any[], + dropResultSource: DraggableLocation, + dropResultDestination: DraggableLocation, + /* Each EuiDraggable needs a unique ID, otherwise subsequent drag attempts on the to-be-copied + * element may result instead in dragging a previously created duplicate of that Draggable. + * `idModification` gives implementers better control over creating unique IDs when copying. + */ + idModification: { + property: string | number; + modifier: () => string | number; + } +): DropResult => { + const sourceClone = [...sourceList]; + const destClone = [...destinationList]; + + destClone.splice(dropResultDestination.index, 0, { + ...sourceList[dropResultSource.index], + [idModification.property]: idModification.modifier(), + }); + + return { + [dropResultSource.droppableId]: sourceClone, + [dropResultDestination.droppableId]: destClone, + }; +}; diff --git a/src/components/index.js b/src/components/index.js index d117aa42a7d..b93742d96cd 100644 --- a/src/components/index.js +++ b/src/components/index.js @@ -92,6 +92,12 @@ export { EuiDescriptionListDescription, } from './description_list'; +export { + EuiDragDropContext, + EuiDraggable, + EuiDroppable, +} from './drag_and_drop'; + export { EuiEmptyPrompt, } from './empty_prompt'; diff --git a/src/components/index.scss b/src/components/index.scss index acb5c1154ef..b76165c3ef2 100644 --- a/src/components/index.scss +++ b/src/components/index.scss @@ -18,6 +18,7 @@ @import 'context_menu/index'; @import 'date_picker/index'; @import 'description_list/index'; +@import 'drag_and_drop/index'; @import 'empty_prompt/index'; @import 'error_boundary/index'; @import 'expression/index'; @@ -58,4 +59,3 @@ @import 'tool_tip/index'; @import 'text/index'; @import 'series_chart/index'; - diff --git a/yarn.lock b/yarn.lock index ec0f7c6718e..4fccc6de3c2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -750,6 +750,14 @@ "@babel/helper-plugin-utils" "^7.0.0" "@babel/plugin-transform-typescript" "^7.1.0" +"@babel/runtime-corejs2@^7.3.4": + version "7.4.0" + resolved "https://registry.yarnpkg.com/@babel/runtime-corejs2/-/runtime-corejs2-7.4.0.tgz#72e94a5b59300482c2a334c06958021057c30da8" + integrity sha512-54PQxRRKLRYTrF89Z9tyjJ+uoxyn05Avt1Ou6UT1x5QWpdo1FPQ15i/lGL+ZLvE5yxzlTO7u2oE06VCU9IYijA== + dependencies: + core-js "^2.6.5" + regenerator-runtime "^0.13.2" + "@babel/runtime@^7.0.0": version "7.3.1" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.3.1.tgz#574b03e8e8a9898eaf4a872a92ea20b7846f6f2a" @@ -757,6 +765,13 @@ dependencies: regenerator-runtime "^0.12.0" +"@babel/runtime@^7.1.2": + version "7.3.4" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.3.4.tgz#73d12ba819e365fcf7fd152aed56d6df97d21c83" + integrity sha512-IvfvnMdSaLBateu0jfsYIpZTxAc2cKEXEMiezGGN75QcBcecDUKd3PgLAncT0oOgxKy8dd8hrJKj9MfzgfZd6g== + dependencies: + regenerator-runtime "^0.12.0" + "@babel/template@7.0.0-beta.36": version "7.0.0-beta.36" resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.0.0-beta.36.tgz#02e903de5d68bd7899bce3c5b5447e59529abb00" @@ -977,6 +992,13 @@ resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.5.9.tgz#f2d14df87b0739041bc53a7d75e3d77d726a3ec0" integrity sha512-Nha5b+jmBI271jdTMwrHiNXM+DvThjHOfyZtMX9kj/c/LUj2xiLHsG/1L3tJ8DjAoQN48cHwUwtqBotjyXaSdQ== +"@types/react-beautiful-dnd@^10.1.0": + version "10.1.0" + resolved "https://registry.yarnpkg.com/@types/react-beautiful-dnd/-/react-beautiful-dnd-10.1.0.tgz#6aa032519051a793154abaca2989d0f070a01680" + integrity sha512-f2qsG2vlydADYF535EmtWj5J71baIOplXcSdticKuQ3Ierf+jtbcOA1Ptb9H3lAcQgscJC64NlkVt6wTQlV7WQ== + dependencies: + "@types/react" "*" + "@types/react-dom@^16.8.2": version "16.8.2" resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-16.8.2.tgz#9bd7d33f908b243ff0692846ef36c81d4941ad12" @@ -3205,6 +3227,11 @@ core-js@^2.5.7: resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.5.7.tgz#f972608ff0cead68b841a16a932d0b183791814e" integrity sha512-RszJCAxg/PP6uzXVXL6BsxSXx/B05oJAQ2vkJRjyjrEcNVycaqOmNb5OTxZPE3xa5gwZduqza6L9JOCenh/Ecw== +core-js@^2.6.5: + version "2.6.5" + resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.6.5.tgz#44bc8d249e7fb2ff5d00e0341a7ffb94fbf67895" + integrity sha512-klh/kDpwX8hryYL14M9w/xei6vrv6sE8gTHDG7/T/+SEovB/G4ejwcfE/CBzO6Edsu+OETZMZ3wcX/EjUkrl5A== + core-util-is@1.0.2, core-util-is@~1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" @@ -3376,6 +3403,13 @@ crypto-random-string@^1.0.0: resolved "https://registry.yarnpkg.com/crypto-random-string/-/crypto-random-string-1.0.0.tgz#a230f64f568310e1498009940790ec99545bca7e" integrity sha1-ojD2T1aDEOFJgAmUB5DsmVRbyn4= +css-box-model@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/css-box-model/-/css-box-model-1.1.1.tgz#c9fd8e7a8b1d59d41d6812fd1765433f671b2ee0" + integrity sha512-ZxbuLFeAPEDb0wPbGfT7783Vb00MVAkvOlMKwr0kA2PD5EGxk6P3MAhedvVuyVJCWb54bb+6HQ7pdPYENf8AZw== + dependencies: + tiny-invariant "^1.0.3" + css-color-names@0.0.4, css-color-names@^0.0.4: version "0.0.4" resolved "https://registry.yarnpkg.com/css-color-names/-/css-color-names-0.0.4.tgz#808adc2e79cf84738069b646cb20ec27beb629e0" @@ -6353,6 +6387,13 @@ hoist-non-react-statics@^2.2.1: resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-2.3.1.tgz#343db84c6018c650778898240135a1420ee22ce0" integrity sha1-ND24TGAYxlB3iJgkATWhQg7iLOA= +hoist-non-react-statics@^3.1.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== + dependencies: + react-is "^16.7.0" + hosted-git-info@^2.1.4: version "2.5.0" resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.5.0.tgz#6d60e34b3abbc8313062c3b798ef8d901a07af3c" @@ -8471,7 +8512,7 @@ lolex@^2.2.0, lolex@^2.3.2: resolved "https://registry.yarnpkg.com/lolex/-/lolex-2.3.2.tgz#85f9450425103bf9e7a60668ea25dc43274ca807" integrity sha512-A5pN2tkFj7H0dGIAM6MFvHKMJcPnjZsOMvR7ujCjfgW5TbV6H9vb1PgxLtHvjqNZTHsUolz+6/WEO0N1xNx2ng== -loose-envify@^1.0.0, loose-envify@^1.3.1: +loose-envify@^1.0.0, loose-envify@^1.3.1, loose-envify@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf" integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q== @@ -8673,6 +8714,11 @@ mem@^4.0.0: mimic-fn "^1.0.0" p-is-promise "^1.1.0" +memoize-one@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-5.0.0.tgz#d55007dffefb8de7546659a1722a5d42e128286e" + integrity sha512-7g0+ejkOaI9w5x6LvQwmj68kUj6rxROywPSCqmclG/HBacmFnZqhVscQ8kovkn9FBCNJmOz6SY42+jnvZzDWdw== + memory-fs@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/memory-fs/-/memory-fs-0.2.0.tgz#f2bb25368bc121e391c2520de92969caee0a0290" @@ -11123,6 +11169,15 @@ prop-types@^15.5.8, prop-types@^15.6.2: loose-envify "^1.3.1" object-assign "^4.1.1" +prop-types@^15.6.1: + version "15.7.2" + resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.7.2.tgz#52c41e75b8c87e72b9d9360e0206b99dcbffa6c5" + integrity sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ== + dependencies: + loose-envify "^1.4.0" + object-assign "^4.1.1" + react-is "^16.8.1" + protochain@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/protochain/-/protochain-1.0.5.tgz#991c407e99de264aadf8f81504b5e7faf7bfa260" @@ -11273,6 +11328,11 @@ querystringify@^2.0.0: resolved "https://registry.yarnpkg.com/querystringify/-/querystringify-2.1.0.tgz#7ded8dfbf7879dcc60d0a644ac6754b283ad17ef" integrity sha512-sluvZZ1YiTLD5jsqZcDmFyV2EwToyXZBfpoVOmktMmW+VEnhgakFHnasVph65fOjGPTWN0Nw3+XQaSeMayr0kg== +raf-schd@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/raf-schd/-/raf-schd-4.0.0.tgz#9855756c5045ff4ed4516e14a47719387c3c907b" + integrity sha512-m7zq0JkIrECzw9mO5Zcq6jN4KayE34yoIS9hJoiZNXyOAT06PPA8PrR+WtJIeFW09YjUfNkMMN9lrmAt6BURCA== + raf@^3.1.0, raf@^3.4.0: version "3.4.0" resolved "https://registry.yarnpkg.com/raf/-/raf-3.4.0.tgz#a28876881b4bc2ca9117d4138163ddb80f781575" @@ -11376,6 +11436,20 @@ react-ace@^5.5.0: lodash.isequal "^4.1.1" prop-types "^15.5.8" +react-beautiful-dnd@^10.1.0: + version "10.1.0" + resolved "https://registry.yarnpkg.com/react-beautiful-dnd/-/react-beautiful-dnd-10.1.0.tgz#11b8958cfba5c54cb013dcff797efd15a3e0f333" + integrity sha512-oij2ZLIQ6SFmqy/MiXbuO8HUFHQ39gaPsTYj0Ewk0XwbLM32L+diVP0NXbEK7nhYv5lF2jviXGnda+BYMi6+nA== + dependencies: + "@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" + tiny-invariant "^1.0.3" + react-clientside-effect@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/react-clientside-effect/-/react-clientside-effect-1.2.0.tgz#db823695f75e9616a5e4dd6d908e5ea627fb2516" @@ -11435,6 +11509,11 @@ react-input-autosize@^2.2.1: dependencies: prop-types "^15.5.8" +react-is@^16.6.0, react-is@^16.8.1: + 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.7.0, react-is@^16.8.2: version "16.8.2" resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.8.2.tgz#09891d324cad1cb0c1f2d91f70a71a4bee34df0f" @@ -11445,6 +11524,11 @@ react-is@~16.3.0: resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.3.2.tgz#f4d3d0e2f5fbb6ac46450641eb2e25bf05d36b22" integrity sha512-ybEM7YOr4yBgFd6w8dJqwxegqZGJNBZl6U27HnGKuTZmDvVrD5quWOK/wAnMywiZzW+Qsk+l4X2c70+thp/A8Q== +react-lifecycles-compat@^3.0.0: + version "3.0.4" + resolved "https://registry.yarnpkg.com/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz#4f1a273afdfc8f3488a8c516bfda78f872352362" + integrity sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA== + react-motion@^0.5.2: version "0.5.2" resolved "https://registry.yarnpkg.com/react-motion/-/react-motion-0.5.2.tgz#0dd3a69e411316567927917c6626551ba0607316" @@ -11466,6 +11550,19 @@ react-redux@^5.0.6: loose-envify "^1.1.0" prop-types "^15.5.10" +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== + dependencies: + "@babel/runtime" "^7.1.2" + hoist-non-react-statics "^3.1.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" + react-router-redux@^4.0.8: version "4.0.8" resolved "https://registry.yarnpkg.com/react-router-redux/-/react-router-redux-4.0.8.tgz#227403596b5151e182377dab835b5d45f0f8054e" @@ -11803,6 +11900,14 @@ redux@^3.7.2: loose-envify "^1.1.0" symbol-observable "^1.0.3" +redux@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/redux/-/redux-4.0.1.tgz#436cae6cc40fbe4727689d7c8fae44808f1bfef5" + integrity sha512-R7bAtSkk7nY6O/OYMVR9RiBI+XghjF9rlbl5806HJbQph0LJVHZrU5oaO4q70eUKiqMRqm4y07KLTlMZ2BlVmg== + dependencies: + loose-envify "^1.4.0" + symbol-observable "^1.2.0" + regenerate-unicode-properties@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/regenerate-unicode-properties/-/regenerate-unicode-properties-7.0.0.tgz#107405afcc4a190ec5ed450ecaa00ed0cafa7a4c" @@ -11830,6 +11935,11 @@ regenerator-runtime@^0.12.0: resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.12.1.tgz#fa1a71544764c036f8c49b13a08b2594c9f8a0de" integrity sha512-odxIc1/vDlo4iZcfXqRYFj0vpXFNoGdKMAUieAlFYO6m/nl5e9KR/beGf41z4a1FI+aQgtjhuaSlDxQ0hmkrHg== +regenerator-runtime@^0.13.2: + version "0.13.2" + resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.2.tgz#32e59c9a6fb9b1a4aff09b4930ca2d4477343447" + integrity sha512-S/TQAZJO+D3m9xeN1WTI8dLKBBiRgXBlTJvbWjCThHWZj9EvHK70Ff50/tYj2J/fvBY6JtFVwRuazHN2E7M9BA== + regenerator-transform@^0.13.3: version "0.13.3" resolved "https://registry.yarnpkg.com/regenerator-transform/-/regenerator-transform-0.13.3.tgz#264bd9ff38a8ce24b06e0636496b2c856b57bcbb" @@ -13458,6 +13568,11 @@ symbol-observable@^1.0.3: resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-1.1.0.tgz#5c68fd8d54115d9dfb72a84720549222e8db9b32" integrity sha512-dQoid9tqQ+uotGhuTKEY11X4xhyYePVnqGSoSm3OGKh2E8LZ6RPULp1uXTctk33IeERlrRJYoVSBglsL05F5Uw== +symbol-observable@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-1.2.0.tgz#c22688aed4eab3cdc2dfeacbb561660560a00804" + integrity sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ== + symbol-tree@^3.2.2: version "3.2.2" resolved "https://registry.yarnpkg.com/symbol-tree/-/symbol-tree-3.2.2.tgz#ae27db38f660a7ae2e1c3b7d1bc290819b8519e6" @@ -13699,6 +13814,11 @@ timsort@^0.3.0: resolved "https://registry.yarnpkg.com/timsort/-/timsort-0.3.0.tgz#405411a8e7e6339fe64db9a234de11dc31e02bd4" integrity sha1-QFQRqOfmM5/mTbmiNN4R3DHgK9Q= +tiny-invariant@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/tiny-invariant/-/tiny-invariant-1.0.3.tgz#91efaaa0269ccb6271f0296aeedb05fc3e067b7a" + integrity sha512-ytQx8T4DL8PjlX53yYzcIC0WhIZbpR0p1qcYjw2pHu3w6UtgWwFJQ/02cnhOnBBhlFx/edUIfcagCaQSe3KMWg== + tinycolor2@^1.1.2, tinycolor2@^1.4.1: version "1.4.1" resolved "https://registry.yarnpkg.com/tinycolor2/-/tinycolor2-1.4.1.tgz#f4fad333447bc0b07d4dc8e9209d8f39a8ac77e8"