Skip to content

Commit

Permalink
Merge pull request #1513 from malloydata/feat/pinned-headers
Browse files Browse the repository at this point in the history
feat: pinned headers
  • Loading branch information
skokenes authored Nov 27, 2023
2 parents bb1013f + 2421b0d commit a17ae8e
Show file tree
Hide file tree
Showing 8 changed files with 286 additions and 81 deletions.
9 changes: 9 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions packages/malloy-render/.storybook/preview-head.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
<link rel="preconnect" href="https://rsms.me/" />
<link rel="stylesheet" href="https://rsms.me/inter/inter.css" />
1 change: 1 addition & 0 deletions packages/malloy-render/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
"build-storybook": "storybook build"
},
"dependencies": {
"@lit/context": "^1.1.0",
"@malloydata/malloy": "^0.0.105",
"@types/luxon": "^2.4.0",
"lit": "^3.0.2",
Expand Down
21 changes: 21 additions & 0 deletions packages/malloy-render/src/component/render.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,10 +39,31 @@ export class MalloyRender extends LitElement {
--table-border: 1px solid #e5e7eb;
--table-background: white;
--table-gutter-size: 15px;
--table-pinned-background: #f5fafc;
--table-pinned-border: 1px solid #daedf3;
font-family: Inter, system-ui, sans-serif;
font-size: var(--table-font-size);
}
@supports (font-variation-settings: normal) {
:host {
font-family:
InterVariable,
Inter,
system-ui sans-serif;
font-variant-numeric: tabular-nums;
font-feature-settings:
'cv01' 1,
'cv02' 1,
'cv03' 1,
'cv04' 1,
'cv09' 1,
'liga' 1,
'calt' 1;
}
}
`;

@property({attribute: false})
Expand Down
233 changes: 184 additions & 49 deletions packages/malloy-render/src/component/table.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,20 @@
*/

import {DataArray, DataRecord, Field} from '@malloydata/malloy';
import {LitElement, css, html} from 'lit';
import {customElement, property} from 'lit/decorators.js';
import {LitElement, TemplateResult, css, html} from 'lit';
import {customElement, eventOptions, property, state} from 'lit/decorators.js';
import {classMap} from 'lit/directives/class-map.js';
import {createContext, provide, consume} from '@lit/context';
import {isFirstChild, isLastChild} from './util';

const tableContext = createContext<{
root: boolean;
}>('table');

type RenderOptions = {
pinnedHeader?: boolean;
};

// TODO: replace with an estimator per column
function getColumnWidth() {
return 130;
Expand All @@ -40,32 +49,57 @@ const getContentStyle = (f: Field) => {
return '';
};

const renderCell = (f: Field, value: unknown) => {
const renderCell = (
f: Field,
value: unknown,
options: {
hideStartGutter: boolean;
hideEndGutter: boolean;
}
) => {
return html`<div class="cell-wrapper">
<div class="cell-gutter-start"></div>
<div
class=${classMap({
'cell-gutter': true,
'hide-gutter-border': options.hideStartGutter,
})}
></div>
<div class="cell-content" style="${getContentStyle(f)}">${value}</div>
<div class="cell-gutter-end"></div>
<div
class=${classMap({
'cell-gutter': true,
'hide-gutter-border': options.hideEndGutter,
})}
></div>
</div>`;
};

const renderFieldContent = (row: DataRecord, f: Field) => {
const renderFieldContent = (
row: DataRecord,
f: Field,
options: RenderOptions
) => {
if (f.isExploreField()) {
return html`<malloy-table
.data=${row.cell(f) as DataArray}
.pinnedHeader=${options.pinnedHeader ?? false}
.rowLimit=${options.pinnedHeader ? 1 : Infinity}
></malloy-table>`;
}
return renderCell(f, row.cell(f).value);
if (options.pinnedHeader)
return renderCell(f, '', {
hideStartGutter: isFirstChild(f),
hideEndGutter: isLastChild(f),
});
return renderCell(f, row.cell(f).value, {
hideStartGutter: isFirstChild(f),
hideEndGutter: isLastChild(f),
});
};

const renderField = (row: DataRecord, f: Field) => {
return html`<td
class=${classMap({
'column-cell': true,
'hide-end-gutter': isLastChild(f),
'hide-start-gutter': isFirstChild(f),
})}
>
${renderFieldContent(row, f)}
const renderField = (row: DataRecord, f: Field, options: RenderOptions) => {
return html`<td class="column-cell">
${renderFieldContent(row, f, options)}
</td>`;
};

Expand All @@ -79,24 +113,48 @@ const renderHeader = (f: Field) => {
const isParentLast = isLastChild(f.parentExplore);
const hideEndGutter = isLast && (isParentLast || isParentNotAField);

return html`<th
class=${classMap({
'column-cell': true,
'hide-end-gutter': hideEndGutter,
'hide-start-gutter': hideStartGutter,
return html`<th class="column-cell">
${renderCell(f, f.name, {
hideStartGutter,
hideEndGutter,
})}
>
${renderCell(f, f.name)}
</th>`;
};

@customElement('malloy-table')
export class Table extends LitElement {
static override styles = css`
.table-wrapper {
width: 100%;
height: 100%;
position: relative;
overflow: auto;
}
.sticky-header {
position: sticky;
top: 0px;
z-index: 100;
}
.sticky-header-content {
position: absolute;
top: 0px;
left: 0px;
pointer-events: none;
}
.sticky-header-content th {
pointer-events: all;
}
table {
border-collapse: collapse;
background: var(--table-background);
font-variant-numeric: tabular-nums;
}
th {
transition: background-color 0.25s;
}
table * {
Expand Down Expand Up @@ -139,53 +197,130 @@ export class Table extends LitElement {
text-overflow: ellipsis;
}
.cell-gutter-start {
.cell-gutter {
border-top: var(--table-border);
height: var(--table-row-height);
width: var(--table-gutter-size);
transition: border-color 0.25s;
}
.cell-gutter-end {
border-top: var(--table-border);
height: var(--table-row-height);
width: var(--table-gutter-size);
.cell-gutter.hide-gutter-border {
border-color: transparent;
}
.hide-end-gutter .cell-gutter-end {
border-top: none;
.pinned-header table {
background: transparent;
}
.hide-start-gutter .cell-gutter-start {
border-top: none;
.pinned-header th {
background: var(--table-background);
}
.pinned-header.scrolled th {
background: var(--table-pinned-background);
box-shadow: 0 0 0.5em rgba(0, 0, 0, 0.5);
}
.pinned-header.scrolled {
.cell-content,
.cell-gutter,
.cell-gutter.hide-gutter-border {
border-top: var(--table-pinned-border);
}
}
`;

@property({attribute: false})
data!: DataArray;

@property({type: Number})
rowLimit = Infinity;

@property({type: Boolean})
pinnedHeader = false;

@state()
protected _scrolling = false;

@consume({context: tableContext})
@property({attribute: false})
public parentCtx;

@provide({context: tableContext})
ctx = {root: false};

override connectedCallback() {
super.connectedCallback();
if (typeof this.parentCtx === 'undefined') {
this.ctx = {
root: true,
};
}
}

@eventOptions({passive: true})
private _handleScroll(e: Event) {
const target = e.target as HTMLElement;
this._scrolling = target.scrollTop > 0;
}

// If rendering a pinned header, render it within the current ShadowDOM root so we can use CSS to style the nested table headers when scrolling
protected override createRenderRoot(): HTMLElement | DocumentFragment {
if (this.pinnedHeader) return this;
return super.createRenderRoot();
}

override render() {
const fields = this.data.field.allFields;

const headers = fields.map(f => renderHeader(f));

const rows = Array.from(
this.data,
row =>
const renderOptions: RenderOptions = {
pinnedHeader: this.pinnedHeader,
};

const rows: TemplateResult[] = [];
let i = 0;
for (const row of this.data) {
if (i >= this.rowLimit) break;
rows.push(
html`<tr>
${fields.map(f => renderField(row, f))}
${fields.map(f => renderField(row, f, renderOptions))}
</tr>`
);

return html`<table>
<thead>
<tr>
${headers}
</tr>
</thead>
<tbody>
${rows}
</tbody>
</table>`;
);
i++;
}

const renderStickyHeader = () => {
if (this.ctx.root)
return html`<div class="sticky-header">
<div class="sticky-header-content">
<malloy-table
class=${classMap({
'pinned-header': true,
'scrolled': this._scrolling,
})}
.rowLimit=${1}
.data=${this.data}
.pinnedHeader=${true}
></malloy-table>
</div>
</div>`;
};

return html`<div @scroll=${this._handleScroll} class="table-wrapper">
${renderStickyHeader()}
<table>
<thead>
<tr>
${headers}
</tr>
</thead>
<tbody>
${rows}
</tbody>
</table>
</div>`;
}
}

Expand Down
Loading

0 comments on commit a17ae8e

Please sign in to comment.