Add overflow scroll component with scroll scrim/shadow (#2578)

This commit is contained in:
Emma Segal-Grossman 2025-05-05 20:24:47 -04:00 committed by GitHub
parent 4ce769ecab
commit 8b6e1ca9af
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 288 additions and 107 deletions

View File

@ -115,7 +115,9 @@ export class OrgsList extends BtrixElement {
library="default" library="default"
></sl-icon ></sl-icon
></sl-input> ></sl-input>
<div class="-mx-3 overflow-x-auto px-3"> <btrix-overflow-scroll
class="-mx-3 [--btrix-overflow-scroll-scrim-color:theme(colors.neutral.50)] part-[content]:px-3"
>
<btrix-table> <btrix-table>
<btrix-table-head class="mb-2"> <btrix-table-head class="mb-2">
<btrix-table-header-cell> <btrix-table-header-cell>
@ -144,7 +146,7 @@ export class OrgsList extends BtrixElement {
${orgs?.map(this.renderOrg)} ${orgs?.map(this.renderOrg)}
</btrix-table-body> </btrix-table-body>
</btrix-table> </btrix-table>
</div> </btrix-overflow-scroll>
${this.renderOrgQuotas()} ${this.renderOrgProxies()} ${this.renderOrgQuotas()} ${this.renderOrgProxies()}
${this.renderOrgReadOnly()} ${this.renderOrgDelete()} ${this.renderOrgReadOnly()} ${this.renderOrgDelete()}

View File

@ -27,6 +27,7 @@ import("./menu-item-link");
import("./meter"); import("./meter");
import("./numbered-list"); import("./numbered-list");
import("./overflow-dropdown"); import("./overflow-dropdown");
import("./overflow-scroll");
import("./pagination"); import("./pagination");
import("./pw-strength-alert"); import("./pw-strength-alert");
import("./relative-duration"); import("./relative-duration");

View File

@ -0,0 +1,105 @@
import { css, html, LitElement } from "lit";
import { customElement, property } from "lit/decorators.js";
/**
* Overflow scroller. Optionally displays a scrim/shadow (a small gradient
* indicating there's more available content) on supported browsers,
* depending on scroll position.
* @slot
* @cssPart content
* @cssproperty --btrix-overflow-scrim-width The width of the scrim. 3rem by default.
* @cssproperty --btrix-overflow-scroll-scrim-color The color of the scrim. White by default.
*/
@customElement("btrix-overflow-scroll")
export class OverflowScroll extends LitElement {
/**
* The direction of the overflow scroll. Currently just horizontal.
*/
// TODO: Implement vertical overflow scroller
@property({ type: String })
// eslint-disable-next-line @typescript-eslint/prefer-as-const
direction: "horizontal" = "horizontal";
/**
* Whether to show a scrim when the overflow scroll is active. Only appears when the inner content is wider than this element.
*
* Progressive enhancement: only works on Chromium-based browsers currently.
* See https://caniuse.com/mdn-css_properties_scroll-timeline for support.
*/
@property({ type: Boolean })
scrim = true;
static styles = css`
:host {
display: block;
position: relative;
}
[direction="horizontal"] {
overflow-x: auto;
}
@supports (scroll-timeline-name: --btrix-overflow-scroll-timeline) {
[scrim][direction="horizontal"] {
scroll-timeline-name: --btrix-overflow-scroll-timeline;
scroll-timeline-axis: inline;
}
[scrim][direction="horizontal"]:before,
[scrim][direction="horizontal"]:after {
content: "";
width: var(--btrix-overflow-scrim-width, 3rem);
position: absolute;
z-index: 1;
top: 0;
height: 100%;
pointer-events: none;
animation-name: btrix-scroll-scrim;
animation-timeline: --btrix-overflow-scroll-timeline;
opacity: 0;
}
[scrim][direction="horizontal"]:before {
left: 0;
background: linear-gradient(
to right,
var(--btrix-overflow-scroll-scrim-color, white),
transparent
);
/* background-color: blue; */
}
[scrim][direction="horizontal"]:after {
right: 0;
background: linear-gradient(
to right,
transparent,
var(--btrix-overflow-scroll-scrim-color, white)
);
/* background-color: blue; */
animation-direction: reverse;
}
@keyframes btrix-scroll-scrim {
0% {
opacity: 0;
}
20% {
opacity: 1;
}
100% {
opacity: 1;
}
}
}
`;
render() {
return html`<div
class="btrix-overflow-scroll"
direction=${this.direction}
?scrim=${this.scrim}
part="content"
>
<slot></slot>
</div>`;
}
}

View File

@ -451,7 +451,7 @@ export class ArchivedItemList extends TailwindElement {
.join(" ")}; .join(" ")};
} }
</style> </style>
<div class="-mx-5 overflow-auto px-5"> <btrix-overflow-scroll class="-mx-5 part-[content]:px-5">
<btrix-table> <btrix-table>
<btrix-table-head class="mb-2"> <btrix-table-head class="mb-2">
<slot <slot
@ -472,7 +472,7 @@ export class ArchivedItemList extends TailwindElement {
<slot></slot> <slot></slot>
</btrix-table-body> </btrix-table-body>
</btrix-table> </btrix-table>
</div> </btrix-overflow-scroll>
`; `;
} }
} }

