Skip to content

Commit d2d1668

Browse files
authored
Merge pull request #3406 from adumesny/master
find drag handles inside open shadow roots
2 parents ff54569 + 744aac5 commit d2d1668

5 files changed

Lines changed: 114 additions & 18 deletions

File tree

‎demo/title_drag.html‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,26 @@ <h1>Title area drag</h1>
1818
<div class="card-header">- Drag here -</div>
1919
<div class="card">the rest of the panel content doesn't drag</div>
2020
</div></div>
21+
<div class="grid-stack-item" gs-x="3" gs-w="3" gs-h="3"><div class="grid-stack-item-content">
22+
<my-card></my-card>
23+
</div></div>
2124
</div>
2225
</div>
2326
<script src="events.js"></script>
2427
<script type="text/javascript">
28+
class MyCard extends HTMLElement {
29+
constructor() {
30+
super();
31+
const shadow = this.attachShadow({mode: 'open'});
32+
shadow.innerHTML = `
33+
<link rel="stylesheet" href="demo.css"/>
34+
<div class="card-header">- Web Component Drag -</div>
35+
<div class="card" style="padding: 8px">Shadow DOM content doesn't drag</div>
36+
`;
37+
}
38+
}
39+
customElements.define('my-card', MyCard);
40+
2541
let grid = GridStack.init({ handle: '.card-header' }); // drag by the header only
2642
addEvents(grid);
2743
</script>

‎doc/API.md‎

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -430,7 +430,7 @@ Destroys a grid instance. DO NOT CALL any methods or access any vars after this
430430
disable(recurse): GridStack;
431431
```
432432

433-
Defined in: [gridstack.ts:2430](https://github.com/adumesny/gridstack.js/blob/master/src/gridstack.ts#L2430)
433+
Defined in: [gridstack.ts:2443](https://github.com/adumesny/gridstack.js/blob/master/src/gridstack.ts#L2443)
434434

435435
Temporarily disables widgets moving/resizing.
436436
If you want a more permanent way (which freezes up resources) use `setStatic(true)` instead.
@@ -471,7 +471,7 @@ grid.disable(false);
471471
enable(recurse): GridStack;
472472
```
473473

