@@ -255,4 +255,50 @@ describe("GridStack React wrapper", () => {
255255 // ...and its React-rendered content must have followed, not been unmounted.
256256 expect ( document . querySelector ( "[data-testid=\"portal\"]" ) ?. textContent ) . toBe ( "hello" ) ;
257257 } ) ;
258+
259+ it ( "#2976 renders a sidebar drag-in widget that has a component but no id" , async ( ) => {
260+ await act ( async ( ) => {
261+ root . render (
262+ < GridStack
263+ options = { { column : 12 , cellHeight : 50 , margin : 0 , children : [ ] } }
264+ components = { {
265+ Side : ( p : Record < string , unknown > ) => (
266+ < span data-testid = "dragin" > { String ( p . label ?? "" ) } </ span >
267+ ) ,
268+ } }
269+ />
270+ ) ;
271+ } ) ;
272+ await act ( flush ) ;
273+
274+ const gridEl = container . querySelector ( ".grid-stack" ) as GridHTMLElement ;
275+ const g = gridEl ?. gridstack as GridStackInstance ;
276+ expect ( g ) . toBeTruthy ( ) ;
277+
278+ // What GS core does at the end of its `drop` handler for a sidebar item
279+ // (gridstack.ts: `el = this.addWidget(node)`): the spec comes straight from
280+ // GridStack.setupDragIn(..., widgets) so it has NO id of its own.
281+ await act ( async ( ) => {
282+ g . addWidget ( { w : 2 , h : 2 , component : "Side" , props : { label : "dropped" } } as GridStackWidget ) ;
283+ } ) ;
284+ await act ( flush ) ;
285+
286+ expect (
287+ document . querySelector ( '[data-testid="dragin"]' ) ?. textContent
288+ ) . toBe ( "dropped" ) ;
289+
290+ // a second drop of the same spec must get its own portal, not collide with the first
291+ await act ( async ( ) => {
292+ g . addWidget ( { w : 2 , h : 2 , component : "Side" , props : { label : "again" } } as GridStackWidget ) ;
293+ } ) ;
294+ await act ( flush ) ;
295+ const both = Array . from ( document . querySelectorAll ( '[data-testid="dragin"]' ) ) . map ( e => e . textContent ) ;
296+ expect ( both . sort ( ) ) . toEqual ( [ "again" , "dropped" ] ) ;
297+
298+ // and the minted id round-trips through save() so layouts stay reloadable
299+ const saved = g . save ( false ) as GridStackWidget [ ] ;
300+ expect ( saved . length ) . toBe ( 2 ) ;
301+ expect ( saved . every ( w => ! ! w . id ) ) . toBe ( true ) ;
302+ expect ( new Set ( saved . map ( w => w . id ) ) . size ) . toBe ( 2 ) ;
303+ } ) ;
258304} ) ;
0 commit comments