Skip to content

Commit

Permalink
Merge branch 'main' into sc-43819-draggable-table-rows
Browse files Browse the repository at this point in the history
  • Loading branch information
bsholmes committed Dec 15, 2023
2 parents 797ab1f + 9106443 commit 19f0a48
Show file tree
Hide file tree
Showing 11 changed files with 218 additions and 98 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.327.1",
"version": "2.331.1",
"author": "Homebound",
"license": "MIT",
"main": "dist/index.js",
Expand Down
35 changes: 27 additions & 8 deletions src/components/Label.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import React, { LabelHTMLAttributes } from "react";
import React, { LabelHTMLAttributes, ReactNode } from "react";
import { VisuallyHidden } from "react-aria";
import { Css } from "src/Css";
import { Css, Font, Only, Palette, Xss } from "src/Css";
import { Icon } from "src";

interface LabelProps {
type LabelXss = Font | "color";

interface LabelProps<X> {
// We don't usually have `fooProps`-style props, but this is for/from react-aria
labelProps?: LabelHTMLAttributes<HTMLLabelElement>;
label: string;
Expand All @@ -11,22 +14,38 @@ interface LabelProps {
hidden?: boolean;
contrast?: boolean;
multiline?: boolean;
tooltip?: ReactNode;
// Removes margin bottom if true - This is different from InlineLabel. InlineLabel expects to be rendered visually within the field element. Rather just on the same line.
inline?: boolean;
xss?: X;
}

/** An internal helper component for rendering form labels. */
export const Label = React.memo((props: LabelProps) => {
const { labelProps, label, hidden, suffix, contrast = false, ...others } = props;
function LabelComponent<X extends Only<Xss<LabelXss>, X>>(props: LabelProps<X>) {
const { labelProps, label, hidden, suffix, contrast = false, tooltip, inline, xss, ...others } = props;
const labelEl = (
<label {...labelProps} {...others} css={Css.dib.sm.gray700.mbPx(4).if(contrast).white.$}>
<label
{...labelProps}
{...others}
css={{ ...Css.dif.aic.gap1.sm.gray700.mbPx(inline ? 0 : 4).if(contrast).white.$, ...xss }}
>
{label}
{suffix && ` ${suffix}`}
{tooltip && (
<span css={Css.fs0.$}>
<Icon icon="infoCircle" tooltip={tooltip} inc={2} color={contrast ? Palette.White : Palette.Gray700} />
</span>
)}
</label>
);
return hidden ? <VisuallyHidden>{labelEl}</VisuallyHidden> : labelEl;
});
}

export const Label = React.memo(LabelComponent) as typeof LabelComponent;

type InlineLabelProps = Omit<LabelProps<unknown>, "xss" | "inline">;
/** Used for showing labels within text fields. */
export function InlineLabel({ labelProps, label, contrast, multiline = false, ...others }: LabelProps) {
export function InlineLabel({ labelProps, label, contrast, multiline = false, ...others }: InlineLabelProps) {
return (
<label
{...labelProps}
Expand Down
27 changes: 27 additions & 0 deletions src/components/Table/GridTable.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -522,6 +522,33 @@ export const StyleCard = newStory(() => {
);
}, {});

export const LeveledStyleCard = newStory(() => {
const nameColumn: GridColumn<NestedRow> = {
header: () => "Name",
parent: (row) => row.name,
child: (row) => row.name,
grandChild: (row) => row.name,
add: () => "Add",
};
const valueColumn: GridColumn<NestedRow> = {
header: () => "Value",
parent: (row) => row.name,
child: (row) => row.name,
grandChild: (row) => row.name,
add: () => "Add",
w: "200px",
};
return (
<div css={Css.wPx(550).$}>
<GridTable
style={cardStyle}
columns={[collapseColumn<NestedRow>(), selectColumn<NestedRow>(), nameColumn, valueColumn]}
rows={rowsWithHeader}
/>
</div>
);
}, {});

export const StyleCardWithOneColumn = newStory(() => {
const nameColumn: GridColumn<Row> = { header: "Name", data: ({ name }) => name };
return (
Expand Down
6 changes: 3 additions & 3 deletions src/components/Table/GridTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -474,7 +474,7 @@ function renderDiv<R extends Kinded>(
<div
css={{
...(style.betweenRowsCss ? Css.addIn(`& > div > *`, style.betweenRowsCss).$ : {}),
...(style.firstNonHeaderRowCss ? Css.addIn(`& > div:first-of-type > *`, style.firstNonHeaderRowCss).$ : {}),
...(style.firstNonHeaderRowCss ? Css.addIn(`& > div:first-of-type`, style.firstNonHeaderRowCss).$ : {}),
...(style.lastRowCss && Css.addIn("& > div:last-of-type", style.lastRowCss).$),
}}
>
Expand Down Expand Up @@ -512,7 +512,7 @@ function renderTable<R extends Kinded>(
...Css.w100.add("borderCollapse", "separate").add("borderSpacing", "0").$,
...Css.addIn("& > tbody > tr > * ", style.betweenRowsCss || {})
// removes border between header and second row
.addIn("& > tbody > tr:first-of-type > *", style.firstNonHeaderRowCss || {}).$,
.addIn("& > tbody > tr:first-of-type", style.firstNonHeaderRowCss || {}).$,
...Css.addIn("& > tbody > tr:last-of-type", style.lastRowCss).$,
...Css.addIn("& > thead > tr:first-of-type", style.firstRowCss).$,
...style.rootCss,
Expand Down Expand Up @@ -706,7 +706,7 @@ const VirtualRoot = memoizeOne<(gs: GridStyle, columns: GridColumn<any>[], id: s
...(isHeader
? Css.addIn("& > div:first-of-type > *", gs.firstRowCss).$
: {
...Css.addIn("& > div:first-of-type > *", gs.firstNonHeaderRowCss).$,
...Css.addIn("& > div:first-of-type", gs.firstNonHeaderRowCss).$,
...Css.addIn("& > div:last-of-type > *", gs.lastRowCss).$,
}),
...gs.rootCss,
Expand Down
34 changes: 24 additions & 10 deletions src/components/Table/TableStyles.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ export interface GridStyle {
lastRowCss?: Properties;
/** Applied on the first row of the table (could be the Header or Totals row). */
firstRowCss?: Properties;
/** Applied to every non-header row of the table */
nonHeaderRowCss?: Properties;
/** Applied to the first non-header row, i.e. if you want to cancel out `betweenRowsCss`. */
firstNonHeaderRowCss?: Properties;
/** Applied to all cell divs (via a selector off the base div). */
Expand All @@ -39,15 +41,25 @@ export interface GridStyle {
/** Applied if there is a fallback/overflow message showing. */
firstRowMessageCss?: Properties;
/** Applied on hover if a row has a rowLink/onClick set. */
rowHoverColor?: Palette;
rowHoverColor?: Palette | "none";
/** Applied on hover of a row */
nonHeaderRowHoverCss?: Properties;
/** Default content to put into an empty cell */
emptyCell?: ReactNode;
presentationSettings?: Pick<PresentationFieldProps, "borderless" | "typeScale"> &
Pick<PresentationContextProps, "wrap">;
/** Minimum table width in pixels. Used when calculating columns sizes */
minWidthPx?: number;
/** Css to apply at each level of a parent/child nested table. */
levels?: Record<number, { cellCss?: Properties; firstContentColumn?: Properties }>;
levels?: Record<
number,
{
/** Number of pixels to indent the row. This value will be subtracted from the "first content column" width. First content column is the first column that is not an 'action' column (i.e. non-checkbox or non-collapse button column) */
rowIndent?: number;
cellCss?: Properties;
firstContentColumn?: Properties;
}
>;
/** Allows for customization of the background color used to denote an "active" row */
activeBgColor?: Palette;
/** Defines styles for the group row which holds the selected rows that have been filtered out */
Expand Down Expand Up @@ -209,17 +221,19 @@ export const condensedStyle: GridStyle = {
export const cardStyle: GridStyle = {
...defaultStyle,
betweenRowsCss: {},
firstNonHeaderRowCss: Css.mt2.$,
cellCss: Css.p2.my1.bt.bb.bGray400.$,
firstCellCss: Css.bl.add({ borderTopLeftRadius: "4px", borderBottomLeftRadius: "4px" }).$,
lastCellCss: Css.br.add({ borderTopRightRadius: "4px", borderBottomRightRadius: "4px" }).$,
nonHeaderRowCss: Css.br4.overflowHidden.ba.bGray400.mt2.add("transition", "all 240ms").$,
firstRowCss: Css.bl.br.bGray200.borderRadius("8px 8px 0 0").overflowHidden.$,
cellCss: Css.p2.$,
// Undo the card look & feel for the header
headerCellCss: {
...defaultStyle.headerCellCss,
...Css.add({
border: "none",
borderRadius: "unset",
}).p1.m0.xsMd.gray700.$,
...Css.p1.m0.xsMd.gray700.$,
},
rowHoverColor: "none",
nonHeaderRowHoverCss: Css.bshHover.bGray700.$,
levels: {
1: { rowIndent: 24 },
2: { rowIndent: 48 },
},
};

Expand Down
16 changes: 14 additions & 2 deletions src/components/Table/components/Row.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -82,15 +82,24 @@ function RowImpl<R extends Kinded, S>(props: RowProps<R>): ReactElement {

const revealOnRowHoverClass = "revealOnRowHover";

const showRowHoverColor = !reservedRowKinds.includes(row.kind) && !omitRowHover;
const showRowHoverColor = !reservedRowKinds.includes(row.kind) && !omitRowHover && style.rowHoverColor !== "none";

const rowStyleCellCss = maybeApplyFunction(row as any, rowStyle?.cellCss);
const levelIndent = style.levels && style.levels[level]?.rowIndent;

const rowCss = {
...(!reservedRowKinds.includes(row.kind) && style.nonHeaderRowCss),
// Optionally include the row hover styles, by default they should be turned on.
...(showRowHoverColor && {
// Even though backgroundColor is set on the cellCss, the hover target is the row.
"&:hover > *": Css.bgColor(style.rowHoverColor ?? Palette.Blue100).$,
}),
...(!reservedRowKinds.includes(row.kind) &&
style.nonHeaderRowHoverCss && {
// Need to spread this to make TS happy.
":hover": { ...style.nonHeaderRowHoverCss },
}),
...(levelIndent && Css.mlPx(levelIndent).$),
// For virtual tables use `display: flex` to keep all cells on the same row. For each cell in the row use `flexNone` to ensure they stay their defined widths
...(as === "table" ? {} : Css.relative.df.fg1.fs1.addIn("&>*", Css.flexNone.$).$),
// Apply `cursorPointer` to the row if it has a link or `onClick` value.
Expand Down Expand Up @@ -296,7 +305,10 @@ function RowImpl<R extends Kinded, S>(props: RowProps<R>): ReactElement {
// Apply cell highlight styles to active cell and hover
...Css.if(applyCellHighlight && isCellActive).br4.boxShadow(`inset 0 0 0 1px ${Palette.Blue700}`).$,
// Define the width of the column on each cell. Supports col spans.
width: `calc(${columnSizes.slice(columnIndex, columnIndex + currentColspan).join(" + ")})`,
// If we have a 'levelIndent' defined, then subtract that amount from the first content column's width to ensure all columns will still line up properly
width: `calc(${columnSizes.slice(columnIndex, columnIndex + currentColspan).join(" + ")}${
applyFirstContentColumnStyles && levelIndent ? ` - ${levelIndent}px` : ""
})`,
...(typeof column.mw === "string" ? Css.mw(column.mw).$ : {}),
};

Expand Down
1 change: 1 addition & 0 deletions src/components/Tag.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export function NoIcon() {
<div css={Css.wPx(200).$}>
<Tag text="Tag text that will truncate as it is too long." />
</div>
<Tag text="Custom color" xss={Css.bgPurple200.$} />
</div>
);
}
Expand Down
2 changes: 1 addition & 1 deletion src/components/Tag.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { Icon, IconKey } from "src/components";
import { Css, Margin, Only, Xss } from "src/Css";
import { useTestIds } from "src/utils";

type TagXss = Margin;
type TagXss = Margin | "backgroundColor" | "color";
export type TagType = "info" | "caution" | "warning" | "success" | "neutral";
interface TagProps<X> {
text: string;
Expand Down
9 changes: 8 additions & 1 deletion src/inputs/Switch.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,14 @@ export const LabelStyles = () => {
<SwitchComponent label="Example Label" onChange={setSelected} selected={selected} labelStyle="hidden" />
<h2 css={Css.baseMd.mb1.mt3.pt2.bt.bGray200.$}>Left</h2>
<SwitchComponent label="Example Label" onChange={setSelected} selected={selected} labelStyle="left" />
<h2 css={Css.baseMd.mb1.mt3.pt2.bt.bGray200.$}>Centered</h2>
<SwitchComponent
label="Example Label"
tooltip="Tooltip example"
onChange={setSelected}
selected={selected}
labelStyle="centered"
/>
</div>
);
};
Expand All @@ -115,7 +123,6 @@ type SwitchWrapperProps = Omit<SwitchProps, "onChange" | "selected"> &

function SwitchWrapper({ isHovered, isFocused, ...props }: SwitchWrapperProps) {
const [selected, setSelected] = useState<boolean>(props.selected || false);

return (
<div
css={{
Expand Down
41 changes: 41 additions & 0 deletions src/inputs/Switch.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { useState } from "react";
import { Switch as SwitchComponent, SwitchProps } from "src/inputs";
import { click, render } from "src/utils/rtl";

describe("Switch", () => {
it("can change", async () => {
const onChange = jest.fn();
// Given a switch
const r = await render(<SwitchTest label="Age" onChange={onChange} />);
// Then it defaults no checked
expect(r.age).not.toBeChecked();
// And when we click it, it flips to checked
click(r.age);
expect(r.age).toBeChecked();
expect(onChange).toHaveBeenCalledTimes(1);
// And if we click it again, it flips back to unchecked
click(r.age);
expect(r.age).not.toBeChecked();
expect(onChange).toHaveBeenCalledTimes(2);
});
});

type SwitchTestProps = Omit<SwitchProps, "onChange" | "selected"> & {
onChange?: (value: boolean) => void;
selected?: boolean;
};

function SwitchTest({ selected: initSelected, onChange: _onChange, ...props }: SwitchTestProps) {
const [selected, setSelected] = useState(initSelected || false);
return (
<SwitchComponent
labelStyle="inline"
selected={selected}
onChange={(value) => {
_onChange?.(value);
setSelected(value);
}}
{...props}
/>
);
}
Loading

0 comments on commit 19f0a48

Please sign in to comment.