Skip to content

Commit

Permalink
feat: IconCard & IconCardGroup
Browse files Browse the repository at this point in the history
  • Loading branch information
0ces committed Oct 3, 2023
1 parent d58e665 commit 01ff949
Show file tree
Hide file tree
Showing 5 changed files with 231 additions and 0 deletions.
39 changes: 39 additions & 0 deletions src/inputs/IconCard.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { Meta } from "@storybook/react";
import { Css, IconCard, IconCardProps } from "src";
import { iconCardStylesHover } from "src/inputs/IconCard";

export default {
component: IconCard,
} as Meta<IconCardProps>;

export function Default() {
return (
<div css={Css.df.gap2.jcfs.$}>
<div>
<h2>Selected</h2>
<IconCard icon="house" label="Structural Design" selected onChange={() => {}} />
</div>
<div>
<h2>Not Selected</h2>
<IconCard icon="house" label="Structural Design" onChange={() => {}} />
</div>
<div>
<h2>Hover</h2>
<HoveredIconCard icon="house" label="Structural Design" onChange={() => {}} />
</div>
<div>
<h2>Disabled</h2>
<IconCard icon="house" label="Structural Design" disabled onChange={() => {}} />
</div>
</div>
);
}

/** Hover styled version of the IconButton */
function HoveredIconCard(args: IconCardProps) {
return (
<div css={{ button: iconCardStylesHover }}>
<IconCard {...args} />
</div>
);
}
70 changes: 70 additions & 0 deletions src/inputs/IconCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { RefObject, useMemo } from "react";
import { useCheckbox, useHover, VisuallyHidden } from "react-aria";
import { useToggleState } from "react-stately";
import { Icon, IconProps, maybeTooltip, resolveTooltip } from "src/components";
import { Css, Palette } from "src/Css";
import { useGetRef } from "src/hooks/useGetRef";
import { noop, useTestIds } from "src/utils";
import { defaultTestId } from "src/utils/defaultTestId";

export interface IconCardProps {
/** The icon to use within the card. */
icon: IconProps["icon"];
label: string;
selected?: boolean;
/** Handler that is called when the element's selection state changes. */
onChange?: (selected: boolean) => void;
cardRef?: RefObject<HTMLInputElement>;
disabled?: boolean;
tooltip?: string;
}

export function IconCard(props: IconCardProps) {
const {
selected: isSelected = false,
disabled: isDisabled = false,
icon,
cardRef,
label,
tooltip,
...otherProps
} = props;
const ref = useGetRef(cardRef);
const ariaProps = { isSelected, isDisabled, ...otherProps };
const checkboxProps = { ...ariaProps, "aria-label": label };

const { hoverProps, isHovered } = useHover({ isDisabled });
const toggleState = useToggleState(ariaProps);
const { inputProps } = useCheckbox(checkboxProps, toggleState, ref);

const styles = useMemo(
() => ({
...baseStyles,
...(isHovered && iconCardStylesHover),
...(isSelected && selectedStyles),
...(isDisabled && disabledStyles),
}),
[isDisabled, isHovered, isSelected],
);

const tid = useTestIds(props, defaultTestId(label));

return maybeTooltip({
title: resolveTooltip(isDisabled, tooltip),
placement: "top",
children: (
<button css={styles} {...hoverProps} onClick={isDisabled ? noop : toggleState.toggle} {...tid}>
<VisuallyHidden>
<input ref={ref} {...inputProps} {...tid.value} />
</VisuallyHidden>
<Icon icon={icon} inc={4} color={isDisabled ? Palette.Gray300 : Palette.Gray900} />
<span css={Css.xsMd.if(isDisabled).gray300.$}>{label}</span>
</button>
),
});
}

const baseStyles = Css.df.fdc.aic.jcc.wPx(130).hPx(114).ba.br8.bGray300.bgWhite.gap("12px").p2.tc.$;
export const selectedStyles = Css.bw2.bBlue500.bgBlue50.$;
const disabledStyles = Css.bGray200.bgGray50.$;
export const iconCardStylesHover = Css.bgGray100.$;
40 changes: 40 additions & 0 deletions src/inputs/IconCardGroup.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { Meta } from "@storybook/react";
import { IconCardGroup, IconCardGroupItemOption, IconCardGroupProps } from "./IconCardGroup";
import { Chips, IconProps } from "src/components";
import { useState } from "react";

