Add overflow scroll component with scroll scrim/shadow (#2578)
This commit is contained in:
parent
4ce769ecab
commit
8b6e1ca9af
@ -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()}
|
||||||
|
|||||||
@ -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");
|
||||||
|
|||||||
105
frontend/src/components/ui/overflow-scroll.ts
Normal file
105
frontend/src/components/ui/overflow-scroll.ts
Normal 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>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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() {
|
||||||
|
|||||||
@ -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`
|
||||||
|
|||||||
@ -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>
|
|
||||||
`,
|
|
||||||
)}
|
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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 />
|
||||||
|
|||||||
30
frontend/src/stories/components/OverflowScroll.stories.ts
Normal file
30
frontend/src/stories/components/OverflowScroll.stories.ts
Normal 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,
|
||||||
|
},
|
||||||
|
};
|
||||||
37
frontend/src/stories/components/OverflowScroll.ts
Normal file
37
frontend/src/stories/components/OverflowScroll.ts
Normal 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>
|
||||||
|
`;
|
||||||
|
};
|
||||||
Loading…
Reference in New Issue
Block a user