Skip to content

Commit

Permalink
Feat: [DS-386] Split Button into Button & LinkButton (#485)
Browse files Browse the repository at this point in the history
  • Loading branch information
vicky-comeau authored Oct 10, 2024
2 parents 242962d + eae8b7d commit 17a1c5f
Show file tree
Hide file tree
Showing 53 changed files with 1,573 additions and 2,374 deletions.
6 changes: 6 additions & 0 deletions .changeset/eleven-dolphins-lick.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@hopper-ui/components": patch
"@hopper-ui/icons": patch
---

Updated react aria versions and hopper styled-system version.
7 changes: 7 additions & 0 deletions .changeset/healthy-mails-impress.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@hopper-ui/components": patch
---

We've separated the Button component into two distinct components: Button and LinkButton. This change clarifies their purposes and simplifies their usage. The Button component now focuses solely on traditional button functionality, without any link-related features.

Meanwhile, the new LinkButton component is specifically designed for link-based interactions, visually styled like a button but meant for navigation. Unlike Button, LinkButton does not support loading states (isLoading), as its primary role is to facilitate navigation rather than trigger actions that require loading feedback.
11 changes: 1 addition & 10 deletions apps/docs/content/components/buttons/Button.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ links:
A `Button` uses the following components.

- [Icon](./Icon)
- [IconList](./IconList)
- [Text](./Text)

## Examples
Expand Down Expand Up @@ -89,16 +90,6 @@ Nonstandard end icons can be provided to handle special cases. However, think tw

<Example src="buttons/docs/button/endIcon" />

### As Link
A button can be rendered as a link by using the href property.

<Example src="buttons/docs/button/asLink" />

### As Router Link
A button can be rendered as a react router link when using the href property and setting the navigate property on the [HopperProvider](./HopperProvider).

<Example src="buttons/docs/button/reactRouterLink" />

<FeatureFlag flag="next">
## Advanced customization

Expand Down
130 changes: 130 additions & 0 deletions apps/docs/content/components/buttons/LinkButton.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
---
title: LinkButton
description: A link link button looks like a link button but behaves like a link
category: "buttons"
links:
source: https://github.com/gsoft-inc/wl-hopper/blob/main/packages/components/src/buttons/src/LinkButton.tsx
aria: https://www.w3.org/WAI/ARIA/apg/patterns/link/
---

<Example src="buttons/docs/linkButton/preview" isOpen />

<FeatureFlag flag="next">
## Guidelines

TODO: If we have some guidelines about this component&apos;s usage

### Accessibility?

TODO: If we have some guidelines about this component and accessibility
</FeatureFlag>

## Anatomy

<FeatureFlag flag="rc">
TODO: We have anatomy screenshots from the Figma, we could most likely use them here

### Concepts

- [Client Side Routing](./client-side-routing)
</FeatureFlag>

### Composed Components

A `LinkButton` uses the following components.

- [Icon](./Icon)
- [IconList](./IconList)
- [Text](./Text)

## Examples

### Variants
A link button can use different variants.

<Example src="buttons/docs/linkButton/variant" />

**Primary** - For the principal call to action on the page. Primary buttons should only appear once per screen — not including the application header, modal or side panel.

**Secondary** - For secondary actions on each page. Secondary buttons can be used in conjunction with a primary link button or on its own. Paired with a Primary link button, the secondary link button usually performs the negative action of the set, such as “Cancel.”

**Upsell** - For upsell actions that relate to upgrading an account or a plan. Use the upsell link button to distinguish it from an existing primary link button. In some cases, a primary link button can be used instead when the general context of the page is about upselling.

**Danger** - For actions that could have destructive effects on the user’s data.

**Ghost-[primary|secondary|danger]** - For less prominent, and sometimes independent, actions. Ghost buttons can be used in isolation or paired with a primary link button when there are multiple calls to action. Ghost buttons can also be used for subtasks on a
page where a primary link button for the main and final action is present.

### Sizes
A link button can vary in size.

<Example src="buttons/docs/linkButton/size" />

### Disabled
A link button can be disabled.

<Example src="buttons/docs/linkButton/state" />

### External

Add `rel="noopener noreferrer"` and `target="_blank"` attributes to render and external link button.

<Example src="buttons/docs/linkButton/external" />

### No Href

When a link button link does not have an href prop, it is rendered as a `<span role="link">` instead of an `<a>`. Events will need to be handled in JavaScript with the `onPress` prop.

Note: this will not behave like a native link. Browser features like context menus and open in a new tab will not apply.

<Example src="buttons/docs/linkButton/noHref" />

### Fluid
A link button can be expanded to full width to fill its parent container.

<Example src="buttons/docs/linkButton/layout" />

### Icon Only
A link button can contain only an icon.

<Example src="buttons/docs/linkButton/icon" />

### Icon
A link button can contain icons.

<Example src="buttons/docs/linkButton/icons" />

### End Icon
Nonstandard end icons can be provided to handle special cases. However, think twice before adding end icons, start icons should be your go-to.

<Example src="buttons/docs/linkButton/endIcon" />

<Example src="buttons/docs/linkButton/reactRouterLink" />

<FeatureFlag flag="next">
## Advanced customization

### Contexts

All Hopper Components export a corresponding context that can be used to send props to them from a parent element.
You can send any prop or ref via context that you could pass to the corresponding component.
The local props and ref on the component are merged with the ones passed via context, with the local props taking precedence

<Example src="buttons/docs/linkButton/advancedCustomization" />

### Custom Children

TODO: Example of passing custom children to the components to fake a slot

### Custom Component

TODO: Example of creating a custom version of this component
</FeatureFlag>

## Props

<PropTable component="LinkButton" />

## Migration Notes

<MigrateGuide src="buttons/docs/migration-notes-link-button" />
42 changes: 36 additions & 6 deletions apps/docs/examples/Preview.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,12 +38,6 @@ export const Previews: Record<string, Preview> = {
"buttons/docs/button/endIcon": {
component: lazy(() => import("@/../../packages/components/src/buttons/docs/button/endIcon.tsx"))
},
"buttons/docs/button/asLink": {
component: lazy(() => import("@/../../packages/components/src/buttons/docs/button/asLink.tsx"))
},
"buttons/docs/button/reactRouterLink": {
component: lazy(() => import("@/../../packages/components/src/buttons/docs/button/reactRouterLink.tsx"))
},
"buttons/docs/button/advancedCustomization": {
component: lazy(() => import("@/../../packages/components/src/buttons/docs/button/advancedCustomization.tsx"))
},
Expand All @@ -65,6 +59,42 @@ export const Previews: Record<string, Preview> = {
"buttons/docs/buttonGroup/sizes": {
component: lazy(() => import("@/../../packages/components/src/buttons/docs/buttonGroup/sizes.tsx"))
},
"buttons/docs/linkButton/preview": {
component: lazy(() => import("@/../../packages/components/src/buttons/docs/linkButton/preview.tsx"))
},
"buttons/docs/linkButton/variant": {
component: lazy(() => import("@/../../packages/components/src/buttons/docs/linkButton/variant.tsx"))
},
"buttons/docs/linkButton/size": {
component: lazy(() => import("@/../../packages/components/src/buttons/docs/linkButton/size.tsx"))
},
"buttons/docs/linkButton/state": {
component: lazy(() => import("@/../../packages/components/src/buttons/docs/linkButton/state.tsx"))
},
"buttons/docs/linkButton/external": {
component: lazy(() => import("@/../../packages/components/src/buttons/docs/linkButton/external.tsx"))
},
"buttons/docs/linkButton/noHref": {
component: lazy(() => import("@/../../packages/components/src/buttons/docs/linkButton/noHref.tsx"))
},
"buttons/docs/linkButton/layout": {
component: lazy(() => import("@/../../packages/components/src/buttons/docs/linkButton/layout.tsx"))
},
"buttons/docs/linkButton/icon": {
component: lazy(() => import("@/../../packages/components/src/buttons/docs/linkButton/icon.tsx"))
},
"buttons/docs/linkButton/icons": {
component: lazy(() => import("@/../../packages/components/src/buttons/docs/linkButton/icons.tsx"))
},
"buttons/docs/linkButton/endIcon": {
component: lazy(() => import("@/../../packages/components/src/buttons/docs/linkButton/endIcon.tsx"))
},
"buttons/docs/linkButton/reactRouterLink": {
component: lazy(() => import("@/../../packages/components/src/buttons/docs/linkButton/reactRouterLink.tsx"))
},
"buttons/docs/linkButton/advancedCustomization": {
component: lazy(() => import("@/../../packages/components/src/buttons/docs/linkButton/advancedCustomization.tsx"))
},
"ListBox/docs/preview": {
component: lazy(() => import("@/../../packages/components/src/ListBox/docs/preview.tsx"))
},
Expand Down
6 changes: 3 additions & 3 deletions apps/docs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,8 @@
"copy:images": "tsx scripts/copyImages.ts"
},
"peerDependencies": {
"react-aria": "^3.34",
"react-aria-components": "^1.4.0"
"react-aria": "^3.35",
"react-aria-components": "^1.4"
},
"dependencies": {
"@tanstack/react-table": "^8.17.3",
Expand All @@ -33,7 +33,7 @@
"next-contentlayer": "0.3.4",
"next-mdx-remote": "^5.0.0",
"react": "18.3.1",
"react-aria": "3.34.3",
"react-aria": "3.35.0",
"react-aria-components": "1.4.0",
"react-dom": "18.3.1",
"react-toggle": "4.1.3",
Expand Down
16 changes: 8 additions & 8 deletions packages/components/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,17 +40,17 @@
"find-types": "tsx scripts/findImportedTypes.ts"
},
"peerDependencies": {
"@hopper-ui/styled-system": "^2.1.0",
"@hopper-ui/styled-system": "^2.4",
"react": "^18",
"react-aria": "^3.34",
"react-aria-components": "^1.2",
"react-aria": "^3.35",
"react-aria-components": "^1.4",
"react-dom": "^18"
},
"dependencies": {
"@hopper-ui/icons": "workspace:*",
"@react-aria/utils": "^3.25.2",
"@react-stately/utils": "^3.10.3",
"@react-types/shared": "^3.24.1",
"@react-aria/utils": "^3.25.3",
"@react-stately/utils": "^3.10.4",
"@react-types/shared": "^3.25.0",
"clsx": "^2.1.1"
},
"devDependencies": {
Expand All @@ -77,8 +77,8 @@
"jest-fail-on-console": "3.3.0",
"jest-fetch-mock": "3.0.3",
"react": "18.3.1",
"react-aria": "3.34.3",
"react-aria-components": "1.2.1",
"react-aria": "3.35.0",
"react-aria-components": "1.4.0",
"react-dom": "18.3.1",
"react-test-renderer": "18.3.1",
"ts-jest": "29.1.4",
Expand Down
4 changes: 2 additions & 2 deletions packages/components/src/ErrorMessage/src/ErrorMessage.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { WarningIcon } from "@hopper-ui/icons";
import { type StyledComponentProps, useStyledSystem } from "@hopper-ui/styled-system";
import { type ForwardedRef, forwardRef, useContext } from "react";
import { type CSSProperties, type ForwardedRef, forwardRef, useContext } from "react";
import {
type FieldErrorProps as RACFieldErrorProps,
FieldErrorContext as RACFieldErrorContext,
Expand Down Expand Up @@ -65,7 +65,7 @@ const ErrorMessageInner = forwardRef((props: ErrorMessageProps, ref: ForwardedRe
...stylingProps.style,
...prev
};
});
}) as CSSProperties;

const warningIcon = !hideIcon && <WarningIcon size="sm" className={styles["hop-ErrorMessage__icon"]} />;

Expand Down
4 changes: 3 additions & 1 deletion packages/components/src/Form/src/Form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import {
type FormProps as RACFormProps
} from "react-aria-components";

import { LinkButtonContext } from "../../buttons/index.ts";
import { cssModule, SlotProvider, type FieldSize, type NecessityIndicator } from "../../utils/index.ts";

import { FormContext } from "./FormContext.ts";
Expand Down Expand Up @@ -163,7 +164,8 @@ function Form(props: FormProps, ref: ForwardedRef<HTMLFormElement>) {
[TagGroupContext, {
necessityIndicator,
size
}]
}],
[LinkButtonContext, { isDisabled, size }]
]}
>
<RACForm
Expand Down
9 changes: 4 additions & 5 deletions packages/components/src/ListBox/src/ListBoxItem.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { CheckmarkIcon, IconContext, type IconSize } from "@hopper-ui/icons";
import { type ResponsiveProp, type StyledComponentProps, useResponsiveValue, useStyledSystem } from "@hopper-ui/styled-system";
import type { forwardRefType } from "@react-types/shared";
import clsx from "clsx";
import { type ForwardedRef, forwardRef, type NamedExoticComponent, type ReactNode, type TransitionEventHandler, useContext, useState } from "react";
import {
Expand Down Expand Up @@ -231,7 +232,7 @@ function ListBoxItemInner(props: ListBoxItemInnerProps) {
);
}

