Skip to content

Commit

Permalink
Implement List
Browse files Browse the repository at this point in the history
  • Loading branch information
Nemobot committed Feb 17, 2022
1 parent c98d76f commit c7edc5e
Show file tree
Hide file tree
Showing 11 changed files with 499 additions and 45 deletions.
32 changes: 17 additions & 15 deletions src/Field/createFormFields.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,21 +44,23 @@ export function createFormFields(
radius: 8,
list: {
paddingY: 8,
itemPaddingX: 16,
itemPaddingY: {
medium: 8,
large: 16,
},
fontSize: {
firstLine: "medium",
secondLine: "small",
overline: "small",
},
internalSpacing: 16,
iconSize: {
leading: 24,
trailing: 16,
//illustration: 32,
item: {
paddingX: 16,
paddingY: {
medium: 8,
large: 16,
},
fontSize: {
firstLine: "medium",
secondLine: "small",
overline: "small",
},
internalSpacing: 16,
iconSize: {
leading: 24,
trailing: 16,
illustration: 32,
},
},
},
},
Expand Down
11 changes: 11 additions & 0 deletions src/Illustrations/IllustrationProps.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
export type IllustrationProps = {
size: 32 | 40 | 80 | 160;
} & (
| {
style: "color";
}
| {
style: "outline";
color: "default" | "disabled";
}
);
33 changes: 33 additions & 0 deletions src/Illustrations/svgIllustrationProps.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { SVGAttributes } from "react";
import { vars } from "../vars.css";
import { IllustrationProps } from "./IllustrationProps";

export function svgIllustrationProps(props: IllustrationProps): SVGAttributes<SVGElement> {
return {
...sizeToDimensions(props.size),
fill: props.style === "outline" ? outlineColor(props.color) : undefined,
viewBox: "0 0 80 80",
};
}

function outlineColor(color: (IllustrationProps & { style: "outline" })["color"]): string {
switch (color) {
case "default":
return vars.foregroundColor.foregroundSecondary;
case "disabled":
return vars.foregroundColor.foregroundDisabled;
}
}

function sizeToDimensions(size: IllustrationProps["size"]): { width: number; height: number } {
switch (size) {
case 32:
return { width: 32, height: 32 };
case 40:
return { width: 40, height: 40 };
case 80:
return { width: 80, height: 80 };
case 160:
return { width: 160, height: 160 };
}
}
16 changes: 16 additions & 0 deletions src/List/ListItem.css.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { bentoSprinkles } from "../internal";
import { strictRecipe } from "../util/strictRecipe";

export const listItemRecipe = strictRecipe({
variants: {
interactive: {
true: bentoSprinkles({
cursor: { default: "pointer", disabled: "notAllowed" },
background: {
hover: "primaryTransparentHoverBackground",
focus: "primaryTransparentFocusBackground",
},
}),
},
},
});
53 changes: 53 additions & 0 deletions src/List/createList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { Omit } from "../util/Omit";
import { BentoSprinkles, Stack, Inset } from "../internal";
import { createListItem, ListItemConfig, ListItemProps } from "./createListItem";

export type ListSize = "medium" | "large";

type Props = {
size: ListSize;
items: Omit<ListItemProps, "size">[];
dividers?: boolean;
};

export type ListConfig = {
paddingY: BentoSprinkles["paddingY"];
item: ListItemConfig;
};

export function createList(
config: ListConfig = {
paddingY: 8,
item: {
paddingX: 16,
paddingY: {
medium: 8,
large: 16,
},
fontSize: {
firstLine: "medium",
secondLine: "small",
overline: "small",
},
internalSpacing: 16,
iconSize: {
leading: 24,
trailing: 16,
illustration: 32,
},
},
}
) {
const ListItem = createListItem(config.item);
return function List({ size, items, dividers }: Props) {
return (
<Inset spaceY={config.paddingY}>
<Stack space={0} as="ul" dividers={dividers}>
{items.map((liProps) => (
<ListItem key={liProps.label} {...liProps} size={size} />
))}
</Stack>
</Inset>
);
};
}
213 changes: 213 additions & 0 deletions src/List/createListItem.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,213 @@
import { useLink } from "@react-aria/link";
import { ComponentProps, useRef } from "react";
import { Body, Label, LocalizedString, useLinkComponent } from "..";
import { Box, Columns, Column, Inset, Stack, BentoSprinkles } from "../internal";
import { IconProps } from "../Icons/IconProps";
import { IllustrationProps } from "../Illustrations/IllustrationProps";
import { listItemRecipe } from "./ListItem.css";
import { ListSize } from "./createList";

type Kind =
| {
kind: "overline";
overline: LocalizedString;
label: LocalizedString;
}
| {
kind: "single-line";
label: LocalizedString;
}
| {
kind: "two-line";
label: LocalizedString;
secondLine: LocalizedString;
};

type LeftItem =
| {
icon?: never;
illustration?: never;
}
| {
icon: (props: IconProps) => JSX.Element;
illustration?: never;
}
| {
icon?: never;
illustration: (props: IllustrationProps) => JSX.Element;
};

type RightItem = {
trailingIcon?: (props: IconProps) => JSX.Element;
};

export type Props = Kind &
LeftItem &
RightItem & {
disabled?: boolean;
size: ListSize;
} & (
| {
onPress?: () => void;
href?: never;
}
| {
href?: string;
onPress?: never;
}
);

export type { Props as ListItemProps };

type ListItemSizeConfig<T> = {
[k in ListSize]: T;
};
export type ListItemConfig = {
paddingX: BentoSprinkles["paddingX"];
paddingY: ListItemSizeConfig<BentoSprinkles["paddingY"]>;
fontSize: {
firstLine: ComponentProps<typeof Body>["size"];
secondLine: ComponentProps<typeof Body>["size"];
overline: ComponentProps<typeof Label>["size"];
};
internalSpacing: BentoSprinkles["gap"];
iconSize: {
leading: IconProps["size"];
trailing: IconProps["size"];
illustration: IllustrationProps["size"];
};
};

export function createListItem(config: ListItemConfig) {
return function ListItem(props: Props) {
const linkRef = useRef<HTMLElement>(null);

const {
linkProps: { color, ...linkProps },
} = useLink(
{
onPress: props.onPress,
isDisabled: props.disabled,
elementType: props.href ? "a" : "div",
},
linkRef
);

const LinkComponent = useLinkComponent();

return (
<Box
as="li"
className={listItemRecipe({ interactive: !!props.onPress || !!props.href })}
disabled={props.disabled}
>
<Box
ref={linkRef}
as={props.href ? LinkComponent : "div"}
{...linkProps}
href={props.href}
display="block"
>
<Inset spaceX={config.paddingX} spaceY={config.paddingY[props.size]}>
<Columns space={config.internalSpacing} alignY="center">
{renderLeft(props)}
{renderContent(props)}
{renderRight(props)}
</Columns>
</Inset>
</Box>
</Box>
);
};

function renderLeft(props: Props) {
if (props.illustration) {
return (
<Column width="content">
{props.illustration({
size: config.iconSize.illustration,
style: "outline",
color: props.disabled ? "disabled" : "default",
})}
</Column>
);
}

if (props.icon) {
return (
<Column width="content">
{props.icon({
size: config.iconSize.leading,
color: props.disabled ? "disabled" : "default",
})}
</Column>
);
}

return null;
}

function renderRight(props: Props) {
if (props.trailingIcon) {
return (
<Column width="content">
{props.trailingIcon({
size: config.iconSize.trailing,
color: props.disabled ? "disabled" : "default",
})}
</Column>
);
}

return null;
}

function renderContent(props: Props) {
switch (props.kind) {
case "single-line":
return <SingleLine {...props} />;
case "two-line":
return <TwoLine {...props} />;
case "overline":
return <Overline {...props} />;
}
}

function SingleLine(props: Props & { kind: "single-line" }) {
return (
<Body size={config.fontSize.firstLine} color={props.disabled ? "disabled" : "default"}>
{props.label}
</Body>
);
}

function TwoLine(props: Props & { kind: "two-line" }) {
return (
<Stack space={4} align="left">
<Body size={config.fontSize.firstLine} color={props.disabled ? "disabled" : "default"}>
{props.label}
</Body>
<Body size={config.fontSize.secondLine} color={props.disabled ? "disabled" : "secondary"}>
{props.secondLine}
</Body>
</Stack>
);
}

function Overline(props: Props & { kind: "overline" }) {
return (
<Stack space={4} align="left">
<Label
size={config.fontSize.overline}
color={props.disabled ? "disabled" : "secondary"}
uppercase
>
{props.overline}
</Label>
<Body size={config.fontSize.firstLine} color={props.disabled ? "disabled" : "default"}>
{props.label}
</Body>
</Stack>
);
}
}
10 changes: 5 additions & 5 deletions src/SelectField/components.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -158,21 +158,21 @@ export function createComponents(inputConfig: InputConfig, dropdownConfig: Dropd
isFocused: props.isFocused,
}),
bentoSprinkles({
paddingX: dropdownConfig.list.itemPaddingX,
paddingY: dropdownConfig.list.itemPaddingY[props.selectProps.size],
paddingX: dropdownConfig.list.item.paddingX,
paddingY: dropdownConfig.list.item.paddingY[props.selectProps.size],
})
)}
>
<Columns space={dropdownConfig.list.internalSpacing} alignY="center">
<Columns space={dropdownConfig.list.item.internalSpacing} alignY="center">
{"icon" in props.data && ( // TODO(vince): should this be just an Icon component?
<Column width="content">{(props.data as unknown as { icon: Children }).icon}</Column>
)}
<Body size={dropdownConfig.list.fontSize.firstLine}>
<Body size={dropdownConfig.list.item.fontSize.firstLine}>
{props.children as TextChildren}
</Body>
{props.isSelected && (
<Column width="content">
<IconCheck size={dropdownConfig.list.iconSize.trailing} />
<IconCheck size={dropdownConfig.list.item.iconSize.trailing} />
</Column>
)}
</Columns>
Expand Down
Loading

0 comments on commit c7edc5e

Please sign in to comment.