@@ -25,6 +25,23 @@ beforeAll(() => {
2525 }
2626} ) ;
2727
28+ function ancestorsOf ( el : Element ) : Element [ ] {
29+ const chain : Element [ ] = [ ] ;
30+ for ( let at = el . parentElement ; at ; at = at . parentElement ) chain . push ( at ) ;
31+ return chain ;
32+ }
33+
34+ // Like a browser: the paint stack under the point, top first, minus computed pointer-events:none.
35+ function emulateHitTest ( painted : ( ) => Element [ ] ) : ( ) => void {
36+ const original = document . elementsFromPoint ;
37+ Object . defineProperty ( document , "elementsFromPoint" , {
38+ configurable : true ,
39+ value : vi . fn ( ( ) => painted ( ) . filter ( ( el ) => getComputedStyle ( el ) . pointerEvents !== "none" ) ) ,
40+ } ) ;
41+ return ( ) =>
42+ Object . defineProperty ( document , "elementsFromPoint" , { configurable : true , value : original } ) ;
43+ }
44+
2845function createMockPostMessage ( ) {
2946 return vi . fn ( ) ;
3047}
@@ -150,6 +167,163 @@ describe("createPickerModule", () => {
150167 }
151168 } ) ;
152169
170+ it ( "picks inside a mounted section whose root sets pointer-events:none" , ( ) => {
171+ const picker = createPickerModule ( { postMessage : createMockPostMessage ( ) } ) ;
172+ picker . installPickerApi ( ) ;
173+ // The mount rescopes the section's own root rule onto its inner root.
174+ document . head . innerHTML =
175+ '<style>[data-composition-id="intro"] > [data-hf-inner-root] { pointer-events: none }' +
176+ " .vignette { pointer-events: none }</style>" ;
177+ document . body . innerHTML = `<div data-composition-id="intro" data-composition-src="intro.html">
178+ <div data-hf-inner-root="true"><div class="card"><code id="code">tl.to()</code></div>
179+ <div class="vignette" id="vignette"></div></div></div>` ;
180+ const code = document . getElementById ( "code" ) ! ;
181+ // Paint order under the point, top first; like a browser, drop what has pointer-events:none.
182+ const painted = [ document . getElementById ( "vignette" ) ! , code , code . parentElement ! ] ;
183+ const originalElementsFromPoint = document . elementsFromPoint ;
184+ Object . defineProperty ( document , "elementsFromPoint" , {
185+ configurable : true ,
186+ value : vi . fn ( ( ) =>
187+ [ ...painted , ...ancestorsOf ( code . parentElement ! ) ] . filter (
188+ ( el ) => getComputedStyle ( el ) . pointerEvents !== "none" ,
189+ ) ,
190+ ) ,
191+ } ) ;
192+
193+ const api = ( window as any ) . __HF_PICKER_API ;
194+ try {
195+ expect ( api . getCandidatesAtPoint ( 10 , 10 ) [ 0 ] ?. selector ) . toBe ( "#code" ) ;
196+ expect ( api . pickAtPoint ( 10 , 10 ) ?. selector ) . toBe ( "#code" ) ;
197+ expect ( document . head . querySelectorAll ( "style" ) ) . toHaveLength ( 1 ) ;
198+ } finally {
199+ Object . defineProperty ( document , "elementsFromPoint" , {
200+ configurable : true ,
201+ value : originalElementsFromPoint ,
202+ } ) ;
203+ }
204+ } ) ;
205+
206+ it ( "picks inside a composition opened on its own whose root sets pointer-events:none" , ( ) => {
207+ const picker = createPickerModule ( { postMessage : createMockPostMessage ( ) } ) ;
208+ picker . installPickerApi ( ) ;
209+ document . head . innerHTML = "<style>#root { pointer-events: none }</style>" ;
210+ document . body . innerHTML = `<div id="root" data-composition-id="intro"><div id="card">
211+ <code id="code">tl.to()</code></div></div>` ;
212+ const at = ( id : string ) => document . getElementById ( id ) ! ;
213+ const restore = emulateHitTest ( ( ) => [ at ( "code" ) , at ( "card" ) , at ( "root" ) ] ) ;
214+ const api = ( window as any ) . __HF_PICKER_API ;
215+ try {
216+ expect ( api . getCandidatesAtPoint ( 10 , 10 ) . map ( ( c : any ) => c . selector ) ) . toEqual ( [
217+ "#code" ,
218+ "#card" ,
219+ ] ) ;
220+ } finally {
221+ restore ( ) ;
222+ }
223+ } ) ;
224+
225+ it ( "adopts the override only for the hit test, leaving the DOM and a saved outerHTML untouched" , ( ) => {
226+ const adopted : CSSStyleSheet [ ] = [ ] ;
227+ Object . defineProperty ( document , "adoptedStyleSheets" , {
228+ configurable : true ,
229+ get : ( ) => adopted . slice ( ) ,
230+ set : ( next : CSSStyleSheet [ ] ) => adopted . splice ( 0 , adopted . length , ...next ) ,
231+ } ) ;
232+ const picker = createPickerModule ( { postMessage : createMockPostMessage ( ) } ) ;
233+ picker . installPickerApi ( ) ;
234+ document . body . innerHTML =
235+ '<div data-composition-id="intro"><div data-hf-inner-root="true" style="pointer-events: none">' +
236+ '<span id="t">hi</span></div></div>' ;
237+ const during : string [ ] [ ] = [ ] ;
238+ const originalElementsFromPoint = document . elementsFromPoint ;
239+ Object . defineProperty ( document , "elementsFromPoint" , {
240+ configurable : true ,
241+ value : vi . fn ( ( ) => {
242+ during . push ( adopted . flatMap ( ( sheet ) => [ ...sheet . cssRules ] . map ( ( rule ) => rule . cssText ) ) ) ;
243+ return [ document . getElementById ( "t" ) ! ] ;
244+ } ) ,
245+ } ) ;
246+ const observer = new MutationObserver ( ( ) => { } ) ;
247+ observer . observe ( document , { childList : true , subtree : true , attributes : true } ) ;
248+ const html = document . documentElement . outerHTML ;
249+
250+ const api = ( window as any ) . __HF_PICKER_API ;
251+ try {
252+ // jsdom ignores adopted sheets for style, so this checks the sheet's lifecycle, not what it picks.
253+ api . getCandidatesAtPoint ( 10 , 10 ) ;
254+ api . pickAtPoint ( 10 , 10 ) ;
255+ expect ( during ) . toHaveLength ( 2 ) ;
256+ expect ( during [ 0 ] ?. join ( ) ) . toContain ( "pointer-events: auto !important" ) ;
257+ expect ( adopted ) . toEqual ( [ ] ) ;
258+ expect ( observer . takeRecords ( ) ) . toEqual ( [ ] ) ;
259+ expect ( document . documentElement . outerHTML ) . toBe ( html ) ;
260+ // With no pass-through root there is nothing to override, so nothing is adopted and nothing restyles.
261+ ( document . querySelector ( "[data-hf-inner-root]" ) as HTMLElement ) . style . pointerEvents = "" ;
262+ api . getCandidatesAtPoint ( 10 , 10 ) ;
263+ expect ( during . at ( - 1 ) ) . toEqual ( [ ] ) ;
264+ } finally {
265+ observer . disconnect ( ) ;
266+ Object . defineProperty ( document , "elementsFromPoint" , {
267+ configurable : true ,
268+ value : originalElementsFromPoint ,
269+ } ) ;
270+ delete ( document as { adoptedStyleSheets ?: unknown } ) . adoptedStyleSheets ;
271+ }
272+ } ) ;
273+
274+ it ( "keeps a click-through overlay host passing through to what is under it" , ( ) => {
275+ const picker = createPickerModule ( { postMessage : createMockPostMessage ( ) } ) ;
276+ picker . installPickerApi ( ) ;
277+ document . body . innerHTML = `<div id="root" data-composition-id="main"><video id="aroll"></video>
278+ <div id="ovl" data-composition-id="intro_outro" data-composition-src="intro.html"
279+ style="pointer-events: none"><div data-hf-inner-root="true"><h1 id="t">Hi</h1></div></div></div>` ;
280+ const at = ( id : string ) => document . getElementById ( id ) ! ;
281+ const inner = document . querySelector ( "[data-hf-inner-root]" ) ! ;
282+ const restore = emulateHitTest ( ( ) => [ at ( "t" ) , inner , at ( "ovl" ) , at ( "aroll" ) , at ( "root" ) ] ) ;
283+ const api = ( window as any ) . __HF_PICKER_API ;
284+ try {
285+ expect ( api . getCandidatesAtPoint ( 10 , 10 ) . map ( ( c : any ) => c . selector ) ) . toEqual ( [
286+ "#aroll" ,
287+ "#root" ,
288+ ] ) ;
289+ } finally {
290+ restore ( ) ;
291+ }
292+ } ) ;
293+
294+ it ( "a section background click picks the host, and an inner root never gets a bare tag selector" , ( ) => {
295+ const picker = createPickerModule ( { postMessage : createMockPostMessage ( ) } ) ;
296+ picker . installPickerApi ( ) ;
297+ document . head . innerHTML =
298+ '<style>[data-composition-id="intro"] > [data-hf-inner-root] { pointer-events: none }</style>' ;
299+ document . body . innerHTML = `<div id="root" data-composition-id="main"><div id="host"
300+ data-composition-id="intro" data-composition-src="intro.html"><div data-hf-inner-root="true">
301+ </div></div><div data-composition-id="outro"><div data-hf-inner-root="true"></div></div></div>` ;
302+ const [ intro , outro ] = Array . from ( document . querySelectorAll ( "[data-hf-inner-root]" ) ) ;
303+ const api = ( window as any ) . __HF_PICKER_API ;
304+ let restore = emulateHitTest ( ( ) => [
305+ intro ! ,
306+ intro ! . parentElement ! ,
307+ document . getElementById ( "root" ) ! ,
308+ ] ) ;
309+ try {
310+ expect ( api . getCandidatesAtPoint ( 10 , 10 ) . map ( ( c : any ) => c . selector ) ) . toEqual ( [
311+ "#host" ,
312+ "#root" ,
313+ ] ) ;
314+ } finally {
315+ restore ( ) ;
316+ }
317+ restore = emulateHitTest ( ( ) => [ outro ! , outro ! . parentElement ! ] ) ;
318+ try {
319+ expect ( api . getCandidatesAtPoint ( 10 , 10 ) [ 0 ] ?. selector ) . toBe (
320+ '[data-composition-id="outro"] > [data-hf-inner-root]' ,
321+ ) ;
322+ } finally {
323+ restore ( ) ;
324+ }
325+ } ) ;
326+
153327 it ( "pickAtPoint returns null for invalid coords" , ( ) => {
154328 const picker = createPickerModule ( { postMessage : createMockPostMessage ( ) } ) ;
155329 picker . installPickerApi ( ) ;
0 commit comments