diff --git a/src/App/hooks/useLayoutMeasurements.js b/src/App/hooks/useLayoutMeasurements.js index eba5b0f0..1daab048 100755 --- a/src/App/hooks/useLayoutMeasurements.js +++ b/src/App/hooks/useLayoutMeasurements.js @@ -11,6 +11,15 @@ const ATTRIBUTIONS_STACKED_CLASS = 'im-o-app__attributions--stacked' const buttonHeight = (ref) => ref?.current?.offsetHeight ?? 0 const buttonWidth = (ref) => ref?.current?.offsetWidth ?? 0 +// A top column whose children are all display:none (hiddenWhen buttons, or items hidden by a +// plugin's exclusive-control CSS) isn't :empty, so it keeps its trailing padding-bottom — which +// would push the side column below it down by a gap. Treat it as empty instead. +const topColHeight = (col) => { + const children = Array.from(col.children) + const isAllHidden = children.length > 0 && children.every(child => child.getClientRects().length === 0) + return isAllHidden ? 0 : col.offsetHeight +} + // Max of both sides, so centred content doesn't lean toward the emptier one. const symmetricWidth = (left, right) => left || right ? Math.max(left, right) : 0 @@ -191,7 +200,7 @@ export function calculateLayout (layoutRefs, breakpoint) { const { rise: attributionsLift } = applyAttributionsLayout({ appContainer, bottom, attributions, dividerGap, primaryGap }) // === Left container offsets === - const leftOffsetTop = sideOffsetTop(topLeftCol.offsetHeight) + const leftOffsetTop = sideOffsetTop(topColHeight(topLeftCol)) const leftColumnHeight = bottom.offsetTop - leftOffsetTop - dividerGap appContainer.style.setProperty('--left-offset-top', `${leftOffsetTop}px`) appContainer.style.setProperty('--left-offset-bottom', `${main.offsetHeight - bottom.offsetTop + dividerGap}px`) @@ -200,7 +209,7 @@ export function calculateLayout (layoutRefs, breakpoint) { // === Right container offsets === (mirrors the left formula) const bottomRightHeight = buttonHeight(bottomRightRef) const bottomContainerPad = main.offsetHeight - bottom.offsetTop - bottom.offsetHeight - const rightOffsetTop = sideOffsetTop(topRightCol.offsetHeight) + const rightOffsetTop = sideOffsetTop(topColHeight(topRightCol)) const rightEffectiveBottom = bottom.offsetTop + bottom.offsetHeight - bottomRightHeight - attributionsLift const rightColumnHeight = rightEffectiveBottom - rightOffsetTop - dividerGap appContainer.style.setProperty('--right-offset-top', `${rightOffsetTop}px`) diff --git a/src/App/hooks/useLayoutMeasurements.test.js b/src/App/hooks/useLayoutMeasurements.test.js index 936e723b..d4d54ec0 100644 --- a/src/App/hooks/useLayoutMeasurements.test.js +++ b/src/App/hooks/useLayoutMeasurements.test.js @@ -77,6 +77,22 @@ describe('useLayoutMeasurements', () => { expect(layoutRefs.appContainerRef.current.style.setProperty).not.toHaveBeenCalled() }) + test('treats a top column whose children are all hidden as empty, ignoring its trailing padding', () => { + const { layoutRefs } = setup({ refs: { topRightCol: { offsetHeight: 10 }, top: { offsetTop: 15 } } }) + // jsdom gives every element empty client rects, i.e. the same as display:none + appendChild(layoutRefs.topRightColRef.current) + renderHook(() => useLayoutMeasurements()) + expect(layoutRefs.appContainerRef.current.style.setProperty).toHaveBeenCalledWith('--right-offset-top', '15px') + }) + + test('measures a top column normally when any child is rendered', () => { + const { layoutRefs } = setup({ refs: { topRightCol: { offsetHeight: 50 }, top: { offsetTop: 15 } } }) + appendChild(layoutRefs.topRightColRef.current) + appendChild(layoutRefs.topRightColRef.current, { getClientRects: () => [{}] }) + renderHook(() => useLayoutMeasurements()) + expect(layoutRefs.appContainerRef.current.style.setProperty).toHaveBeenCalledWith('--right-offset-top', '65px') + }) + test('calculates and sets all CSS custom properties', () => { const { layoutRefs } = setup() renderHook(() => useLayoutMeasurements()) diff --git a/src/config/appConfig.js b/src/config/appConfig.js index b4278913..6d82d7fa 100755 --- a/src/config/appConfig.js +++ b/src/config/appConfig.js @@ -9,6 +9,11 @@ const keyboardBasePanelSlots = { } const buttonSlots = { + slot: 'top-right', + showLabel: false +} + +const zoomButtonSlots = { slot: 'right-top', showLabel: false } @@ -81,9 +86,9 @@ export const defaultAppConfig = { onClick: (_e, { mapProvider, appConfig }) => mapProvider.zoomIn(appConfig.zoomDelta), excludeWhen: ({ appState, appConfig }) => !appConfig.enableZoomControls || appConfig.enableMapControls || appState.interfaceType === 'touch', enableWhen: ({ mapState }) => !mapState.isAtMaxZoom, - mobile: buttonSlots, - tablet: buttonSlots, - desktop: buttonSlots + mobile: zoomButtonSlots, + tablet: zoomButtonSlots, + desktop: zoomButtonSlots }, { id: 'zoomOut', group: { label: 'Zoom controls', slotOrder: 0 }, @@ -93,9 +98,9 @@ export const defaultAppConfig = { onClick: (_e, { mapProvider, appConfig }) => mapProvider.zoomOut(appConfig.zoomDelta), excludeWhen: ({ appState, appConfig }) => !appConfig.enableZoomControls || appConfig.enableMapControls || appState.interfaceType === 'touch', enableWhen: ({ mapState }) => !mapState.isAtMinZoom, - mobile: buttonSlots, - tablet: buttonSlots, - desktop: buttonSlots + mobile: zoomButtonSlots, + tablet: zoomButtonSlots, + desktop: zoomButtonSlots }, { id: 'mapControls', label: 'Map controls',