Honeycomb

panel

Props

Prop nameTypeDefaultDescription
extraClassesstring | undefined

Injection in className.

idstring

Panel ID to be used within panel components for accessibility attributes.

activeboolean | undefined

Toggles Panel visibility.

hasOverlayboolean | undefined

Toggles Panel overlay.

onOverlayClick((event: SyntheticEvent<Element, Event>) => void) | undefined

Custom overlay click handler.

position"left" | "bottom" | "default" | "center" | undefinedright

Controls panel positioning. The default is on the right.

fullSizeboolean | undefined

Toggles Panel full size modifier.

portalIdstring | undefined

ID of the element you want your popup to be rendered into.

childrenReactElement<any, string | JSXElementConstructor<any>>[]Required

Panel body containing the following components: PanelHeader PanelContent PanelFooter

innerRefRef<HTMLDivElement> | undefined

React ref forwarded to the panel container.

targetRefRef<HTMLElement> | undefined

Target React Ref. Panel will attempt to set focus to this element when closed.

disableBodyStyleModificationsboolean | undefined

By default the Panel will toggle `overflow: hidden` in the document body when hasOverlay is true. This option disables that.

disableFocusManagementboolean | undefined

By default the Panel will create a tab trap inside. This option disables that. You will have to implement an acceptable replacement on your side as this is important for accessibility. MDN - Dialog W3C - Dialog

loadingboolean | undefined

Sets aria-busy="true" and aria-live="polite". Also delays Tab Trap setup until loading is finished.

Panel Composition

Besides the Panel there are 4 more components you can use to compose and build the Panel body. Even if you add the panel sections in a different layout, the Panel will rearrange them in the correct order.:

`PanelHeader` (required)
Renders the panel title and can have two buttons (back and close) which are optional.
`PanelContent` (required)
A simple wrapper for the panel content, accepts any type content as children.
`PanelFooter`
Is optional and if omitted the `PanelContent` will expand to fill the remaining vertical space.
Accepts one or two `PanelFooterColumn` components, check the panel footer docs bellow for more information.

In addition to that, the Panel will ignore any multiple sections you provide as only one of each is allowed. For example: if you add 2 PanelHeader by mistake, only the first one will be rendered at the top, followed by the first PanelContent and finally, a PanelFooter if one was given.

Positions and "Full Screen"

The position prop controls the Panel position and direction it slides in.

`left`
Makes the panel slide from the left side of the window.
The default width of panels sliding from the sides is 380px.
`bottom`
Panel slides from the bottom of the window.
By default, the height of panels sliding from the bottom depends on the content.
`center`
Panel is displayed in the center of the screen.
The height of centered panels depends on the content. On mobile this variation behaves like a "bottom" panel.

The fullSize prop makes the Panel cover the entire window.

Panel accessibility

Panels implement the dialog role that require a set of accessibility functionalities to be implemented.

When the Panel has overlay it becomes a modal dialog (aria-modal="true") that blocks interaction with background content.

Opening the Panel:

  • The focus is moved to the first interactive element of the panel, that's usually the back or close button in the Panel Header;
  • It blocks users from scrolling the page outside of the panel by adding overflow: hidden; to the body element:
    • You can disable this behavior passing disableBodyStyleModifications as a prop.
  • Blocks users from tabbing away from the Panel while it's open by creating a tab trap:
    • In case of trouble you can disable the popup focus management behavior by passing disableFocusManagement as a prop, you will need to implement your own tab trap on your end following the dialog focus management instructions from MDN and the W3C.

Closing the panel:

  • The component will fire onOverlayClick callback when the user presses ESC on the keyboard to close the panel:
    • You should always provide a close function to panels using this prop, even if hasOverlay is false, or implement an accessible close function on your side.
  • Then Panel will also attempt to set focus to the target element that triggered, you can provide a React ref object as targetRef prop to make this work.
    • You can also disable this by passing disableFocusManagement as a prop.
