@@ -10,6 +10,7 @@ import { makeStyles, mergeClasses, shorthands } from '@griffel/react';
1010import { useMergedRefs } from '@fluentui/react-utilities' ;
1111import { tokens } from '@fluentui/react-theme' ;
1212import { storiesOf } from '@storybook/react' ;
13+ import * as ReactDOM from 'react-dom' ;
1314import { useFluent_unstable as useFluent } from '@fluentui/react-shared-contexts' ;
1415import { Steps , StoryWright } from 'storywright' ;
1516
@@ -481,7 +482,6 @@ const DisableTether = () => {
481482 position : 'above' ,
482483 // eslint-disable-next-line @typescript-eslint/ban-ts-comment
483484 // @ts -ignore
484- // eslint-disable-next-line @typescript-eslint/naming-convention
485485 unstable_disableTether : 'all' ,
486486 } ) ;
487487
@@ -710,6 +710,129 @@ const FallbackPositioning = () => {
710710 ) ;
711711} ;
712712
713+ const ScrollJump : React . FC = ( ) => {
714+ const buttonRef = React . useRef < HTMLButtonElement > ( null ) ;
715+ const [ open , setOpen ] = React . useState ( false ) ;
716+
717+ React . useEffect ( ( ) => {
718+ if ( open && buttonRef . current ) {
719+ const scrollY = window . scrollY ;
720+ buttonRef . current . focus ( ) ;
721+
722+ setTimeout ( ( ) => {
723+ const element = document . querySelector < HTMLElement > (
724+ scrollY === window . scrollY ? '#test-passed' : '#test-failed' ,
725+ ) ;
726+
727+ if ( element ) {
728+ element . style . setProperty ( 'display' , 'block' ) ;
729+ element . setAttribute ( 'id' , 'test-completed' ) ;
730+ }
731+ } , 500 ) ;
732+ }
733+ } , [ open ] ) ;
734+
735+ const { containerRef, targetRef } = usePositioning ( {
736+ position : 'above' ,
737+ align : 'start' ,
738+ } ) ;
739+
740+ const floating = ReactDOM . createPortal (
741+ < Box ref = { containerRef } >
742+ Focusable element < button ref = { buttonRef } > Focus me</ button >
743+ </ Box > ,
744+ document . body ,
745+ ) ;
746+
747+ return (
748+ < >
749+ < div
750+ style = { {
751+ position : 'fixed' ,
752+ top : 10 ,
753+ left : 100 ,
754+ right : 100 ,
755+
756+ background : 'white' ,
757+ } }
758+ >
759+ < p style = { { fontWeight : 20 , border : '8px dotted magenta' , padding : 10 , margin : 0 } } >
760+ This example simulates a scroll jump on autofocus when opening a floating element. The example uses a layout
761+ effect to focus on the content of the floating box before usePopper is called. This results in the focus
762+ executing before the layout effect to position the floating is executed. The scroll jump is fixed internally
763+ in usePositioning by using position: fixed on the floating element before it is first positioned.
764+ </ p >
765+ < div
766+ id = "test-failed"
767+ style = { {
768+ border : '8px dotted magenta' ,
769+ borderTop : 'none' ,
770+ padding : 10 ,
771+ fontSize : 20 ,
772+ color : 'red' ,
773+ display : 'none' ,
774+ } }
775+ >
776+ Test failed, scroll jump occurred 💥
777+ </ div >
778+ < div
779+ id = "test-passed"
780+ style = { {
781+ border : '8px dotted magenta' ,
782+ borderTop : 'none' ,
783+ padding : 10 ,
784+ fontSize : 20 ,
785+ color : 'green' ,
786+ display : 'none' ,
787+ } }
788+ >
789+ Test passed ✅
790+ </ div >
791+ </ div >
792+
793+ < div
794+ style = { {
795+ display : 'flex' ,
796+ flexDirection : 'column' ,
797+ background : 'repeating-linear-gradient(45deg, transparent, transparent 10px, #ccc 10px, #ccc 20px)' ,
798+ } }
799+ >
800+ < div style = { { background : 'white' , border : '4px dotted green' , margin : 10 } } >
801+ < div
802+ style = { {
803+ display : 'flex' ,
804+
805+ background :
806+ 'repeating-linear-gradient(135deg, transparent, transparent 15px, #0f6cbd 10px, #0f6cbd 20px)' ,
807+ margin : 30 ,
808+ height : '100vh' ,
809+ } }
810+ />
811+
812+ < div style = { { border : '4px dotted black' , padding : 10 , margin : 100 } } >
813+ < button id = "target" ref = { targetRef } onClick = { ( ) => setOpen ( s => ! s ) } >
814+ Target
815+ </ button >
816+ </ div >
817+
818+ < div
819+ style = { {
820+ display : 'flex' ,
821+
822+ background :
823+ 'repeating-linear-gradient(135deg, transparent, transparent 15px, #0f6cbd 10px, #0f6cbd 20px)' ,
824+ margin : 30 ,
825+ height : '100vh' ,
826+ } }
827+ />
828+
829+ { open && floating }
830+ </ div >
831+ </ div >
832+ </ >
833+ ) ;
834+ } ;
835+
713836storiesOf ( 'Positioning' , module )
714837 . addDecorator ( story => (
715838 < div
@@ -765,3 +888,16 @@ storiesOf('Positioning', module)
765888 ( ) => < PositionAndAlignProps positionFixed useTransform = { false } /> ,
766889 { includeRtl : true } ,
767890 ) ;
891+
892+ storiesOf ( 'Positioning (no decorator)' , module ) . addStory ( 'scroll jumps' , ( ) => (
893+ < StoryWright
894+ steps = { new Steps ( )
895+ . focus ( '#target' )
896+ . click ( '#target' )
897+ . wait ( '#test-completed' )
898+ . snapshot ( 'positions without scroll jump' )
899+ . end ( ) }
900+ >
901+ < ScrollJump />
902+ </ StoryWright >
903+ ) ) ;
0 commit comments