@@ -580,7 +580,21 @@ const preserveExpansionStates = ExecutionEnvironment.canUseDOM ? function(skipEv
580580 } , 50 ) ;
581581 } ;
582582
583- document . querySelectorAll ( 'details, .tabs-container' ) . forEach ( function ( el , index ) {
583+ // Sort elements by depth (deepest first) to process children before parents
584+ // This prevents race condition where both parent and child get highlighted
585+ const elements = Array . from ( document . querySelectorAll ( 'details, .tabs-container' ) ) ;
586+ const elementsWithDepth = elements . map ( el => {
587+ let depth = 0 ;
588+ let current = el . parentElement ;
589+ while ( current ) {
590+ if ( current . tagName === 'DETAILS' ) depth ++ ;
591+ current = current . parentElement ;
592+ }
593+ return { el, depth } ;
594+ } ) ;
595+ elementsWithDepth . sort ( ( a , b ) => b . depth - a . depth ) ; // Deepest first
596+
597+ elementsWithDepth . forEach ( function ( { el, depth } , index ) {
584598 const expansionKey = "x" + ( el . id || index ) ;
585599 const stateChangeElAll = el . querySelectorAll ( ':scope > summary, :scope > [role="tablist"] > *' ) ;
586600 const anchorLinks = el . querySelectorAll ( ':scope a[href="' + location . hash + '"]' ) ;
@@ -589,8 +603,17 @@ const preserveExpansionStates = ExecutionEnvironment.canUseDOM ? function(skipEv
589603 const targetEl = targetId ? document . getElementById ( targetId ) : null ;
590604 const containsTarget = targetEl && ( el . id === targetId || el . contains ( targetEl ) ) ;
591605
606+ // Check if this element's DIRECT summary (not nested) has a link to the target
607+ const hasSummaryLink = el . querySelectorAll ( ':scope > summary a[href="' + location . hash + '"]' ) . length > 0 ;
608+
609+ // Only highlight if no child details element already has the highlight class
610+ // (children are processed first due to depth sorting)
611+ const childDetailsHighlighted = Array . from ( el . querySelectorAll ( 'details' ) ) . some (
612+ child => child . classList . contains ( '-contains-target-link' )
613+ ) ;
614+
592615 if ( anchorLinks . length > 0 || containsTarget ) {
593- if ( el . querySelectorAll ( ':scope > summary a[href="' + location . hash + '"]' ) . length > 0 ) {
616+ if ( hasSummaryLink && ! childDetailsHighlighted ) {
594617 el . classList . add ( "-contains-target-link" ) ;
595618 }
596619 state . set ( expansionKey , 1 ) ;
@@ -610,14 +633,14 @@ const preserveExpansionStates = ExecutionEnvironment.canUseDOM ? function(skipEv
610633 const state = new URLSearchParams ( window . location . search . substring ( 1 ) ) ;
611634 if ( ( el . open && el . getAttribute ( "data-expandable" ) != "false" ) || el . classList . contains ( "tabs-container" ) ) {
612635 if ( anchorLinks . length == 1 ) {
613- if ( anchorLinks [ 0 ] . getAttribute ( "href" ) == location . hash ) {
614- el . classList . add ( "-contains-target-link" ) ;
615- }
636+ // NOTE: Highlighting is managed by initializeDetailsElement (initial load)
637+ // and universal click handler (user clicks). Do NOT manage highlights here
638+ // to avoid race conditions and double highlighting.
616639 } else {
617640 state . set ( expansionKey , i ) ;
618641 }
619642 } else {
620- el . classList . remove ( "-contains-target-link" ) ;
643+ // NOTE: Do NOT remove highlight class here - only click handler manages highlights
621644 state . delete ( expansionKey ) ;
622645 }
623646
@@ -650,6 +673,7 @@ const preserveExpansionStates = ExecutionEnvironment.canUseDOM ? function(skipEv
650673 const newHash = anchorLink . getAttribute ( "href" ) ;
651674 const targetId = newHash . substring ( 1 ) ;
652675
676+ // Remove highlight from elements that don't link to the new hash
653677 document . querySelectorAll ( ".-contains-target-link" ) . forEach ( function ( el ) {
654678 if ( el . querySelectorAll ( ':scope > summary a[href="' + newHash + '"]' ) . length == 0 ) {
655679 el . classList . remove ( "-contains-target-link" ) ;
@@ -670,17 +694,18 @@ const preserveExpansionStates = ExecutionEnvironment.canUseDOM ? function(skipEv
670694 closeOtherDetails ( keepOpenSet ) ;
671695 parentDetails . forEach ( openDetailsElement ) ;
672696
697+ // Add orange border ONLY to the innermost/direct details element
698+ if ( parentDetails . length > 0 ) {
699+ const directTargetDetails = parentDetails [ 0 ] ; // First element is innermost
700+ directTargetDetails . classList . add ( "-contains-target-link" ) ;
701+ }
702+
673703 // replaceState doesn't trigger hashchange, so we must scroll here
674- setTimeout ( ( ) => {
704+ requestAnimationFrame ( ( ) => {
675705 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 ) ;
706+ const y = window . scrollY + targetRect . top - 100 ;
707+ window . scrollTo ( { top : y , behavior : 'smooth' } ) ;
708+ } ) ;
684709 } else if ( ! el . hasAttribute ( "open" ) ) {
685710 anchorLink . parentElement . click ( ) ;
686711 }
@@ -729,6 +754,8 @@ const preserveExpansionStates = ExecutionEnvironment.canUseDOM ? function(skipEv
729754// Universal hash link handler (including TOC sidebar)
730755// Fixes Docusaurus's buggy hash navigation and enables CSS :target highlighting
731756if ( ExecutionEnvironment . canUseDOM ) {
757+ let scrollTimeout = null ;
758+
732759 document . addEventListener ( 'click' , function ( e ) {
733760 let target = e . target ;
734761 let anchor = null ;
@@ -756,26 +783,33 @@ if (ExecutionEnvironment.canUseDOM) {
756783 closeOtherDetails ( keepOpenSet ) ;
757784 parentDetails . forEach ( openDetailsElement ) ;
758785
759- setTimeout ( ( ) => {
760- const currentScrollY = window . scrollY ;
786+ // Update URL hash for history/bookmarking
787+ const oldURL = window . location . href ;
788+ window . history . pushState ( null , '' , hash ) ;
789+ const newURL = window . location . href ;
761790
762- // Setting location.hash triggers :target CSS for highlighting
763- if ( window . location . hash !== hash ) {
764- window . location . hash = hash ;
765- }
791+ // Trigger hashchange event manually for other listeners (including sidebar highlighting)
792+ window . dispatchEvent ( new HashChangeEvent ( 'hashchange' , {
793+ oldURL : oldURL ,
794+ newURL : newURL
795+ } ) ) ;
766796
767- // Cancel browser's automatic scroll
768- window . scrollTo ( 0 , currentScrollY ) ;
797+ // Cancel any pending scroll
798+ if ( scrollTimeout ) {
799+ clearTimeout ( scrollTimeout ) ;
800+ }
769801
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 ) ;
802+ // Smooth scroll to target with proper offset
803+ scrollTimeout = requestAnimationFrame ( ( ) => {
804+ const rect = targetEl . getBoundingClientRect ( ) ;
805+ const scrollTop = window . pageYOffset || document . documentElement . scrollTop ;
806+ const targetY = scrollTop + rect . top - 100 ;
807+
808+ window . scrollTo ( {
809+ top : targetY ,
810+ behavior : 'smooth'
811+ } ) ;
812+ } ) ;
779813 }
780814 }
781815 } , true ) ; // Capture phase
0 commit comments