From 77e4e7d40cd010960b2d00164bbe350b373efc3e Mon Sep 17 00:00:00 2001 From: Edwar Plata Date: Thu, 30 Nov 2023 11:28:09 -0500 Subject: [PATCH 1/6] fix: IconCardGroup generic typing --- src/forms/BoundIconCardGroupField.test.tsx | 35 ++++++++----- src/forms/BoundIconCardGroupField.tsx | 12 +++-- src/inputs/IconCardGroup.tsx | 61 +++++++++++++++------- 3 files changed, 73 insertions(+), 35 deletions(-) diff --git a/src/forms/BoundIconCardGroupField.test.tsx b/src/forms/BoundIconCardGroupField.test.tsx index 48503dbe1..6498e9dd2 100644 --- a/src/forms/BoundIconCardGroupField.test.tsx +++ b/src/forms/BoundIconCardGroupField.test.tsx @@ -4,25 +4,36 @@ import { ObjectConfig, ObjectState, createObjectState, required } from "@homebou import { BoundIconCardGroupField } from "./BoundIconCardGroupField"; import { click, render } from "src/utils/rtl"; -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" }, +enum Category { + Math, + History, + Finance, + Engineering, + Management, + Media, +} + +const categories: IconCardGroupItemOption[] = [ + { icon: "abacus", label: "Math", value: Category.Math }, + { icon: "archive", label: "History", value: Category.History }, + { icon: "dollar", label: "Finance", value: Category.Finance }, + { icon: "hardHat", label: "Engineering", value: Category.Engineering }, + { icon: "kanban", label: "Management", value: Category.Management }, + { icon: "camera", label: "Media", value: Category.Media }, ]; +type NewAuthor = Omit & { favoriteGenres?: Category[] | null }; + describe("BoundIconCardGroupField", () => { it("shows the label", async () => { - const author = createObjectState(formConfig, { favoriteGenres: ["math"] }); + const author = createObjectState(formConfig, { favoriteGenres: [Category.Math] }); const r = await render(); expect(r.favoriteGenres_label).toHaveTextContent("Favorite Genres"); }); it("triggers 'maybeAutoSave' on change", async () => { const autoSave = jest.fn(); // Given a BoundIconCardGroupField with auto save - const author: ObjectState = createObjectState( + const author: ObjectState = createObjectState( formConfig, {}, { maybeAutoSave: () => autoSave(author.favoriteGenres.value) }, @@ -30,12 +41,12 @@ describe("BoundIconCardGroupField", () => { const r = await render(); // When toggling the checkbox off - click(r.favoriteGenres_math); + click(r.favoriteGenres_Math); // Then the callback should be triggered with the current value - expect(autoSave).toBeCalledWith(["math"]); + expect(autoSave).toBeCalledWith([Category.Math]); }); }); -const formConfig: ObjectConfig = { +const formConfig: ObjectConfig = { favoriteGenres: { type: "value", rules: [required], strictOrder: false }, }; diff --git a/src/forms/BoundIconCardGroupField.tsx b/src/forms/BoundIconCardGroupField.tsx index 928462abe..151bfd410 100644 --- a/src/forms/BoundIconCardGroupField.tsx +++ b/src/forms/BoundIconCardGroupField.tsx @@ -3,16 +3,20 @@ import { Observer } from "mobx-react"; import { IconCardGroup, IconCardGroupProps } from "src/inputs/IconCardGroup"; import { useTestIds } from "src/utils"; import { defaultLabel } from "src/utils/defaultLabel"; +import { Value } from "src/inputs"; -export type BoundIconCardGroupFieldProps = Omit & { - field: FieldState; +export type BoundIconCardGroupFieldProps = Omit< + IconCardGroupProps, + "label" | "values" | "onChange" +> & { + field: FieldState; /** Make optional so that callers can override if they want to. */ - onChange?: (values: string[]) => void; + onChange?: (values: V[]) => void; label?: string; }; /** Wraps `IconCardGroup` and binds it to a form field. */ -export function BoundIconCardGroupField(props: BoundIconCardGroupFieldProps) { +export function BoundIconCardGroupField(props: BoundIconCardGroupFieldProps) { const { field, onChange = (value) => field.set(value), label = defaultLabel(field.key), ...others } = props; const testId = useTestIds(props, field.key); return ( diff --git a/src/inputs/IconCardGroup.tsx b/src/inputs/IconCardGroup.tsx index 5da3557a4..89e1ad6d8 100644 --- a/src/inputs/IconCardGroup.tsx +++ b/src/inputs/IconCardGroup.tsx @@ -1,6 +1,4 @@ -import { ReactNode } from "react"; -import { useCheckboxGroup } from "react-aria"; -import { useCheckboxGroupState } from "react-stately"; +import { ReactNode, useCallback, useState } from "react"; import { Css } from "src/Css"; import { IconProps } from "src/components/Icon"; import { Label } from "src/components/Label"; @@ -9,29 +7,34 @@ import { useTestIds } from "src/utils"; import { IconCard } from "src/inputs/IconCard"; import { HelperText } from "src/components/HelperText"; import { ErrorMessage } from "./ErrorMessage"; +import { Value } from "src/inputs"; +import { mergeProps, useField } from "react-aria"; +import { filterDOMProps } from "@react-aria/utils"; -export interface IconCardGroupItemOption { +export interface IconCardGroupItemOption { icon: IconProps["icon"]; label: string; disabled?: boolean; /** The value of the IconCardGroup item, stored in value array in state. */ - value: string; + value: V; + /** Exclusive: if true, this option will override all other options when selected. */ + exclusive?: boolean; } -export interface IconCardGroupProps extends Pick { +export interface IconCardGroupProps extends Pick { label: string; /** Called when a card is selected */ - onChange: (values: string[]) => void; + onChange: (values: V[]) => void; /** Options for the cards contained within the IconCardGroup. */ - options: IconCardGroupItemOption[]; + options: IconCardGroupItemOption[]; /** The values currently selected. */ - values: string[]; + values: V[]; errorMsg?: string; helperText?: string | ReactNode; disabled?: boolean; } -export function IconCardGroup(props: IconCardGroupProps) { +export function IconCardGroup(props: IconCardGroupProps) { const { fieldProps } = usePresentationContext(); const { options, @@ -41,14 +44,35 @@ export function IconCardGroup(props: IconCardGroupProps) { errorMsg, helperText, disabled: isDisabled = false, + onChange, } = props; - const state = useCheckboxGroupState({ ...props, isDisabled, value: values }); - const { groupProps, labelProps } = useCheckboxGroup(props, state); + const [selected, setSelected] = useState(values); + + const toggleValue = useCallback( + (value: V) => { + if (selected.includes(value)) { + setSelected(selected.filter((v) => v !== value)); + } else { + setSelected([...selected, value]); + } + onChange([...selected, value]); + }, + [onChange, selected], + ); + const tid = useTestIds(props); + const { labelProps, fieldProps: fieldPropsAria } = useField(props); + + const groupProps = mergeProps(tid, { + role: "group", + "aria-disabled": isDisabled || undefined, + ...fieldPropsAria, + }); + return ( -
+
{labelStyle !== "hidden" && (