diff --git a/.gitignore b/.gitignore index 0206be8c..b18cc5dc 100644 --- a/.gitignore +++ b/.gitignore @@ -16,5 +16,6 @@ docs/build dist node_modules packages/atomic-fuel/libs +.vscode diff --git a/packages/atomic-elements/src/components/Banners/ActionBanner/ActionBanner.component.tsx b/packages/atomic-elements/src/components/Banners/ActionBanner/ActionBanner.component.tsx index eedf9e42..0478126d 100644 --- a/packages/atomic-elements/src/components/Banners/ActionBanner/ActionBanner.component.tsx +++ b/packages/atomic-elements/src/components/Banners/ActionBanner/ActionBanner.component.tsx @@ -8,7 +8,7 @@ import { } from "../../../types"; import { MaterialIcon } from "../../Icons/MaterialIcon"; import { Banner, BannerVariants } from "../Banner"; -import { ButtonVariants } from "../../Buttons/Buttons.types"; +import { ButtonVariants } from "../../Buttons/Button"; export interface ActionBannerProps extends HasChildren, HasClassName { readonly variant?: BannerVariants; 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 dc61d15f..05f94be7 100644 --- a/packages/atomic-elements/src/components/Buttons/Button/Button.component.tsx +++ b/packages/atomic-elements/src/components/Buttons/Button/Button.component.tsx @@ -1,61 +1,90 @@ import { forwardRef } from "react"; import { AriaButtonOptions } from "@react-aria/button"; -import { mergeProps } from "@react-aria/utils"; +import { mergeProps, useObjectRef } from "@react-aria/utils"; import { SpinnerLoader } from "../../Loaders/SpinnerLoader"; import { - BaseProps, - HasChildren, + ExtendedSize, HasVariant, LoadingProps, + RenderBaseProps, + SuggestStrings, } from "../../../types"; -import { StyledButton } from "./Button.styles"; -import { ButtonVariants } from "../Buttons.types"; -import useForwardedRef from "../../../hooks/useForwardedRef"; -import { useFocusRing } from "../../../hooks/useFocusRing"; +import { useFocusRing } from "@hooks/useFocusRing"; import { useRenderProps } from "@hooks/useRenderProps"; import { useButtonLink } from "@hooks/useButtonLink"; -import { useContextProps } from "@hooks/useContextProps"; +import { useContextPropsV2 } from "@hooks/useContextProps"; +import { StyledButton } from "./Button.styles"; import { ButtonContext } from "./Button.context"; -export type ButtonProps = AriaButtonOptions<"button"> & - LoadingProps & - BaseProps & - HasChildren & - HasVariant & { - as?: "button" | "a"; - }; +export type ButtonVariants = SuggestStrings< + | "primary" + | "secondary" + | "link" + | "success" + | "error" + | "inverted" + | "content" + | "border" + | "ghost" +>; + +interface ButtonRenderProps { + isLoading: boolean; + isPressed: boolean; + isFocusVisible: boolean; + isFocused: boolean; +} + +export interface ButtonProps + extends AriaButtonOptions<"button">, + LoadingProps, + RenderBaseProps, + HasVariant { + as?: "button" | "a"; + size?: ExtendedSize; +} +/** A button component that can be used to trigger actions or events + * + * @example + */ export const Button = forwardRef( - (props, ref) => { + function Button(props, forwardedRef) { + [props, forwardedRef] = useContextPropsV2( + ButtonContext, + props, + forwardedRef + ); + + const ref = useObjectRef(forwardedRef); + const { - children, - size = "auto", - variant = "primary", isLoading = false, loadingLabel, loadingComplete = false, - className, as = props.href ? "a" : "button", - } = useContextProps(ButtonContext, props); + variant = "primary", + size = "auto", + } = props; - const internalRef = useForwardedRef(ref); const { buttonProps, isPressed } = useButtonLink( { ...props, elementType: as, "aria-label": isLoading ? loadingLabel : props["aria-label"], }, - internalRef + ref ); - const { focusProps } = useFocusRing(); + const { focusProps, isFocusVisible, isFocused } = useFocusRing(); const renderProps = useRenderProps({ componentClassName: "aje-btn", - className, + ...props, variant, size, + values: { isPressed, isLoading, isFocusVisible, isFocused }, selectors: { "data-pressed": isPressed, "data-loading": isLoading, @@ -65,7 +94,7 @@ export const Button = forwardRef( return ( {isLoading && ( @@ -74,12 +103,8 @@ export const Button = forwardRef( placement="absolute center" /> )} - {children} + {renderProps.children} ); } ); - -Button.displayName = "Button"; - -export default Button; 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 60b485ad..29a63a1c 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,8 @@ import React from "react"; import { ButtonProps } from "."; -import { IconButtonProps } from "../IconButton"; import { createComponentContext } from "@utils/index"; +import { HasIcon } from '../../../types'; export const ButtonContext = createComponentContext< - ButtonProps & IconButtonProps + ButtonProps & HasIcon >(); diff --git a/packages/atomic-elements/src/components/Buttons/Button/Button.stories.tsx b/packages/atomic-elements/src/components/Buttons/Button/Button.stories.tsx index d307482a..7b6d7a23 100644 --- a/packages/atomic-elements/src/components/Buttons/Button/Button.stories.tsx +++ b/packages/atomic-elements/src/components/Buttons/Button/Button.stories.tsx @@ -1,6 +1,6 @@ import { Meta, StoryObj } from "@storybook/react"; import { Button } from "."; -import { PressableArgTypes } from "@sb/helpers"; +import { PressableArgTypes, RenderPropsArgTypes } from "@sb/helpers"; import { MaterialIcon } from "@components/Icons/MaterialIcon"; import { getCssProps } from "@sb/cssprops"; @@ -13,6 +13,7 @@ const meta: Meta = { }, argTypes: { ...PressableArgTypes, + ...RenderPropsArgTypes, children: { control: "text", }, @@ -48,11 +49,6 @@ const meta: Meta = { description: "If true, the button will be excluded from the tab order and will not be focusable via keyboard navigation.", }, - // elementType: { - // control: "text", - // description: - // "The type of element to render. By default, it will render a button element.", - // }, }, }; diff --git a/packages/atomic-elements/src/components/Buttons/Button/Button.styles.ts b/packages/atomic-elements/src/components/Buttons/Button/Button.styles.ts index 3ed80009..d79dcf15 100644 --- a/packages/atomic-elements/src/components/Buttons/Button/Button.styles.ts +++ b/packages/atomic-elements/src/components/Buttons/Button/Button.styles.ts @@ -1,7 +1,133 @@ import styled from "styled-components"; -import { BaseStyledButton } from "../Buttons.styles"; import mixins from "@styles/mixins"; -export const StyledButton = styled(BaseStyledButton)` +export const StyledButton = styled.button` ${mixins.SizingX} + ${mixins.Bold} + ${mixins.FocusVisible(2)} + padding: var(--btn-padding-vert) var(--btn-padding-horiz); + border-radius: var(--btn-border-radius); + font-size: var(--btn-font-size); + min-height: var(--btn-height); + display: inline-flex; + align-items: center; + justify-content: center; + gap: var(--btn-icon-gap); + text-decoration: none; + transition: + background 100ms ease, + color 100ms ease, + transform 100ms ease, + box-shadow 100ms ease; + + color: var(--btn-text-clr); + background-color: var(--btn-bg-clr); + border: var(--btn-border, none); + box-shadow: var(--btn-shadow, none); + --loader-clr: var(--btn-text-clr); + + &:hover { + cursor: pointer; + color: var(--btn-hover-text-clr); + background-color: var(--btn-hover-bg-clr); + box-shadow: var(--btn-hover-shadow); + } + + &:disabled { + opacity: 0.5; + pointer-events: none; + } + + &[data-pressed] { + transform: var(--btn-pressed-transform); + } + + &[data-loading] { + position: relative; + color: transparent; + + .aje-spinner, + .aje-three-dot-loader { + --loader-clr: var(--btn-text-clr); + --loader-size: 1em; + } + } + + &.aje-btn--primary { + --btn-text-clr: var(--text-clr-inverted); + --btn-bg-clr: var(--accent-clr); + --btn-hover-text-clr: var(--btn-text-clr); + --btn-hover-bg-clr: var(--accent-clr-alt); + } + + &.aje-btn--secondary { + --btn-text-clr: var(--text-clr-alt); + --btn-bg-clr: var(--neutral100); + --btn-hover-text-clr: var(--text-clr); + --btn-hover-bg-clr: var(--neutral200); + --btn-border: var(--border); + } + + &.aje-btn--link { + --btn-text-clr: var(--primary700); + --btn-bg-clr: var(--neutral50); + --btn-hover-text-clr: var(--text-clr); + --btn-hover-bg-clr: var(--neutral100); + text-decoration: underline; + } + + &.aje-btn--error { + --btn-text-clr: var(--text-clr-inverted); + --btn-bg-clr: var(--error700); + --btn-hover-text-clr: var(--btn-text-clr); + --btn-hover-bg-clr: var(--error800); + } + + &.aje-btn--success { + --btn-text-clr: var(--text-clr-inverted); + --btn-bg-clr: var(--success700); + --btn-hover-text-clr: var(--btn-text-clr); + --btn-hover-bg-clr: var(--success800); + } + + &.aje-btn--inverted { + --btn-text-clr: var(--text-clr); + --btn-bg-clr: var(--neutral50); + --btn-hover-text-clr: var(--btn-text-clr); + --btn-hover-bg-clr: var(--btn-bg-clr); + --btn-hover-shadow: 0 1px 3px hsla(221, 39%, 11%, 0.5); + } + + &.aje-btn--content { + --btn-text-clr: var(--text-clr); + --btn-bg-clr: transparent; + --btn-hover-text-clr: var(--btn-text-clr); + --btn-hover-bg-clr: transparent; + --btn-hover-shadow: none; + --btn-padding-horiz: 0px; + --btn-padding-vert: 0px; + --btn-height: auto; + } + + &.aje-btn--border { + --btn-text-clr: var(--text-clr-alt); + --btn-bg-clr: var(--neutral50); + --btn-hover-text-clr: var(--text-clr); + --btn-hover-bg-clr: var(--neutral100); + --btn-border: var(--border); + } + + &.aje-btn--ghost { + --btn-text-clr: var(--text-clr-alt); + --btn-bg-clr: transparent; + --btn-hover-text-clr: var(--text-clr); + --btn-hover-bg-clr: var(--neutral100); + --btn-border: transparent; + } + + & > i { + color: inherit; + font-size: var(--btn-icon-size); + margin-left: calc(var(--btn-padding-horiz) / -2.5); + } `; diff --git a/packages/atomic-elements/src/components/Buttons/Button/__snapshots__/Button.spec.tsx.snap b/packages/atomic-elements/src/components/Buttons/Button/__snapshots__/Button.spec.tsx.snap index 57e01472..d59f0362 100644 --- a/packages/atomic-elements/src/components/Buttons/Button/__snapshots__/Button.spec.tsx.snap +++ b/packages/atomic-elements/src/components/Buttons/Button/__snapshots__/Button.spec.tsx.snap @@ -3,7 +3,7 @@ exports[`Button > matches snapshots > changes variant 1`] = ` + * + * */ export function ButtonGroup(props: ButtonGroupProps) { const { @@ -31,30 +25,22 @@ export function ButtonGroup(props: ButtonGroupProps) { isMerged, className, id, - size, gap, direction = "row", - ...overrides + ...buttonProps } = props; - const mergedPropsChildren = React.Children.map(children, (child) => { - return React.cloneElement(child, { - ...overrides, - buttonVariant: props.variant, - ...child.props, - }); - }); - return ( - - {mergedPropsChildren} - + + + {children} + + ); } diff --git a/packages/atomic-elements/src/components/Buttons/ButtonGroup/index.tsx b/packages/atomic-elements/src/components/Buttons/ButtonGroup/index.tsx index 46a85760..3e59c056 100644 --- a/packages/atomic-elements/src/components/Buttons/ButtonGroup/index.tsx +++ b/packages/atomic-elements/src/components/Buttons/ButtonGroup/index.tsx @@ -1,6 +1,2 @@ export { ButtonGroup } from "./ButtonGroup.component"; -export type { - ButtonGroupProps, - ButtonGroupChild, - ButtonGroupChildProps, -} from "./ButtonGroup.component"; +export type { ButtonGroupProps } from "./ButtonGroup.component"; diff --git a/packages/atomic-elements/src/components/Buttons/Buttons.styles.ts b/packages/atomic-elements/src/components/Buttons/Buttons.styles.ts deleted file mode 100644 index 65482b63..00000000 --- a/packages/atomic-elements/src/components/Buttons/Buttons.styles.ts +++ /dev/null @@ -1,132 +0,0 @@ -import styled from "styled-components"; -import mixins from "../../styles/mixins"; - -export const BaseStyledButton = styled.button` - ${mixins.Bold} - ${mixins.FocusVisible(2)} - padding: var(--btn-padding-vert) var(--btn-padding-horiz); - border-radius: var(--btn-border-radius); - font-size: var(--btn-font-size); - min-height: var(--btn-height); - display: inline-flex; - align-items: center; - justify-content: center; - gap: var(--btn-icon-gap); - text-decoration: none; - transition: - background 100ms ease, - color 100ms ease, - transform 100ms ease, - box-shadow 100ms ease; - - color: var(--btn-text-clr); - background-color: var(--btn-bg-clr); - border: var(--btn-border, none); - box-shadow: var(--btn-shadow, none); - --loader-clr: var(--btn-text-clr); - - &:hover { - cursor: pointer; - color: var(--btn-hover-text-clr); - background-color: var(--btn-hover-bg-clr); - box-shadow: var(--btn-hover-shadow); - } - - &:disabled { - opacity: 0.5; - pointer-events: none; - } - - &[data-pressed] { - transform: var(--btn-pressed-transform); - } - - &[data-loading] { - position: relative; - color: transparent; - - .aje-spinner, - .aje-three-dot-loader { - --loader-clr: var(--btn-text-clr); - --loader-size: 1em; - } - } - - &.aje-btn--primary { - --btn-text-clr: var(--text-clr-inverted); - --btn-bg-clr: var(--accent-clr); - --btn-hover-text-clr: var(--btn-text-clr); - --btn-hover-bg-clr: var(--accent-clr-alt); - } - - &.aje-btn--secondary { - --btn-text-clr: var(--text-clr-alt); - --btn-bg-clr: var(--neutral100); - --btn-hover-text-clr: var(--text-clr); - --btn-hover-bg-clr: var(--neutral200); - --btn-border: var(--border); - } - - &.aje-btn--link { - --btn-text-clr: var(--primary700); - --btn-bg-clr: var(--neutral50); - --btn-hover-text-clr: var(--text-clr); - --btn-hover-bg-clr: var(--neutral100); - text-decoration: underline; - } - - &.aje-btn--error { - --btn-text-clr: var(--text-clr-inverted); - --btn-bg-clr: var(--error700); - --btn-hover-text-clr: var(--btn-text-clr); - --btn-hover-bg-clr: var(--error800); - } - - &.aje-btn--success { - --btn-text-clr: var(--text-clr-inverted); - --btn-bg-clr: var(--success700); - --btn-hover-text-clr: var(--btn-text-clr); - --btn-hover-bg-clr: var(--success800); - } - - &.aje-btn--inverted { - --btn-text-clr: var(--text-clr); - --btn-bg-clr: var(--neutral50); - --btn-hover-text-clr: var(--btn-text-clr); - --btn-hover-bg-clr: var(--btn-bg-clr); - --btn-hover-shadow: 0 1px 3px hsla(221, 39%, 11%, 0.5); - } - - &.aje-btn--content { - --btn-text-clr: var(--text-clr); - --btn-bg-clr: transparent; - --btn-hover-text-clr: var(--btn-text-clr); - --btn-hover-bg-clr: transparent; - --btn-hover-shadow: none; - --btn-padding-horiz: 0px; - --btn-padding-vert: 0px; - --btn-height: auto; - } - - &.aje-btn--border { - --btn-text-clr: var(--text-clr-alt); - --btn-bg-clr: var(--neutral50); - --btn-hover-text-clr: var(--text-clr); - --btn-hover-bg-clr: var(--neutral100); - --btn-border: var(--border); - } - - &.aje-btn--ghost { - --btn-text-clr: var(--text-clr-alt); - --btn-bg-clr: transparent; - --btn-hover-text-clr: var(--text-clr); - --btn-hover-bg-clr: var(--neutral100); - --btn-border: transparent; - } - - & > i { - color: inherit; - font-size: var(--btn-icon-size); - margin-left: calc(var(--btn-padding-horiz) / -2.5); - } -`; diff --git a/packages/atomic-elements/src/components/Buttons/Buttons.types.ts b/packages/atomic-elements/src/components/Buttons/Buttons.types.ts deleted file mode 100644 index 0fdec885..00000000 --- a/packages/atomic-elements/src/components/Buttons/Buttons.types.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { SuggestStrings } from "../../types"; - -export type ButtonVariants = SuggestStrings< - | "primary" - | "secondary" - | "link" - | "success" - | "error" - | "inverted" - | "content" - | "border" - | "ghost" ->; 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 0d507532..3973b9a9 100644 --- a/packages/atomic-elements/src/components/Buttons/IconButton/IconButton.component.tsx +++ b/packages/atomic-elements/src/components/Buttons/IconButton/IconButton.component.tsx @@ -1,79 +1,48 @@ import { forwardRef } from "react"; -import { mergeProps } from "@react-aria/utils"; import { HasIcon } from "../../../types"; -import { SpinnerLoader } from "../../Loaders/SpinnerLoader"; import { MaterialIcon } from "../../Icons/MaterialIcon"; import { StyledIconButton } from "./IconButton.styles"; -import { useForwardedRef } from "../../../hooks/useForwardedRef"; -import { useRenderProps } from "@hooks/useRenderProps"; import { ButtonProps } from "../Button"; -import { useFocusRing } from "../../../hooks/useFocusRing"; -import { useButtonLink } from "@hooks/useButtonLink"; import { useContextPropsV2 } from "@hooks/useContextProps"; import { ButtonContext } from "../Button/Button.context"; -export type IconButtonProps = Omit & HasIcon; +export interface IconButtonProps + extends Omit, + HasIcon {} -/** Similar to the Button component, but is intended to display just an icon instead of text. - * Because of this, you should provide an `aria-label` for accessiblity */ +/** A button designed for displaying a single icon. The button has no text content, + * so you should provide an aria-label for accessiblity + * + * @example alert("Hello, world!")} /> + * */ export const IconButton = forwardRef( - (props, ref) => { - [props, ref] = useContextPropsV2(ButtonContext, props as any, ref); + function IconButton(props, forwardedRef) { + [props, forwardedRef] = useContextPropsV2( + ButtonContext, + props, + forwardedRef + ); const { icon, - isLoading, - loadingComplete, - loadingLabel, - variant = "border", - iconVariant = "default", - className, + iconVariant, size = "medium", - as = props.href ? "a" : "button", + variant = "border", + as, + ...rest } = props; - const innerRef = useForwardedRef(ref); - const { buttonProps, isPressed } = useButtonLink( - { - ...props, - elementType: as, - "aria-label": isLoading ? loadingLabel : props["aria-label"], - }, - innerRef - ); - - const { focusProps } = useFocusRing(); - - const renderProps = useRenderProps({ - componentClassName: "aje-btn", - className: ["aje-btn--icon", className], - size, - variant, - selectors: { - "data-loading": isLoading, - "data-pressed": isPressed, - }, - }); - return ( + // @ts-expect-error - forwardedAs isn't being typed correctly - {isLoading && ( - - )} ); } ); - -IconButton.displayName = "IconButton"; - -export default IconButton; diff --git a/packages/atomic-elements/src/components/Buttons/IconButton/IconButton.styles.ts b/packages/atomic-elements/src/components/Buttons/IconButton/IconButton.styles.ts index a6032697..48a5a07c 100644 --- a/packages/atomic-elements/src/components/Buttons/IconButton/IconButton.styles.ts +++ b/packages/atomic-elements/src/components/Buttons/IconButton/IconButton.styles.ts @@ -1,9 +1,7 @@ import styled from "styled-components"; -import { BaseStyledButton } from "../Buttons.styles"; -import mixins from "@styles/mixins"; +import { Button } from '../Button'; -export const StyledIconButton = styled(BaseStyledButton)` - ${mixins.Sizing} +export const StyledIconButton = styled(Button)` --size-sm-x: 30px; --size-sm-y: 30px; --size-md-x: 40px; diff --git a/packages/atomic-elements/src/components/Buttons/IconButton/__snapshots__/IconButton.spec.tsx.snap b/packages/atomic-elements/src/components/Buttons/IconButton/__snapshots__/IconButton.spec.tsx.snap index b72c51ff..d2b973e8 100644 --- a/packages/atomic-elements/src/components/Buttons/IconButton/__snapshots__/IconButton.spec.tsx.snap +++ b/packages/atomic-elements/src/components/Buttons/IconButton/__snapshots__/IconButton.spec.tsx.snap @@ -4,12 +4,12 @@ exports[`matches snapshot 1`] = `