View File

@ -283,7 +283,7 @@ export class CrawlList extends TailwindElement {
[clickable-end] min-content; [clickable-end] min-content;
} }
</style> </style>
<div class="overflow-auto"> <btrix-overflow-scroll class="-mx-3 part-[content]:px-3">
<btrix-table> <btrix-table>
<btrix-table-head class="mb-2"> <btrix-table-head class="mb-2">
<btrix-table-header-cell class="pr-0"> <btrix-table-header-cell class="pr-0">
@ -320,7 +320,7 @@ export class CrawlList extends TailwindElement {
<slot @slotchange=${this.handleSlotchange}></slot> <slot @slotchange=${this.handleSlotchange}></slot>
</btrix-table-body> </btrix-table-body>
</btrix-table> </btrix-table>
</div>`; </btrix-overflow-scroll>`;
} }
private handleSlotchange() { private handleSlotchange() {

View File

@ -163,52 +163,54 @@ export class BrowserProfilesList extends BtrixElement {
}; };
return html` return html`
<btrix-table class="-mx-3 overflow-x-auto px-3"> <btrix-overflow-scroll class="-mx-3 part-[content]:px-3">
<btrix-table-head class="mb-2"> <btrix-table>
${headerCells.map(({ sortBy, sortDirection, label, className }) => { <btrix-table-head class="mb-2">
const isSorting = sortBy === this.sort.sortBy; ${headerCells.map(({ sortBy, sortDirection, label, className }) => {
const sortValue = const isSorting = sortBy === this.sort.sortBy;
(isSorting && SortDirection.get(this.sort.sortDirection)) || const sortValue =
"none"; (isSorting && SortDirection.get(this.sort.sortDirection)) ||
// TODO implement sort render logic in table-header-cell "none";
return html` // TODO implement sort render logic in table-header-cell
<btrix-table-header-cell return html`
class="${className} group cursor-pointer rounded transition-colors hover:bg-primary-50" <btrix-table-header-cell
ariaSort=${sortValue} class="${className} group cursor-pointer rounded transition-colors hover:bg-primary-50"
@click=${() => { ariaSort=${sortValue}
if (isSorting) { @click=${() => {
this.sort = { if (isSorting) {
...this.sort, this.sort = {
sortDirection: this.sort.sortDirection * -1, ...this.sort,
}; sortDirection: this.sort.sortDirection * -1,
} else { };
this.sort = { } else {
sortBy, this.sort = {
sortDirection, sortBy,
}; sortDirection,
} };
}} }
> }}
${label} ${getSortIcon(sortValue)} >
</btrix-table-header-cell> ${label} ${getSortIcon(sortValue)}
`; </btrix-table-header-cell>
})} `;
<btrix-table-header-cell> })}
<span class="sr-only">${msg("Row Actions")}</span> <btrix-table-header-cell>
</btrix-table-header-cell> <span class="sr-only">${msg("Row Actions")}</span>
</btrix-table-head> </btrix-table-header-cell>
<btrix-table-body </btrix-table-head>
class=${clsx( <btrix-table-body
"relative rounded border", class=${clsx(
this.browserProfiles == null && this.isLoading && tw`min-h-48`, "relative rounded border",
)} this.browserProfiles == null && this.isLoading && tw`min-h-48`,
> )}
${when(this.browserProfiles, ({ total, items }) => >
total ? html` ${items.map(this.renderItem)} ` : nothing, ${when(this.browserProfiles, ({ total, items }) =>
)} total ? html` ${items.map(this.renderItem)} ` : nothing,
${when(this.isLoading, this.renderLoading)} )}
</btrix-table-body> ${when(this.isLoading, this.renderLoading)}
</btrix-table> </btrix-table-body>
</btrix-table>
</btrix-overflow-scroll>
${when(this.browserProfiles, ({ total, page, pageSize }) => ${when(this.browserProfiles, ({ total, page, pageSize }) =>
total total
? html` ? html`

View File

@ -195,14 +195,39 @@ export class CollectionsList extends BtrixElement {
> >
${this.renderControls()} ${this.renderControls()}
</div> </div>
<div class="-mx-3 overflow-auto px-3 pb-1"> <btrix-overflow-scroll class="-mx-3 pb-1 part-[content]:px-3">
${guard( ${guard(
[this.collections, this.listView, this.collectionRefreshing], [this.collections, this.listView, this.collectionRefreshing],
this.listView === ListView.List this.listView === ListView.List
? this.renderList ? this.renderList
: this.renderGrid, : this.renderGrid,
)} )}
</div> </btrix-overflow-scroll>
${when(this.listView === ListView.List, () =>
when(
(this.collections &&
this.collections.total > this.collections.pageSize) ||
(this.collections && this.collections.page > 1),
() => html`
<footer class="mt-6 flex justify-center">
<btrix-pagination
page=${this.collections!.page}
totalCount=${this.collections!.total}
size=${this.collections!.pageSize}
@page-change=${async (e: PageChangeEvent) => {
await this.fetchCollections({
page: e.detail.page,
});
// Scroll to top of list
// TODO once deep-linking is implemented, scroll to top of pushstate
this.scrollIntoView({ behavior: "smooth" });
}}
></btrix-pagination>
</footer>
`,
),
)}
` `
: this.renderLoading(), : this.renderLoading(),
)} )}
@ -507,29 +532,6 @@ export class CollectionsList extends BtrixElement {
${this.collections.items.map(this.renderItem)} ${this.collections.items.map(this.renderItem)}
</btrix-table-body> </btrix-table-body>
</btrix-table> </btrix-table>
${when(
this.collections.total > this.collections.pageSize ||
this.collections.page > 1,
() => html`
<footer class="mt-6 flex justify-center">
<btrix-pagination
page=${this.collections!.page}
totalCount=${this.collections!.total}
size=${this.collections!.pageSize}
@page-change=${async (e: PageChangeEvent) => {
await this.fetchCollections({
page: e.detail.page,
});
// Scroll to top of list
// TODO once deep-linking is implemented, scroll to top of pushstate
this.scrollIntoView({ behavior: "smooth" });
}}
></btrix-pagination>
</footer>
`,
)}
`; `;
} }

View File

@ -355,36 +355,38 @@ export class Org extends BtrixElement {
<div <div
class="mx-auto box-border w-full overflow-x-hidden overscroll-contain" class="mx-auto box-border w-full overflow-x-hidden overscroll-contain"
> >
<nav class="-mx-3 flex items-end overflow-x-auto px-3 xl:px-6"> <btrix-overflow-scroll class="-mx-3 part-[content]:px-3">
${this.renderNavTab({ <nav class="flex items-end xl:px-6">
tabName: OrgTab.Dashboard, ${this.renderNavTab({
label: msg("Dashboard"), tabName: OrgTab.Dashboard,
})} label: msg("Dashboard"),
${this.renderNavTab({ })}
tabName: OrgTab.Workflows, ${this.renderNavTab({
label: msg("Crawling"), tabName: OrgTab.Workflows,
})} label: msg("Crawling"),
${this.renderNavTab({ })}
tabName: OrgTab.Items, ${this.renderNavTab({
label: msg("Archived Items"), tabName: OrgTab.Items,
})} label: msg("Archived Items"),
${this.renderNavTab({ })}
tabName: OrgTab.Collections, ${this.renderNavTab({
label: msg("Collections"), tabName: OrgTab.Collections,
})} label: msg("Collections"),
${when(this.appState.isCrawler, () => })}
this.renderNavTab({ ${when(this.appState.isCrawler, () =>
tabName: OrgTab.BrowserProfiles, this.renderNavTab({
label: msg("Browser Profiles"), tabName: OrgTab.BrowserProfiles,
}), label: msg("Browser Profiles"),
)} }),
${when(this.appState.isAdmin || this.userInfo?.isSuperAdmin, () => )}
this.renderNavTab({ ${when(this.appState.isAdmin || this.userInfo?.isSuperAdmin, () =>
tabName: OrgTab.Settings, this.renderNavTab({
label: msg("Settings"), tabName: OrgTab.Settings,
}), label: msg("Settings"),
)} }),
</nav> )}
</nav>
</btrix-overflow-scroll>
</div> </div>
<hr /> <hr />

View File

@ -0,0 +1,30 @@
import type { Meta, StoryObj } from "@storybook/web-components";
import { renderOverflowScroll, type RenderProps } from "./OverflowScroll";
const meta = {
title: "Components/Overflow Scroll",
component: "btrix-overflow-scroll",
tags: ["autodocs"],
render: renderOverflowScroll,
argTypes: {
direction: {
control: { type: "select" },
options: ["horizontal"] satisfies RenderProps["direction"][],
},
scrim: {
control: { type: "boolean" },
},
},
} satisfies Meta<RenderProps>;
export default meta;
type Story = StoryObj<RenderProps>;
export const Default: Story = {
args: {
direction: "horizontal",
scrim: true,
},
};

View File

@ -0,0 +1,37 @@
import clsx from "clsx";
import { html } from "lit";
import { ifDefined } from "lit/directives/if-defined.js";
import { defaultArgs, renderTable } from "./Table";
import "@/components/ui/overflow-scroll";
import type { OverflowScroll } from "@/components/ui/overflow-scroll";
import { tw } from "@/utils/tailwind";
export type RenderProps = OverflowScroll;
export const renderOverflowScroll = ({
direction,
scrim,
}: Partial<RenderProps>) => {
return html`
<div
class="w-[400px] min-w-16 max-w-[818px] resize-x overflow-hidden rounded rounded-lg rounded-br-none border p-2"
>
<btrix-overflow-scroll
direction=${ifDefined(direction)}
?scrim=${ifDefined(scrim)}
>
<!-- Table just as a demo of where this might be used -->
${renderTable({
...defaultArgs,
classes: clsx(
...defaultArgs.classes,
tw`w-[800px] rounded border bg-neutral-50 p-2 [--btrix-table-cell-padding:var(--sl-spacing-2x-small)]`,
),
})}
</btrix-overflow-scroll>
</div>
`;
};