Skip to content

Commit

Permalink
Merge branch 'main' into feat/icon-card
Browse files Browse the repository at this point in the history
  • Loading branch information
0ces authored Oct 13, 2023
2 parents 129787e + dd24eb5 commit 1ec4ee9
Show file tree
Hide file tree
Showing 21 changed files with 465 additions and 371 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@homebound/beam",
"version": "2.318.4",
"version": "2.322.0",
"author": "Homebound",
"license": "MIT",
"main": "dist/index.js",
Expand Down
23 changes: 23 additions & 0 deletions src/components/Banner.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { Banner } from "src";
import { click, render } from "src/utils/rtl";

describe(Banner, () => {
it("should render", async () => {
// Given the Banner with a message and no onClose callback
const r = await render(<Banner type="warning" message="Banner message" />);
// Then the banner should be visible
expect(r.banner_message).toHaveTextContent("Banner message");
// And there should be no close button
expect(r.query.banner_close).not.toBeInTheDocument();
});

it("should trigger onClose", async () => {
const onClose = jest.fn();
// Given the Banner with a message and an onClose callback
const r = await render(<Banner type="warning" message="Banner message" onClose={onClose} />);
// When clicking on the close button
click(r.banner_close);
// Then the onClose callback should be called
expect(onClose).toHaveBeenCalledTimes(1);
});
});
48 changes: 48 additions & 0 deletions src/components/Banner.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { ReactNode } from "react";
import { Icon, IconKey } from "src/components/Icon";
import { IconButton } from "src/components/IconButton";
import { Css, Palette, Properties } from "src/Css";
import { useTestIds } from "src/utils";

export interface BannerProps {
type: BannerTypes;
message: ReactNode;
onClose?: VoidFunction;
}

export function Banner(props: BannerProps) {
const { type, message, onClose = false, ...others } = props;
const tid = useTestIds(others, "banner");
return (
<div css={{ ...variantStyles[type], ...Css.df.aic.w100.gap1.p2.gray900.base.bshBasic.$ }} {...tid} role="alert">
<span css={Css.fs0.$}>
<Icon icon={typeToIcon[type]} {...tid.type} color={Palette.Gray900} />
</span>
<span css={Css.fg1.$} {...tid.message}>
{message}
</span>
{onClose && (
<span css={Css.lh(0).$}>
<IconButton icon="x" onClick={onClose} {...tid.close} color={Palette.Gray900} />
</span>
)}
</div>
);
}
const typeToIcon: Record<BannerTypes, IconKey> = {
success: "checkCircle",
info: "infoCircle",
warning: "error",
alert: "errorCircle",
error: "xCircle",
};

const variantStyles: Record<BannerTypes, Properties> = {
success: Css.bgGreen100.gray900.$,
info: Css.bgBlue100.gray900.$,
warning: Css.bgYellow200.gray900.$,
alert: Css.bgGray200.gray900.$,
error: Css.bgRed100.gray900.$,
};

export type BannerTypes = "error" | "warning" | "success" | "info" | "alert";
2 changes: 1 addition & 1 deletion src/components/Filters/SingleFilter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ class SingleFilter<O, V extends Key> extends BaseFilter<V, SingleFilterProps<O,

const options = Array.isArray(maybeOptions)
? [allOption as O, ...maybeOptions]
: { ...maybeOptions, initial: [allOption as O, ...maybeOptions.initial] };
: { ...maybeOptions, current: maybeOptions.current };

