@hello-pangea/dnd
Version:
Beautiful and accessible drag and drop for lists with React
170 lines (142 loc) • 5.32 kB
text/typescript
import type { ContextId } from '../../types';
import { transitions } from '../../animation';
import * as attributes from '../data-attributes';
export interface Styles {
always: string;
dragging: string;
resting: string;
dropAnimating: string;
userCancel: string;
}
interface Rule {
selector: string;
styles: {
always?: string;
resting?: string;
dragging?: string;
dropAnimating?: string;
userCancel?: string;
};
}
const makeGetSelector = (context: string) => (attribute: string) =>
`[${attribute}="${context}"]`;
const getStyles = (rules: Rule[], property: keyof Rule['styles']): string =>
rules
.map((rule: Rule): string => {
const value = rule.styles[property];
if (!value) {
return '';
}
return `${rule.selector} { ${value} }`;
})
.join(' ');
const noPointerEvents = 'pointer-events: none;';
export default (contextId: ContextId): Styles => {
const getSelector = makeGetSelector(contextId);
// ## Drag handle styles
// -webkit-touch-callout
// A long press on anchors usually pops a content menu that has options for
// the link such as 'Open in new tab'. Because long press is used to start
// a drag we need to opt out of this behavior
// -webkit-tap-highlight-color
// Webkit based browsers add a grey overlay to anchors when they are active.
// We remove this tap overlay as it is confusing for users
// https://css-tricks.com/snippets/css/remove-gray-highlight-when-tapping-links-in-mobile-safari/
// touch-action: manipulation
// Avoid the *pull to refresh action* and *delayed anchor focus* on Android Chrome
// cursor: grab
// We apply this by default for an improved user experience. It is such a common default that we
// bake it right in. Consumers can opt out of this by adding a selector with higher specificity
// The cursor will not apply when pointer-events is set to none
// pointer-events: none
// this is used to prevent pointer events firing on draggables during a drag
// Reasons:
// 1. performance: it stops the other draggables from processing mouse events
// 2. scrolling: it allows the user to scroll through the current draggable
// to scroll the list behind
// 3.* function: it blocks other draggables from starting. This is not relied on though as there
// is a function on the context (canLift) which is a more robust way of controlling this
const dragHandle: Rule = (() => {
const grabCursor = `
cursor: -webkit-grab;
cursor: grab;
`;
return {
selector: getSelector(attributes.dragHandle.contextId),
styles: {
always: `
-webkit-touch-callout: none;
-webkit-tap-highlight-color: rgba(0,0,0,0);
touch-action: manipulation;
`,
resting: grabCursor,
dragging: noPointerEvents,
// it is fine for users to start dragging another item when a drop animation is occurring
dropAnimating: grabCursor,
// Not applying grab cursor during a user cancel as it is not possible for users to reorder
// items during a cancel
},
};
})();
// ## Draggable styles
// transition: transform
// This controls the animation of draggables that are moving out of the way
// The main draggable is controlled by react-motion.
const draggable: Rule = (() => {
const transition = `
transition: ${transitions.outOfTheWay};
`;
return {
selector: getSelector(attributes.draggable.contextId),
styles: {
dragging: transition,
dropAnimating: transition,
userCancel: transition,
},
};
})();
// ## Droppable styles
// overflow-anchor: none;
// Opting out of the browser feature which tries to maintain
// the scroll position when the DOM changes above the fold.
// This does not work well with reordering DOM nodes.
// When we drop a Draggable it already has the correct scroll applied.
const droppable: Rule = {
selector: getSelector(attributes.droppable.contextId),
styles: {
always: `overflow-anchor: none;`,
// need pointer events on the droppable to allow manual scrolling
},
};
// ## Body styles
// cursor: grab
// We apply this by default for an improved user experience. It is such a common default that we
// bake it right in. Consumers can opt out of this by adding a selector with higher specificity
// user-select: none
// This prevents the user from selecting text on the page while dragging
// overflow-anchor: none
// We are in control and aware of all of the window scrolls that occur
// we do not want the browser to have behaviors we do not expect
const body: Rule = {
selector: 'body',
styles: {
dragging: `
cursor: grabbing;
cursor: -webkit-grabbing;
user-select: none;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
overflow-anchor: none;
`,
},
};
const rules: Rule[] = [draggable, dragHandle, droppable, body];
return {
always: getStyles(rules, 'always'),
resting: getStyles(rules, 'resting'),
dragging: getStyles(rules, 'dragging'),
dropAnimating: getStyles(rules, 'dropAnimating'),
userCancel: getStyles(rules, 'userCancel'),
};
};