474-
Defined in: [gridstack.ts:2457](https://github.com/adumesny/gridstack.js/blob/master/src/gridstack.ts#L2457)
474+
Defined in: [gridstack.ts:2470](https://github.com/adumesny/gridstack.js/blob/master/src/gridstack.ts#L2470)
475475

476476
Re-enables widgets moving/resizing - see disable().
477477
Note: This is a no-op for static grids.
@@ -510,7 +510,7 @@ grid.enable(false);
510510
enableMove(doEnable, recurse): GridStack;
511511
```
512512

513-
Defined in: [gridstack.ts:2483](https://github.com/adumesny/gridstack.js/blob/master/src/gridstack.ts#L2483)
513+
Defined in: [gridstack.ts:2496](https://github.com/adumesny/gridstack.js/blob/master/src/gridstack.ts#L2496)
514514

515515
Enables/disables widget moving for all widgets. No-op for static grids.
516516
Note: locally defined items (with noMove property) still override this setting.
@@ -547,7 +547,7 @@ grid.enableMove(true, false);
547547
enableResize(doEnable, recurse): GridStack;
548548
```
549549

550-
Defined in: [gridstack.ts:2511](https://github.com/adumesny/gridstack.js/blob/master/src/gridstack.ts#L2511)
550+
Defined in: [gridstack.ts:2524](https://github.com/adumesny/gridstack.js/blob/master/src/gridstack.ts#L2524)
551551

552552
Enables/disables widget resizing for all widgets. No-op for static grids.
553553
Note: locally defined items (with noResize property) still override this setting.
@@ -1098,7 +1098,7 @@ grid.mode('list'); // continuous sequential reflow, like a re-orderable list
10981098
movable(els, val): GridStack;
10991099
```
11001100

1101-
Defined in: [gridstack.ts:2371](https://github.com/adumesny/gridstack.js/blob/master/src/gridstack.ts#L2371)
1101+
Defined in: [gridstack.ts:2384](https://github.com/adumesny/gridstack.js/blob/master/src/gridstack.ts#L2384)
11021102

11031103
Enables/Disables dragging by the user for specific grid elements.
11041104
For all items and future items, use enableMove() instead. No-op for static grids.
@@ -1457,7 +1457,7 @@ or `sizeToContent` gridItem options.
14571457
prepareDragDrop(el, force?): GridStack;
14581458
```
14591459

1460-
Defined in: [gridstack.ts:2900](https://github.com/adumesny/gridstack.js/blob/master/src/gridstack.ts#L2900)
1460+
Defined in: [gridstack.ts:2913](https://github.com/adumesny/gridstack.js/blob/master/src/gridstack.ts#L2913)
14611461

14621462
prepares the element for drag&drop - this is normally called by makeWidget() unless are are delay loading
14631463

@@ -1478,7 +1478,7 @@ prepares the element for drag&drop - this is normally called by makeWidget() unl
14781478
refreshDragHandles(els): GridStack;
14791479
```
14801480

1481-
Defined in: [gridstack.ts:2888](https://github.com/adumesny/gridstack.js/blob/master/src/gridstack.ts#L2888)
1481+
Defined in: [gridstack.ts:2901](https://github.com/adumesny/gridstack.js/blob/master/src/gridstack.ts#L2901)
14821482

14831483
Re-scans one or more widget elements for drag handle elements after delayed content
14841484
(React portal, Angular component, etc.) has been rendered inside the item.
@@ -1599,7 +1599,7 @@ Removes widget from the grid.
15991599
resizable(els, val): GridStack;
16001600
```
16011601

1602-
Defined in: [gridstack.ts:2397](https://github.com/adumesny/gridstack.js/blob/master/src/gridstack.ts#L2397)
1602+
Defined in: [gridstack.ts:2410](https://github.com/adumesny/gridstack.js/blob/master/src/gridstack.ts#L2410)
16031603

16041604
Enables/Disables user resizing for specific grid elements.
16051605
For all items and future items, use enableResize() instead. No-op for static grids.
@@ -1811,7 +1811,7 @@ is dynamically create and needs to be set later.
18111811
protected triggerEvent(event, target): void;
18121812
```
18131813

1814-
Defined in: [gridstack.ts:3162](https://github.com/adumesny/gridstack.js/blob/master/src/gridstack.ts#L3162)
1814+
Defined in: [gridstack.ts:3175](https://github.com/adumesny/gridstack.js/blob/master/src/gridstack.ts#L3175)
18151815

18161816
call given event callback on our main top-most grid (if we're nested)
18171817

@@ -4094,7 +4094,7 @@ Defined in: [dd-draggable.ts:70](https://github.com/adumesny/gridstack.js/blob/m
40944094
destroy(): void;
40954095
```
40964096

4097-
Defined in: [dd-draggable.ts:135](https://github.com/adumesny/gridstack.js/blob/master/src/dd-draggable.ts#L135)
4097+
Defined in: [dd-draggable.ts:154](https://github.com/adumesny/gridstack.js/blob/master/src/dd-draggable.ts#L154)
40984098

40994099
Destroy this drag & drop implementation and clean up resources.
41004100
Removes all event handlers and clears internal state.
@@ -4113,7 +4113,7 @@ Removes all event handlers and clears internal state.
41134113
disable(forDestroy): void;
41144114
```
41154115

4116-
Defined in: [dd-draggable.ts:121](https://github.com/adumesny/gridstack.js/blob/master/src/dd-draggable.ts#L121)
4116+
Defined in: [dd-draggable.ts:140](https://github.com/adumesny/gridstack.js/blob/master/src/dd-draggable.ts#L140)
41174117

41184118
Disable this drag & drop implementation.
41194119
Subclasses should override to perform additional cleanup.
@@ -4138,7 +4138,7 @@ Subclasses should override to perform additional cleanup.
41384138
enable(): void;
41394139
```
41404140

4141-
Defined in: [dd-draggable.ts:107](https://github.com/adumesny/gridstack.js/blob/master/src/dd-draggable.ts#L107)
4141+
Defined in: [dd-draggable.ts:126](https://github.com/adumesny/gridstack.js/blob/master/src/dd-draggable.ts#L126)
41424142

41434143
Enable this drag & drop implementation.
41444144
Subclasses should override to perform additional setup.
@@ -4171,7 +4171,7 @@ return all handles omitting other nested `.grid-stack-item` children (in case no
41714171
off(event): void;
41724172
```
41734173

4174-
Defined in: [dd-draggable.ts:103](https://github.com/adumesny/gridstack.js/blob/master/src/dd-draggable.ts#L103)
4174+
Defined in: [dd-draggable.ts:122](https://github.com/adumesny/gridstack.js/blob/master/src/dd-draggable.ts#L122)
41754175

41764176
Unregister an event callback for the specified event.
41774177

@@ -4195,7 +4195,7 @@ Unregister an event callback for the specified event.
41954195
on(event, callback): void;
41964196
```
41974197

4198-
Defined in: [dd-draggable.ts:99](https://github.com/adumesny/gridstack.js/blob/master/src/dd-draggable.ts#L99)
4198+
Defined in: [dd-draggable.ts:118](https://github.com/adumesny/gridstack.js/blob/master/src/dd-draggable.ts#L118)
41994199

42004200
Register an event callback for the specified event.
42014201

@@ -4220,7 +4220,7 @@ Register an event callback for the specified event.
42204220
refreshHandles(): void;
42214221
```
42224222

4223-
Defined in: [dd-draggable.ts:156](https://github.com/adumesny/gridstack.js/blob/master/src/dd-draggable.ts#L156)
4223+
Defined in: [dd-draggable.ts:175](https://github.com/adumesny/gridstack.js/blob/master/src/dd-draggable.ts#L175)
42244224

42254225
Re-scans the item element for drag-handle elements after delayed content (React portal,
42264226
Angular component, etc.) has been rendered into the item. Removes listeners from the
@@ -4264,7 +4264,7 @@ Result from the callback function, if any
42644264
updateOption(opts): DDDraggable;
42654265
```
42664266

4267-
Defined in: [dd-draggable.ts:145](https://github.com/adumesny/gridstack.js/blob/master/src/dd-draggable.ts#L145)
4267+
Defined in: [dd-draggable.ts:164](https://github.com/adumesny/gridstack.js/blob/master/src/dd-draggable.ts#L164)
42684268

42694269
Method to update the options and return the DD implementation
42704270

‎doc/CHANGES.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,8 @@ Change log
155155
* feat: [#2627](https://github.com/gridstack/gridstack.js/issues/2627), [#2761](https://github.com/gridstack/gridstack.js/issues/2761) emit dragstart/drag/dragstop on sidebar drag-in items
156156
* fix: [#3012](https://github.com/gridstack/gridstack.js/issues/3012), [#3179](https://github.com/gridstack/gridstack.js/issues/3179) prevent crash when calling update() during change event
157157
* fix: [#3226](https://github.com/gridstack/gridstack.js/issues/3226) don't reveal other widgets' resize handles during a resize - thank you [dvd233](https://github.com/dvd233)
158+
* fix: [#3148](https://github.com/gridstack/gridstack.js/issues/3148) find drag handles inside open shadow roots. See
159+
[title_drag.html](https://gridstackjs.com/demo/title_drag.html).
158160

159161
## 13.3.0 (2026-09-11)
160162
* feat: [#2781](https://github.com/gridstack/gridstack.js/issues/3177), [#2781](https://github.com/gridstack/gridstack.js/issues/3177) mobile: pause to drag/reszie vs scroll behavior

‎spec/regression-spec.ts‎

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { GridItemHTMLElement, GridStack, GridStackWidget } from '../src/gridstac
22
import type { GridStackNode } from '../src/types';
33
import { Utils } from '../src/utils';
44
import { DDManager } from '../src/dd-manager';
5+
import { DDElement } from '../src/dd-element';
56

67
describe('regression >', () => {
78
'use strict';
@@ -514,4 +515,62 @@ describe('regression >', () => {
514515
expect(detail?.ui).toBeDefined();
515516
});
516517
});
518+
519+
describe('3148 drag handle inside a shadow root >', () => {
520+
let host: GridItemHTMLElement;
521+
afterEach(() => {
522+
delete DDManager.mouseHandled;
523+
host?.remove();
524+
});
525+
526+
/** a card whose drag handle lives inside a web component's shadow root */
527+
const build = () => {
528+
document.body.insertAdjacentHTML('afterbegin',
529+
'<div class="grid-stack-item"><div class="grid-stack-item-content">' +
530+
'<div class="card"></div></div></div>');
531+
host = document.querySelector('.grid-stack-item') as GridItemHTMLElement;
532+
const card = host.querySelector('.card') as HTMLElement;
533+
const shadow = card.attachShadow({mode: 'open'});
534+
shadow.innerHTML = '<button class="drag-handle">::</button><div class="body">scroll me</div>';
535+
const dd = DDElement.init(host).setupDraggable({handle: '.drag-handle'}).ddDraggable!;
536+
return { dd, shadow };
537+
};
538+
539+
it('finds it, instead of falling back to the whole card', () => {
540+
const { dd, shadow } = build();
541+
const handle = shadow.querySelector('.drag-handle') as HTMLElement;
542+
// querySelectorAll() stops at the shadow boundary, so this used to come back empty and
543+
// dragEls fell back to [the whole item] - the entire card stayed draggable
544+
expect(dd['dragEls']).toEqual([handle]);
545+
expect(dd['dragEls']).not.toContain(host);
546+
});
547+
548+
it('only the handle starts a drag, so the rest can still scroll', () => {
549+
const { dd, shadow } = build();
550+
const started = (target: HTMLElement): boolean => {
551+
target.dispatchEvent(new MouseEvent('mousedown', {button: 0, bubbles: true, composed: true}));
552+
const ok = !!dd['mouseDownEvent'];
553+
dd['_mouseUp'](new MouseEvent('mouseup'));
554+
delete DDManager.mouseHandled;
555+
return ok;
556+
};
557+
expect(started(shadow.querySelector('.drag-handle') as HTMLElement)).toBe(true);
558+
expect(started(shadow.querySelector('.body') as HTMLElement)).toBe(false);
559+
});
560+
561+
it("does not steal a nested item's handle", () => {
562+
document.body.insertAdjacentHTML('afterbegin',
563+
'<div class="grid-stack-item outer"><div class="grid-stack-item-content">' +
564+
'<div class="grid-stack"><div class="grid-stack-item inner">' +
565+
'<div class="grid-stack-item-content"><div class="card"></div></div>' +
566+
'</div></div></div></div>');
567+
host = document.querySelector('.outer') as GridItemHTMLElement;
568+
const card = host.querySelector('.card') as HTMLElement;
569+
const shadow = card.attachShadow({mode: 'open'});
570+
shadow.innerHTML = '<button class="drag-handle">::</button>';
571+
const dd = DDElement.init(host).setupDraggable({handle: '.drag-handle'}).ddDraggable!;
572+
// that handle belongs to .inner (through the shadow host), not to us
573+
expect(dd['dragEls']).toEqual([host]); // fell back to ourself, correctly
574+
});
575+
});
517576
});

‎src/dd-draggable.ts‎

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -89,9 +89,28 @@ export class DDDraggable extends DDBaseImplement implements HTMLElementExtendOpt
8989
protected getAllHandles(): HTMLElement[] {
9090
const handle = this.option.handle;
9191
if (!handle) return []; // caller falls back to the item itself
92-
return Array.from(this.el.querySelectorAll(handle)).filter((node): node is HTMLElement => {
92+
const found: Element[] = [];
93+
// querySelectorAll() stops dead at a shadow boundary, so a handle living inside a web component
94+
// could never be found and the whole card stayed draggable (#3148). Descend into open shadow
95+
// roots too - closed ones are not reachable by design.
96+
const scan = (root: Element | ShadowRoot): void => {
97+
found.push(...Array.from(root.querySelectorAll(handle)));
98+
root.querySelectorAll('*').forEach(child => {
99+
const shadow = (child as HTMLElement).shadowRoot;
100+
if (shadow) scan(shadow);
101+
});
102+
};
103+
scan(this.el);
104+
return found.filter((node): node is HTMLElement => {
93105
if (!(node instanceof HTMLElement)) return false;
94-
const owner = node.closest('.grid-stack-item');
106+
// a handle inside a shadow root has no light-DOM .grid-stack-item ancestor to check against,
107+
// so walk out through the host(s) to find who really owns it
108+
let owner: Element | null = node.closest('.grid-stack-item');
109+
let root = node.getRootNode();
110+
while (!owner && root instanceof ShadowRoot) {
111+
owner = root.host.closest('.grid-stack-item');
112+
root = root.host.getRootNode();
113+
}
95114
return owner === this.el || !owner;
96115
});
97116
}

0 commit comments

Comments
 (0)