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"
></sl-icon
></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-head class="mb-2">
<btrix-table-header-cell>
@ -144,7 +146,7 @@ export class OrgsList extends BtrixElement {
${orgs?.map(this.renderOrg)}
</btrix-table-body>
</btrix-table>
</div>
</btrix-overflow-scroll>
${this.renderOrgQuotas()} ${this.renderOrgProxies()}
${this.renderOrgReadOnly()} ${this.renderOrgDelete()}

View File

@ -27,6 +27,7 @@ import("./menu-item-link");
import("./meter");
import("./numbered-list");
import("./overflow-dropdown");
import("./overflow-scroll");
import("./pagination");
import("./pw-strength-alert");
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(" ")};
}
</style>
<div class="-mx-5 overflow-auto px-5">
<btrix-overflow-scroll class="-mx-5 part-[content]:px-5">
<btrix-table>
<btrix-table-head class="mb-2">
<slot
@ -472,7 +472,7 @@ export class ArchivedItemList extends TailwindElement {
<slot></slot>
</btrix-table-body>
</btrix-table>
</div>
</btrix-overflow-scroll>
`;
}
}

View File

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

View File

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

View File

@ -195,14 +195,39 @@ export class CollectionsList extends BtrixElement {
>
${this.renderControls()}
</div>
<div class="-mx-3 overflow-auto px-3 pb-1">
<btrix-overflow-scroll class="-mx-3 pb-1 part-[content]:px-3">
${guard(
[this.collections, this.listView, this.collectionRefreshing],
this.listView === ListView.List
? this.renderList
: 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(),
)}
@ -507,29 +532,6 @@ export class CollectionsList extends BtrixElement {
${this.collections.items.map(this.renderItem)}
</btrix-table-body>
</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
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">
${this.renderNavTab({
tabName: OrgTab.Dashboard,
label: msg("Dashboard"),
})}
${this.renderNavTab({
tabName: OrgTab.Workflows,
label: msg("Crawling"),
})}
${this.renderNavTab({
tabName: OrgTab.Items,
label: msg("Archived Items"),
})}
${this.renderNavTab({
tabName: OrgTab.Collections,
label: msg("Collections"),
})}
${when(this.appState.isCrawler, () =>
this.renderNavTab({
tabName: OrgTab.BrowserProfiles,
label: msg("Browser Profiles"),
}),
)}
${when(this.appState.isAdmin || this.userInfo?.isSuperAdmin, () =>
this.renderNavTab({
tabName: OrgTab.Settings,
label: msg("Settings"),
}),
)}
</nav>
<btrix-overflow-scroll class="-mx-3 part-[content]:px-3">
<nav class="flex items-end xl:px-6">
${this.renderNavTab({
tabName: OrgTab.Dashboard,
label: msg("Dashboard"),
})}
${this.renderNavTab({
tabName: OrgTab.Workflows,
label: msg("Crawling"),
})}
${this.renderNavTab({
tabName: OrgTab.Items,
label: msg("Archived Items"),
})}
${this.renderNavTab({
tabName: OrgTab.Collections,
label: msg("Collections"),
})}
${when(this.appState.isCrawler, () =>
this.renderNavTab({
tabName: OrgTab.BrowserProfiles,
label: msg("Browser Profiles"),
}),
)}
${when(this.appState.isAdmin || this.userInfo?.isSuperAdmin, () =>
this.renderNavTab({
tabName: OrgTab.Settings,
label: msg("Settings"),
}),
)}
</nav>
</btrix-overflow-scroll>
</div>
<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>
`;
};