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

Supporting use in shadow roots #2319

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 9 additions & 9 deletions .size-snapshot.json
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
{
"dist/react-beautiful-dnd.js": {
"bundled": 364998,
"minified": 133574,
"gzipped": 39437
"bundled": 366336,
"minified": 133967,
"gzipped": 39652
},
"dist/react-beautiful-dnd.min.js": {
"bundled": 306810,
"minified": 108365,
"gzipped": 31340
"bundled": 308197,
"minified": 108795,
"gzipped": 31547
},
"dist/react-beautiful-dnd.esm.js": {
"bundled": 240910,
"minified": 125371,
"gzipped": 32650,
"bundled": 242065,
"minified": 125885,
"gzipped": 32813,
"treeshaked": {
"rollup": {
"code": 21121,
Expand Down
1 change: 1 addition & 0 deletions docs/api/drag-drop-context.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ type Props = {|
- `nonce`: Used for strict content security policy setups. See our [content security policy guide](/docs/guides/content-security-policy.md)
- `sensors`: Used to pass in your own `sensor`s for a `<DragDropContext />`. See our [sensor api documentation](/docs/sensors/sensor-api.md)
- `enableDefaultSensors`: Whether or not the default sensors ([mouse](/docs/sensors/mouse.md), [keyboard](/docs/sensors/keyboard.md), and [touch](/docs/sensors/touch.md)) are enabled. You can also import them separately as `useMouseSensor`, `useKeyboardSensor`, or `useTouchSensor` and reuse just some of them via `sensors` prop. See our [sensor api documentation](/docs/sensors/sensor-api.md)
- `stylesInsertionPoint`: Specify the DOM node where to append styles. This is useful when used inside shadowRoots like web components. If not specified it will use document's head.

> See our [type guide](/docs/guides/types.md) for more details

Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@
"raf-stub": "3.0.0",
"react": "16.13.1",
"react-dom": "16.13.1",
"react-shadow-dom-retarget-events": "1.1.0",
"react-test-renderer": "16.13.1",
"react-virtualized": "9.21.2",
"react-window": "1.8.6",
Expand Down
2 changes: 1 addition & 1 deletion src/native-with-fallback.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ export function values<T>(map: Map<T>): T[] {
}

// Could also extend to pass index and list
type PredicateFn<T> = (value: T) => boolean;
export type PredicateFn<T> = (value: T) => boolean;

// TODO: swap order
export function findIndex<T>(
Expand Down
7 changes: 6 additions & 1 deletion src/view/drag-drop-context/app.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ export type Props = {|

// screen reader
dragHandleUsageInstructions: string,
stylesInsertionPoint?: ?HTMLElement,
|};

const createResponders = (props: Props): Responders => ({
Expand Down Expand Up @@ -107,7 +108,11 @@ export default function App(props: Props) {
contextId,
text: dragHandleUsageInstructions,
});
const styleMarshal: StyleMarshal = useStyleMarshal(contextId, nonce);
const styleMarshal: StyleMarshal = useStyleMarshal(
contextId,
nonce,
props.stylesInsertionPoint,
);

const lazyDispatch: (Action) => void = useCallback((action: Action): void => {
getStore(lazyStoreRef).dispatch(action);
Expand Down
3 changes: 3 additions & 0 deletions src/view/drag-drop-context/drag-drop-context.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ type Props = {|
// See our [sensor api](/docs/sensors/sensor-api.md)
sensors?: Sensor[],
enableDefaultSensors?: ?boolean,
// Allows customizing the element to add the stylesheets to e.g. when being used in a ShadowRoot
stylesInsertionPoint?: ?HTMLElement,
|};

// Reset any context that gets persisted across server side renders
Expand Down Expand Up @@ -51,6 +53,7 @@ export default function DragDropContext(props: Props) {
onDragStart={props.onDragStart}
onDragUpdate={props.onDragUpdate}
onDragEnd={props.onDragEnd}
stylesInsertionPoint={props.stylesInsertionPoint}
>
{props.children}
</App>
Expand Down
2 changes: 1 addition & 1 deletion src/view/draggable/get-style.js
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ function getDraggingStyle(dragging: DraggingMapProps): DraggingStyle {
function getSecondaryStyle(secondary: SecondaryMapProps): NotDraggingStyle {
return {
transform: transforms.moveTo(secondary.offset),
// transition style is applied in the head
// transition style is applied in the head or stylesInsertionPoint
transition: secondary.shouldAnimateDisplacement ? null : 'none',
};
}
Expand Down
2 changes: 1 addition & 1 deletion src/view/draggable/use-validation.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ export function useValidation(
// When not enabled there is no drag handle props
if (props.isEnabled) {
invariant(
findDragHandle(contextId, id),
findDragHandle(contextId, id, getRef()),
`${prefix(id)} Unable to find drag handle`,
);
}
Expand Down
20 changes: 9 additions & 11 deletions src/view/get-elements/find-drag-handle.js
Original file line number Diff line number Diff line change
@@ -1,26 +1,24 @@
// @flow
import { queryElements } from './query-elements';
import type { DraggableId, ContextId } from '../../types';
import { dragHandle as dragHandleAttr } from '../data-attributes';
import { warning } from '../../dev-warning';
import { find, toArray } from '../../native-with-fallback';
import isHtmlElement from '../is-type-of-element/is-html-element';

export default function findDragHandle(
contextId: ContextId,
draggableId: DraggableId,
ref: ?HTMLElement,
): ?HTMLElement {
// cannot create a selector with the draggable id as it might not be a valid attribute selector
const selector: string = `[${dragHandleAttr.contextId}="${contextId}"]`;
const possible: Element[] = toArray(document.querySelectorAll(selector));

if (!possible.length) {
warning(`Unable to find any drag handles in the context "${contextId}"`);
return null;
}

const handle: ?Element = find(possible, (el: Element): boolean => {
return el.getAttribute(dragHandleAttr.draggableId) === draggableId;
});
const handle: ?Element = queryElements(
ref,
selector,
(el: Element): boolean => {
return el.getAttribute(dragHandleAttr.draggableId) === draggableId;
},
);

if (!handle) {
warning(
Expand Down
15 changes: 9 additions & 6 deletions src/view/get-elements/find-draggable.js
Original file line number Diff line number Diff line change
@@ -1,21 +1,24 @@
// @flow
import { queryElements } from './query-elements';
import type { DraggableId, ContextId } from '../../types';
import * as attributes from '../data-attributes';
import { find, toArray } from '../../native-with-fallback';
import { warning } from '../../dev-warning';
import isHtmlElement from '../is-type-of-element/is-html-element';

export default function findDraggable(
contextId: ContextId,
draggableId: DraggableId,
ref: ?Node,
): ?HTMLElement {
// cannot create a selector with the draggable id as it might not be a valid attribute selector
const selector: string = `[${attributes.draggable.contextId}="${contextId}"]`;
const possible: Element[] = toArray(document.querySelectorAll(selector));

const draggable: ?Element = find(possible, (el: Element): boolean => {
return el.getAttribute(attributes.draggable.id) === draggableId;
});
const draggable: ?Element = queryElements(
ref,
selector,
(el: Element): boolean => {
return el.getAttribute(attributes.draggable.id) === draggableId;
},
);

if (!draggable) {
return null;
Expand Down
42 changes: 42 additions & 0 deletions src/view/get-elements/query-elements.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
// @flow
import { find, toArray } from '../../native-with-fallback';
import type { PredicateFn } from '../../native-with-fallback';

// declaring composedPath manually, seems it is not defined on Event yet
type EventWithComposedPath = Event & {
composedPath?: () => HTMLElement[],
};

export function getEventTarget(event: EventWithComposedPath): ?EventTarget {
const target = event.composedPath && event.composedPath()[0];
return target || event.target;
}

export function getEventTargetRoot(event: ?EventWithComposedPath): Node {
const source = event && event.composedPath && event.composedPath()[0];
const root = source && source.getRootNode();
return root || document;
}

export function queryElements(
ref: ?Node,
selector: string,
filterFn: PredicateFn<Element>,
): ?Element {
const rootNode: any = ref && ref.getRootNode();
const documentOrShadowRoot: ShadowRoot | Document =
rootNode && rootNode.querySelectorAll ? rootNode : document;
const possible = toArray(documentOrShadowRoot.querySelectorAll(selector));
const filtered = find(possible, filterFn);

// in case nothing was found in this document/shadowRoot we recursievly try the parent document(Fragment) given
// by the host property. This is needed in case the the draggable/droppable itself contains a shadow root
if (!filtered && documentOrShadowRoot.host) {
return queryElements(
((documentOrShadowRoot: any).host: ShadowRoot),
selector,
filterFn,
);
}
return filtered;
}
16 changes: 15 additions & 1 deletion src/view/use-sensor-marshal/closest.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,11 +40,25 @@ function closestPonyfill(el: ?Element, selector: string) {
return closestPonyfill(el.parentElement, selector);
}

export default function closest(el: Element, selector: string): ?Element {
function closestImpl(el: Element, selector: string): ?Element {
// Using native closest for maximum speed where we can
if (el.closest) {
return el.closest(selector);
}
// ie11: damn you!
return closestPonyfill(el, selector);
}

export default function closest(el: ?Element, selector: string): ?Element {
if (!el || el === document || el === window) {
return null;
}
const found = closestImpl(el, selector);

if (found) {
return found;
}

const root: ShadowRoot = (el.getRootNode(): any);
return closest(root.host, selector);
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import * as attributes from '../data-attributes';
import isElement from '../is-type-of-element/is-element';
import isHtmlElement from '../is-type-of-element/is-html-element';
import closest from './closest';
import { getEventTarget } from '../get-elements/query-elements';
import { warning } from '../../dev-warning';

function getSelector(contextId: ContextId): string {
Expand All @@ -14,7 +15,7 @@ function findClosestDragHandleFromEvent(
contextId: ContextId,
event: Event,
): ?HTMLElement {
const target: ?EventTarget = event.target;
const target: ?EventTarget = getEventTarget(event);

if (!isElement(target)) {
warning('event.target must be a Element');
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
// @flow
import isHtmlElement from '../is-type-of-element/is-html-element';
import { getEventTarget } from '../get-elements/query-elements';

export type TagNameMap = {
[tagName: string]: true,
Expand Down Expand Up @@ -56,7 +57,7 @@ export default function isEventInInteractiveElement(
draggable: Element,
event: Event,
): boolean {
const target: EventTarget = event.target;
const target: ?EventTarget = getEventTarget(event);

if (!isHtmlElement(target)) {
return false;
Expand Down
7 changes: 6 additions & 1 deletion src/view/use-sensor-marshal/use-sensor-marshal.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ import { noop } from '../../empty';
import findClosestDraggableIdFromEvent from './find-closest-draggable-id-from-event';
import findDraggable from '../get-elements/find-draggable';
import bindEvents from '../event-bindings/bind-events';
import { getEventTargetRoot } from '../get-elements/query-elements';

function preventDefault(event: Event) {
event.preventDefault();
Expand Down Expand Up @@ -172,7 +173,11 @@ function tryStart({
}

const entry: DraggableEntry = registry.draggable.getById(draggableId);
const el: ?HTMLElement = findDraggable(contextId, entry.descriptor.id);
const el: ?HTMLElement = findDraggable(
contextId,
entry.descriptor.id,
getEventTargetRoot(sourceEvent),
);

if (!el) {
warning(`Unable to find draggable element with id: ${draggableId}`);
Expand Down
2 changes: 1 addition & 1 deletion src/view/use-style-marshal/get-styles.js
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,7 @@ export default (contextId: ContextId): Styles => {
// we do not want the browser to have behaviors we do not expect

const body: Rule = {
selector: 'body',
selector: 'body, :host',
styles: {
dragging: `
cursor: grabbing;
Expand Down
25 changes: 16 additions & 9 deletions src/view/use-style-marshal/use-style-marshal.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,11 @@ import getStyles, { type Styles } from './get-styles';
import { prefix } from '../data-attributes';
import useLayoutEffect from '../use-isomorphic-layout-effect';

const getHead = (): HTMLHeadElement => {
const head: ?HTMLHeadElement = document.querySelector('head');
invariant(head, 'Cannot find the head to append a style to');
return head;
const getStylesRoot = (stylesInsertionPoint: ?HTMLElement): HTMLElement => {
const stylesRoot: ?HTMLElement =
stylesInsertionPoint || document.querySelector('head');
invariant(stylesRoot, 'Cannot find the head or root to append a style to');
return stylesRoot;
};

const createStyleEl = (nonce?: string): HTMLStyleElement => {
Expand All @@ -24,7 +25,11 @@ const createStyleEl = (nonce?: string): HTMLStyleElement => {
return el;
};

export default function useStyleMarshal(contextId: ContextId, nonce?: string) {
export default function useStyleMarshal(
contextId: ContextId,
nonce?: string,
stylesInsertionPoint?: ?HTMLElement,
) {
const styles: Styles = useMemo(() => getStyles(contextId), [contextId]);
const alwaysRef = useRef<?HTMLStyleElement>(null);
const dynamicRef = useRef<?HTMLStyleElement>(null);
Expand Down Expand Up @@ -63,9 +68,10 @@ export default function useStyleMarshal(contextId: ContextId, nonce?: string) {
always.setAttribute(`${prefix}-always`, contextId);
dynamic.setAttribute(`${prefix}-dynamic`, contextId);

// add style tags to head
getHead().appendChild(always);
getHead().appendChild(dynamic);
// add style tags to styles root
const stylesRoot = getStylesRoot(stylesInsertionPoint);
stylesRoot.appendChild(always);
stylesRoot.appendChild(dynamic);

// set initial style
setAlwaysStyle(styles.always);
Expand All @@ -75,7 +81,7 @@ export default function useStyleMarshal(contextId: ContextId, nonce?: string) {
const remove = (ref) => {
const current: ?HTMLStyleElement = ref.current;
invariant(current, 'Cannot unmount ref as it is not set');
getHead().removeChild(current);
stylesRoot.removeChild(current);
ref.current = null;
};

Expand All @@ -89,6 +95,7 @@ export default function useStyleMarshal(contextId: ContextId, nonce?: string) {
styles.always,
styles.resting,
contextId,
stylesInsertionPoint,
]);

const dragging = useCallback(() => setDynamicStyle(styles.dragging), [
Expand Down
Loading