@@ -45,16 +45,27 @@ export const stepAxis = (
4545 return { pos : p , vel : v }
4646}
4747
48+ /**
49+ * Convert one axis between pixels and a percent (0-100) of the free space
50+ * between the two walls. The stored spot is a percent, so it maps back into
51+ * view at any window size.
52+ */
53+ export const toPercent = ( pos : number , min : number , max : number ) =>
54+ max > min ? clamp ( ( ( pos - min ) / ( max - min ) ) * 100 , 0 , 100 ) : 0
55+
56+ export const toPixels = ( percent : number , min : number , max : number ) =>
57+ min + ( Math . max ( max , min ) - min ) * ( clamp ( percent , 0 , 100 ) / 100 )
58+
4859export const Trigger = ( props : {
4960 isOpen : Accessor < boolean >
5061 setIsOpen : ( isOpen : boolean ) => void
5162} ) => {
5263 const { settings, setSettings } = createDevtoolsSettings ( )
5364 const [ containerRef , setContainerRef ] = createSignal < HTMLElement > ( )
5465 const [ buttonRef , setButtonRef ] = createSignal < HTMLButtonElement > ( )
55- const [ coords , setCoords ] = createSignal < TriggerCoords | null > (
56- settings ( ) . triggerCoords ?? null ,
57- )
66+ // On-screen pixels, used by drag and throw. The stored `triggerCoords` is a
67+ // percent of the free space, see `persist` and `placeFromSettings`.
68+ const [ coords , setCoords ] = createSignal < TriggerCoords | null > ( null )
5869 const styles = createStyles ( )
5970
6071 const isFloating = createMemo ( ( ) => settings ( ) . triggerMode === 'floating' )
@@ -83,12 +94,13 @@ export const Trigger = (props: {
8394
8495 const bounds = ( el : HTMLElement ) => {
8596 const pad = edgePadding ( el )
86- const rect = el . getBoundingClientRect ( )
97+ // offsetWidth/Height ignore the hover scale, so bounds do not change
98+ // while the pointer is over the trigger.
8799 return {
88100 minX : pad ,
89101 minY : pad ,
90- maxX : window . innerWidth - rect . width - pad ,
91- maxY : window . innerHeight - rect . height - pad ,
102+ maxX : window . innerWidth - el . offsetWidth - pad ,
103+ maxY : window . innerHeight - el . offsetHeight - pad ,
92104 }
93105 }
94106
@@ -113,7 +125,18 @@ export const Trigger = (props: {
113125 }
114126 }
115127
116- const persist = ( ) => setSettings ( { triggerCoords : coords ( ) ?? undefined } )
128+ const persist = ( ) => {
129+ const el = buttonRef ( )
130+ const current = coords ( )
131+ if ( ! el || ! current ) return
132+ const b = bounds ( el )
133+ setSettings ( {
134+ triggerCoords : {
135+ x : toPercent ( current . x , b . minX , b . maxX ) ,
136+ y : toPercent ( current . y , b . minY , b . maxY ) ,
137+ } ,
138+ } )
139+ }
117140
118141 const startThrow = ( ) => {
119142 cancelThrow ( )
@@ -217,42 +240,41 @@ export const Trigger = (props: {
217240 props . setIsOpen ( ! props . isOpen ( ) )
218241 }
219242
220- // On going floating: seed coords from the button's current (fixed) position
221- // if there's no stored spot, otherwise clamp the restored spot into view
222- // (a saved position from a larger window must not load off-screen).
223- // Reads/writes coords untracked so this only runs on mode/ref changes.
243+ // Place the trigger from its stored percent, or seed the percent from the
244+ // button's current (fixed) position when there is no stored spot. Runs on
245+ // going floating and on every window resize, so the trigger keeps the same
246+ // relative spot and never ends up off-screen.
247+ let seededSpot : TriggerCoords | undefined
248+ const placeFromSettings = ( el : HTMLElement ) => {
249+ const b = bounds ( el )
250+ let spot = settings ( ) . triggerCoords ?? seededSpot
251+ if ( ! spot ) {
252+ const rect = el . getBoundingClientRect ( )
253+ spot = seededSpot = {
254+ x : toPercent ( rect . left , b . minX , b . maxX ) ,
255+ y : toPercent ( rect . top , b . minY , b . maxY ) ,
256+ }
257+ }
258+ setCoords ( {
259+ x : toPixels ( spot . x , b . minX , b . maxX ) ,
260+ y : toPixels ( spot . y , b . minY , b . maxY ) ,
261+ } )
262+ }
263+
264+ // Reads settings untracked so this only runs on mode/ref changes.
224265 createEffect ( ( ) => {
225266 if ( ! isFloating ( ) ) return
226267 const el = buttonRef ( )
227268 if ( ! el ) return
228- untrack ( ( ) => {
229- const current = coords ( )
230- if ( ! current ) {
231- const rect = el . getBoundingClientRect ( )
232- setCoords ( { x : rect . left , y : rect . top } )
233- return
234- }
235- const b = bounds ( el )
236- setCoords ( {
237- x : clamp ( current . x , b . minX , b . maxX ) ,
238- y : clamp ( current . y , b . minY , b . maxY ) ,
239- } )
240- } )
269+ untrack ( ( ) => placeFromSettings ( el ) )
241270 } )
242271
243- // Keep the trigger on screen when the window is resized.
244272 createEffect ( ( ) => {
245273 if ( ! isFloating ( ) ) return
246274 const onResize = ( ) => {
247275 const el = buttonRef ( )
248- const current = coords ( )
249- if ( ! el || ! current ) return
250- const b = bounds ( el )
251- setCoords ( {
252- x : clamp ( current . x , b . minX , b . maxX ) ,
253- y : clamp ( current . y , b . minY , b . maxY ) ,
254- } )
255- persist ( )
276+ // A throw in flight owns the position; it persists when it settles.
277+ if ( el && ! dragging && raf === undefined ) placeFromSettings ( el )
256278 }
257279 window . addEventListener ( 'resize' , onResize )
258280 onCleanup ( ( ) => window . removeEventListener ( 'resize' , onResize ) )
0 commit comments