Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WRP-17880: Finish DropManager and make the component public #752

Open
wants to merge 11 commits into
base: develop
Choose a base branch
from
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
119 changes: 85 additions & 34 deletions DropManager/DropManager.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
* @exports DropManager
* @exports Draggable
* @exports ResponsiveBox
* @private
* @public
*/

import hoc from '@enact/core/hoc';
Expand All @@ -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
});

Expand Down Expand Up @@ -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});
Expand All @@ -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
Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -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 (
Expand All @@ -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) => {
Expand All @@ -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
},

Expand Down Expand Up @@ -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) => (
Expand All @@ -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;
Expand Down
100 changes: 100 additions & 0 deletions samples/sampler/stories/default/DropManager.js
Original file line number Diff line number Diff line change
@@ -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 (
<Layout {...rest} orientation="vertical">
<DraggableCell name="top" shrink style={styles.top}>{top}</DraggableCell>
<DraggableCell name="center" style={styles.center}>{center}</DraggableCell>
<DraggableCell name="bottom" shrink style={styles.bottom}>{bottom}</DraggableCell>
</Layout>
);
}
});

const CustomLayout = Droppable({slots: ['bottom', 'center', 'top']}, CustomLayoutBase);

const ResponsiveLayout = ResponsiveBox(({containerShape, ...rest}) => {
const orientation = (containerShape.orientation === 'portrait') ? 'vertical' : 'horizontal';

return (
<Layout orientation={orientation} {...rest} />
);
});

export default {
title: 'Agate/DropManager',
component: 'DropManager'
};

export const _DropManager = (args) => (
<CustomLayout
arrangeable={args['arrangeable']}
arrangement={{bottom: "bottom", center: "center", top: "top"}}
onArrange={action('onArrange')}
>
<top>
<ResponsiveLayout orientation={args['orientation']}>
<BodyText size="small" style={styles.text}>Top container with responsive layout</BodyText>
<BodyText size="small">Use <b>arrangeable</b> control to drag and drop containers.</BodyText>
</ResponsiveLayout>
</top>
<center>
<ResponsiveLayout orientation={args['orientation']}>
<BodyText size="small" style={styles.text}>Center container with {args['orientation']} layout</BodyText>
<BodyText size="small">Use <b>orientation</b> control to change layout.</BodyText>
<Button size="small" style={{width: 'fit-content'}}>Button</Button>
</ResponsiveLayout>
</center>
<bottom>
<BodyText size="small" style={styles.text}>Bottom container with unresponsive layout</BodyText>
<BodyText size="small">Bottom content remains vertically aligned no matter the slot&apos;s position.</BodyText>
</bottom>
</CustomLayout>
);

boolean('arrangeable', _DropManager, Config);
select('orientation', _DropManager, prop.orientation, Config, 'vertical');

_DropManager.storyName = 'DropManager';
_DropManager.parameters = {
info: {
text: 'Basic usage of DropManager'
}
};