Skip to content

Commit 51c4d4b

Browse files
committed
fix: Drag and resize behavior
* Drag now works when the tile manager is inside a Shadow Root container, e.g. inside another web component without being slotted. View transitions are borked in that scenario. * The resize ghost element is now created at the body-level of the document preventing clipping from the manager container. * Both drag and resize controllers remove any lingering ghost elements if their hosts are removed from the DOM.
1 parent 3ba69d2 commit 51c4d4b

File tree

11 files changed

+78
-56
lines changed

11 files changed

+78
-56
lines changed

src/animations/player.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@ type ViewTransitionResult = {
9393
export function startViewTransition(
9494
callback?: ViewTransitionUpdateCallback
9595
): ViewTransitionResult {
96+
/* c8 ignore next 4 */
9697
if (getPrefersReducedMotion() || !document.startViewTransition) {
9798
callback?.();
9899
return {};

src/components/common/controllers/drag.spec.ts

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -308,8 +308,6 @@ describe('Drag controller', () => {
308308
expect(defaultGhost.getBoundingClientRect()).to.eql(
309309
instance.getBoundingClientRect()
310310
);
311-
312-
instance.controller.dispose();
313311
});
314312

315313
it('should create a custom ghost element in "deferred" mode when a configuration is passed', async () => {
@@ -322,8 +320,6 @@ describe('Drag controller', () => {
322320

323321
expect(customGhost).to.exist;
324322
expect(customGhost.localName).to.equal(instance.localName);
325-
326-
instance.controller.dispose();
327323
});
328324

329325
it('should correctly place the ghost element in the configured layer container', async () => {
@@ -334,8 +330,6 @@ describe('Drag controller', () => {
334330
await elementUpdated(instance);
335331

336332
expect(getDefaultGhost().parentElement).to.eql(instance.parentElement);
337-
338-
instance.controller.dispose();
339333
});
340334

341335
it('should invoke start callback on drag operation', async () => {
@@ -346,8 +340,6 @@ describe('Drag controller', () => {
346340

347341
expect(dragStart.called).is.true;
348342
expect(dragStart.callCount).to.equal(1);
349-
350-
instance.controller.dispose();
351343
});
352344

353345
it('should not invoke move unless a start is invoked', async () => {
@@ -377,8 +369,6 @@ describe('Drag controller', () => {
377369
expect(dragStart.called).is.true;
378370
expect(dragMove.called).is.true;
379371
expect(dragMove.callCount).to.equal(10);
380-
381-
instance.controller.dispose();
382372
});
383373

384374
it('should invoke end when releasing the dragged element', async () => {

src/components/common/controllers/drag.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import type {
55
} from 'lit';
66
import type { Ref } from 'lit/directives/ref.js';
77

8+
import { getDefaultLayer } from '../../resize-container/default-ghost.js';
89
import { findElementFromEventPath } from '../util.js';
910

1011
type DragEnterCallback = (target: Element) => unknown;
@@ -202,6 +203,7 @@ class DragController implements ReactiveController {
202203
this._host.removeEventListener('touchstart', this);
203204
this._host.removeEventListener('pointerdown', this);
204205
this._setDragCancelListener(false);
206+
this._removeGhost();
205207
}
206208

207209
/** @internal */
@@ -404,10 +406,6 @@ class DragController implements ReactiveController {
404406
}
405407
}
406408

407-
function getDefaultLayer() {
408-
return document.body;
409-
}
410-
411409
function createDefaultDragGhost({ x, y, width, height }: DOMRect): HTMLElement {
412410
const element = document.createElement('div');
413411

src/components/common/util.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -171,8 +171,15 @@ export function* iterNodes<T = Node>(
171171
}
172172
}
173173

174+
export function getRoot(
175+
element: Element,
176+
options?: GetRootNodeOptions
177+
): Document | ShadowRoot {
178+
return element.getRootNode(options) as Document | ShadowRoot;
179+
}
180+
174181
export function getElementByIdFromRoot(root: HTMLElement, id: string) {
175-
return (root.getRootNode() as Document | ShadowRoot).getElementById(id);
182+
return getRoot(root).getElementById(id);
176183
}
177184

178185
export function isElement(node: unknown): node is Element {
Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,29 @@
11
/**
22
* Default ghost element factory for the igc-resize implementation.
33
*/
4-
export function createDefaultGhostElement(
5-
width: number,
6-
height: number
7-
): HTMLElement {
4+
export function createDefaultGhostElement({
5+
x,
6+
y,
7+
width,
8+
height,
9+
}: DOMRect): HTMLElement {
810
const element = document.createElement('div');
11+
const { scrollX, scrollY } = window;
912

1013
Object.assign(element.style, {
1114
position: 'absolute',
12-
top: 0,
13-
left: 0,
14-
zIndex: 1000, // REVIEW: Expose as a CSS variable
15-
background: 'pink', // REVIEW: Expose as a CSS variable
16-
opacity: 0.85, // REVIEW: Expose as a CSS variable
15+
top: `${y + scrollY}px`,
16+
left: `${x + scrollX}px`,
17+
zIndex: 1000,
18+
background: 'pink',
19+
opacity: 0.85,
1720
width: `${width}px`,
1821
height: `${height}px`,
1922
});
2023

2124
return element;
2225
}
26+
27+
export function getDefaultLayer(): HTMLElement {
28+
return document.body;
29+
}

src/components/resize-container/resize-container.spec.ts

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -34,12 +34,12 @@ describe('Resize container', () => {
3434
});
3535

3636
it('should be defined', () => {
37-
expect(resizeContainer).to.not.be.null;
37+
expect(resizeContainer).is.not.null;
3838
});
3939

4040
it('should initialize with correct default state', () => {
4141
expect(resizeContainer.mode).to.equal('immediate');
42-
expect(resizeContainer.ghostFactory).to.be.undefined;
42+
expect(resizeContainer.ghostFactory).is.undefined;
4343
});
4444

4545
it('should be accessible', async () => {
@@ -143,7 +143,7 @@ describe('Resize container', () => {
143143
await elementUpdated(element);
144144

145145
const { ghost } = getResizeEventState(eventSpy);
146-
expect(ghost).to.be.null;
146+
expect(ghost).is.null;
147147
});
148148

149149
it('should fire `resizeStart` on pointer interaction', async () => {
@@ -504,17 +504,17 @@ describe('Resize container', () => {
504504
it('adorners visibility is toggled on pointer enter/leave events', async () => {
505505
const DOM = getDOM(element);
506506

507-
expect(Object.values(DOM.adorners).every((e) => e === null)).to.be.true;
507+
expect(Object.values(DOM.adorners).every((e) => e === null)).is.true;
508508

509509
// Pointerenter - adorners rendered
510510
await setResizeActiveState(element);
511511

512-
expect(Object.values(DOM.adorners).every((e) => e !== null)).to.be.true;
512+
expect(Object.values(DOM.adorners).every((e) => e !== null)).is.true;
513513

514514
// Pointerleave - adorners removed
515515
await setResizeActiveState(element, false);
516516

517-
expect(Object.values(DOM.adorners).every((e) => e === null)).to.be.true;
517+
expect(Object.values(DOM.adorners).every((e) => e === null)).is.true;
518518
});
519519

520520
it('resize behavior should only start when interacting with the trigger element', async () => {
@@ -548,7 +548,7 @@ describe('Resize container', () => {
548548
simulatePointerDown(DOM.adorners.corner);
549549
await elementUpdated(element);
550550

551-
expect(DOM.ghostElement).to.not.be.null;
551+
expect(DOM.ghostElement).is.not.null;
552552
expect(DOM.ghostElement?.style.background).to.equal('pink');
553553
});
554554

@@ -567,8 +567,8 @@ describe('Resize container', () => {
567567
simulatePointerDown(DOM.adorners.corner);
568568
await elementUpdated(element);
569569

570-
expect(DOM.ghostElement).to.not.be.null;
571-
expect(DOM.ghostElement?.classList.contains('custom-ghost')).to.be.true;
570+
expect(DOM.ghostElement).is.not.null;
571+
expect(DOM.ghostElement?.classList.contains('custom-ghost')).is.true;
572572
});
573573

574574
it('should fire `resizeStart` on pointer interaction', async () => {
@@ -641,7 +641,7 @@ describe('Resize container', () => {
641641
await elementUpdated(element);
642642

643643
expect(eventSpy).calledWith('igcResizeEnd');
644-
expect(DOM.ghostElement).to.be.null;
644+
expect(DOM.ghostElement).is.null;
645645
});
646646

647647
it('should not fire `resizeCancel` when escape key is pressed without active resizing', async () => {
@@ -693,7 +693,7 @@ describe('Resize container', () => {
693693
await elementUpdated(element);
694694

695695
expect(eventSpy).calledWith('igcResizeCancel');
696-
expect(DOM.ghostElement).to.be.null;
696+
expect(DOM.ghostElement).is.null;
697697
});
698698
});
699699

@@ -939,7 +939,7 @@ function getDOM(resizeElement: IgcResizeContainerComponent) {
939939
},
940940
/** The ghost element when in deferred mode */
941941
get ghostElement() {
942-
return resizeElement.querySelector<HTMLElement>('[data-resize-ghost]')!;
942+
return document.querySelector<HTMLElement>('[data-resize-ghost]')!;
943943
},
944944
};
945945
}

src/components/resize-container/resize-controller.ts

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import type { ReactiveController, ReactiveControllerHost } from 'lit';
22

33
import { findElementFromEventPath } from '../common/util.js';
4-
import { createDefaultGhostElement } from './default-ghost.js';
4+
import { createDefaultGhostElement, getDefaultLayer } from './default-ghost.js';
55
import type { ResizeControllerConfiguration, ResizeState } from './types.js';
66

77
const additionalEvents = ['pointermove', 'lostpointercapture'] as const;
@@ -13,7 +13,10 @@ type State = {
1313

1414
class ResizeController implements ReactiveController {
1515
private _host: ReactiveControllerHost & HTMLElement;
16-
private _options: ResizeControllerConfiguration = { enabled: true };
16+
private _options: ResizeControllerConfiguration = {
17+
enabled: true,
18+
layer: getDefaultLayer,
19+
};
1720

1821
private _id = -1;
1922
private _hasPointerCapture = false;
@@ -30,6 +33,14 @@ class ResizeController implements ReactiveController {
3033
return this._options.resizeTarget?.call(this._host) ?? this._host;
3134
}
3235

36+
private get _layer(): HTMLElement {
37+
if (!this._isDeferred) {
38+
return this._host;
39+
}
40+
41+
return this._options.layer?.() ?? this._host;
42+
}
43+
3344
/** Whether the controller is in deferred mode. */
3445
private get _isDeferred(): boolean {
3546
return this._options.mode === 'deferred';
@@ -98,6 +109,7 @@ class ResizeController implements ReactiveController {
98109
this._host.removeEventListener('pointerdown', this);
99110
this._host.removeEventListener('touchstart', this);
100111
this._setResizeCancelListener(false);
112+
this._removeGhostElement();
101113
}
102114

103115
/** @internal */
@@ -248,13 +260,10 @@ class ResizeController implements ReactiveController {
248260

249261
this._ghost =
250262
this._options.deferredFactory?.call(this._host) ??
251-
createDefaultGhostElement(
252-
this._state.initial.width,
253-
this._state.initial.height
254-
);
263+
createDefaultGhostElement(this._resizeTarget.getBoundingClientRect());
255264

256265
this._ghost.setAttribute('data-resize-ghost', '');
257-
this._host.append(this._ghost);
266+
this._layer.append(this._ghost);
258267
}
259268

260269
private _removeGhostElement(): void {

src/components/resize-container/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ export type ResizeControllerConfiguration = {
2424
ref?: Ref<HTMLElement>[];
2525
mode?: ResizeMode;
2626
deferredFactory?: ResizeGhostFactory;
27+
layer?: () => HTMLElement;
2728
/** Callback invoked at the start of a resize operation. */
2829
start?: ResizeCallback;
2930
/** Callback invoked on each pointer move during a resize operation. */

src/components/tile-manager/tile-resize.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -454,7 +454,7 @@ function getResizeContainerDOM(tile: IgcTileComponent) {
454454
},
455455
/** The ghost element when in deferred mode */
456456
get ghostElement() {
457-
return resizeContainer.querySelector<HTMLElement>('[data-resize-ghost]')!;
457+
return document.querySelector<HTMLElement>('[data-resize-ghost]')!;
458458
},
459459
};
460460
}

src/components/tile-manager/tile-util.ts

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -437,31 +437,35 @@ export function createTileDragGhost(tile: IgcTileComponent): IgcTileComponent {
437437
width: `${width}px`,
438438
height: `${height}px`,
439439
opacity: 0.6,
440-
background: `${computed.getPropertyValue('--tile-background')}`,
440+
background: computed.getPropertyValue('--tile-background'),
441441
border: `1px solid ${computed.getPropertyValue('--hover-border-color')}`,
442442
borderRadius: computed.getPropertyValue('--border-radius'),
443-
boxShadow: `${computed.getPropertyValue('--drag-elevation')}`,
443+
boxShadow: computed.getPropertyValue('--drag-elevation'),
444444
zIndex: 1000,
445445
viewTransitionName: 'dragged-tile-ghost',
446446
});
447447

448448
return clone;
449449
}
450450

451-
export function createTileGhost(): HTMLElement {
451+
export function createTileGhost(tile: IgcTileComponent): HTMLElement {
452452
const element = document.createElement('div');
453+
const computed = getComputedStyle(tile);
454+
const { x, y, width, height } = tile.getBoundingClientRect();
455+
const { scrollX, scrollY } = window;
453456

454457
Object.assign(element.style, {
458+
boxSizing: 'border-box',
455459
position: 'absolute',
456460
contain: 'strict',
457-
top: 0,
458-
left: 0,
461+
top: `${y + scrollY}px`,
462+
left: `${x + scrollX}px`,
459463
zIndex: 1000,
460-
background: 'var(--placeholder-background)',
461-
border: '1px solid var(--ghost-border)',
462-
borderRadius: 'var(--border-radius)',
463-
width: '100%',
464-
height: '100%',
464+
background: computed.getPropertyValue('--placeholder-background'),
465+
border: `1px solid ${computed.getPropertyValue('--ghost-border')}`,
466+
borderRadius: computed.getPropertyValue('--border-radius'),
467+
width: `${width}px`,
468+
height: `${height}px`,
465469
});
466470

467471
return element;

0 commit comments

Comments
 (0)