Honeycomb

use-smart-position

Since: ver.10.9.0 Calculates smart position for tooltips based on position of the target element on the screen or within a certain DOM element.

Props

Prop nameTypeDefaultDescription
activebooleanRequired

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

smartPositionRefRefObject<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

smartPositionAllowedPositionsAllowedPositionsType | undefinedDEFAULT_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 your Tooltip or Dropdown component;
  • smartAlignment - resulting smart alignment value to apply to your Tooltip or Dropdown component;
  • 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>