export default {
component: IconCardGroup,
} as Meta<IconCardGroupProps>;

const categories: IconCardGroupItemOption[] = [
{ icon: "abacus", label: "Math", value: "math" },
{ icon: "archive", label: "History", value: "history" },
{ icon: "dollar", label: "Finance", value: "finance" },
{ icon: "hardHat", label: "Engineering", value: "engineering" },
{ icon: "kanban", label: "Management", value: "management" },
{ icon: "camera", label: "Media", value: "media" },
];

export function Default() {
const [values, setValues] = useState<string[]>(["math"]);

return (
<div>
<Chips values={values} />
<IconCardGroup
label="Icon Card Group"
// options={Object.values(IconCardGroupValues).map((value, i) => ({
// icon: valuesToIconMap[value],
// label: value,
// value,
// disabled: i === 2, // disable the third option
// }))}
options={categories}
onChange={(values) => setValues(values)}
values={values}
columns={3}
/>
</div>
);
}
81 changes: 81 additions & 0 deletions src/inputs/IconCardGroup.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import { ReactNode } from "react";
import { useCheckboxGroup } from "react-aria";
import { useCheckboxGroupState } from "react-stately";
import { Css } from "src/Css";
import { IconProps } from "src/components/Icon";
import { Label } from "src/components/Label";
import { PresentationFieldProps, usePresentationContext } from "src/components/PresentationContext";
import { useTestIds } from "src/utils";
import { IconCard } from "src/inputs/IconCard";
import { HelperText } from "src/components/HelperText";
import { ErrorMessage } from "./ErrorMessage";

export interface IconCardGroupItemOption {
icon: IconProps["icon"];
label: string;
disabled?: boolean;
/** The value of the IconCardGroup item, stored in value array in state. */
value: string;
}

export interface IconCardGroupProps extends Pick<PresentationFieldProps, "labelStyle"> {
label: string;
/** Called when a card is selected */
onChange: (values: string[]) => void;
/** Options for the cards contained within the IconCardGroup. */
options: IconCardGroupItemOption[];
/** The values currently selected. */
values: string[];
errorMsg?: string;
helperText?: string | ReactNode;
columns?: number;
disabled?: boolean;
}

export function IconCardGroup(props: IconCardGroupProps) {
const { fieldProps } = usePresentationContext();
const {
options,
label,
labelStyle = fieldProps?.labelStyle ?? "above",
values,
errorMsg,
helperText,
columns = 1,
disabled: isDisabled = false,
} = props;

const state = useCheckboxGroupState({ ...props, isDisabled, value: values });
const { groupProps, labelProps } = useCheckboxGroup(props, state);
const tid = useTestIds(props);

return (
<div {...groupProps} {...tid}>
{labelStyle !== "hidden" && (
<div css={Css.if(labelStyle === "left").w50.$}>
<Label label={label} {...labelProps} {...tid.label} />
</div>
)}
<div css={Css.dg.gtc(`repeat(${columns}, 130px)`).gap2.$}>
{options.map((option) => {
const { icon, label, disabled } = option;
const isSelected = state.isSelected(option.value);
const isDisabled = disabled || state.isDisabled;
return (
<IconCard
key={option.value}
icon={icon}
label={label}
selected={isSelected}
disabled={isDisabled}
onChange={() => state.toggleValue(option.value)}
{...tid[option.value]}
/>
);
})}
</div>
{errorMsg && <ErrorMessage errorMsg={errorMsg} {...tid.errorMsg} />}
{helperText && <HelperText helperText={helperText} {...tid.helperText} />}
</div>
);
}
1 change: 1 addition & 0 deletions src/inputs/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,5 @@ export type { TextFieldApi } from "./TextField";
export * from "./ToggleButton";
export * from "./ToggleChipGroup";
export * from "./TreeSelectField";
export * from "./IconCard";
export type { Value } from "./Value";

0 comments on commit 01ff949

Please sign in to comment.