@@ -580,7 +580,27 @@ const preserveExpansionStates = ExecutionEnvironment.canUseDOM ? function(skipEv
580580 } , 50 ) ;
581581 } ;
582582
583- document . querySelectorAll ( 'details, .tabs-container' ) . forEach ( function ( el , index ) {
583+ // Remove all existing highlights before re-initializing
584+ // This prevents stale highlights from previous navigation
585+ document . querySelectorAll ( '.-contains-target-link' ) . forEach ( el => {
586+ el . classList . remove ( '-contains-target-link' ) ;
587+ } ) ;
588+
589+ // Sort elements by depth (deepest first) to process children before parents
590+ // This prevents race condition where both parent and child get highlighted
591+ const elements = Array . from ( document . querySelectorAll ( 'details, .tabs-container' ) ) ;
592+ const elementsWithDepth = elements . map ( el => {
593+ let depth = 0 ;
594+ let current = el . parentElement ;
595+ while ( current ) {
596+ if ( current . tagName === 'DETAILS' ) depth ++ ;
597+ current = current . parentElement ;
598+ }
599+ return { el, depth } ;
600+ } ) ;
601+ elementsWithDepth . sort ( ( a , b ) => b . depth - a . depth ) ; // Deepest first
602+
603+ elementsWithDepth . forEach ( function ( { el, depth } , index ) {
584604 const expansionKey = "x" + ( el . id || index ) ;
585605 const stateChangeElAll = el . querySelectorAll ( ':scope > summary, :scope > [role="tablist"] > *' ) ;
586606 const anchorLinks = el . querySelectorAll ( ':scope a[href="' + location . hash + '"]' ) ;
@@ -589,8 +609,17 @@ const preserveExpansionStates = ExecutionEnvironment.canUseDOM ? function(skipEv
589609 const targetEl = targetId ? document . getElementById ( targetId ) : null ;
590610 const containsTarget = targetEl && ( el . id === targetId || el . contains ( targetEl ) ) ;
591611
612+ // Check if this element's DIRECT summary (not nested) has a link to the target
613+ const hasSummaryLink = el . querySelectorAll ( ':scope > summary a[href="' + location . hash + '"]' ) . length > 0 ;
614+
615+ // Only highlight if no child details element already has the highlight class
616+ // (children are processed first due to depth sorting)
617+ const childDetailsHighlighted = Array . from ( el . querySelectorAll ( 'details' ) ) . some (
618+ child => child . classList . contains ( '-contains-target-link' )
619+ ) ;
620+
592621 if ( anchorLinks . length > 0 || containsTarget ) {
593- if ( el . querySelectorAll ( ':scope > summary a[href="' + location . hash + '"]' ) . length > 0 ) {
622+ if ( hasSummaryLink && ! childDetailsHighlighted ) {
594623 el . classList . add ( "-contains-target-link" ) ;
595624 }
596625 state . set ( expansionKey , 1 ) ;
@@ -600,7 +629,6 @@ const preserveExpansionStates = ExecutionEnvironment.canUseDOM ? function(skipEv
600629 // NOTE: Initial page load scrolling is handled by ConfigNavigationClient
601630 }
602631 } else {
603- el . classList . remove ( "-contains-target-link" ) ;
604632 state . delete ( expansionKey ) ;
605633 }
606634
@@ -610,14 +638,14 @@ const preserveExpansionStates = ExecutionEnvironment.canUseDOM ? function(skipEv
610638 const state = new URLSearchParams ( window . location . search . substring ( 1 ) ) ;
611639 if ( ( el . open && el . getAttribute ( "data-expandable" ) != "false" ) || el . classList . contains ( "tabs-container" ) ) {
612640 if ( anchorLinks . length == 1 ) {
613- if ( anchorLinks [ 0 ] . getAttribute ( "href" ) == location . hash ) {
614- el . classList . add ( "-contains-target-link" ) ;
615- }
641+ // NOTE: Highlighting is managed by initializeDetailsElement (initial load)
642+ // and universal click handler (user clicks). Do NOT manage highlights here
643+ // to avoid race conditions and double highlighting.
616644 } else {
617645 state . set ( expansionKey , i ) ;
618646 }
619647 } else {
620- el . classList . remove ( "-contains-target-link" ) ;
648+ // NOTE: Do NOT remove highlight class here - only click handler manages highlights
621649 state . delete ( expansionKey ) ;
622650 }
623651
@@ -650,6 +678,7 @@ const preserveExpansionStates = ExecutionEnvironment.canUseDOM ? function(skipEv
650678 const newHash = anchorLink . getAttribute ( "href" ) ;
651679 const targetId = newHash . substring ( 1 ) ;
652680
681+ // Remove highlight from elements that don't link to the new hash
653682 document . querySelectorAll ( ".-contains-target-link" ) . forEach ( function ( el ) {
654683 if ( el . querySelectorAll ( ':scope > summary a[href="' + newHash + '"]' ) . length == 0 ) {
655684 el . classList . remove ( "-contains-target-link" ) ;
@@ -670,17 +699,18 @@ const preserveExpansionStates = ExecutionEnvironment.canUseDOM ? function(skipEv
670699 closeOtherDetails ( keepOpenSet ) ;
671700 parentDetails . forEach ( openDetailsElement ) ;
672701
702+ // Add orange border ONLY to the innermost/direct details element
703+ if ( parentDetails . length > 0 ) {
704+ const directTargetDetails = parentDetails [ 0 ] ; // First element is innermost
705+ directTargetDetails . classList . add ( "-contains-target-link" ) ;
706+ }
707+
673708 // replaceState doesn't trigger hashchange, so we must scroll here
674- setTimeout ( ( ) => {
709+ requestAnimationFrame ( ( ) => {
675710 const targetRect = targetEl . getBoundingClientRect ( ) ;
676- if ( targetRect . top !== 0 || targetRect . left !== 0 ) {
677- window . scroll ( {
678- behavior : 'smooth' ,
679- left : 0 ,
680- top : window . scrollY + targetRect . top - 280
681- } ) ;
682- }
683- } , 150 ) ;
711+ const y = window . scrollY + targetRect . top - 100 ;
712+ window . scrollTo ( { top : y , behavior : 'smooth' } ) ;
713+ } ) ;
684714 } else if ( ! el . hasAttribute ( "open" ) ) {
685715 anchorLink . parentElement . click ( ) ;
686716 }
@@ -729,6 +759,8 @@ const preserveExpansionStates = ExecutionEnvironment.canUseDOM ? function(skipEv
729759// Universal hash link handler (including TOC sidebar)
730760// Fixes Docusaurus's buggy hash navigation and enables CSS :target highlighting
731761if ( ExecutionEnvironment . canUseDOM ) {
762+ let scrollTimeout = null ;
763+
732764 document . addEventListener ( 'click' , function ( e ) {
733765 let target = e . target ;
734766 let anchor = null ;
@@ -756,26 +788,43 @@ if (ExecutionEnvironment.canUseDOM) {
756788 closeOtherDetails ( keepOpenSet ) ;
757789 parentDetails . forEach ( openDetailsElement ) ;
758790
759- setTimeout ( ( ) => {
760- const currentScrollY = window . scrollY ;
791+ // Remove all existing highlights first
792+ document . querySelectorAll ( '.-contains-target-link' ) . forEach ( el => {
793+ el . classList . remove ( '-contains-target-link' ) ;
794+ } ) ;
761795
762- // Setting location.hash triggers :target CSS for highlighting
763- if ( window . location . hash !== hash ) {
764- window . location . hash = hash ;
765- }
796+ // Add highlight to the innermost parent details element
797+ if ( parentDetails . length > 0 ) {
798+ parentDetails [ 0 ] . classList . add ( '-contains-target-link' ) ;
799+ }
766800
767- // Cancel browser's automatic scroll
768- window . scrollTo ( 0 , currentScrollY ) ;
801+ // Update URL hash for history/bookmarking
802+ const oldURL = window . location . href ;
803+ window . history . pushState ( null , '' , hash ) ;
804+ const newURL = window . location . href ;
769805
770- // Do our own controlled scroll
771- setTimeout ( ( ) => {
772- const rect = targetEl . getBoundingClientRect ( ) ;
773- if ( rect . top !== 0 || rect . left !== 0 ) {
774- const y = window . scrollY + rect . top - 280 ;
775- window . scrollTo ( { top : y , behavior : 'smooth' } ) ;
776- }
777- } , 50 ) ;
778- } , 150 ) ;
806+ // Trigger hashchange event manually for other listeners (including sidebar highlighting)
807+ window . dispatchEvent ( new HashChangeEvent ( 'hashchange' , {
808+ oldURL : oldURL ,
809+ newURL : newURL
810+ } ) ) ;
811+
812+ // Cancel any pending scroll
813+ if ( scrollTimeout ) {
814+ clearTimeout ( scrollTimeout ) ;
815+ }
816+
817+ // Smooth scroll to target with proper offset
818+ scrollTimeout = requestAnimationFrame ( ( ) => {
819+ const rect = targetEl . getBoundingClientRect ( ) ;
820+ const scrollTop = window . pageYOffset || document . documentElement . scrollTop ;
821+ const targetY = scrollTop + rect . top - 100 ;
822+
823+ window . scrollTo ( {
824+ top : targetY ,
825+ behavior : 'smooth'
826+ } ) ;
827+ } ) ;
779828 }
780829 }
781830 } , true ) ; // Capture phase
0 commit comments