return (
<SelectField<O, V>
Expand Down
1 change: 1 addition & 0 deletions src/components/Icon.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ export const Icon = (props: IconProps) => {
"openBook",
];
const miscIcons: IconProps["icon"][] = [
"inbox",
"dollar",
"userCircle",
"calendar",
Expand Down
3 changes: 3 additions & 0 deletions src/components/Icon.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -368,6 +368,9 @@ export const Icons = {
<path d="M21 3H14C13.229 3 12.532 3.301 12 3.78C11.468 3.301 10.771 3 10 3H3C2.447 3 2 3.448 2 4V19C2 19.553 2.447 20 3 20H8.758C9.284 20 9.8 20.214 10.172 20.586L11.293 21.707C11.302 21.716 11.314 21.719 11.323 21.728C11.409 21.807 11.505 21.877 11.617 21.924C11.618 21.924 11.618 21.924 11.619 21.924C11.74 21.974 11.869 22 12 22C12.131 22 12.26 21.974 12.381 21.924C12.382 21.924 12.382 21.924 12.383 21.924C12.495 21.877 12.591 21.807 12.677 21.728C12.686 21.719 12.698 21.716 12.707 21.707L13.828 20.586C14.2 20.214 14.716 20 15.242 20H21C21.553 20 22 19.553 22 19V4C22 3.448 21.553 3 21 3ZM8.758 18H4V5H10C10.552 5 11 5.449 11 6V18.689C10.342 18.246 9.557 18 8.758 18ZM20 18H15.242C14.443 18 13.658 18.246 13 18.689V6C13 5.449 13.448 5 14 5H20V18Z" />
),
// Misc
inbox: (
<path d="M20 3H4C2.897 3 2 3.897 2 5V13V14V19C2 20.104 2.896 21 4 21H20C21.104 21 22 20.104 22 19V14V13V5C22 3.897 21.103 3 20 3ZM19 12H16H15.858C15.412 13.722 13.861 15 12 15C10.139 15 8.588 13.722 8.142 12H8H4V5H20V12H19Z" />
),
criticalPath: (
<path d="M6.5 20H14.844C15.2 20.753 15.636 21.422 16.093 22H6.5C4.019 22 2 19.981 2 17.5C2 15.019 4.019 13 6.5 13H13.5C14.327 13 15 12.327 15 11.5C15 10.673 14.327 10 13.5 10H7.285C7.771 9.443 8.248 8.771 8.639 8H13.5C15.43 8 17 9.57 17 11.5C17 13.43 15.43 15 13.5 15H6.5C5.121 15 4 16.121 4 17.5C4 18.879 5.121 20 6.5 20ZM2 5C2 3.346 3.346 2 5 2C6.654 2 8 3.346 8 5C8 8.187 5 10 5 10C5 10 2 8.188 2 5ZM3.5 5C3.5 5.828 4.172 6.5 5 6.5C5.828 6.5 6.5 5.828 6.5 5C6.5 4.172 5.828 3.5 5 3.5C4.172 3.5 3.5 4.172 3.5 5ZM16 17C16 15.346 17.346 14 19 14C20.654 14 22 15.346 22 17C22 20.187 19 22 19 22C19 22 16 20.188 16 17ZM17.5 17C17.5 17.828 18.172 18.5 19 18.5C19.828 18.5 20.5 17.828 20.5 17C20.5 16.172 19.828 15.5 19 15.5C18.172 15.5 17.5 16.172 17.5 17Z" />
),
Expand Down
2 changes: 1 addition & 1 deletion src/components/ScrollShadows.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ export function ScrollShadows(props: ScrollShadowsProps) {
// The shadow styles will rarely every change. Memoize them to avoid recomputing them when we don't have to.
const [startShadowStyles, endShadowStyles] = useMemo(() => {
const transparentBgColor = bgColor.replace(/,1\)$/, ",0)");
const commonStyles = Css.absolute.z3.$;
const commonStyles = Css.absolute.z3.add({ pointerEvents: "none" }).$;
const startShadowStyles = !horizontal ? Css.top0.left0.right0.hPx(40).$ : Css.left0.top0.bottom0.wPx(25).$;
const endShadowStyles = !horizontal ? Css.bottom0.left0.right0.hPx(40).$ : Css.right0.top0.bottom0.wPx(25).$;
const startGradient = `linear-gradient(${!horizontal ? 180 : 90}deg, ${bgColor} 0%, ${transparentBgColor} 92%);`;
Expand Down
27 changes: 27 additions & 0 deletions src/components/Table/GridTable.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import {
type Data = { name: string; value: number | undefined | null };
type Row = SimpleHeaderAndData<Data>;

const idColumn: GridColumn<Row> = { id: "id", header: () => "Id", data: (data, { row }) => row.id };
const nameColumn: GridColumn<Row> = { id: "name", header: () => "Name", data: ({ name }) => name };
const valueColumn: GridColumn<Row> = { id: "value", header: () => "Value", data: ({ value }) => value };
const columns = [nameColumn, valueColumn];
Expand Down Expand Up @@ -3265,6 +3266,32 @@ describe("GridTable", () => {
`);
});

it("tableSnapshot can use a subset of columns", async () => {
// Given a table with simple data
const r = await render(
<GridTable
columns={[idColumn, nameColumn, valueColumn]}
rows={[
simpleHeader,
{ kind: "data", id: "1", data: { name: "Row 1", value: 200 } },
{ kind: "data", id: "2", data: { name: "Row 2", value: 300 } },
{ kind: "data", id: "3", data: { name: "Row 3", value: 1000 } },
]}
/>,
);

// Then a text snapshot should be generated when using `tableSnapshot`
expect(tableSnapshot(r, ["Id", "Value"])).toMatchInlineSnapshot(`
"
| Id | Value |
| -- | ----- |
| 1 | 200 |
| 2 | 300 |
| 3 | 1000 |
"
`);
});

it("renders totals row in the correct order", async () => {
type Row = SimpleHeaderAndData<Data> | TotalsRow;
// Given a table with simple header, totals, and data row
Expand Down
44 changes: 2 additions & 42 deletions src/components/Toast/Toast.tsx
Original file line number Diff line number Diff line change
@@ -1,49 +1,9 @@
import { Icon, IconKey } from "src/components/Icon";
import { Css, Palette, Properties } from "src/Css";
import { Banner } from "src/components";
import { useTestIds } from "src/utils";
import { IconButton } from "../IconButton";
import { useToastContext } from "./ToastContext";

export function Toast() {
const { setNotice, notice } = useToastContext();
const tid = useTestIds({}, "toast");
return (
<>
{notice && (
<div
css={{ ...variantStyles[notice.type], ...Css.df.aic.w100.gap1.p2.gray900.base.bshBasic.$ }}
{...tid}
role="alert"
>
<span css={Css.fs0.$}>
<Icon icon={typeToIcon[notice.type]} {...tid.type} color={Palette.Gray900} />
</span>
<span css={Css.fg1.$} {...tid.message}>
{notice.message}
</span>
<span css={Css.lh(0).$}>
<IconButton icon="x" onClick={() => setNotice(undefined)} {...tid.close} color={Palette.Gray900} />
</span>
</div>
)}
</>
);
return <>{notice && <Banner {...notice} {...tid} onClose={() => setNotice(undefined)} />}</>;
}

const typeToIcon: Record<ToastTypes, IconKey> = {
success: "checkCircle",
info: "infoCircle",
warning: "error",
alert: "errorCircle",
error: "xCircle",
};

const variantStyles: Record<ToastTypes, Properties> = {
success: Css.bgGreen100.gray900.$,
info: Css.bgBlue100.gray900.$,
warning: Css.bgYellow200.gray900.$,
alert: Css.bgGray200.gray900.$,
error: Css.bgRed100.gray900.$,
};

export type ToastTypes = "error" | "warning" | "success" | "info" | "alert";
7 changes: 2 additions & 5 deletions src/components/Toast/ToastContext.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,7 @@
import React, { createContext, ReactNode, useContext, useMemo, useState } from "react";
import { ToastTypes } from "./Toast";
import { BannerProps } from "src/components";

export interface ToastNoticeProps {
type: ToastTypes;
message: ReactNode;
}
export interface ToastNoticeProps extends Omit<BannerProps, "onClose"> {}

export type ToastContextProps = {
notice: ToastNoticeProps | undefined;
Expand Down
2 changes: 2 additions & 0 deletions src/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ export * from "./Accordion";
export * from "./AccordionList";
export * from "./AutoSaveIndicator";
export * from "./Avatar";
export * from "./Banner";
export type { BannerProps } from "./Banner";
export { BeamProvider } from "./BeamContext";
export * from "./Button";
export * from "./ButtonDatePicker";
Expand Down
42 changes: 21 additions & 21 deletions src/inputs/SelectField.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,7 @@ Contrast.args = { compact: true, contrast: true };
const loadTestOptions: TestOption[] = zeroTo(1000).map((i) => ({ id: String(i), name: `Project ${i}` }));

export function PerfTest() {
const [loaded, setLoaded] = useState<TestOption[]>([]);
const [selectedValue, setSelectedValue] = useState<string | undefined>(loadTestOptions[2].id);
return (
<SelectField
Expand All @@ -232,13 +233,12 @@ export function PerfTest() {
onSelect={setSelectedValue}
errorMsg={selectedValue !== undefined ? "" : "Select an option. Plus more error text to force it to wrap."}
options={{
initial: [loadTestOptions[2]],
current: loadTestOptions[2],
load: async () => {
return new Promise((resolve) => {
// @ts-ignore - believes `options` should be of type `never[]`
setTimeout(() => resolve({ options: loadTestOptions }), 1500);
});
await sleep(1500);
setLoaded(loadTestOptions);
},
options: loaded,
}}
onBlur={action("onBlur")}
onFocus={action("onFocus")}
Expand All @@ -248,6 +248,7 @@ export function PerfTest() {
PerfTest.parameters = { chromatic: { disableSnapshot: true } };

export function LazyLoadStateFields() {
const [loaded, setLoaded] = useState<TestOption[]>([]);
const [selectedValue, setSelectedValue] = useState<string | undefined>(loadTestOptions[2].id);
return (
<>
Expand All @@ -257,13 +258,12 @@ export function LazyLoadStateFields() {
onSelect={setSelectedValue}
unsetLabel={"-"}
options={{
initial: [loadTestOptions.find((o) => o.id === selectedValue)!],
current: loadTestOptions.find((o) => o.id === selectedValue)!,
load: async () => {
return new Promise((resolve) => {
// @ts-ignore - believes `options` should be of type `never[]`
setTimeout(() => resolve({ options: loadTestOptions }), 1500);
});
await sleep(1500);
setLoaded(loadTestOptions);
},
options: loaded,
}}
/>
<SelectField
Expand All @@ -272,13 +272,12 @@ export function LazyLoadStateFields() {
onSelect={setSelectedValue}
unsetLabel={"-"}
options={{
initial: [loadTestOptions.find((o) => o.id === selectedValue)!],
current: loadTestOptions.find((o) => o.id === selectedValue)!,
load: async () => {
return new Promise((resolve) => {
// @ts-ignore - believes `options` should be of type `never[]`
setTimeout(() => resolve({ options: loadTestOptions }), 1500);
});
await sleep(1500);
setLoaded(loadTestOptions);
},
options: loaded,
}}
/>
</>
Expand All @@ -287,21 +286,20 @@ export function LazyLoadStateFields() {
LazyLoadStateFields.parameters = { chromatic: { disableSnapshot: true } };

export function LoadingState() {
const [loaded, setLoaded] = useState<TestOption[]>([]);
const [selectedValue, setSelectedValue] = useState<string | undefined>(loadTestOptions[2].id);

return (
<SelectField
label="Project"
value={selectedValue}
onSelect={setSelectedValue}
options={{
initial: [loadTestOptions[2]],
current: loadTestOptions[2],
load: async () => {
return new Promise((resolve) => {
// @ts-ignore - believes `options` should be of type `never[]`
setTimeout(() => resolve({ options: loadTestOptions }), 5000);
});
await sleep(5000);
setLoaded(loadTestOptions);
},
options: loadTestOptions,
}}
/>
);
Expand Down Expand Up @@ -392,3 +390,5 @@ function TestSelectField<T extends object, V extends Value>(
</div>
);
}

const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
Loading

0 comments on commit 1ec4ee9

Please sign in to comment.