Lots of regenerating
|
After Width: | Height: | Size: 45 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 44 KiB |
|
After Width: | Height: | Size: 51 KiB |
|
After Width: | Height: | Size: 43 KiB |
|
After Width: | Height: | Size: 51 KiB |
|
After Width: | Height: | Size: 39 KiB |
|
After Width: | Height: | Size: 39 KiB |
|
After Width: | Height: | Size: 40 KiB |
@ -75,6 +75,18 @@ const meta = {
|
||||
type: "figma",
|
||||
url: "https://www.figma.com/design/1UCf1hF507QaRus3CUBKGn/Nectcloud-File-Picker?node-id=2138-14865&t=njpHkpdk8tVhp7cr-0",
|
||||
},
|
||||
a11y: {
|
||||
config: {
|
||||
rules: [
|
||||
{
|
||||
// TODO: We need a new folder icon, the current one is a emoji and we
|
||||
// can't determine the contrast.
|
||||
id: "color-contrast",
|
||||
enabled: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
} satisfies Meta<typeof FileListWrapper>;
|
||||
|
||||
|
||||
@ -0,0 +1,70 @@
|
||||
/*
|
||||
* Copyright 2026 Element Creations Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import { render } from "@test-utils";
|
||||
import { composeStories } from "@storybook/react-vite";
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { fn } from "storybook/test";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
|
||||
import * as stories from "./GridFileList.stories";
|
||||
import { BaseViewModel } from "../../core/viewmodel";
|
||||
import type { FileShareViewModel, FileShareViewSnapshot } from "./Viewmodel";
|
||||
import { FileListView } from "./FileList";
|
||||
|
||||
const { Default } = composeStories(stories);
|
||||
|
||||
class MockViewModel extends BaseViewModel<FileShareViewSnapshot, unknown> implements FileShareViewModel {
|
||||
public constructor(snapshot: FileShareViewSnapshot) {
|
||||
super(undefined, snapshot);
|
||||
}
|
||||
public loadFiles = fn();
|
||||
public setCurrentDirectory = fn();
|
||||
public goBackDirectory = fn();
|
||||
public onFileSelected = fn();
|
||||
public getThumbnailForFile = fn().mockResolvedValue(null);
|
||||
public setFileViewSetting = fn();
|
||||
}
|
||||
|
||||
function renderView(initialSnapshot?: Partial<FileShareViewSnapshot>): [ReturnType<typeof render>, MockViewModel] {
|
||||
const snapshot: FileShareViewSnapshot = {
|
||||
currentDirectory: [],
|
||||
directories: [],
|
||||
files: [
|
||||
{
|
||||
id: "a_file",
|
||||
name: "myfile.txt",
|
||||
},
|
||||
],
|
||||
selectedFiles: [],
|
||||
loading: false,
|
||||
sending: false,
|
||||
viewSetting: "list",
|
||||
...initialSnapshot,
|
||||
};
|
||||
const vm = new MockViewModel(snapshot);
|
||||
const result = render(<FileListView vm={vm} />);
|
||||
return [result, vm];
|
||||
}
|
||||
|
||||
describe("<FileListView />", () => {
|
||||
it("renders Default story", () => {
|
||||
const { container } = render(<Default />);
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
it("can select a file in details mode", async () => {
|
||||
const [{ getByLabelText }, vm] = renderView();
|
||||
await userEvent.click(getByLabelText("myfile.txt"));
|
||||
expect(vm.onFileSelected).toHaveBeenCalledWith("a_file");
|
||||
});
|
||||
it("can select a file in grid mode", async () => {
|
||||
const [{ getByLabelText }, vm] = renderView({ viewSetting: "grid" });
|
||||
await userEvent.click(getByLabelText("myfile.txt"));
|
||||
expect(vm.onFileSelected).toHaveBeenCalledWith("a_file");
|
||||
});
|
||||
});
|
||||
@ -4,8 +4,17 @@
|
||||
display: grid;
|
||||
gap: 0 var(--cpd-space-4x);
|
||||
padding: var(--cpd-space-4x);
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
grid-template-columns: repeat(auto-fit, minmax(128px, 1fr));
|
||||
border-radius: var(--cpd-space-2x);
|
||||
|
||||
li {
|
||||
list-style: none;
|
||||
gap: var(--cpd-space-3x);
|
||||
> button {
|
||||
width: fit-content;
|
||||
}
|
||||
}
|
||||
|
||||
button.fileTile:hover {
|
||||
background: var(--cpd-color-bg-action-tertiary-hovered);
|
||||
}
|
||||
@ -17,7 +26,6 @@
|
||||
label {
|
||||
display: block;
|
||||
}
|
||||
gap: var(--cpd-space-3x);
|
||||
border: none;
|
||||
|
||||
label {
|
||||
@ -29,6 +37,8 @@
|
||||
}
|
||||
|
||||
.previewThumb {
|
||||
width: 128px;
|
||||
height: 128px;
|
||||
width: 100%;
|
||||
aspect-ratio: 1;
|
||||
background-size: cover;
|
||||
@ -42,15 +52,16 @@
|
||||
}
|
||||
|
||||
.bigIcon {
|
||||
width: fit-content;
|
||||
max-width: 128px;
|
||||
max-height: 128px;
|
||||
padding: 4em;
|
||||
aspect-ratio: 1;
|
||||
margin: 0 auto;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
color: var(--cpd-color-icon-secondary);
|
||||
> span {
|
||||
line-height: 0.6;
|
||||
font-size: 36px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
margin: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -62,6 +62,23 @@ const meta = {
|
||||
type: "figma",
|
||||
url: "https://www.figma.com/design/1UCf1hF507QaRus3CUBKGn/Nectcloud-File-Picker?node-id=2138-14865&t=njpHkpdk8tVhp7cr-0",
|
||||
},
|
||||
a11y: {
|
||||
config: {
|
||||
rules: [
|
||||
{
|
||||
// TODO: We need a new folder icon, the current one is a emoji and we
|
||||
// can't determine the contrast.
|
||||
id: "color-contrast",
|
||||
enabled: false,
|
||||
},
|
||||
{
|
||||
// So that we can hide the button which is just a bigger target for the checkbox.
|
||||
id: "aria-hidden-focus",
|
||||
enabled: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
} satisfies Meta<typeof GridFileListView>;
|
||||
|
||||
|
||||
@ -38,21 +38,23 @@ function DirectoryItem({
|
||||
);
|
||||
|
||||
return (
|
||||
<button
|
||||
data-kind="primary"
|
||||
className={classNames(styles.fileTile)}
|
||||
disabled={disabled}
|
||||
onClick={onTileClick}
|
||||
id={id}
|
||||
>
|
||||
<BigIcon size="md" className={styles.bigIcon}>
|
||||
<span>🗀</span>
|
||||
</BigIcon>
|
||||
<div>
|
||||
<label htmlFor={id}>{name}</label>
|
||||
{updatedAt && <span className={styles.timestamp}>{i18n.humanizeTime(updatedAt.getTime())}</span>}
|
||||
</div>
|
||||
</button>
|
||||
<li>
|
||||
<button
|
||||
data-kind="primary"
|
||||
className={classNames(styles.fileTile)}
|
||||
disabled={disabled}
|
||||
onClick={onTileClick}
|
||||
id={id}
|
||||
>
|
||||
<BigIcon size="md" className={styles.bigIcon}>
|
||||
<span>🗀</span>
|
||||
</BigIcon>
|
||||
<div>
|
||||
<label htmlFor={id}>{name}</label>
|
||||
{updatedAt && <span className={styles.timestamp}>{i18n.humanizeTime(updatedAt.getTime())}</span>}
|
||||
</div>
|
||||
</button>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
@ -83,24 +85,29 @@ function FileItem({
|
||||
}, [fileId, previewEngine]);
|
||||
|
||||
return (
|
||||
<button
|
||||
data-kind="primary"
|
||||
className={classNames(styles.fileTile)}
|
||||
disabled={disabled}
|
||||
onClick={() => onChange()}
|
||||
id={id}
|
||||
>
|
||||
<div
|
||||
className={classNames(styles.previewThumb)}
|
||||
style={{ backgroundImage: previewUrl ? `url("${previewUrl}")` : undefined }}
|
||||
<li>
|
||||
<button
|
||||
data-kind="primary"
|
||||
className={classNames(styles.fileTile)}
|
||||
disabled={disabled}
|
||||
onClick={() => onChange()}
|
||||
id={id}
|
||||
tabIndex={-1}
|
||||
/* For keyboard nav, use the checkbox. The button is just a bigger target. */
|
||||
aria-hidden
|
||||
>
|
||||
<Checkbox checked={selected} id={id} disabled={disabled} onChange={() => onChange()} />
|
||||
</div>
|
||||
<div>
|
||||
<label htmlFor={id}>{name}</label>
|
||||
{updatedAt && <span className={styles.timestamp}>{i18n.humanizeTime(updatedAt.getTime())}</span>}
|
||||
</div>
|
||||
</button>
|
||||
<div
|
||||
className={classNames(styles.previewThumb)}
|
||||
style={{ backgroundImage: previewUrl ? `url("${previewUrl}")` : undefined }}
|
||||
>
|
||||
<Checkbox aria-labelledby={id} checked={selected} />
|
||||
</div>
|
||||
<div>
|
||||
<label htmlFor={id}>{name}</label>
|
||||
{updatedAt && <span className={styles.timestamp}>{i18n.humanizeTime(updatedAt.getTime())}</span>}
|
||||
</div>
|
||||
</button>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -55,6 +55,18 @@ const meta = {
|
||||
type: "figma",
|
||||
url: "https://www.figma.com/design/1UCf1hF507QaRus3CUBKGn/Nectcloud-File-Picker?node-id=2138-14865&t=njpHkpdk8tVhp7cr-0",
|
||||
},
|
||||
a11y: {
|
||||
config: {
|
||||
rules: [
|
||||
{
|
||||
// TODO: We need a new folder icon, the current one is a emoji and we
|
||||
// can't determine the contrast.
|
||||
id: "color-contrast",
|
||||
enabled: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
} satisfies Meta<typeof VerticalFileListView>;
|
||||
|
||||
|
||||
@ -62,8 +62,10 @@ function FileItem({
|
||||
<div className={styles.fileEntryComponentName}>
|
||||
<Checkbox checked={selected} id={id} disabled={disabled} onChange={() => onChange()} />
|
||||
<div>
|
||||
<label htmlFor={id}>{fileName}</label>
|
||||
{fileExt[0] && <span className={styles.fileEntryNameExtension}>.{fileExt.join(".")}</span>}
|
||||
<label htmlFor={id}>
|
||||
{fileName}
|
||||
{fileExt[0] && <span className={styles.fileEntryNameExtension}>.{fileExt.join(".")}</span>}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
{updatedAt && <span className={styles.timestamp}>{i18n.humanizeTime(updatedAt.getTime())}</span>}
|
||||
|
||||
@ -5,7 +5,7 @@
|
||||
*/
|
||||
|
||||
import type { ViewModel } from "../../core/viewmodel";
|
||||
import { GridFileListPreviewEngine } from "./GridFileList";
|
||||
import type { GridFileListPreviewEngine } from "./GridFileList";
|
||||
|
||||
export type FileId = string;
|
||||
export type FileShareViewSetting = "list" | "grid";
|
||||
@ -23,7 +23,6 @@ export interface FileShareViewSnapshot {
|
||||
}
|
||||
|
||||
export interface FileShareActions {
|
||||
loadFiles(): Promise<void>;
|
||||
setCurrentDirectory(name: string): void;
|
||||
goBackDirectory(index?: number): void;
|
||||
onFileSelected(name: string): void;
|
||||
|
||||
@ -0,0 +1,262 @@
|
||||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||
|
||||
exports[`<FileListView /> > renders Default story 1`] = `
|
||||
<div>
|
||||
<ol
|
||||
class="GridFileList-module_container"
|
||||
>
|
||||
<button
|
||||
class="GridFileList-module_fileTile"
|
||||
data-kind="primary"
|
||||
id="_r_0_"
|
||||
>
|
||||
<div
|
||||
class="_big-icon_1ssbv_8 GridFileList-module_bigIcon"
|
||||
data-kind="primary"
|
||||
data-size="md"
|
||||
>
|
||||
<span>
|
||||
🗀
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<label
|
||||
for="_r_0_"
|
||||
>
|
||||
A directory
|
||||
</label>
|
||||
</div>
|
||||
</button>
|
||||
<button
|
||||
class="GridFileList-module_fileTile"
|
||||
data-kind="primary"
|
||||
id="_r_1_"
|
||||
>
|
||||
<div
|
||||
class="GridFileList-module_previewThumb"
|
||||
>
|
||||
<div
|
||||
class="_container_153f2_10"
|
||||
>
|
||||
<input
|
||||
class="_input_153f2_18"
|
||||
tabindex="-1"
|
||||
type="checkbox"
|
||||
/>
|
||||
<div
|
||||
class="_ui_153f2_19"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M9.55 17.575q-.2 0-.375-.062a.9.9 0 0 1-.325-.213L4.55 13q-.274-.274-.262-.713.012-.437.287-.712a.95.95 0 0 1 .7-.275q.425 0 .7.275L9.55 15.15l8.475-8.475q.274-.275.713-.275.437 0 .712.275.275.274.275.713 0 .437-.275.712l-9.2 9.2q-.15.15-.325.212a1.1 1.1 0 0 1-.375.063"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label
|
||||
for="_r_1_"
|
||||
>
|
||||
testfile.txt
|
||||
</label>
|
||||
<span
|
||||
class="GridFileList-module_timestamp"
|
||||
>
|
||||
89 days ago
|
||||
</span>
|
||||
</div>
|
||||
</button>
|
||||
<button
|
||||
class="GridFileList-module_fileTile"
|
||||
data-kind="primary"
|
||||
id="_r_2_"
|
||||
>
|
||||
<div
|
||||
class="GridFileList-module_previewThumb"
|
||||
>
|
||||
<div
|
||||
class="_container_153f2_10"
|
||||
>
|
||||
<input
|
||||
class="_input_153f2_18"
|
||||
tabindex="-1"
|
||||
type="checkbox"
|
||||
/>
|
||||
<div
|
||||
class="_ui_153f2_19"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M9.55 17.575q-.2 0-.375-.062a.9.9 0 0 1-.325-.213L4.55 13q-.274-.274-.262-.713.012-.437.287-.712a.95.95 0 0 1 .7-.275q.425 0 .7.275L9.55 15.15l8.475-8.475q.274-.275.713-.275.437 0 .712.275.275.274.275.713 0 .437-.275.712l-9.2 9.2q-.15.15-.325.212a1.1 1.1 0 0 1-.375.063"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label
|
||||
for="_r_2_"
|
||||
>
|
||||
image.png
|
||||
</label>
|
||||
<span
|
||||
class="GridFileList-module_timestamp"
|
||||
>
|
||||
89 days ago
|
||||
</span>
|
||||
</div>
|
||||
</button>
|
||||
<button
|
||||
class="GridFileList-module_fileTile"
|
||||
data-kind="primary"
|
||||
id="_r_3_"
|
||||
>
|
||||
<div
|
||||
class="GridFileList-module_previewThumb"
|
||||
>
|
||||
<div
|
||||
class="_container_153f2_10"
|
||||
>
|
||||
<input
|
||||
class="_input_153f2_18"
|
||||
tabindex="-1"
|
||||
type="checkbox"
|
||||
/>
|
||||
<div
|
||||
class="_ui_153f2_19"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M9.55 17.575q-.2 0-.375-.062a.9.9 0 0 1-.325-.213L4.55 13q-.274-.274-.262-.713.012-.437.287-.712a.95.95 0 0 1 .7-.275q.425 0 .7.275L9.55 15.15l8.475-8.475q.274-.275.713-.275.437 0 .712.275.275.274.275.713 0 .437-.275.712l-9.2 9.2q-.15.15-.325.212a1.1 1.1 0 0 1-.375.063"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label
|
||||
for="_r_3_"
|
||||
>
|
||||
compressed.tar.gz
|
||||
</label>
|
||||
<span
|
||||
class="GridFileList-module_timestamp"
|
||||
>
|
||||
89 days ago
|
||||
</span>
|
||||
</div>
|
||||
</button>
|
||||
<button
|
||||
class="GridFileList-module_fileTile"
|
||||
data-kind="primary"
|
||||
id="_r_4_"
|
||||
>
|
||||
<div
|
||||
class="GridFileList-module_previewThumb"
|
||||
>
|
||||
<div
|
||||
class="_container_153f2_10"
|
||||
>
|
||||
<input
|
||||
class="_input_153f2_18"
|
||||
tabindex="-1"
|
||||
type="checkbox"
|
||||
/>
|
||||
<div
|
||||
class="_ui_153f2_19"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M9.55 17.575q-.2 0-.375-.062a.9.9 0 0 1-.325-.213L4.55 13q-.274-.274-.262-.713.012-.437.287-.712a.95.95 0 0 1 .7-.275q.425 0 .7.275L9.55 15.15l8.475-8.475q.274-.275.713-.275.437 0 .712.275.275.274.275.713 0 .437-.275.712l-9.2 9.2q-.15.15-.325.212a1.1 1.1 0 0 1-.375.063"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label
|
||||
for="_r_4_"
|
||||
>
|
||||
no-extension
|
||||
</label>
|
||||
<span
|
||||
class="GridFileList-module_timestamp"
|
||||
>
|
||||
30 days ago
|
||||
</span>
|
||||
</div>
|
||||
</button>
|
||||
<button
|
||||
class="GridFileList-module_fileTile"
|
||||
data-kind="primary"
|
||||
id="_r_5_"
|
||||
>
|
||||
<div
|
||||
class="GridFileList-module_previewThumb"
|
||||
>
|
||||
<div
|
||||
class="_container_153f2_10"
|
||||
>
|
||||
<input
|
||||
class="_input_153f2_18"
|
||||
tabindex="-1"
|
||||
type="checkbox"
|
||||
/>
|
||||
<div
|
||||
class="_ui_153f2_19"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M9.55 17.575q-.2 0-.375-.062a.9.9 0 0 1-.325-.213L4.55 13q-.274-.274-.262-.713.012-.437.287-.712a.95.95 0 0 1 .7-.275q.425 0 .7.275L9.55 15.15l8.475-8.475q.274-.275.713-.275.437 0 .712.275.275.274.275.713 0 .437-.275.712l-9.2 9.2q-.15.15-.325.212a1.1 1.1 0 0 1-.375.063"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label
|
||||
for="_r_5_"
|
||||
>
|
||||
no modified time
|
||||
</label>
|
||||
</div>
|
||||
</button>
|
||||
</ol>
|
||||
</div>
|
||||
`;
|
||||