import {
  Button,
  Fieldset,
  Switch,
  Panel,
  PanelHeader,
  PanelContent,
  PanelFooter,
  PanelFooterColumn
} from '@flixbus/honeycomb-react';
const targetRef = React.useRef(null);
const [foodPanelActive, setFoodPanelActive] = React.useState(false);
const togglePanel = () => setFoodPanelActive(!foodPanelActive);

<>
  <Button innerRef={targetRef} appearance="primary" onClick={togglePanel}>
    Food Preferences
  </Button>

  <Panel
    id="food-panel"
    active={foodPanelActive}
    hasOverlay
    onOverlayClick={togglePanel}
    targetRef={targetRef}
  >
    <PanelHeader
      backButtonProps={{ label: 'Back', onClick: togglePanel }}
      closeButtonProps={{ label: 'Close', onClick: togglePanel }}
    >
      Food Preferences
    </PanelHeader>
    <PanelContent>
      <Fieldset>
        <Switch
          label="Notify about breakfast"
          id="switch-breakfast"
          value="breakfast"
        />
        <Switch
          label="Notify about lunch"
          id="switch-lunch"
          value="lunch"
        />
        <Switch
          label="Notify about dinner"
          id="switch-dinner"
          value="dinner"
        />
      </Fieldset>
    </PanelContent>
    <PanelFooter>
      <PanelFooterColumn>
        <Button
          appearance="primary"
          display="block"
          onClick={togglePanel}
        >
          Confirm
        </Button>
      </PanelFooterColumn>
    </PanelFooter>
  </Panel>
</>

Non modal panel with loading state

When the Panel does not have an Overlay they are not considered modals, and the user can still interact with the website. In this case you don't need to worry about blocking scroll from the body.

You should still provide an onOverlayClick function that closes the panel so it can be used when the user presses ESC, even if hasOverlay is false.

The example bellow also simulates a panel with a loading state, since the tab trap can only be activated after the panel content has been fully loaded.

import { Button, ButtonGroup, Panel, PanelHeader, PanelContent, PanelFooter, PanelFooterColumn, Grid, GridCol, Text, Spinner, alignmentHelpers } from '@flixbus/honeycomb-react';
import { Icon, IconSeat } from '@flixbus/honeycomb-icons-react';

const seatMap = [
  { row: '1', seats: [{ n: 'A', free: false }, { n: 'B', free: false }, { n: 'C', free: false }, { n: 'D', free: false }] },
  { row: '2', seats: [{ n: 'A', free: true  }, { n: 'B', free: true  }, { n: 'C', free: false }, { n: 'D', free: false }] },
  { row: '3', seats: [{ n: 'A', free: false }, { n: 'B', free: false }, { n: 'C', free: false }, { n: 'D', free: false }] },
  { row: '4', seats: [{ n: 'A', free: false }, { n: 'B', free: true  }, { n: 'C', free: true  }, { n: 'D', free: false }] },
  { row: '5', seats: [{ n: 'A', free: false }, { n: 'B', free: false }, { n: 'C', free: false }, { n: 'D', free: true  }] },
  { row: '6', seats: [{ n: 'A', free: true  }, { n: 'B', free: true  }, { n: 'C', free: true  }, { n: 'D', free: true  }] },
];

const targetRef = React.useRef(null);
const [seatsPanelActive, setSeatsPanelActive] = React.useState(false);
const [seatsPanelLoading, setSeatsPanelLoading] = React.useState(true);
const [selectedSeat, setSelectedSeat] = React.useState();
const [reservedSeat, setReservedSeat] = React.useState();

function closePanel() {
  setSeatsPanelActive(false);
  setSeatsPanelLoading(true);
  setSelectedSeat();
}

function loadSeatsPanel() {
  setSeatsPanelActive(true);

  setTimeout(() => {
    setSeatsPanelLoading(false);
  }, 2000);
}

