diff --git a/CHANGELOG.md b/CHANGELOG.md index a78f1f1d1..56a4a96ff 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,12 @@ The following is a curated list of changes in the Enact agate module, newest changes on the top. +## [unreleased] + +### Added + +- `agate/DropManager` component + ## [2.0.6] - 2023-06-08 No significant changes. diff --git a/DropManager/DropManager.js b/DropManager/DropManager.js index 1b2ddc739..0539da4ce 100644 --- a/DropManager/DropManager.js +++ b/DropManager/DropManager.js @@ -6,7 +6,7 @@ * @exports DropManager * @exports Draggable * @exports ResponsiveBox - * @private + * @public */ import hoc from '@enact/core/hoc'; @@ -22,27 +22,69 @@ import Rearrangeable from '../Rearrangeable'; import css from './DropManager.module.less'; -// https://stackoverflow.com/questions/9907419/how-to-get-a-key-in-a-javascript-object-by-its-value -// By: ZER0 - Mar 28 '12 at 12:51 const getKeyByValue = (obj, value) => Object.keys(obj).find(key => obj[key] === value); // Establish the base container shape, to be shared with all components as a consistent starting point. const containerShapePropTypes = PropTypes.shape({ + /** + * Specifies if the draggable container takes all the available space up to all the edges of the screen. + * + * @type {Boolean} + * @default false + * @private + */ edge: PropTypes.bool, + + /** + * Defines the position of the draggable container relative to screen edges. + * + * @type {Object} + * @private + */ edges: PropTypes.shape({ bottom: PropTypes.bool, left: PropTypes.bool, right: PropTypes.bool, top: PropTypes.bool }), + + /** + * Specifies if the draggable container takes all the available space between the horizontal screen edges. + * + * @type {Boolean} + * @default false + * @private + */ horizontalEdge: PropTypes.bool, + + /** + * The layout orientation of the components inside a draggable container. + * + * @type {String} + * @private + */ orientation: PropTypes.string, + + /** + * The size of the draggable container relative to the screen. + * + * @type {Object} + * @private + */ size: PropTypes.shape({ relative: PropTypes.string // Relative size: small, medium, large, full // proposed - height: PropTypes.number, // proposed - width: PropTypes.number }), + + /** + * Specifies if the draggable container takes all the available space between the vertical screen edges. + * + * @type {Boolean} + * @default false + * @private + */ verticalEdge: PropTypes.bool }); @@ -97,12 +139,12 @@ const fallbackArrangementProp = 'arrangement'; const DropManagerContext = createContext(defaultContainerShape); /** - * TBD. + * Applies Agate specific behaviors to {@link agate/DropManager.DropManagerBase|DropManagerBase} components. * * @class DropManager * @hoc * @memberof agate/DropManager - * @private + * @public */ const DropManager = hoc(defaultConfig, (configHoc, Wrapped) => { const ArrangementState = Changeable({prop: configHoc.arrangementProp || fallbackArrangementProp}); @@ -112,7 +154,7 @@ const DropManager = hoc(defaultConfig, (configHoc, Wrapped) => { static propTypes = /** @lends agate/DropManager.DropManager.prototype */ { /** - * The ready-state to indicate that the contents are allowed to be rearranged + * The ready-state to indicate that the contents are allowed to be rearranged. * * @type {Boolean} * @default false @@ -225,8 +267,6 @@ const DropManager = hoc(defaultConfig, (configHoc, Wrapped) => { this.dragOriginNode.dataset.slot = dragDestination; dragDropNode.dataset.slot = dragOrigin; - // console.log('from:', dragOrigin, 'to:', dragDestination); - // We successfully completed the drag, blank-out the node. this.dragOriginNode = null; @@ -328,12 +368,6 @@ const DropManager = hoc(defaultConfig, (configHoc, Wrapped) => { this.setState({dragging: false}); }; - // handleDragEnd = () => { - // if (this.props.onArrange) { - // this.props.onArrange({arrangement: this.state.arrangement}); - // } - // }; - render () { const {arrangement, arrangeable, className, ...rest} = {...this.props}; delete rest.onArrange; @@ -343,7 +377,7 @@ const DropManager = hoc(defaultConfig, (configHoc, Wrapped) => { if (configHoc.arrangingProp) rest[configHoc.arrangingProp] = this.state.dragging; if (arrangeable) { - // Add all of the necessary events, but only if we're in edit mode + // Add all the necessary events, but only if we're in edit mode rest.onDragStart = this.handleDragStart; rest.onDragEnter = this.handleDragEnter; rest.onDragLeave = this.handleDragLeave; @@ -352,7 +386,6 @@ const DropManager = hoc(defaultConfig, (configHoc, Wrapped) => { rest.onTouchStart = this.handleTouchStart; rest.onTouchMove = this.handleTouchMove; rest.onTouchEnd = this.handleTouchEnd; - // rest.onDragEnd = this.handleDragEnd; } return ( @@ -376,7 +409,6 @@ const DropManager = hoc(defaultConfig, (configHoc, Wrapped) => { return ArrangementState(DropManagerBase); }); - // Draw conclusions from supplied shape information and auto-set some values based on logical // conclusions to save time and improve consistency for consumers. const logicallyPopulateContainerShape = (cs) => { @@ -391,24 +423,51 @@ const logicallyPopulateContainerShape = (cs) => { // return cs; }; - const DraggableContainerContext = createContext(null); /** - * TBD. + * Draggable passes props to support the dragging feature in DropManager. * * @class Draggable * @memberof agate/DropManager * @ui - * @private + * @public */ const Draggable = (Wrapped) => kind({ name: 'Draggable', propTypes: /** @lends agate/DropManager.Draggable.prototype */ { + /** + * Defines the shape of the draggable container relative to the screen edges. + * + * @type {Object} + * @public + */ containerShape: containerShapePropTypes, + + /** + * Allows a container to be dragged. + * + * @type {Boolean} + * @default false + * @public + */ draggable: PropTypes.bool, + + /** + * The draggable container's name. + * + * @type {String} + * @public + */ name: PropTypes.string, + + /** + * Nodes to be inserted in a draggable container. + * + * @type {Node} + * @public + */ slot: PropTypes.string }, @@ -444,12 +503,12 @@ const Draggable = (Wrapped) => kind({ }); /** - * TBD. + * ResponsiveBox passes props to support the responsive layout feature in DropManager. * * @class ResponsiveBox * @memberof agate/DropManager * @ui - * @private + * @public */ const ResponsiveBox = (Wrapped) => { const Decorator = (props) => ( @@ -466,24 +525,16 @@ const ResponsiveBox = (Wrapped) => { return Decorator; }; -// -// -// DEV TO-DO LATER: -// -// Simplify draggable usage, maybe include a "hide with no content" prop -// -// Relocate data-slot junk to context child->parent relay of child identification information -// -// - // Consolidate usage pattern into a simple reusable piece /** - * TBD. + * Applies Agate specific behaviors to {@link agate/DropManager.DropManager|DropManager} components. * * @class Droppable * @hoc * @memberof agate/DropManager - * @private + * @mixes ui/Slottable.Slottable + * @mixes agate/Rearrangeable.Rearrangeable + * @public */ const Droppable = hoc((configHoc, Wrapped) => { const {arrangementProp, slots, ...rest} = configHoc; diff --git a/samples/sampler/stories/default/DropManager.js b/samples/sampler/stories/default/DropManager.js new file mode 100644 index 000000000..6007b7744 --- /dev/null +++ b/samples/sampler/stories/default/DropManager.js @@ -0,0 +1,100 @@ +import BodyText from '@enact/agate/BodyText'; +import Button from '@enact/agate/Button'; +import Droppable, {Draggable, DropManager, ResponsiveBox} from '@enact/agate/DropManager'; +import kind from '@enact/core/kind'; +import {mergeComponentMetadata} from '@enact/storybook-utils'; +import {action} from '@enact/storybook-utils/addons/actions'; +import {boolean, select} from '@enact/storybook-utils/addons/controls'; +import {Cell, Layout} from '@enact/ui/Layout'; +import ri from '@enact/ui/resolution'; +import PropTypes from 'prop-types'; + +DropManager.displayName = 'DropManager'; +const Config = mergeComponentMetadata('DropManager', Draggable, DropManager, Droppable, ResponsiveBox); + +const prop = { + orientation: { + 'landscape': 'horizontal', + 'portrait': 'vertical' + } +}; + +const DraggableCell = Draggable(Cell); + +const styles = { + top: {backgroundColor: 'pink', borderRadius: ri.scaleToRem(6), marginBottom: ri.scaleToRem(9), padding: ri.scaleToRem(9)}, + center: {backgroundColor: 'wheat', borderRadius: ri.scaleToRem(6), marginBottom: ri.scaleToRem(9), padding: ri.scaleToRem(9)}, + bottom: {backgroundColor: 'lightblue', borderRadius: ri.scaleToRem(6), marginBottom: ri.scaleToRem(9), padding: ri.scaleToRem(9)}, + text: {fontWeight: 'bold', marginBottom: 0} +}; + +const CustomLayoutBase = kind({ + name: 'CustomLayoutBase', + + propTypes: { + bottom: PropTypes.node, + center: PropTypes.node, + top: PropTypes.node + }, + + render: ({bottom, center, top, ...rest}) => { + return ( + + {top} + {center} + {bottom} + + ); + } +}); + +const CustomLayout = Droppable({slots: ['bottom', 'center', 'top']}, CustomLayoutBase); + +const ResponsiveLayout = ResponsiveBox(({containerShape, ...rest}) => { + const orientation = (containerShape.orientation === 'portrait') ? 'vertical' : 'horizontal'; + + return ( + + ); +}); + +export default { + title: 'Agate/DropManager', + component: 'DropManager' +}; + +export const _DropManager = (args) => ( + + + + Top container with responsive layout + Use arrangeable control to drag and drop containers. + + +
+ + Center container with {args['orientation']} layout + Use orientation control to change layout. + + +
+ + Bottom container with unresponsive layout + Bottom content remains vertically aligned no matter the slot's position. + +
+); + +boolean('arrangeable', _DropManager, Config); +select('orientation', _DropManager, prop.orientation, Config, 'vertical'); + +_DropManager.storyName = 'DropManager'; +_DropManager.parameters = { + info: { + text: 'Basic usage of DropManager' + } +};