@@ -61,7 +61,9 @@ interface ToolbarProps {
6161
6262export const Toolbar : Component < ToolbarProps > = ( props ) => {
6363 let containerRef : HTMLDivElement | undefined ;
64+ let expandableButtonsRef : HTMLDivElement | undefined ;
6465 let unfreezeUpdatesCallback : ( ( ) => void ) | null = null ;
66+ let lastKnownExpandableWidth = 0 ;
6567
6668 const [ isVisible , setIsVisible ] = createSignal ( false ) ;
6769 const [ isCollapsed , setIsCollapsed ] = createSignal ( false ) ;
@@ -83,6 +85,7 @@ export const Toolbar: Component<ToolbarProps> = (props) => {
8385 const [ isToggleTooltipVisible , setIsToggleTooltipVisible ] =
8486 createSignal ( false ) ;
8587 const [ isShakeTooltipVisible , setIsShakeTooltipVisible ] = createSignal ( false ) ;
88+ const [ isToggleAnimating , setIsToggleAnimating ] = createSignal ( false ) ;
8689
8790 const tooltipPosition = ( ) => ( snapEdge ( ) === "top" ? "bottom" : "top" ) ;
8891
@@ -439,9 +442,73 @@ export const Toolbar: Component<ToolbarProps> = (props) => {
439442 } , TOOLBAR_COLLAPSE_ANIMATION_DURATION_MS ) ;
440443 } ) ;
441444
442- const handleToggleEnabled = createDragAwareHandler ( ( ) =>
443- props . onToggleEnabled ?.( ) ,
444- ) ;
445+ const handleToggleEnabled = createDragAwareHandler ( ( ) => {
446+ const isCurrentlyEnabled = Boolean ( props . enabled ) ;
447+ const edge = snapEdge ( ) ;
448+ const preTogglePosition = position ( ) ;
449+ const expandableWidth = lastKnownExpandableWidth ;
450+ const shouldCompensatePosition = expandableWidth > 0 && edge !== "left" ;
451+
452+ if ( shouldCompensatePosition ) {
453+ setIsToggleAnimating ( true ) ;
454+ }
455+
456+ props . onToggleEnabled ?.( ) ;
457+
458+ if ( expandableWidth > 0 ) {
459+ const widthChange = isCurrentlyEnabled ? - expandableWidth : expandableWidth ;
460+ expandedDimensions = {
461+ width : expandedDimensions . width + widthChange ,
462+ height : expandedDimensions . height ,
463+ } ;
464+ }
465+
466+ if ( shouldCompensatePosition ) {
467+ const viewport = getVisualViewport ( ) ;
468+ const positionOffset = isCurrentlyEnabled ? expandableWidth : - expandableWidth ;
469+ const clampMin = viewport . offsetLeft + TOOLBAR_SNAP_MARGIN_PX ;
470+ const clampMax = viewport . offsetLeft + viewport . width - expandedDimensions . width - TOOLBAR_SNAP_MARGIN_PX ;
471+ const compensatedX = clampToViewport (
472+ preTogglePosition . x + positionOffset ,
473+ clampMin ,
474+ clampMax ,
475+ ) ;
476+
477+ setPosition ( { x : compensatedX , y : preTogglePosition . y } ) ;
478+
479+ clearTimeout ( toggleAnimationTimeout ) ;
480+ toggleAnimationTimeout = setTimeout ( ( ) => {
481+ setIsToggleAnimating ( false ) ;
482+ const newRatio = getRatioFromPosition (
483+ edge ,
484+ position ( ) . x ,
485+ position ( ) . y ,
486+ expandedDimensions . width ,
487+ expandedDimensions . height ,
488+ ) ;
489+ setPositionRatio ( newRatio ) ;
490+ saveAndNotify ( {
491+ edge,
492+ ratio : newRatio ,
493+ collapsed : isCollapsed ( ) ,
494+ enabled : ! isCurrentlyEnabled ,
495+ } ) ;
496+ } , TOOLBAR_COLLAPSE_ANIMATION_DURATION_MS ) ;
497+ } else if ( ! isCurrentlyEnabled && lastKnownExpandableWidth === 0 ) {
498+ // HACK: When toolbar mounts disabled, expandable buttons are hidden (grid-cols-[0fr])
499+ // so we can't measure their width. Learn it after the first enable animation completes.
500+ clearTimeout ( toggleAnimationTimeout ) ;
501+ toggleAnimationTimeout = setTimeout ( ( ) => {
502+ if ( expandableButtonsRef ) {
503+ lastKnownExpandableWidth = expandableButtonsRef . offsetWidth ;
504+ }
505+ const rect = containerRef ?. getBoundingClientRect ( ) ;
506+ if ( rect ) {
507+ expandedDimensions = { width : rect . width , height : rect . height } ;
508+ }
509+ } , TOOLBAR_COLLAPSE_ANIMATION_DURATION_MS ) ;
510+ }
511+ } ) ;
445512
446513 const getSnapPosition = (
447514 currentX : number ,
@@ -725,6 +792,7 @@ export const Toolbar: Component<ToolbarProps> = (props) => {
725792 let resizeTimeout : ReturnType < typeof setTimeout > | undefined ;
726793 let collapseAnimationTimeout : ReturnType < typeof setTimeout > | undefined ;
727794 let snapAnimationTimeout : ReturnType < typeof setTimeout > | undefined ;
795+ let toggleAnimationTimeout : ReturnType < typeof setTimeout > | undefined ;
728796
729797 const handleResize = ( ) => {
730798 if ( isDragging ( ) ) return ;
@@ -815,10 +883,14 @@ export const Toolbar: Component<ToolbarProps> = (props) => {
815883 setPosition ( defaultPosition ) ;
816884 }
817885
886+ if ( props . enabled && expandableButtonsRef ) {
887+ lastKnownExpandableWidth = expandableButtonsRef . offsetWidth ;
888+ }
889+
818890 if ( props . onSubscribeToStateChanges ) {
819891 const unsubscribe = props . onSubscribeToStateChanges (
820892 ( state : ToolbarState ) => {
821- if ( isCollapseAnimating ( ) ) return ;
893+ if ( isCollapseAnimating ( ) || isToggleAnimating ( ) ) return ;
822894
823895 const rect = containerRef ?. getBoundingClientRect ( ) ;
824896 if ( ! rect ) return ;
@@ -835,18 +907,14 @@ export const Toolbar: Component<ToolbarProps> = (props) => {
835907 calculateExpandedPositionFromCollapsed ( collapsedPos , state . edge ) ;
836908 setPosition ( newPos ) ;
837909 setPositionRatio ( newRatio ) ;
838- if ( collapseAnimationTimeout ) {
839- clearTimeout ( collapseAnimationTimeout ) ;
840- }
910+ clearTimeout ( collapseAnimationTimeout ) ;
841911 collapseAnimationTimeout = setTimeout ( ( ) => {
842912 setIsCollapseAnimating ( false ) ;
843913 } , TOOLBAR_COLLAPSE_ANIMATION_DURATION_MS ) ;
844914 } else {
845915 if ( didCollapsedChange ) {
846916 setIsCollapseAnimating ( true ) ;
847- if ( collapseAnimationTimeout ) {
848- clearTimeout ( collapseAnimationTimeout ) ;
849- }
917+ clearTimeout ( collapseAnimationTimeout ) ;
850918 collapseAnimationTimeout = setTimeout ( ( ) => {
851919 setIsCollapseAnimating ( false ) ;
852920 } , TOOLBAR_COLLAPSE_ANIMATION_DURATION_MS ) ;
@@ -886,18 +954,11 @@ export const Toolbar: Component<ToolbarProps> = (props) => {
886954 window . visualViewport ?. removeEventListener ( "scroll" , handleResize ) ;
887955 window . removeEventListener ( "pointermove" , handleWindowPointerMove ) ;
888956 window . removeEventListener ( "pointerup" , handleWindowPointerUp ) ;
889- if ( resizeTimeout ) {
890- clearTimeout ( resizeTimeout ) ;
891- }
892- if ( collapseAnimationTimeout ) {
893- clearTimeout ( collapseAnimationTimeout ) ;
894- }
895- if ( shakeTooltipTimeout ) {
896- clearTimeout ( shakeTooltipTimeout ) ;
897- }
898- if ( snapAnimationTimeout ) {
899- clearTimeout ( snapAnimationTimeout ) ;
900- }
957+ clearTimeout ( resizeTimeout ) ;
958+ clearTimeout ( collapseAnimationTimeout ) ;
959+ clearTimeout ( shakeTooltipTimeout ) ;
960+ clearTimeout ( snapAnimationTimeout ) ;
961+ clearTimeout ( toggleAnimationTimeout ) ;
901962 unfreezeUpdatesCallback ?.( ) ;
902963 } ) ;
903964
@@ -923,7 +984,7 @@ export const Toolbar: Component<ToolbarProps> = (props) => {
923984 if ( isSnapping ( ) ) {
924985 return "transition-[transform,opacity] duration-300 ease-out" ;
925986 }
926- if ( isCollapseAnimating ( ) ) {
987+ if ( isCollapseAnimating ( ) || isToggleAnimating ( ) ) {
927988 return "transition-[transform,opacity] duration-150 ease-out" ;
928989 }
929990 return "transition-opacity duration-300 ease-out" ;
@@ -1013,92 +1074,94 @@ export const Toolbar: Component<ToolbarProps> = (props) => {
10131074 ) }
10141075 >
10151076 < div class = "flex items-center min-w-0" >
1016- < div
1017- class = { cn (
1018- "grid transition-all duration-150 ease-out" ,
1019- props . enabled
1020- ? "grid-cols-[1fr] opacity-100"
1021- : "grid-cols-[0fr] opacity-0" ,
1022- ) }
1023- >
1024- < div class = "relative overflow-visible min-w-0" >
1025- { /* HACK: Native events with stopImmediatePropagation prevent page-level dropdowns from closing */ }
1026- < button
1027- data-react-grab-ignore-events
1028- data-react-grab-toolbar-toggle
1029- class = "contain-layout flex items-center justify-center cursor-pointer interactive-scale touch-hitbox mr-1.5"
1030- on :pointerdown = { ( event ) => {
1031- stopEventPropagation ( event ) ;
1032- handlePointerDown ( event ) ;
1033- } }
1034- on :mousedown = { stopEventPropagation }
1035- onClick = { ( event ) => {
1036- setIsSelectTooltipVisible ( false ) ;
1037- handleToggle ( event ) ;
1038- } }
1039- { ...createFreezeHandlers ( setIsSelectTooltipVisible ) }
1040- >
1041- < IconSelect
1042- size = { 14 }
1043- class = { cn (
1044- "transition-colors" ,
1045- getToolbarIconColor (
1046- Boolean ( props . isActive ) && ! props . isCommentMode ,
1047- Boolean ( props . isCommentMode ) ,
1048- ) ,
1049- ) }
1050- />
1051- </ button >
1052- < Tooltip
1053- visible = { isSelectTooltipVisible ( ) && ! isCollapsed ( ) }
1054- position = { tooltipPosition ( ) }
1055- >
1056- Select
1057- </ Tooltip >
1077+ < div ref = { expandableButtonsRef } class = "flex items-center" >
1078+ < div
1079+ class = { cn (
1080+ "grid transition-all duration-150 ease-out" ,
1081+ props . enabled
1082+ ? "grid-cols-[1fr] opacity-100"
1083+ : "grid-cols-[0fr] opacity-0" ,
1084+ ) }
1085+ >
1086+ < div class = "relative overflow-visible min-w-0" >
1087+ { /* HACK: Native events with stopImmediatePropagation prevent page-level dropdowns from closing */ }
1088+ < button
1089+ data-react-grab-ignore-events
1090+ data-react-grab-toolbar-toggle
1091+ class = "contain-layout flex items-center justify-center cursor-pointer interactive-scale touch-hitbox mr-1.5"
1092+ on :pointerdown = { ( event ) => {
1093+ stopEventPropagation ( event ) ;
1094+ handlePointerDown ( event ) ;
1095+ } }
1096+ on :mousedown = { stopEventPropagation }
1097+ onClick = { ( event ) => {
1098+ setIsSelectTooltipVisible ( false ) ;
1099+ handleToggle ( event ) ;
1100+ } }
1101+ { ...createFreezeHandlers ( setIsSelectTooltipVisible ) }
1102+ >
1103+ < IconSelect
1104+ size = { 14 }
1105+ class = { cn (
1106+ "transition-colors" ,
1107+ getToolbarIconColor (
1108+ Boolean ( props . isActive ) && ! props . isCommentMode ,
1109+ Boolean ( props . isCommentMode ) ,
1110+ ) ,
1111+ ) }
1112+ />
1113+ </ button >
1114+ < Tooltip
1115+ visible = { isSelectTooltipVisible ( ) && ! isCollapsed ( ) }
1116+ position = { tooltipPosition ( ) }
1117+ >
1118+ Select
1119+ </ Tooltip >
1120+ </ div >
10581121 </ div >
1059- </ div >
1060- < div
1061- class = { cn (
1062- "grid transition-all duration-150 ease-out" ,
1063- props . enabled
1064- ? "grid-cols-[1fr ] opacity-100"
1065- : "grid-cols-[0fr] opacity-0" ,
1066- ) }
1067- >
1068- < div class = "relative overflow-visible min-w-0" >
1069- { /* HACK: Native events with stopImmediatePropagation prevent page-level dropdowns from closing */ }
1070- < button
1071- data-react-grab-ignore-events
1072- data-react-grab-toolbar-comment
1073- class = "contain-layout flex items-center justify-center cursor-pointer interactive-scale touch-hitbox mr-1.5"
1074- on : pointerdown = { ( event ) => {
1075- stopEventPropagation ( event ) ;
1076- handlePointerDown ( event ) ;
1077- } }
1078- on : mousedown = { stopEventPropagation }
1079- onClick = { ( event ) => {
1080- setIsCommentTooltipVisible ( false ) ;
1081- handleComment ( event ) ;
1082- } }
1083- { ... createFreezeHandlers ( setIsCommentTooltipVisible ) }
1084- >
1085- < IconComment
1086- size = { 14 }
1087- class = { cn (
1088- "transition-colors" ,
1089- getToolbarIconColor (
1090- Boolean ( props . isCommentMode ) ,
1091- Boolean ( props . isActive ) && ! props . isCommentMode ,
1092- ) ,
1093- ) }
1094- / >
1095- </ button >
1096- < Tooltip
1097- visible = { isCommentTooltipVisible ( ) && ! isCollapsed ( ) }
1098- position = { tooltipPosition ( ) }
1099- >
1100- Comment
1101- </ Tooltip >
1122+ < div
1123+ class = { cn (
1124+ "grid transition-all duration-150 ease-out" ,
1125+ props . enabled
1126+ ? "grid-cols-[1fr] opacity-100"
1127+ : "grid-cols-[0fr ] opacity-0" ,
1128+ ) }
1129+ >
1130+ < div class = "relative overflow-visible min-w-0" >
1131+ { /* HACK: Native events with stopImmediatePropagation prevent page-level dropdowns from closing */ }
1132+ < button
1133+ data-react-grab-ignore-events
1134+ data-react-grab-toolbar-comment
1135+ class = "contain-layout flex items-center justify-center cursor-pointer interactive-scale touch-hitbox mr-1.5"
1136+ on : pointerdown = { ( event ) => {
1137+ stopEventPropagation ( event ) ;
1138+ handlePointerDown ( event ) ;
1139+ } }
1140+ on : mousedown = { stopEventPropagation }
1141+ onClick = { ( event ) => {
1142+ setIsCommentTooltipVisible ( false ) ;
1143+ handleComment ( event ) ;
1144+ } }
1145+ { ... createFreezeHandlers ( setIsCommentTooltipVisible ) }
1146+ >
1147+ < IconComment
1148+ size = { 14 }
1149+ class = { cn (
1150+ "transition-colors" ,
1151+ getToolbarIconColor (
1152+ Boolean ( props . isCommentMode ) ,
1153+ Boolean ( props . isActive ) && ! props . isCommentMode ,
1154+ ) ,
1155+ ) }
1156+ />
1157+ </ button >
1158+ < Tooltip
1159+ visible = { isCommentTooltipVisible ( ) && ! isCollapsed ( ) }
1160+ position = { tooltipPosition ( ) }
1161+ >
1162+ Comment
1163+ </ Tooltip >
1164+ </ div >
11021165 </ div >
11031166 </ div >
11041167 < div class = "relative shrink-0 overflow-visible" >
0 commit comments