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'), }; };