@@ -1012,6 +1012,129 @@ async function fetchRevealBannersTogether() {
1012
1012
} , 320 ) ;
1013
1013
}
1014
1014
1015
+ /*******************************************************************************
1016
+ * Set up expand/collapse button for primary sidebar
1017
+ */
1018
+ function setupCollapseSidebarButton ( ) {
1019
+ const button = document . getElementById ( "pst-collapse-sidebar-button" ) ;
1020
+ const sidebar = document . getElementById ( "pst-primary-sidebar" ) ;
1021
+
1022
+ // If this page rendered without the button or sidebar, then there's nothing to do.
1023
+ if ( ! button || ! sidebar ) {
1024
+ return ;
1025
+ }
1026
+
1027
+ const sidebarSections = Array . from ( sidebar . children ) ;
1028
+
1029
+ const expandTooltip = new bootstrap . Tooltip ( button , {
1030
+ title : button . querySelector ( ".pst-expand-sidebar-label" ) . textContent ,
1031
+
1032
+ // In manual testing, relying on Bootstrap to handle "hover" and "focus" was buggy.
1033
+ trigger : "manual" ,
1034
+
1035
+ placement : "left" ,
1036
+ fallbackPlacements : [ "right" ] ,
1037
+
1038
+ // Offsetting the tooltip a bit more than the default [0, 0] solves an issue
1039
+ // where the appearance of the tooltip triggers a mouseleave event which in
1040
+ // turn triggers the call to hide the tooltip. So in certain areas around
1041
+ // the button, it would appear to the user that tooltip flashes in and then
1042
+ // back out.
1043
+ offset : [ 0 , 12 ] ,
1044
+ } ) ;
1045
+
1046
+ const showTooltip = ( ) => {
1047
+ // Only show the "expand sidebar" tooltip when the sidebar is not expanded
1048
+ if ( button . getAttribute ( "aria-expanded" ) === "false" ) {
1049
+ expandTooltip . show ( ) ;
1050
+ }
1051
+ } ;
1052
+ const hideTooltip = ( ) => {
1053
+ expandTooltip . hide ( ) ;
1054
+ } ;
1055
+
1056
+ function squeezeSidebar ( prefersReducedMotion , done ) {
1057
+ // Before squeezing the sidebar, freeze the widths of its subsections.
1058
+ // Otherwise, the subsections will also narrow and cause the text in the
1059
+ // sidebar to reflow and wrap, which we don't want. This is necessary
1060
+ // because we do not remove the sidebar contents from the layout (with
1061
+ // `display: none`). Rather, we hide the contents from both sighted users
1062
+ // and screen readers (with `visibility: hidden`). This provides better
1063
+ // stability to the overall layout.
1064
+ sidebarSections . forEach (
1065
+ ( el ) => ( el . style . width = el . getBoundingClientRect ( ) . width + "px" ) ,
1066
+ ) ;
1067
+
1068
+ const afterSqueeze = ( ) => {
1069
+ // After squeezing the sidebar, set aria-expanded to false
1070
+ button . setAttribute ( "aria-expanded" , "false" ) ; // "false" is in quotes because HTML attributes are strings
1071
+
1072
+ button . dataset . busy = false ;
1073
+ } ;
1074
+
1075
+ if ( prefersReducedMotion ) {
1076
+ sidebar . classList . add ( "pst-squeeze" ) ;
1077
+ afterSqueeze ( ) ;
1078
+ } else {
1079
+ sidebar . addEventListener ( "transitionend" , function onTransitionEnd ( ) {
1080
+ afterSqueeze ( ) ;
1081
+ sidebar . removeEventListener ( "transitionend" , onTransitionEnd ) ;
1082
+ } ) ;
1083
+ sidebar . classList . add ( "pst-squeeze" ) ;
1084
+ }
1085
+ }
1086
+
1087
+ function expandSidebar ( prefersReducedMotion , done ) {
1088
+ hideTooltip ( ) ;
1089
+
1090
+ const afterExpand = ( ) => {
1091
+ // After expanding the sidebar (which may be delayed by a CSS transition),
1092
+ // unfreeze the widths of the subsections that were frozen when the sidebar
1093
+ // was squeezed.
1094
+ sidebarSections . forEach ( ( el ) => ( el . style . width = null ) ) ;
1095
+
1096
+ // After expanding the sidebar, set aria-expanded to "true" - in quotes
1097
+ // because HTML attributes are strings.
1098
+ button . setAttribute ( "aria-expanded" , "true" ) ;
1099
+
1100
+ button . dataset . busy = false ;
1101
+ } ;
1102
+
1103
+ if ( prefersReducedMotion ) {
1104
+ sidebar . classList . remove ( "pst-squeeze" ) ;
1105
+ afterExpand ( ) ;
1106
+ } else {
1107
+ sidebar . addEventListener ( "transitionend" , function onTransitionEnd ( ) {
1108
+ afterExpand ( ) ;
1109
+ sidebar . removeEventListener ( "transitionend" , onTransitionEnd ) ;
1110
+ } ) ;
1111
+ sidebar . classList . remove ( "pst-squeeze" ) ;
1112
+ }
1113
+ }
1114
+
1115
+ button . addEventListener ( "click" , ( ) => {
1116
+ if ( button . dataset . busy === "true" ) {
1117
+ return ;
1118
+ }
1119
+ button . dataset . busy = "true" ;
1120
+
1121
+ const prefersReducedMotion = window . matchMedia (
1122
+ "(prefers-reduced-motion)" , // must be in parentheses
1123
+ ) . matches ;
1124
+
1125
+ if ( button . getAttribute ( "aria-expanded" ) === "true" ) {
1126
+ squeezeSidebar ( prefersReducedMotion ) ;
1127
+ } else {
1128
+ expandSidebar ( prefersReducedMotion ) ;
1129
+ }
1130
+ } ) ;
1131
+
1132
+ button . addEventListener ( "focus" , showTooltip ) ;
1133
+ button . addEventListener ( "mouseenter" , showTooltip ) ;
1134
+ button . addEventListener ( "mouseleave" , hideTooltip ) ;
1135
+ button . addEventListener ( "blur" , hideTooltip ) ;
1136
+ }
1137
+
1015
1138
/*******************************************************************************
1016
1139
* Call functions after document loading.
1017
1140
*/
@@ -1026,6 +1149,15 @@ documentReady(addTOCInteractivity);
1026
1149
documentReady ( setupSearchButtons ) ;
1027
1150
documentReady ( setupSearchAsYouType ) ;
1028
1151
documentReady ( setupMobileSidebarKeyboardHandlers ) ;
1152
+ documentReady ( ( ) => {
1153
+ try {
1154
+ setupCollapseSidebarButton ( ) ;
1155
+ } catch ( err ) {
1156
+ // This exact error message is used in pytest tests
1157
+ console . log ( "[PST] Error setting up collapse sidebar button" ) ;
1158
+ console . error ( err ) ;
1159
+ }
1160
+ } ) ;
1029
1161
1030
1162
// Determining whether an element has scrollable content depends on stylesheets,
1031
1163
// so we're checking for the "load" event rather than "DOMContentLoaded"
0 commit comments