function reserveSeat(event) {
  if (seatsPanelLoading) return;

  setSeatsPanelLoading(true);

  setTimeout(() => {
    setReservedSeat(selectedSeat);
    setSeatsPanelActive(false);
    setSeatsPanelLoading(true);
  }, 2000);
}

<>
  <Button innerRef={targetRef} appearance="primary" onClick={loadSeatsPanel}>
    {reservedSeat ? `Seat: ${reservedSeat}` : 'Choose a seat'}
  </Button>

  <Panel
    id="panel-second-example"
    active={seatsPanelActive}
    onOverlayClick={closePanel}
    targetRef={targetRef}
    loading={seatsPanelLoading}
  >
    <PanelHeader closeButtonProps={{ label: 'Close', onClick: closePanel }}>
      Seat Map
    </PanelHeader>
    <PanelContent>
      {seatsPanelLoading ? (
        <Grid justify="center" align="center">
          <GridCol inline>
            <Spinner />
            <Text small style={{ textAlign: 'center' }}>Loading...</Text>
          </GridCol>
        </Grid>
      ) : seatMap.map(({ row, seats }) => (
        <ButtonGroup alignment="center" key={`row-${row}`}>
          {seats.map(({ n, free }) => {
            const seatNumber = `${row}${n}`;
            return (
              <Button
                key={`seat-${seatNumber}`}
                disabled={!free}
                display="stacked"
                size="lg"
                aria-pressed={selectedSeat === seatNumber}
                appearance={selectedSeat === seatNumber ? 'primary' : undefined }
                onClick={(event) => setSelectedSeat(seatNumber)}
              >
                <Icon InlineIcon={IconSeat} size={4} />
                {seatNumber}
              </Button>
            )
          })}
        </ButtonGroup>
      ))}
    </PanelContent>
    <PanelFooter>
      <PanelFooterColumn>
        <Button
          id="save-seat-selection-button"
          appearance="primary"
          display="block"
          onClick={reserveSeat}
          disabled={seatsPanelLoading}
        >
          {selectedSeat ? `Reserve seat ${selectedSeat}` : 'Select a seat'}
        </Button>
      </PanelFooterColumn>
    </PanelFooter>
  </Panel>
</>

Using React Portals to make a "Bottom Sheet"

The Panel bellow uses a React Portal ID to render the panel inside of another root element.

It also uses the fullSize variation and is positioned at the bottom of the screen. This is the setup you will need to make a "Bottom Sheet" effect.

import { Button, ButtonGroup, Panel, PanelHeader, PanelContent, PanelFooter, PanelFooterColumn, Heading, Text } from '@flixbus/honeycomb-react';
const targetRef = React.useRef(null);
const [bottomSheetActive, setBottomSheetActive] = React.useState(false);
const togglePanel = () => setBottomSheetActive(!bottomSheetActive);

<>
  <Button innerRef={targetRef} appearance="primary" onClick={togglePanel}>Send a Panel through a Portal</Button>

  <Panel
    id="panel-third-example"
    fullSize
    position="bottom"
    active={bottomSheetActive}
    onOverlayClick={togglePanel}
    portalId="render-panel-inside-this-div"
    targetRef={targetRef}
  >
    <PanelHeader>
      The 🎂 is a lie!
    </PanelHeader>
    <PanelContent>
      <Heading size={4}>Now you're thinking with Portals</Heading>
      <Text>
        If you give "portalId" an ID of some other element, the portal will render the Panel on that element.
      </Text>
    </PanelContent>
    <PanelFooter>
      <PanelFooterColumn narrow>
        Some auxiliary footer text
      </PanelFooterColumn>
      <PanelFooterColumn narrow>
        <ButtonGroup>
          <Button onClick={togglePanel}>Cancel</Button>
          <Button appearance="primary" onClick={togglePanel}>Success</Button>
        </ButtonGroup>
      </PanelFooterColumn>
    </PanelFooter>
  </Panel>
  <div id="render-panel-inside-this-div"></div>
</>