Sitelet https://github.com/gridstack/gridstack.js/commit/09d57c87ae7f80fb68ae4b3cf91a3add7e87d262
Skip to content

Commit 09d57c8

Browse files
authored
Merge pull request #3424 from adumesny/master
* fix #2729 match the cancel selector through shadow DOM
2 parents 46b0dd2 + f100643 commit 09d57c8

3 files changed

Lines changed: 63 additions & 1 deletion

File tree

‎doc/CHANGES.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,7 @@ Change log
149149
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
150150

151151
## 14.0.1 (TBD)
152+
* fix: [#2729](https://github.com/gridstack/gridstack.js/issues/2729) `cancel` selector now matches elements inside a shadow root
152153
* fix: [#2728](https://github.com/gridstack/gridstack.js/issues/2728) drag helper drifts off the cursor when the page scrolls under a CSS-transformed containing block
153154
* fix: [#1959](https://github.com/gridstack/gridstack.js/issues/1959) `update()` refuses a move that would land on a locked item
154155
* fix: [#2666](https://github.com/gridstack/gridstack.js/issues/2666) iOS auto-scroll uses visualViewport, and un-bind touch handlers

‎spec/regression-spec.ts‎

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -962,4 +962,50 @@ describe('regression >', () => {
962962
expect(dd['_cbDrift']).toEqual({dx: 0, dy: 0});
963963
});
964964
});
965+
966+
describe('2729 draggable cancel inside shadow DOM >', () => {
967+
let host: HTMLElement;
968+
afterEach(() => {
969+
delete DDManager.mouseHandled;
970+
host?.remove();
971+
});
972+
973+
/** item whose content holds a web component with `.no-drag` inside its shadow root */
974+
const build = (cancel: string) => {
975+
document.body.insertAdjacentHTML('afterbegin',
976+
'<div class="grid-stack-item"><div class="grid-stack-item-content">' +
977+
'<div class="wc"></div><span class="plain">plain</span>' +
978+
'</div></div>');
979+
host = document.querySelector('.grid-stack-item');
980+
const wc = host.querySelector('.wc') as HTMLElement;
981+
const shadow = wc.attachShadow({mode: 'open'});
982+
shadow.innerHTML = '<div class="no-drag">handle off</div><div class="ok">draggable</div>';
983+
DDElement.init(host as GridItemHTMLElement).setupDraggable({handle: '.grid-stack-item-content', cancel});
984+
return shadow;
985+
};
986+
987+
/** dispatch a real composed mousedown so the browser/jsdom retargets e.target like it would live */
988+
const dragStarted = (target: HTMLElement): boolean => {
989+
const dd = (host as GridItemHTMLElement).ddElement!.ddDraggable!;
990+
target.dispatchEvent(new MouseEvent('mousedown', {button: 0, bubbles: true, composed: true, cancelable: true}));
991+
const started = !!dd['mouseDownEvent'];
992+
dd['_mouseUp'](new MouseEvent('mouseup'));
993+
delete DDManager.mouseHandled;
994+
return started;
995+
};
996+
997+
it('honors cancel for an element inside a shadow root', () => {
998+
const shadow = build('.no-drag');
999+
expect(dragStarted(shadow.querySelector('.ok') as HTMLElement)).toBe(true);
1000+
// #2729: e.target is retargeted to the <div class="wc"> host out here, and closest()
1001+
// does not cross the shadow boundary, so this used to start a drag anyway
1002+
expect(dragStarted(shadow.querySelector('.no-drag') as HTMLElement)).toBe(false);
1003+
});
1004+
1005+
it('still honors cancel in the light DOM', () => {
1006+
build('.plain');
1007+
expect(dragStarted(host.querySelector('.plain') as HTMLElement)).toBe(false);
1008+
expect(dragStarted(host.querySelector('.grid-stack-item-content') as HTMLElement)).toBe(true);
1009+
});
1010+
});
9651011
});

‎src/dd-draggable.ts‎

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,21 @@ export class DDDraggable extends DDBaseImplement implements HTMLElementExtendOpt
195195
if (!wasDisabled) this.enable();
196196
}
197197

198+
/** @internal does any element on the event's path match the selector?
199+
* Same walk as `e.target.closest(sel)` but through shadow DOM: for a listener outside the shadow
200+
* tree the browser retargets `e.target` to the *host*, and `closest()` won't cross the boundary
201+
* either, so a `cancel` selector living inside a web component could never match (#2729). */
202+
protected _matchInPath(e: Event, selector: string): HTMLElement | undefined {
203+
const path = e.composedPath?.() as EventTarget[] | undefined;
204+
if (!path?.length) return (e.target as HTMLElement)?.closest(selector) as HTMLElement ?? undefined;
205+
for (const t of path) {
206+
const el = t as HTMLElement;
207+
if (el?.nodeType !== 1) continue; // skip ShadowRoot / document / window
208+
if (el.matches?.(selector)) return el;
209+
}
210+
return undefined;
211+
}
212+
198213
/** @internal call when mouse goes down before a dragstart happens */
199214
protected _mouseDown(e: MouseEvent): boolean {
200215
// if real browser event (trusted:true vs false for our simulated ones) and prior touch/mouse state didn't clean up, reset it.
@@ -217,7 +232,7 @@ export class DDDraggable extends DDBaseImplement implements HTMLElementExtendOpt
217232
const skipEl = (e.target as HTMLElement).closest(skipMouseDown);
218233
if (skipEl && !this.dragEls.some(el => el === e.target || el === skipEl)) return true;
219234
if (this.option.cancel) {
220-
if ((e.target as HTMLElement).closest(this.option.cancel)) return true;
235+
if (this._matchInPath(e, this.option.cancel)) return true;
221236
}
222237

223238
this.mouseDownEvent = e;

0 commit comments

Comments
 (0)