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"
|
||||
></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()}
|
||||
|
||||
@ -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");
|
||||
|
||||
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(" ")};
|
||||
}
|
||||
</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>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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() {
|
||||
|
||||
@ -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`
|
||||
|
||||
@ -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>
|
||||
`,
|
||||
)}
|
||||
`;
|
||||
}
|
||||
|
||||
|
||||
@ -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 />
|
||||
|
||||
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