Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

316 tile component #1094

Draft
wants to merge 4 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,11 @@
&[data-empty="true"] {
border: none;
}

&.tiles {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
border: none;
gap: var(--list-item--spacing);
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { FC } from "react";
import React from "react";
import { useList } from "@/components/List/hooks/useList";
import styles from "./Items.module.css";
import styles from "./Items.module.scss";
import clsx from "clsx";
import * as Aria from "react-aria-components";
import Item from "@/components/List/components/Items/components/Item/Item";
Expand All @@ -21,7 +21,11 @@ export const Items: FC = () => {
<Item key={item.id} data={item.data} id={item.id} />
));

const rootClassName = clsx(styles.items, isLoading && styles.isLoading);
const rootClassName = clsx(
styles.items,
isLoading && styles.isLoading,
list.tile && styles.tiles,
);

return (
<div aria-hidden={isInitiallyLoading} aria-busy={isLoading}>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,27 @@
background-color: var(--list-item--background-color--pressed);
}
}

&.tile {
border-color: var(--list-item--border-color);
border-width: var(--list-item--border-width);
border-style: var(--list-item--border-style);
border-radius: var(--list-item--corner-radius);

&:where(.hasAction) {
&:not(.isSelected) {
&:hover {
:global(.flow--avatar) {
filter: brightness(var(--image-button--brightness--hover));
}
}
}

&[data-pressed] {
:global(.flow--avatar) {
filter: brightness(var(--image-button--brightness--pressed));
}
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ export const Item = (props: Props) => {
styles.item,
hasAction && styles.hasAction,
props.isSelected && styles.isSelected,
list.tile && styles.tile,
)
}
textValue={textValue}
Expand All @@ -48,10 +49,21 @@ export const Item = (props: Props) => {
);
};

export const ItemContainer: FC<PropsWithChildren> = (props) => (
<Aria.GridListItem textValue="-" className={styles.item}>
{props.children}
</Aria.GridListItem>
);
export const ItemContainer: FC<PropsWithChildren> = (props) => {
const list = useList();

return (
<Aria.GridListItem
textValue="-"
className={clsx(
styles.item,
styles.fallbackItem,
list.tile && styles.tile,
)}
>
{props.children}
</Aria.GridListItem>
);
};

export default Item;
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,21 @@ import { Heading } from "@/components/Heading";
import { Text } from "@/components/Text";
import View from "@/components/List/components/Items/components/Item/components/View/View";
import SkeletonText from "@/components/SkeletonText";
import { useList } from "@/components/List";
import { Skeleton } from "@/components/Skeleton";

export const SkeletonView: FC = () => (
<View>
<Heading>
<SkeletonText width="200px" />
</Heading>
<Text>
<SkeletonText width="300px" />
</Text>
</View>
);
export const SkeletonView: FC = () => {
const list = useList();

return (
<View>
{list.tile && <Skeleton style={{ aspectRatio: 16 / 9 }} />}
<Heading>
<SkeletonText width="200px" />
</Heading>
<Text>
<SkeletonText width="300px" />
</Text>
</View>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@
display: flex;
padding: var(--list-item--padding);
gap: var(--list-item--spacing);
align-items: start;
align-items: center;
flex-wrap: wrap;

.content {
display: flex;
Expand Down Expand Up @@ -77,4 +78,45 @@
order: 4;
width: 100%;
}

&.tile {
padding: 0;

.avatarContainer {
width: 100%;
overflow: hidden;
border-bottom-color: var(--list-item--border-color);
border-bottom-width: var(--list-item--border-width);
border-bottom-style: var(--list-item--border-style);
aspect-ratio: 16 / 9;
}

.avatar {
width: 100%;
height: 100%;
border-radius: 0;

:global(.flow--avatar--icon) {
width: var(--size-px--xxl);
height: var(--size-px--xxl);
}

:global(.flow--avatar--initials) {
font-size: var(--size-px--xxl);
}
}

.content {
display: flex;
justify-content: space-between;
align-items: start;
padding-inline: var(--list-item--spacing);
padding-bottom: var(--list-item--spacing);
width: 100%;
}

.topContent {
width: 100%;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { OptionsButton } from "@/components/List/components/Items/components/Ite
import { TunnelExit, TunnelProvider } from "@mittwald/react-tunnel";
import type { PropsWithClassName } from "@/lib/types/props";
import clsx from "clsx";
import { useList } from "@/components/List";

type Props = PropsWithChildren & PropsWithClassName;

Expand All @@ -19,6 +20,7 @@ const getStyleForContentSlot = (slot?: string) =>

export const View = (props: Props) => {
const { children, className } = props;
const list = useList();

const propsContext: PropsContext = {
ContextMenu: {
Expand All @@ -40,10 +42,11 @@ export const View = (props: Props) => {
},
Content: {
className: dynamic((p) => getStyleForContentSlot(p.slot)),
tunnelId: dynamic((p) => (p.slot === "bottom" ? p.slot : undefined)),
},
Avatar: {
className: styles.avatar,
tunnelId: "title",
tunnelId: "avatar",
},
Heading: {
className: styles.heading,
Expand All @@ -59,22 +62,47 @@ export const View = (props: Props) => {
},
};

const rootClassName = clsx(styles.view, className);
const rootClassName = clsx(styles.view, list.tile && styles.tile, className);

return (
<div className={rootClassName}>
<PropsContextProvider props={propsContext} mergeInParentContext>
<TunnelProvider>
<div className={styles.content}>
{children}
<div className={styles.title}>
<TunnelExit id="title" />
<div className={styles.subTitle}>
<TunnelExit id="text" />
{list.tile && (
<>
<div className={styles.avatarContainer}>
<TunnelExit id="avatar" />
</div>
<div className={styles.content}>
<div className={styles.title}>
<TunnelExit id="title" />
<div className={styles.subTitle}>
<TunnelExit id="text" />
</div>
</div>
<TunnelExit id="button" />
{children}
<TunnelExit id="bottom" />
</div>
</>
)}

{!list.tile && (
<>
<div className={styles.content}>
<div className={styles.title}>
<TunnelExit id="avatar" />
<TunnelExit id="title" />
<div className={styles.subTitle}>
<TunnelExit id="text" />
</div>
</div>
{children}
</div>
</div>
</div>
<TunnelExit id="button" />
<TunnelExit id="button" />
<TunnelExit id="bottom" />
</>
)}
</TunnelProvider>
</PropsContextProvider>
</div>
Expand Down
13 changes: 7 additions & 6 deletions packages/components/src/components/List/model/List.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ import type { SettingsStore } from "@/components/SettingsProvider/models/Setting
import z from "zod";

export class List<T> {
public static readonly viewModeSettingsStorageSchema = z
.enum(["list", "table"])
.optional();
public readonly filters: Filter<T, never, never>[];
public readonly itemView?: ItemView<T>;
public readonly table?: Table<T>;
Expand All @@ -32,21 +35,17 @@ export class List<T> {
public readonly loader: IncrementalLoader<T>;
public readonly onAction?: ItemActionFn<T>;
public readonly accordion: boolean;
public readonly tile: boolean;
public readonly getItemId?: GetItemId<T>;
public readonly componentProps: ListSupportedComponentProps;
public viewMode: ListViewMode;
public readonly setViewMode: (viewMode: ListViewMode) => void;

private readonly settingsStore?: SettingsStore;
public readonly supportsSettingsStorage: boolean;
public readonly settingStorageKey?: string;
private readonly settingsStore?: SettingsStore;
private readonly viewModeStorageKey?: string;
private readonly filterSettingsStorageKey?: string;

public static readonly viewModeSettingsStorageSchema = z
.enum(["list", "table"])
.optional();

public constructor(shape: ListShape<T>) {
const {
settingStorageKey,
Expand All @@ -62,6 +61,7 @@ export class List<T> {
getItemId,
defaultViewMode,
accordion = false,
tile = false,
...componentProps
} = shape;

Expand All @@ -81,6 +81,7 @@ export class List<T> {
this.search = search ? new Search(this, search) : undefined;
this.itemView = itemView ? new ItemView(this, itemView) : undefined;
this.accordion = accordion;
this.tile = tile;
this.table = table ? new Table(this, table) : undefined;
this.batches = new BatchesController(this, batchesController);
this.componentProps = componentProps;
Expand Down
1 change: 1 addition & 0 deletions packages/components/src/components/List/model/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ export interface ListShape<T> extends ListSupportedComponentProps {

onAction?: ItemActionFn<T>;
accordion?: boolean;
tile?: boolean;
getItemId?: GetItemId<T>;
onChange?: OnListChanged<T>;
defaultViewMode?: ListViewMode;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -218,3 +218,62 @@ export const WithAccordion: Story = {
);
},
};

export const WithTile: Story = {
render: () => {
const DomainList = typedList<Domain>();
const availableTypes = usePromise(getTypes, []);

return (
<Section>
<Heading>Domains</Heading>
<DomainList.List
batchSize={5}
aria-label="Domains"
onAction={(domain) => console.log(domain.hostname)}
tile
>
<DomainList.LoaderAsync manualPagination manualSorting={false}>
{loadDomains}
</DomainList.LoaderAsync>
<DomainList.Filter
values={availableTypes}
property="type"
mode="all"
name="Typ"
defaultSelected={["Domain"]}
/>
<DomainList.Search autoFocus autoSubmit />
<DomainList.Sorting property="domain" name="A-Z" />
<DomainList.Sorting property="domain" name="Z-A" direction="desc" />

<DomainList.Item textValue={(domain) => domain.hostname}>
{(domain) => (
<DomainList.ItemView>
<Avatar color={domain.type === "Domain" ? "blue" : "teal"}>
{domain.type === "Domain" ? (
<IconDomain />
) : (
<IconSubdomain />
)}
</Avatar>
<Heading>
{domain.hostname}
{!domain.verified && (
<AlertBadge status="warning">Not verified</AlertBadge>
)}
</Heading>
<Text>{domain.type}</Text>

<ContextMenu>
<MenuItem>Show details</MenuItem>
<MenuItem>Delete</MenuItem>
</ContextMenu>
</DomainList.ItemView>
)}
</DomainList.Item>
</DomainList.List>
</Section>
);
},
};
Loading
Loading