mirror of
https://github.com/bulwarkmail/webmail.git
synced 2026-11-06 00:15:32 +00:00
fix: mark the active Pro tab and draw the stored tab count in the skeleton
This commit is contained in:
+14
-14
@@ -154,20 +154,20 @@
|
||||
/* The focused pane's active tab carries the accent top edge. */
|
||||
--sk-pro-pane-accent: var(--color-primary);
|
||||
}
|
||||
:root:not([data-boot-pro-main-kind]) [data-sk-pro-pane="main"] > [data-sk-pro-body="mail"],
|
||||
:root:not([data-boot-pro-split-kind]) [data-sk-pro-pane="split"] > [data-sk-pro-body="mail"],
|
||||
:root[data-boot-pro-main-kind="mail"] [data-sk-pro-pane="main"] > [data-sk-pro-body="mail"],
|
||||
:root[data-boot-pro-main-kind="calendar"] [data-sk-pro-pane="main"] > [data-sk-pro-body="calendar"],
|
||||
:root[data-boot-pro-main-kind="contacts"] [data-sk-pro-pane="main"] > [data-sk-pro-body="contacts"],
|
||||
:root[data-boot-pro-main-kind="files"] [data-sk-pro-pane="main"] > [data-sk-pro-body="files"],
|
||||
:root[data-boot-pro-main-kind="settings"] [data-sk-pro-pane="main"] > [data-sk-pro-body="settings"],
|
||||
:root[data-boot-pro-main-kind="other"] [data-sk-pro-pane="main"] > [data-sk-pro-body="other"],
|
||||
:root[data-boot-pro-split-kind="mail"] [data-sk-pro-pane="split"] > [data-sk-pro-body="mail"],
|
||||
:root[data-boot-pro-split-kind="calendar"] [data-sk-pro-pane="split"] > [data-sk-pro-body="calendar"],
|
||||
:root[data-boot-pro-split-kind="contacts"] [data-sk-pro-pane="split"] > [data-sk-pro-body="contacts"],
|
||||
:root[data-boot-pro-split-kind="files"] [data-sk-pro-pane="split"] > [data-sk-pro-body="files"],
|
||||
:root[data-boot-pro-split-kind="settings"] [data-sk-pro-pane="split"] > [data-sk-pro-body="settings"],
|
||||
:root[data-boot-pro-split-kind="other"] [data-sk-pro-pane="split"] > [data-sk-pro-body="other"] {
|
||||
:root:not([data-boot-pro-main-kind]) [data-sk-pro-pane="main"] [data-sk-pro-body="mail"],
|
||||
:root:not([data-boot-pro-split-kind]) [data-sk-pro-pane="split"] [data-sk-pro-body="mail"],
|
||||
:root[data-boot-pro-main-kind="mail"] [data-sk-pro-pane="main"] [data-sk-pro-body="mail"],
|
||||
:root[data-boot-pro-main-kind="calendar"] [data-sk-pro-pane="main"] [data-sk-pro-body="calendar"],
|
||||
:root[data-boot-pro-main-kind="contacts"] [data-sk-pro-pane="main"] [data-sk-pro-body="contacts"],
|
||||
:root[data-boot-pro-main-kind="files"] [data-sk-pro-pane="main"] [data-sk-pro-body="files"],
|
||||
:root[data-boot-pro-main-kind="settings"] [data-sk-pro-pane="main"] [data-sk-pro-body="settings"],
|
||||
:root[data-boot-pro-main-kind="other"] [data-sk-pro-pane="main"] [data-sk-pro-body="other"],
|
||||
:root[data-boot-pro-split-kind="mail"] [data-sk-pro-pane="split"] [data-sk-pro-body="mail"],
|
||||
:root[data-boot-pro-split-kind="calendar"] [data-sk-pro-pane="split"] [data-sk-pro-body="calendar"],
|
||||
:root[data-boot-pro-split-kind="contacts"] [data-sk-pro-pane="split"] [data-sk-pro-body="contacts"],
|
||||
:root[data-boot-pro-split-kind="files"] [data-sk-pro-pane="split"] [data-sk-pro-body="files"],
|
||||
:root[data-boot-pro-split-kind="settings"] [data-sk-pro-pane="split"] [data-sk-pro-body="settings"],
|
||||
:root[data-boot-pro-split-kind="other"] [data-sk-pro-pane="split"] [data-sk-pro-body="other"] {
|
||||
display: block;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
||||
import { renderToString } from 'react-dom/server';
|
||||
import { AppShellSkeleton } from '../shell';
|
||||
import { ProSkeleton } from '../pro-skeleton';
|
||||
@@ -48,15 +50,17 @@ describe('AppShellSkeleton under the Pro interface', () => {
|
||||
const panes = pro?.querySelectorAll('[data-sk-pro-pane]') ?? [];
|
||||
expect([...panes].map((p) => p.getAttribute('data-sk-pro-pane'))).toEqual(['main', 'split']);
|
||||
for (const pane of panes) {
|
||||
// Tab strip right above the pane body, eight tab slots for the CSS to pick from.
|
||||
expect(pane.previousElementSibling?.querySelectorAll('[data-sk-pro-tab]')).toHaveLength(8);
|
||||
expect(classes(pane.previousElementSibling)).toEqual(expect.arrayContaining(['h-9', 'bg-secondary']));
|
||||
expect([...pane.children].map((b) => b.getAttribute('data-sk-pro-body'))).toEqual([
|
||||
// The tab strip, with eight tab slots for the CSS to pick from, right
|
||||
// above the pane body.
|
||||
const [strip, body] = [...pane.children];
|
||||
expect(strip.querySelectorAll('[data-sk-pro-tab]')).toHaveLength(8);
|
||||
expect(classes(strip)).toEqual(expect.arrayContaining(['h-9', 'bg-secondary']));
|
||||
expect([...body.children].map((b) => b.getAttribute('data-sk-pro-body'))).toEqual([
|
||||
'mail', 'calendar', 'contacts', 'files', 'settings', 'other',
|
||||
]);
|
||||
}
|
||||
// The split pane only shows with a split.
|
||||
expect(classes(panes[1]?.parentElement)).toEqual(expect.arrayContaining(['hidden', 'boot-pro-split:flex']));
|
||||
expect(classes(panes[1])).toEqual(expect.arrayContaining(['hidden', 'boot-pro-split:flex']));
|
||||
});
|
||||
|
||||
it('lets the login card win for a signed-out visitor', () => {
|
||||
@@ -75,3 +79,58 @@ describe('ProSkeleton', () => {
|
||||
expect(container.querySelector('[data-boot-skeleton="login"]')).not.toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('the Pro CSS picks the stored tabs and pages', () => {
|
||||
// The "pro" section of app/boot-skeleton.css, minus the Tailwind variants,
|
||||
// applied to the real markup with the attributes the boot script sets.
|
||||
const css = readFileSync(path.resolve(__dirname, '../../../app/boot-skeleton.css'), 'utf8')
|
||||
.split('/* pro */')[1]
|
||||
.replace(/@custom-variant[^;]*;/g, '');
|
||||
|
||||
let container: HTMLElement;
|
||||
beforeEach(() => {
|
||||
const style = document.createElement('style');
|
||||
style.id = 'pro-css';
|
||||
style.textContent = css;
|
||||
document.head.appendChild(style);
|
||||
container = document.createElement('div');
|
||||
container.innerHTML = renderToString(<ProSkeleton label="Loading" />);
|
||||
document.body.appendChild(container);
|
||||
});
|
||||
afterEach(() => {
|
||||
document.getElementById('pro-css')?.remove();
|
||||
container.remove();
|
||||
for (const a of [...document.documentElement.attributes]) {
|
||||
if (a.name.startsWith('data-boot-')) document.documentElement.removeAttribute(a.name);
|
||||
}
|
||||
});
|
||||
|
||||
const boot = (attrs: Record<string, string>) => {
|
||||
for (const [k, v] of Object.entries(attrs)) document.documentElement.setAttribute(`data-boot-${k}`, v);
|
||||
};
|
||||
const pane = (which: string) => container.querySelector(`[data-sk-pro-pane="${which}"]`)!;
|
||||
const shown = (els: Iterable<Element>) => [...els].filter((el) => getComputedStyle(el).display !== 'none');
|
||||
const tabs = (which: string) => shown(pane(which).querySelectorAll('[data-sk-pro-tab]'));
|
||||
const active = (which: string) =>
|
||||
[...pane(which).querySelectorAll('[data-sk-pro-tab]')].findIndex(
|
||||
(el) => !['', 'transparent', 'rgba(0, 0, 0, 0)'].includes(getComputedStyle(el).backgroundColor),
|
||||
);
|
||||
const body = (which: string) =>
|
||||
shown(pane(which).querySelectorAll('[data-sk-pro-body]')).map((el) => el.getAttribute('data-sk-pro-body'));
|
||||
|
||||
it('draws one active mail tab without attributes', () => {
|
||||
expect(tabs('main')).toHaveLength(1);
|
||||
expect(active('main')).toBe(0);
|
||||
expect(body('main')).toEqual(['mail']);
|
||||
});
|
||||
|
||||
it('draws the stored count, the active tab and its page in each pane', () => {
|
||||
boot({ pro: 'on', 'pro-main-tabs': '3', 'pro-main-active': '2', 'pro-main-kind': 'calendar', 'pro-split-tabs': '2', 'pro-split-active': '0', 'pro-split-kind': 'other', 'pro-split': 'right' });
|
||||
expect(tabs('main')).toHaveLength(3);
|
||||
expect(active('main')).toBe(2);
|
||||
expect(body('main')).toEqual(['calendar']);
|
||||
expect(tabs('split')).toHaveLength(2);
|
||||
expect(active('split')).toBe(0);
|
||||
expect(body('split')).toEqual(['other']);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -67,12 +67,9 @@ function TabStripSkeleton() {
|
||||
/** One pane: its tab strip over the active tab's page. */
|
||||
function PaneSkeleton({ pane, className }: { pane: "main" | "split"; className?: string }) {
|
||||
return (
|
||||
<div className={cn("flex flex-1 flex-col overflow-hidden min-w-0 min-h-0", className)}>
|
||||
<div data-sk-pro-pane={pane} className={cn("flex flex-1 flex-col overflow-hidden min-w-0 min-h-0", className)}>
|
||||
<TabStripSkeleton />
|
||||
<div
|
||||
data-sk-pro-pane={pane}
|
||||
className="@container relative flex flex-1 flex-col overflow-hidden min-w-0 min-h-0"
|
||||
>
|
||||
<div className="@container relative flex flex-1 flex-col overflow-hidden min-w-0 min-h-0">
|
||||
{BODIES.map((surface) => (
|
||||
<div key={surface} data-sk-pro-body={surface} className="absolute inset-0 overflow-hidden">
|
||||
<div className="flex h-full min-h-0 overflow-hidden">
|
||||
|
||||
Reference in New Issue
Block a user