-
Notifications
You must be signed in to change notification settings - Fork 1.8k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[feat] Free positioning of the legend (#2874)
- Legend can be moved around - Position is saved as offsets to the closest corner to handle window resizes Signed-off-by: Ihor Dykhta <dikhta.igor@gmail.com> Co-authored-by: Ilya Boyandin <iboyandin@foursquare.com>
- Loading branch information
1 parent
2d1d8e5
commit cccc4be
Showing
19 changed files
with
682 additions
and
138 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
import React, {Component} from 'react'; | ||
import PropTypes from 'prop-types'; | ||
import Base from './base'; | ||
|
||
export default class DraggableDots extends Component { | ||
static propTypes = { | ||
/** Set the height of the icon, ex. '16px' */ | ||
height: PropTypes.string | ||
}; | ||
|
||
static defaultProps = { | ||
height: '16px', | ||
viewBox: '0 0 16 16', | ||
predefinedClassName: 'data-ex-icons-draggabledots' | ||
}; | ||
|
||
render() { | ||
return ( | ||
<Base {...this.props}> | ||
<circle cx="14" cy="5.5" r="1" transform="rotate(90 14 5.5)" /> | ||
<circle cx="14" cy="10.5" r="1" transform="rotate(90 14 10.5)" /> | ||
<circle cx="10" cy="5.5" r="1" transform="rotate(90 10 5.5)" /> | ||
<circle cx="10" cy="10.5" r="1" transform="rotate(90 10 10.5)" /> | ||
<circle cx="6" cy="5.5" r="1" transform="rotate(90 6 5.5)" /> | ||
<circle cx="6" cy="10.5" r="1" transform="rotate(90 6 10.5)" /> | ||
<circle cx="2" cy="5.5" r="1" transform="rotate(90 2 5.5)" /> | ||
<circle cx="2" cy="10.5" r="1" transform="rotate(90 2 10.5)" /> | ||
</Base> | ||
); | ||
} | ||
} |
25 changes: 25 additions & 0 deletions
25
src/components/src/common/icons/horizontal-resize-handle.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
import React, {Component} from 'react'; | ||
import PropTypes from 'prop-types'; | ||
import Base from './base'; | ||
|
||
export default class HorizontalResizeHandle extends Component { | ||
static propTypes = { | ||
/** Set the height of the icon, ex. '16px' */ | ||
height: PropTypes.string | ||
}; | ||
|
||
static defaultProps = { | ||
height: '16px', | ||
viewBox: '0 0 16 16', | ||
predefinedClassName: 'data-ex-icons-horizontalresizehandle' | ||
}; | ||
|
||
render() { | ||
return ( | ||
<Base {...this.props}> | ||
<rect x="15" y="6" width="1" height="14" rx="0.5" transform="rotate(90 15 6)" /> | ||
<rect x="15" y="9" width="1" height="14" rx="0.5" transform="rotate(90 15 9)" /> | ||
</Base> | ||
); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,137 @@ | ||
import {useMemo, useRef, useCallback, useEffect} from 'react'; | ||
|
||
export type MapLegendControlSettings = { | ||
position: { | ||
x: number; | ||
y: number; | ||
anchorX: 'left' | 'right'; | ||
anchorY: 'top' | 'bottom'; | ||
}; | ||
contentHeight: number; | ||
}; | ||
|
||
type Params = { | ||
legendContentRef: React.MutableRefObject<HTMLElement | null>; | ||
isSidePanelShown: boolean; | ||
settings: MapLegendControlSettings; | ||
onChangeSettings: (settings: Partial<MapLegendControlSettings>) => void; | ||
theme: Record<string, any>; | ||
}; | ||
|
||
type ReturnType = { | ||
positionStyles: Record<string, unknown>; | ||
updatePosition: () => void; | ||
contentHeight: number; | ||
startResize: () => void; | ||
resize: (deltaY: number) => void; | ||
}; | ||
|
||
const MARGIN = { | ||
left: 10, | ||
top: 10, | ||
right: 10, | ||
bottom: 30 | ||
}; | ||
const DEFAULT_POSITION: MapLegendControlSettings['position'] = { | ||
x: MARGIN.right, | ||
y: MARGIN.bottom, | ||
anchorX: 'right', | ||
anchorY: 'bottom' | ||
}; | ||
const MIN_CONTENT_HEIGHT = 100; | ||
|
||
/** | ||
* Returns a function that calculates the anchored position of the map legend | ||
* that is being dragged. | ||
*/ | ||
export default function useLegendPosition({ | ||
legendContentRef, | ||
isSidePanelShown, | ||
settings, | ||
onChangeSettings, | ||
theme | ||
}: Params): ReturnType { | ||
const pos = settings?.position ?? DEFAULT_POSITION; | ||
const contentHeight = settings?.contentHeight ?? -1; | ||
const positionStyles = useMemo(() => ({[pos.anchorX]: pos.x, [pos.anchorY]: pos.y}), [pos]); | ||
const startHeightRef = useRef(0); | ||
|
||
const calcPosition = useCallback((): MapLegendControlSettings['position'] => { | ||
const root = legendContentRef.current?.closest('.kepler-gl'); | ||
const legendContent = legendContentRef.current; | ||
if (!legendContent || !(root instanceof HTMLElement)) { | ||
return DEFAULT_POSITION; | ||
} | ||
const legendRect = legendContent.getBoundingClientRect(); | ||
const mapRootBounds = root.getBoundingClientRect(); | ||
const leftSidebarOffset = isSidePanelShown ? sidePanelWidth : 0; | ||
|
||
const leftOffset = Math.max( | ||
MARGIN.left, | ||
legendRect.left - mapRootBounds.left - leftSidebarOffset | ||
); | ||
const rightOffset = Math.max(MARGIN.right, mapRootBounds.right - legendRect.right); | ||
|
||
const topOffset = Math.max(MARGIN.top, legendRect.top); | ||
const bottomOffset = Math.max(MARGIN.bottom, mapRootBounds.bottom - legendRect.bottom); | ||
|
||
return { | ||
...(leftOffset < rightOffset | ||
? {x: leftOffset + leftSidebarOffset, anchorX: 'left'} | ||
: {x: rightOffset, anchorX: 'right'}), | ||
...(topOffset < bottomOffset | ||
? {y: topOffset, anchorY: 'top'} | ||
: {y: bottomOffset, anchorY: 'bottom'}) | ||
}; | ||
}, [isSidePanelShown]); | ||
const updatePosition = useCallback(() => onChangeSettings({position: calcPosition()}), [ | ||
calcPosition, | ||
onChangeSettings | ||
]); | ||
|
||
const startResize = useCallback(() => { | ||
const content = legendContentRef.current?.querySelector('.map-control__panel-content'); | ||
if (content instanceof HTMLElement) { | ||
startHeightRef.current = content.offsetHeight; | ||
} | ||
}, []); | ||
const resize = useCallback( | ||
deltaY => { | ||
const root = legendContentRef.current?.closest('.kepler-gl'); | ||
const legendContent = legendContentRef.current; | ||
if (root instanceof HTMLElement && legendContent) { | ||
const legendRect = legendContent.getBoundingClientRect(); | ||
const nextHeight = Math.min( | ||
root.offsetHeight - legendRect.top - 100, | ||
Math.max(MIN_CONTENT_HEIGHT, startHeightRef.current + deltaY) | ||
); | ||
onChangeSettings({contentHeight: nextHeight}); | ||
if (contentHeight > 0 && pos.anchorY === 'bottom') { | ||
onChangeSettings({position: {...pos, y: pos.y - (nextHeight - contentHeight)}}); | ||
} | ||
} | ||
}, | ||
[contentHeight, pos, onChangeSettings] | ||
); | ||
|
||
// Shift when side panel is shown/hidden | ||
const sidePanelWidth = theme.sidePanel?.width || 0; | ||
const posRef = useRef(pos); | ||
posRef.current = pos; | ||
useEffect(() => { | ||
const currentPos = posRef.current; | ||
if (currentPos.anchorX === 'left') { | ||
if (isSidePanelShown) { | ||
if (currentPos.x <= sidePanelWidth + MARGIN.left) { | ||
onChangeSettings({position: {...currentPos, x: sidePanelWidth + MARGIN.left}}); | ||
} | ||
} else { | ||
onChangeSettings({ | ||
position: {...currentPos, x: Math.max(MARGIN.left, currentPos.x - sidePanelWidth)} | ||
}); | ||
} | ||
} | ||
}, [isSidePanelShown, onChangeSettings]); | ||
|
||
return {positionStyles, updatePosition, contentHeight, startResize, resize}; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.