function ListBoxItem<T extends object>(props: ListBoxItemProps<T>, ref: ForwardedRef<HTMLDivElement>) {
function ListBoxItem<T extends object>(props: ListBoxItemProps<T>, ref: ForwardedRef<HTMLElement>) {
[props, ref] = useContextProps(props, ref, ListBoxItemContext);
const { stylingProps, ...ownProps } = useStyledSystem(props);
const {
Expand Down Expand Up @@ -276,7 +277,7 @@ function ListBoxItem<T extends object>(props: ListBoxItemProps<T>, ref: Forwarde
return (
<RACListBoxItem
{...otherProps}
ref={ref}
ref={ref as ForwardedRef<T>} /* Needed until this bug is fixed: https://github.com/adobe/react-spectrum/issues/6799 */
className={classNames}
style={style}
textValue={textValue}
Expand Down Expand Up @@ -318,9 +319,7 @@ function ListBoxItem<T extends object>(props: ListBoxItemProps<T>, ref: Forwarde
*
* [View Documentation](TODO)
*/
const _ListBoxItem = forwardRef(ListBoxItem) as <T>(
props: ListBoxItemProps<T> & { ref?: ForwardedRef<HTMLDivElement> }
) => ReturnType<typeof ListBoxItem>;
const _ListBoxItem = (forwardRef as forwardRefType)(ListBoxItem);
(_ListBoxItem as NamedExoticComponent).displayName = "ListBoxItem";

export { _ListBoxItem as ListBoxItem };
2 changes: 1 addition & 1 deletion packages/components/src/ListBox/src/ListBoxItemContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,6 @@ import type { ContextValue } from "react-aria-components";

import type { ListBoxItemProps } from "./ListBoxItem.tsx";

export const ListBoxItemContext = createContext<ContextValue<ListBoxItemProps<object>, HTMLDivElement>>({});
export const ListBoxItemContext = createContext<ContextValue<ListBoxItemProps<object>, HTMLElement>>({});

ListBoxItemContext.displayName = "ListBoxItemContext";
4 changes: 2 additions & 2 deletions packages/components/src/Select/src/SelectValue.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { IconContext } from "@hopper-ui/icons";
import { type ResponsiveProp, type StyledComponentProps, useResponsiveValue, useStyledSystem } from "@hopper-ui/styled-system";
import { filterDOMProps } from "@react-aria/utils";
import { type ForwardedRef, forwardRef, type NamedExoticComponent, useContext, useRef } from "react";
import { type CSSProperties, type ForwardedRef, forwardRef, type NamedExoticComponent, useContext, useRef } from "react";
import {
composeRenderProps,
DEFAULT_SLOT,
Expand Down Expand Up @@ -73,7 +73,7 @@ function SelectValue<T extends object>(props: SelectValueProps<T>, ref: Forwarde
...stylingProps.style,
...prev
};
});
}) as CSSProperties;

const renderProps = useRenderProps({
...otherProps,
Expand Down
Loading

0 comments on commit 17a1c5f

Please sign in to comment.