diff --git a/.changeset/curvy-pans-obey.md b/.changeset/curvy-pans-obey.md new file mode 100644 index 00000000..bebe90df --- /dev/null +++ b/.changeset/curvy-pans-obey.md @@ -0,0 +1,5 @@ +--- +"@atomicjolt/atomic-elements": major +--- + +Implemented a Composition API for the Calendar component diff --git a/.changeset/dull-wolves-look.md b/.changeset/dull-wolves-look.md new file mode 100644 index 00000000..42abe38c --- /dev/null +++ b/.changeset/dull-wolves-look.md @@ -0,0 +1,5 @@ +--- +"@atomicjolt/atomic-elements": major +--- + +Implemented ChipGroupField using the new Collections API & re-implemented ChipGroup & Chip on top of that base diff --git a/packages/atomic-elements/README.md b/packages/atomic-elements/README.md index 1d520812..7d003eb0 100644 --- a/packages/atomic-elements/README.md +++ b/packages/atomic-elements/README.md @@ -29,13 +29,11 @@ $ yarn add @atomicjolt/atomic-elements Include the following in your project ```js -import { LoadFonts, CssVariables, CssGlobalDefaults } from "@atomicjolt/atomic-elements"; +import { CssVariables } from "@atomicjolt/atomic-elements"; const App = () => ( <> - - ); diff --git a/packages/atomic-elements/src/components/Accessibility/LiveMessage/LiveMessage.component.tsx b/packages/atomic-elements/src/components/Accessibility/LiveMessage/LiveMessage.component.tsx index 16ba7405..f56577da 100644 --- a/packages/atomic-elements/src/components/Accessibility/LiveMessage/LiveMessage.component.tsx +++ b/packages/atomic-elements/src/components/Accessibility/LiveMessage/LiveMessage.component.tsx @@ -1,6 +1,6 @@ import { useEffect, useRef } from "react"; import { useAnnouncer } from "../LiveAnnouncer"; -import { useContextPropsV2 } from "@hooks/useContextProps"; +import { useContextProps } from "@hooks/useContextProps"; import { LiveMessageContext } from "./LiveMessage.context"; export interface LiveMessageProps { @@ -19,7 +19,7 @@ export interface LiveMessageProps { */ export function LiveMessage(props: LiveMessageProps) { let ref = useRef(null); - [props, ref] = useContextPropsV2(LiveMessageContext, props, ref); + [props, ref] = useContextProps(LiveMessageContext, props, ref); const { message, polite = true, assertive = false, timeout } = props; const { announceAssertive, announcePolite } = useAnnouncer(); diff --git a/packages/atomic-elements/src/components/Buttons/Button/Button.component.tsx b/packages/atomic-elements/src/components/Buttons/Button/Button.component.tsx index ae731454..9c97bd78 100644 --- a/packages/atomic-elements/src/components/Buttons/Button/Button.component.tsx +++ b/packages/atomic-elements/src/components/Buttons/Button/Button.component.tsx @@ -5,7 +5,6 @@ import { mergeProps, useObjectRef } from "@react-aria/utils"; import { SpinnerLoader } from "../../Loaders/SpinnerLoader"; import { ExtendedSize, - HasIcon, HasVariant, LoadingProps, RenderBaseProps, @@ -14,9 +13,10 @@ import { import { useFocusRing } from "@hooks/useFocusRing"; import { useRenderProps } from "@hooks/useRenderProps"; import { useButtonLink } from "@hooks/useButtonLink"; -import { useContextPropsV2 } from "@hooks/useContextProps"; +import { useContextProps } from "@hooks/useContextProps"; import { StyledButton } from "./Button.styles"; import { ButtonContext } from "./Button.context"; +import { SlotProps } from "@hooks/useSlottedContext"; export type ButtonVariants = SuggestStrings< | "primary" @@ -41,7 +41,8 @@ export interface ButtonProps extends AriaButtonOptions<"button">, LoadingProps, RenderBaseProps, - HasVariant { + HasVariant, + SlotProps { as?: "button" | "a"; size?: ExtendedSize; } @@ -52,12 +53,7 @@ export interface ButtonProps */ export const Button = forwardRef( function Button(props, forwardedRef) { - [props, forwardedRef] = useContextPropsV2( - ButtonContext, - // Button doesn't have Icon props, but the context does - props as ButtonProps & HasIcon, - forwardedRef - ); + [props, forwardedRef] = useContextProps(ButtonContext, props, forwardedRef); const ref = useObjectRef(forwardedRef); diff --git a/packages/atomic-elements/src/components/Buttons/Button/Button.context.ts b/packages/atomic-elements/src/components/Buttons/Button/Button.context.ts index 29a63a1c..8b7dd2bc 100644 --- a/packages/atomic-elements/src/components/Buttons/Button/Button.context.ts +++ b/packages/atomic-elements/src/components/Buttons/Button/Button.context.ts @@ -1,8 +1,7 @@ -import React from "react"; import { ButtonProps } from "."; import { createComponentContext } from "@utils/index"; -import { HasIcon } from '../../../types'; +import { CanHaveIcon } from '../../../types'; export const ButtonContext = createComponentContext< - ButtonProps & HasIcon + ButtonProps & CanHaveIcon >(); diff --git a/packages/atomic-elements/src/components/Buttons/IconButton/IconButton.component.tsx b/packages/atomic-elements/src/components/Buttons/IconButton/IconButton.component.tsx index 3973b9a9..591befd0 100644 --- a/packages/atomic-elements/src/components/Buttons/IconButton/IconButton.component.tsx +++ b/packages/atomic-elements/src/components/Buttons/IconButton/IconButton.component.tsx @@ -3,7 +3,7 @@ import { HasIcon } from "../../../types"; import { MaterialIcon } from "../../Icons/MaterialIcon"; import { StyledIconButton } from "./IconButton.styles"; import { ButtonProps } from "../Button"; -import { useContextPropsV2 } from "@hooks/useContextProps"; +import { useContextProps } from "@hooks/useContextProps"; import { ButtonContext } from "../Button/Button.context"; export interface IconButtonProps @@ -17,8 +17,8 @@ export interface IconButtonProps * */ export const IconButton = forwardRef( function IconButton(props, forwardedRef) { - [props, forwardedRef] = useContextPropsV2( - ButtonContext, + [props, forwardedRef] = useContextProps( + ButtonContext as any, props, forwardedRef ); diff --git a/packages/atomic-elements/src/components/Chips/Chip/Chip.component.tsx b/packages/atomic-elements/src/components/Chips/Chip/Chip.component.tsx index df8ee24d..3ee81383 100644 --- a/packages/atomic-elements/src/components/Chips/Chip/Chip.component.tsx +++ b/packages/atomic-elements/src/components/Chips/Chip/Chip.component.tsx @@ -1,98 +1,130 @@ -import React from "react"; -import classNames from "classnames"; -import type { ItemProps } from "react-stately"; -import { PressEvent, PressProps } from "@react-aria/interactions"; -import { AriaButtonProps } from "@react-aria/button"; -import { mergeProps } from "@react-aria/utils"; +import React, { useContext } from "react"; +import { filterDOMProps, mergeProps, useObjectRef } from "@react-aria/utils"; +import { useTag } from "@react-aria/tag"; +import { createLeafComponent } from "@react-aria/collections"; -import { copyStaticProperties } from "@utils/clone"; -import { useVariantClass } from "@hooks/variants"; -import { Item } from "@components/Collection"; import { IconButton } from "@components/Buttons/IconButton"; import { useConditionalPress } from "@hooks/useConditionalPress"; +import { useFocusRing } from "@hooks/useFocusRing"; +import { useContextProps } from "@hooks/useContextProps"; +import { useRenderProps } from "@hooks"; +import { ChipGroupStateContext } from "@components/Fields/ChipGroupField/ChipGroupField.context"; + +import { ChipArgs, ChipGroupChipProps, ChipInternalProps } from "./Chip.types"; import { ChipContent, ChipWrapper } from "./Chip.styles"; -import { SuggestStrings, HasClassName } from "../../../types"; +import { ChipContext } from "./Chip.context"; -type ChipVariants = SuggestStrings< - "default" | "warning" | "success" | "danger" ->; +export function ChipLeaf(...args: ChipArgs) { + const [props, ref] = useContextProps(ChipContext, args[0], args[1]); -export interface ChipProps extends ItemProps, PressProps, HasClassName { - children: React.ReactNode; - variant?: ChipVariants; - /** Handler that is called when the user - * clicks the remove button for the chip */ - onRemove?: (e: PressEvent) => void; - isDisabled?: boolean; -} + // We're being rendered standalone + if (args.length === 2) { + return ( + + ); + } -/** Chip component */ -export function Chip(props: ChipProps) { - return ; + // We're being rendered as part of a collection (i.e ChipGroup) + const item = args[2]; + return ; } -copyStaticProperties(Item, Chip); +/** + * Chip component. Can be used stand-alone, or within a parent + * `ChipGroup` + */ +export const Chip = createLeafComponent("item", ChipLeaf); + +function ChipGroupChip(props: ChipGroupChipProps) { + const { item } = props; + const ref = useObjectRef(props.itemRef); + const state = useContext(ChipGroupStateContext)!; + const { + rowProps, + gridCellProps, + removeButtonProps, + allowsRemoving, + isFocused, + isSelected, + } = useTag(props, state, ref); + + const { focusProps, isFocusVisible } = useFocusRing({ within: true }); + + const isDisabled = state.disabledKeys.has(item.key); -interface ChipInternalProps extends ChipProps { - wrapperProps?: React.DOMAttributes; - contentProps?: React.DOMAttributes; - removeButtonProps?: AriaButtonProps<"button">; - allowsRemoving?: boolean; + const renderProps = useRenderProps({ + componentClassName: "aje-chip", + values: { + isSelected, + isFocusVisible, + isFocused, + }, + ...props, + }); + + return ( + + + {renderProps.children} + {allowsRemoving && ( + + )} + + + ); } -export const ChipInternal = React.forwardRef< - HTMLDivElement, - ChipInternalProps ->(function ChipInternal( +export const ChipInternal = React.forwardRef(function ChipInternal( props: ChipInternalProps, ref: React.Ref ) { const { className, - variant = "default", + variant, onRemove, isDisabled, children, - wrapperProps = {}, - contentProps = {}, - removeButtonProps = {}, allowsRemoving = false, ...rest } = props; - const variantClass = useVariantClass("aje-chip", variant); const { pressProps } = useConditionalPress(rest); - const allWrapperProps = [ - wrapperProps, - { "aria-disabled": isDisabled || undefined }, - ]; - - if (!isDisabled) { - allWrapperProps.push(pressProps); - } + const renderProps = useRenderProps({ + componentClassName: "aje-chip", + values: { + isSelected: false, + isFocusVisible: false, + isFocused: false, + }, + ...props, + }); return ( - - {children} - + + {renderProps.children} {allowsRemoving && ( )} diff --git a/packages/atomic-elements/src/components/Chips/Chip/Chip.context.ts b/packages/atomic-elements/src/components/Chips/Chip/Chip.context.ts new file mode 100644 index 00000000..3c76fb8e --- /dev/null +++ b/packages/atomic-elements/src/components/Chips/Chip/Chip.context.ts @@ -0,0 +1,4 @@ +import { createComponentContext } from "@utils/index"; +import { ChipProps } from "./Chip.types"; + +export const ChipContext = createComponentContext>(); diff --git a/packages/atomic-elements/src/components/Chips/Chip/Chip.spec.tsx b/packages/atomic-elements/src/components/Chips/Chip/Chip.spec.tsx index f31188f5..ffd00b3d 100644 --- a/packages/atomic-elements/src/components/Chips/Chip/Chip.spec.tsx +++ b/packages/atomic-elements/src/components/Chips/Chip/Chip.spec.tsx @@ -1,5 +1,5 @@ -import { render } from "@testing-library/react"; -import { expect, describe, test } from "vitest"; +import { fireEvent, render, screen } from "@testing-library/react"; +import { expect, describe, test, vi } from "vitest"; import { Chip } from "."; describe("Chip", () => { @@ -7,4 +7,11 @@ describe("Chip", () => { const result = render(Item); expect(result.asFragment()).toMatchSnapshot(); }); + + test("onRemove", () => { + const onRemove = vi.fn(); + render(Item); + fireEvent.click(screen.getByRole("button")); + expect(onRemove).toHaveBeenCalledOnce(); + }); }); diff --git a/packages/atomic-elements/src/components/Chips/Chip/Chip.stories.tsx b/packages/atomic-elements/src/components/Chips/Chip/Chip.stories.tsx index 1d377aa0..4569d5ac 100644 --- a/packages/atomic-elements/src/components/Chips/Chip/Chip.stories.tsx +++ b/packages/atomic-elements/src/components/Chips/Chip/Chip.stories.tsx @@ -23,6 +23,11 @@ export default { category: "Events", }, }, + onAction: { + table: { + disable: true, + }, + }, }, } as Meta; diff --git a/packages/atomic-elements/src/components/Chips/Chip/Chip.types.ts b/packages/atomic-elements/src/components/Chips/Chip/Chip.types.ts new file mode 100644 index 00000000..305d39ae --- /dev/null +++ b/packages/atomic-elements/src/components/Chips/Chip/Chip.types.ts @@ -0,0 +1,29 @@ +import { Node } from "react-stately"; +import { AriaLabelProps, DomProps, SuggestStrings } from "../../../types"; +import { PressEvent, PressProps } from "@react-aria/interactions"; +import { ItemProps } from "../../Collection"; + +type ChipVariants = SuggestStrings< + "default" | "warning" | "success" | "danger" +>; + +export interface ChipProps extends ItemProps, PressProps { + variant?: ChipVariants; + /** Handler that is called when the user + * clicks the remove button for the chip */ + onRemove?: (e: PressEvent) => void; + isDisabled?: boolean; +} + +export type ChipArgs = + | [ChipProps, React.ForwardedRef] + | [ChipProps, React.ForwardedRef, Node]; + +export interface ChipGroupChipProps extends ChipProps { + item: Node; + itemRef: React.ForwardedRef; +} + +export interface ChipInternalProps extends ChipProps { + allowsRemoving?: boolean; +} diff --git a/packages/atomic-elements/src/components/Chips/Chip/__snapshots__/Chip.spec.tsx.snap b/packages/atomic-elements/src/components/Chips/Chip/__snapshots__/Chip.spec.tsx.snap index 8f5b6891..e3ec1788 100644 --- a/packages/atomic-elements/src/components/Chips/Chip/__snapshots__/Chip.spec.tsx.snap +++ b/packages/atomic-elements/src/components/Chips/Chip/__snapshots__/Chip.spec.tsx.snap @@ -3,7 +3,7 @@ exports[`Chip > matches snapshot 1`] = `
extends AriaProps>, @@ -22,93 +11,36 @@ export interface ChipGroupProps labelPlacement?: "above" | "inline"; } -/** Collection Component for displaying a group of chips. - * Chips can be selected and removed by the user. +/** + * A generic ChipGroup component that renders a group of chips with optional labels, messages, and error messages. */ export function ChipGroup(props: ChipGroupProps) { const { label, message, error, - isInvalid, - isDisabled, - isRequired, - className, labelPlacement = "above", - size = "full", + items, + children, + ...rest } = props; - const ref = useRef(null); - - const state = useListState(props); - const { gridProps, labelProps, descriptionProps, errorMessageProps } = - useTagGroup(props, state, ref); - return ( - + {labelPlacement === "above" && label && ( - + )} - - {labelPlacement === "inline" && label && ( - - )} - {[...state.collection].map((item) => ( - - ))} - - {message && {message}} - {isInvalid && error && ( - {error} - )} - - ); -} - -// NOTE: when Chip is rendered standalone, it renders the actual Chip component -// When rendered within a ChipGroup, it actually renders the ChipInternal component - -interface ChipGroupChipProps extends AriaTagProps { - state: ListState; -} - -function ChipGroupChip(props: ChipGroupChipProps) { - const { item, state } = props; - const ref = useRef(null); - const { rowProps, gridCellProps, removeButtonProps, allowsRemoving } = useTag( - props, - state, - ref - ); - - const { focusProps } = useFocusRing({ within: true }); - - const isDisabled = state.disabledKeys.has(item.key); - - return ( - - {item.rendered} - + {label} + } + > + {children} + + {message && {message}} + {error && {error}} + ); } diff --git a/packages/atomic-elements/src/components/Chips/ChipGroup/ChipGroup.spec.tsx b/packages/atomic-elements/src/components/Chips/ChipGroup/ChipGroup.spec.tsx index cfcddbfb..6b168263 100644 --- a/packages/atomic-elements/src/components/Chips/ChipGroup/ChipGroup.spec.tsx +++ b/packages/atomic-elements/src/components/Chips/ChipGroup/ChipGroup.spec.tsx @@ -24,13 +24,13 @@ describe("ChipGroup", () => { test("renders chips", () => { const chips = [ - { key: "1", rendered: "Chip 1" }, - { key: "2", rendered: "Chip 2" }, - { key: "3", rendered: "Chip 3" }, + { id: "1", rendered: "Chip 1" }, + { id: "2", rendered: "Chip 2" }, + { id: "3", rendered: "Chip 3" }, ]; render( - {({ key, rendered }) => {rendered}} + {({ rendered }) => {rendered}} ); chips.forEach((chip) => { diff --git a/packages/atomic-elements/src/components/Chips/ChipGroup/ChipGroup.stories.tsx b/packages/atomic-elements/src/components/Chips/ChipGroup/ChipGroup.stories.tsx index 2e8e01ed..87d29309 100644 --- a/packages/atomic-elements/src/components/Chips/ChipGroup/ChipGroup.stories.tsx +++ b/packages/atomic-elements/src/components/Chips/ChipGroup/ChipGroup.stories.tsx @@ -20,6 +20,9 @@ export default { }, onRemove: { description: "Function to call when a chip is removed", + table: { + category: "Events", + }, }, }, } as Meta; @@ -30,10 +33,10 @@ export const Primary: Story = { args: { label: "Chip Group", children: [ - News, - Travel, - Gaming, - Shopping, + News, + Travel, + Gaming, + Shopping, ], }, }; diff --git a/packages/atomic-elements/src/components/Chips/ChipGroup/__snapshots__/ChipGroup.spec.tsx.snap b/packages/atomic-elements/src/components/Chips/ChipGroup/__snapshots__/ChipGroup.spec.tsx.snap index b460350b..27b205bf 100644 --- a/packages/atomic-elements/src/components/Chips/ChipGroup/__snapshots__/ChipGroup.spec.tsx.snap +++ b/packages/atomic-elements/src/components/Chips/ChipGroup/__snapshots__/ChipGroup.spec.tsx.snap @@ -3,7 +3,7 @@ exports[`ChipGroup > matches snapshot 1`] = `