PCF DetailsList Layout with Fluent UI and Sticky

One of the challenges with PCF controls is getting them to reflow to the available space that they are stretched to fill the available space. Doing this using standard HTML involves using the flexbox. The really nice aspect of the Fluent UI react library is that it comes with an abstraction of the flexbox called the ‘Stack’.

The aim of this post is to layout a dataset PCF as follows:

  • Left Panel - A fixed width vertical stack panel that fills 100% of the available space
  • Top Bar - A fixed height top bar that can contain a command bar etc.
  • Footer - A centre aligned footer that can contain status messages etc.
  • Grid - a DetailsList with a sticky headers that occupies 100% of the middle area.

The main challenges of this exercise are:

  1. Expanding the areas to use 100% of the container space - this is done using a combination of verticalFill and height:100%
  2. Ensure that the DetailsList header row is always visible when scrolling - this is done using the onRenderDetailsHeader event of the DetailsList in combination with Sticky and ScrollablePane
  3. Ensure that the view selector and other command bar overlay appear on top of the stick header.
    This requires a bit of a ‘hack’ in that we have to apply a z-order css rule to the Model Driven overlays for the ViewSelector and Command Bar flyoutRootNode. If this is not applied then flyout menus will show behind the Stick header:

Here is the React component for the layout:

/* eslint-disable @typescript-eslint/no-non-null-assertion */
/* eslint-disable @typescript-eslint/explicit-function-return-type */
import * as React from "react";
import {
  Stack,
  ScrollablePane,
  DetailsList,
  TooltipHost,
  IRenderFunction,
  IDetailsColumnRenderTooltipProps,
  IDetailsHeaderProps,
  StickyPositionType,
  Sticky,
  ScrollbarVisibility,
} from "office-ui-fabric-react";

export class DatasetLayout extends React.Component {
  private onRenderDetailsHeader: IRenderFunction<IDetailsHeaderProps> = (props, defaultRender) => {
    if (!props) {
      return null;
    }
    const onRenderColumnHeaderTooltip: IRenderFunction<IDetailsColumnRenderTooltipProps> = tooltipHostProps => (
      <TooltipHost {...tooltipHostProps} />
    );
    return (
      <Sticky stickyPosition={StickyPositionType.Header} isScrollSynced>
        {defaultRender!({
          ...props,
          onRenderColumnHeaderTooltip,
        })}
      </Sticky>
    );
  };
  private columns = [
    {
      key: "name",
      name: "Name",
      isResizable: true,
      minWidth: 100,
      onRender: (item: string) => {
        return <span>{item}</span>;
      },
    },
  ];
  render() {
    return (
      <>
        <Stack horizontal styles={{ root: { height: "100%" } }}>
          <Stack.Item>
            {/*Left column*/}
            <Stack verticalFill>
              <Stack.Item
                verticalFill
                styles={{
                  root: {
                    textAlign: "left",
                    width: "150px",
                    paddingLeft: "8px",
                    paddingRight: "8px",
                    overflowY: "auto",
                    overflowX: "hidden",
                    height: "100%",
                    background: "#DBADB1",
                  },
                }}
              >
                <Stack>
                  <Stack.Item>Left Item 1</Stack.Item>
                  <Stack.Item>Left Item 2</Stack.Item>
                </Stack>
              </Stack.Item>
            </Stack>
          </Stack.Item>
          <Stack.Item styles={{ root: { width: "100%" } }}>
            {/*Right column*/}
            <Stack
              grow
              styles={{
                root: {
                  width: "100%",
                  height: "100%",
                },
              }}
            >
              <Stack.Item verticalFill>
                <Stack
                  grow
                  styles={{
                    root: {
                      height: "100%",
                      width: "100%",
                      background: "#65A3DB",
                    },
                  }}
                >
                  <Stack.Item>Top Bar</Stack.Item>
                  <Stack.Item
                    verticalFill
                    styles={{
                      root: {
                        height: "100%",
                        overflowY: "auto",
                        overflowX: "auto",
                      },
                    }}
                  >
                    <div style={{ position: "relative", height: "100%" }}>
                      <ScrollablePane scrollbarVisibility={ScrollbarVisibility.auto}>
                        <DetailsList
                          onRenderDetailsHeader={this.onRenderDetailsHeader}
                          compact={true}
                          items={[...Array(200)].map((_, i) => `Item ${i + 1}`)}
                          columns={this.columns}
                        ></DetailsList>
                      </ScrollablePane>
                    </div>
                  </Stack.Item>
                  <Stack.Item align="center">Footer</Stack.Item>
                </Stack>
              </Stack.Item>
            </Stack>
          </Stack.Item>
        </Stack>
      </>
    );
  }
}

Here is the css:

div[id^="ViewSelector"]{
    z-index: 20;
}
#__flyoutRootNode .flexbox {
    z-index: 20;
}

Hope this helps!

@ScottDurow

Comments are closed