use-smart-position
Props
| Prop name | Type | Default | Description |
|---|---|---|---|
active | boolean | Required | Passes the information of the target element is shown |
alignment | "start" | "end" | undefined | Default alignment before the smart alignment is applied | |
position | "top" | "bottom" | "left" | "right" | undefined | Controls default positioning before the smart position is applied | |
smartPositionRef | RefObject<HTMLElement | null> | undefined | Allows specifying a specific DOM element via ref to calculate smart position against. | |
smartPositionOffset | { top?: number | undefined; bottom?: number | undefined; left?: number | undefined; right?: number | undefined; } | undefined | {
top: 0,
bottom: 0,
left: 0,
right: 0,
} | Container offsets to calculate smart position with, in pixels |
smartPositionAllowedPositions | AllowedPositionsType | undefined | DEFAULT_ALLOWED_POSITIONS as unknown as AllowedPositions | Allows for restricting smart positioning to certain values, if, for example, you only want your tooltip (switching between "top" and "bottom" only) |
useSmartPosition hook makes it possible to enhance Tooltip or Dropdown with smart positioning.
This functionality was extracted from the tooltip to make the component simpler, while allowing you to optionally include smart position functionality only when you need it.
The hook returns following array of values:
smartPosition- resulting smart position value to apply to yourTooltiporDropdowncomponent;smartAlignment- resulting smart alignment value to apply to yourTooltiporDropdowncomponent;targetRef- ref callback you need to attach to the trigger from which smart position is calculated (normally a button triggering a dropdown or a tooltip);elementRef- ref callback of the element to calculate the position for.
The example bellow creates a simple HOC around Tooltip component adding smart position calculations to it. Notice that the hook requires passing the active state, this is used to recalculate smart position on every state change. Additionally, see how the targetRef and elementRef refs returning from the hook are attached to their respective components.
To see things in action try scrolling and resizing the screen for example bellow. You will notice how default tooltip position and alignment changes depending on where it's located on the screen:
import React from 'react'; import { Tooltip, Button, Heading, Grid, GridCol, Text, useSmartPosition } from '@flixbus/honeycomb-react'; import { Icon, IconInfo } from '@flixbus/honeycomb-icons-react'; const SmartTooltip = ({ id, content, targetContent }) => { const [isActive, setIsActive] = React.useState(false); const [smartPosition, smartAlignment, targetRef, elementRef] = useSmartPosition({ active: isActive, }); return ( <Tooltip balloonRef={elementRef} id={id} content={content} position={smartPosition} alignment={smartAlignment} onToggle={setIsActive} > <Button innerRef={targetRef} appearance="link"><Icon InlineIcon={IconInfo}/>{targetContent}</Button> </Tooltip> ); } const tooltipContent = ( <> <Heading size={3} sectionHeader>This tooltip is smart!</Heading> <Text>Scroll to the edges of the screen to see how Tooltip position changes depending on its coordinates on the screen.</Text> </> ); <Grid justify="center" applyContainer> <GridCol size={3}> <SmartTooltip id="smart-tooltip-1" targetContent="I'm on the left edge" content={tooltipContent} /> </GridCol> <GridCol size={3}> <SmartTooltip id="smart-tooltip-2" targetContent="I'm in the middle" content={tooltipContent} /> </GridCol> <GridCol size={3}> <SmartTooltip id="smart-tooltip-3" targetContent="I'm on the right" content={tooltipContent} /> </GridCol> </Grid>
Limited smart positioning for Dropdown
It is also possible to use this hook for Dropdown to position itself depending on its coordinates on the screen.
Note the usage of smartPositionAllowedPositions prop to limit the number of positions to "top" and "bottom" only, since dropdown only supports these.
Here it is also important to wire the refs properly. For Dropdown the elementRef needs to be attached via menuRef prop.
This is because we actually need to position the menu of the Dropdown and not the Dropdown component itself.
See for yourself by scrolling and resizing the screen for example bellow, notice how default dropdown position changes when parts of it being cut by the screen edges:
import React from 'react'; import { Dropdown, DropdownItem, Button, useSmartPosition } from '@flixbus/honeycomb-react'; import { Icon, IconArrowDownL, IconArrowUpL } from '@flixbus/honeycomb-icons-react'; const [arrowUp, setArrowUp] = React.useState(false); const [isActive, setIsActive] = React.useState(false); const [smartPosition, smartAlignment, targetRef, elementRef] = useSmartPosition({ active: isActive, smartPositionAllowedPositions: ['top', 'bottom'], }); const toggleArrowButton = (isActive, event) => { console.log(event.type); setArrowUp(isActive); }; <Dropdown menuRef={elementRef} links={[ <DropdownItem key="share" href="/">Share</DropdownItem>, <DropdownItem key="copy" href="/">Copy</DropdownItem>, <DropdownItem key="delete" href="/">Delete</DropdownItem>, ]} onToggle={(isActive, event) => { setIsActive(isActive); toggleArrowButton(isActive, event); }} yPosition={smartPosition} xPosition={smartAlignment} > <Button innerRef={targetRef} appearance="primary"> Toggle me! <Icon InlineIcon={(arrowUp ? IconArrowUpL : IconArrowDownL)} /> </Button> </Dropdown>
Smart position within a DOM element
By default, position and alignment are calculated against the browser window.
If you need to have your components positioned within a specific DOM element (e.g. DataTable or Panel) you should pass smartPositionRef prop to the hook.
This prop should hold a reference to the parent element within witch you want the tooltip to be positioned.
This element will be used instead of the browser window for position calculations.
Here is an example of tooltips positioning themselves within the DataTable container.
Note how we pass containerRef to the hook by also attaching it via innerRef prop to DataTable:
import React from 'react'; import { DataTable, DataTableHead, DataTableRow, DataTableCell, Tooltip, Button, Heading, Text, useSmartPosition, } from '@flixbus/honeycomb-react'; import { Icon, IconInfo } from '@flixbus/honeycomb-icons-react'; const containerRef = React.useRef(null); const ExampleSmartTooltip = ({ id, content, targetContent }) => { const [isActive, setIsActive] = React.useState(false); const [smartPosition, smartAlignment, targetRef, elementRef] = useSmartPosition({ active: isActive, smartPositionRef: containerRef, }); return ( <Tooltip innerRef={targetRef} balloonRef={elementRef} id={id} content={content} position={smartPosition} alignment={smartAlignment} onToggle={setIsActive} > <Button appearance="link"><Icon InlineIcon={IconInfo}/>{targetContent}</Button> </Tooltip> ); } const ExampleActionButton = ({ id }) => ( <ExampleSmartTooltip id={`smart-element-tooltip-${id}`} content={( <> <Heading sectionHeader size={3}>This is a smart tooltip</Heading> <Text>Scroll the data table to see how the Tooltip direction changes depending on its position within the table.</Text> </> )} targetContent="Show info tooltip" /> ); const pokemons = [ {n: '004', name: 'Charmander', type: 'Fire' }, {n: '005', name: 'Charmeleon', type: 'Fire' }, {n: '006', name: 'Charizard', type: 'Fire/Flying' }, {n: '007', name: 'Squirtle', type: 'Water' }, {n: '008', name: 'Wartortle', type: 'Water' }, {n: '009', name: 'Blastoise', type: 'Water' }, {n: '010', name: 'Caterpie', type: 'Bug' }, {n: '011', name: 'Metapod', type: 'Bug' }, {n: '012', name: 'Butterfree', type: 'Bug/Flying' }, {n: '013', name: 'Weedle', type: 'Bug/Poison' }, {n: '014', name: 'Kakuna', type: 'Bug/Poison' }, {n: '015', name: 'Beedrill', type: 'Bug/Poison' }, ]; <DataTable innerRef={containerRef} headers={( <DataTableHead> <DataTableCell scope="col">#</DataTableCell> <DataTableCell scope="col">Name</DataTableCell> <DataTableCell scope="col">Type</DataTableCell> <DataTableCell scope="col">Action</DataTableCell> </DataTableHead> )} small scrollable="sticky-header" style={{ height: '300px', position: 'relative' }} > {pokemons.map(({ n, name, type, weight }) => ( <DataTableRow key={n}> <DataTableCell scope="row">{n}</DataTableCell> <DataTableCell>{name}</DataTableCell> <DataTableCell>{type}</DataTableCell> <DataTableCell><ExampleActionButton id={n} /></DataTableCell> </DataTableRow> ))} </DataTable>