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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
/* 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:

1
2
3
4
5
6
div[id^="ViewSelector"]{
    z-index: 20;
}
#__flyoutRootNode .flexbox {
    z-index: 20;
}

Hope this helps!

@ScottDurow

Comments are closed