@@ -14,6 +14,7 @@ type PopoverState = {
1414 hiddenItems : Item [ ] ;
1515 left : number ;
1616 top : number ;
17+ anchorLeft : number ;
1718} ;
1819
1920const items : Item [ ] = [
@@ -28,16 +29,38 @@ const items: Item[] = [
2829const githubUrl =
2930 "https://github.com/SincerelyFaust/react-fit-list?tab=readme-ov-file" ;
3031const npmUrl = "https://www.npmjs.com/package/react-fit-list" ;
32+ const DESKTOP_FRAME_WIDTH = 360 ;
33+ const MOBILE_FRAME_MIN_WIDTH = 220 ;
3134
3235function Tag ( { children } : { children : React . ReactNode } ) {
3336 return < span className = "tag" > { children } </ span > ;
3437}
3538
3639function App ( ) {
3740 const [ frameWidth , setFrameWidth ] = useState ( 320 ) ;
41+ const [ mobileFrameWidth , setMobileFrameWidth ] = useState ( DESKTOP_FRAME_WIDTH ) ;
42+ const [ isMobileViewport , setIsMobileViewport ] = useState ( false ) ;
3843 const [ popover , setPopover ] = useState < PopoverState | null > ( null ) ;
3944 const frameShellRef = useRef < HTMLDivElement | null > ( null ) ;
4045 const frameRef = useRef < HTMLDivElement | null > ( null ) ;
46+ const popoverRef = useRef < HTMLDivElement | null > ( null ) ;
47+
48+ useEffect ( ( ) => {
49+ if ( typeof window === "undefined" ) return ;
50+
51+ const mediaQuery = window . matchMedia ( "(max-width: 640px)" ) ;
52+ const syncViewportMode = ( ) => setIsMobileViewport ( mediaQuery . matches ) ;
53+
54+ syncViewportMode ( ) ;
55+
56+ if ( typeof mediaQuery . addEventListener === "function" ) {
57+ mediaQuery . addEventListener ( "change" , syncViewportMode ) ;
58+ return ( ) => mediaQuery . removeEventListener ( "change" , syncViewportMode ) ;
59+ }
60+
61+ mediaQuery . addListener ( syncViewportMode ) ;
62+ return ( ) => mediaQuery . removeListener ( syncViewportMode ) ;
63+ } , [ ] ) ;
4164
4265 useEffect ( ( ) => {
4366 const frame = frameRef . current ;
@@ -55,7 +78,23 @@ function App() {
5578 return ( ) => {
5679 observer . disconnect ( ) ;
5780 } ;
58- } , [ ] ) ;
81+ } , [ isMobileViewport , mobileFrameWidth ] ) ;
82+
83+ useEffect ( ( ) => {
84+ if ( typeof window === "undefined" ) return ;
85+
86+ const syncMobileWidth = ( ) => {
87+ const nextWidth = Math . min ( DESKTOP_FRAME_WIDTH , window . innerWidth - 32 ) ;
88+ setMobileFrameWidth ( ( current ) => {
89+ if ( ! isMobileViewport ) return current ;
90+ return Math . max ( MOBILE_FRAME_MIN_WIDTH , nextWidth ) ;
91+ } ) ;
92+ } ;
93+
94+ syncMobileWidth ( ) ;
95+ window . addEventListener ( "resize" , syncMobileWidth ) ;
96+ return ( ) => window . removeEventListener ( "resize" , syncMobileWidth ) ;
97+ } , [ isMobileViewport ] ) ;
5998
6099 useEffect ( ( ) => {
61100 if ( ! popover ) return ;
@@ -80,6 +119,43 @@ function App() {
80119 } ;
81120 } , [ popover ] ) ;
82121
122+ useEffect ( ( ) => {
123+ if ( ! popover ) return ;
124+
125+ const updatePopoverPosition = ( ) => {
126+ const frameShell = frameShellRef . current ;
127+ const node = popoverRef . current ;
128+ if ( ! frameShell || ! node ) return ;
129+
130+ const shellRect = frameShell . getBoundingClientRect ( ) ;
131+ const popoverWidth = node . offsetWidth ;
132+ const viewportPadding = 8 ;
133+ const minLeft = Math . max ( viewportPadding - shellRect . left , 8 ) ;
134+ const maxLeft = Math . max (
135+ minLeft ,
136+ window . innerWidth - viewportPadding - shellRect . left - popoverWidth
137+ ) ;
138+ const nextLeft = Math . min ( Math . max ( popover . anchorLeft , minLeft ) , maxLeft ) ;
139+
140+ setPopover ( ( current ) => {
141+ if ( ! current || current . left === nextLeft ) return current ;
142+ return {
143+ ...current ,
144+ left : nextLeft ,
145+ } ;
146+ } ) ;
147+ } ;
148+
149+ updatePopoverPosition ( ) ;
150+ window . addEventListener ( "resize" , updatePopoverPosition ) ;
151+ window . addEventListener ( "scroll" , updatePopoverPosition , true ) ;
152+
153+ return ( ) => {
154+ window . removeEventListener ( "resize" , updatePopoverPosition ) ;
155+ window . removeEventListener ( "scroll" , updatePopoverPosition , true ) ;
156+ } ;
157+ } , [ popover ] ) ;
158+
83159 const openOverflowPopover = (
84160 args : FitListOverflowRenderArgs < Item > ,
85161 event : React . MouseEvent < HTMLElement >
@@ -90,6 +166,7 @@ function App() {
90166
91167 const triggerRect = event . currentTarget . getBoundingClientRect ( ) ;
92168 const frameRect = frameShell . getBoundingClientRect ( ) ;
169+ const anchorLeft = Math . max ( 8 , triggerRect . right - frameRect . left - 46 ) ;
93170
94171 setPopover ( ( current ) => {
95172 const isSame =
@@ -105,12 +182,21 @@ function App() {
105182
106183 return {
107184 hiddenItems : args . hiddenItems ,
108- left : Math . max ( 8 , triggerRect . right - frameRect . left - 46 ) ,
185+ left : anchorLeft ,
186+ anchorLeft,
109187 top : triggerRect . bottom - frameRect . top + 10 ,
110188 } ;
111189 } ) ;
112190 } ;
113191
192+ const mobileSliderMax =
193+ typeof window === "undefined"
194+ ? DESKTOP_FRAME_WIDTH
195+ : Math . max (
196+ MOBILE_FRAME_MIN_WIDTH ,
197+ Math . min ( DESKTOP_FRAME_WIDTH , window . innerWidth - 32 )
198+ ) ;
199+
114200 return (
115201 < main className = "page" >
116202 < div className = "content" >
@@ -154,16 +240,39 @@ function App() {
154240 < span className = "width-value" > { frameWidth } px</ span >
155241 </ div >
156242 < p className = "panel-description" >
157- Drag the resize handle to test how the list fits and when the
158- overflow button appears.
243+ { isMobileViewport
244+ ? "Use the slider to preview how the list behaves at smaller widths."
245+ : "Drag the resize handle to test how the list fits and when the overflow button appears." }
159246 </ p >
160247 </ div >
161248
249+ { isMobileViewport && (
250+ < label className = "mobile-width-control" >
251+ < span > Preview width</ span >
252+ < input
253+ type = "range"
254+ min = { MOBILE_FRAME_MIN_WIDTH }
255+ max = { mobileSliderMax }
256+ step = { 1 }
257+ value = { Math . min ( mobileFrameWidth , mobileSliderMax ) }
258+ onChange = { ( event ) => {
259+ setMobileFrameWidth ( Number ( event . target . value ) ) ;
260+ setPopover ( null ) ;
261+ } }
262+ aria-label = "Preview width"
263+ />
264+ </ label >
265+ ) }
266+
162267 < div ref = { frameShellRef } className = "example-frame-shell" >
163268 < div
164269 ref = { frameRef }
165270 className = "example-frame"
166- style = { { width : "min(100%, 360px)" } }
271+ style = { {
272+ width : isMobileViewport
273+ ? `min(100%, ${ Math . min ( mobileFrameWidth , mobileSliderMax ) } px)`
274+ : "min(100%, 360px)" ,
275+ } }
167276 >
168277 < div className = "frame-toolbar" aria-hidden = "true" >
169278 < span />
@@ -185,6 +294,7 @@ function App() {
185294
186295 { popover && (
187296 < div
297+ ref = { popoverRef }
188298 className = "popover"
189299 style = { { left : `${ popover . left } px` , top : `${ popover . top } px` } }
190300 role = "dialog"
0 commit comments