refactor(ui): migrate ButtonIcon and ButtonLink to useDefinition hook
Migrates ButtonIcon and ButtonLink to use the useDefinition hook with fully independent styling. Each component now has its own complete CSS, types, and definition without sharing internals with Button. - ButtonIcon: Add complete styles, use defineComponent with ButtonIconOwnProps, extend LeafSurfaceProps - ButtonLink: Add new CSS module with complete styles, use defineComponent with ButtonLinkOwnProps, extend LeafSurfaceProps - Update API report Signed-off-by: Johan Persson <johanopersson@gmail.com>
This commit is contained in:
@@ -0,0 +1,15 @@
|
||||
---
|
||||
'@backstage/ui': minor
|
||||
---
|
||||
|
||||
**BREAKING (CSS)**: Changed CSS selectors for `ButtonIcon` and `ButtonLink` components. Custom styles targeting `.bui-Button` to style these components must be updated to use `.bui-ButtonIcon` or `.bui-ButtonLink` respectively.
|
||||
|
||||
```diff
|
||||
-/* This no longer styles ButtonIcon or ButtonLink */
|
||||
-.bui-Button[data-variant="primary"] { ... }
|
||||
+/* Use component-specific selectors */
|
||||
+.bui-ButtonIcon[data-variant="primary"] { ... }
|
||||
+.bui-ButtonLink[data-variant="primary"] { ... }
|
||||
```
|
||||
|
||||
Affected components: ButtonIcon, ButtonLink
|
||||
+85
-35
@@ -3,7 +3,7 @@
|
||||
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
|
||||
|
||||
```ts
|
||||
import { ButtonProps as ButtonProps_2 } from 'react-aria-components';
|
||||
import type { ButtonProps as ButtonProps_2 } from 'react-aria-components';
|
||||
import { CellProps as CellProps_2 } from 'react-aria-components';
|
||||
import { CheckboxProps as CheckboxProps_2 } from 'react-aria-components';
|
||||
import { ColumnProps as ColumnProps_2 } from 'react-aria-components';
|
||||
@@ -22,7 +22,7 @@ import { ForwardRefExoticComponent } from 'react';
|
||||
import type { HeadingProps } from 'react-aria-components';
|
||||
import { HTMLAttributes } from 'react';
|
||||
import { JSX as JSX_2 } from 'react/jsx-runtime';
|
||||
import { LinkProps as LinkProps_2 } from 'react-aria-components';
|
||||
import type { LinkProps as LinkProps_2 } from 'react-aria-components';
|
||||
import type { ListBoxItemProps } from 'react-aria-components';
|
||||
import type { ListBoxProps } from 'react-aria-components';
|
||||
import type { MenuItemProps as MenuItemProps_2 } from 'react-aria-components';
|
||||
@@ -33,7 +33,7 @@ import type { ModalOverlayProps } from 'react-aria-components';
|
||||
import { PopoverProps as PopoverProps_2 } from 'react-aria-components';
|
||||
import type { RadioGroupProps as RadioGroupProps_2 } from 'react-aria-components';
|
||||
import type { RadioProps as RadioProps_2 } from 'react-aria-components';
|
||||
import { ReactElement } from 'react';
|
||||
import type { ReactElement } from 'react';
|
||||
import { ReactNode } from 'react';
|
||||
import { RefAttributes } from 'react';
|
||||
import { RowProps } from 'react-aria-components';
|
||||
@@ -290,28 +290,48 @@ export const ButtonIcon: ForwardRefExoticComponent<
|
||||
|
||||
// @public
|
||||
export const ButtonIconDefinition: {
|
||||
readonly styles: {
|
||||
readonly [key: string]: string;
|
||||
};
|
||||
readonly classNames: {
|
||||
readonly root: 'bui-ButtonIcon';
|
||||
readonly content: 'bui-ButtonIconContent';
|
||||
readonly spinner: 'bui-ButtonIconSpinner';
|
||||
};
|
||||
readonly surface: 'leaf';
|
||||
readonly propDefs: {
|
||||
readonly size: {
|
||||
readonly dataAttribute: true;
|
||||
readonly default: 'small';
|
||||
};
|
||||
readonly variant: {
|
||||
readonly dataAttribute: true;
|
||||
readonly default: 'primary';
|
||||
};
|
||||
readonly loading: {
|
||||
readonly dataAttribute: true;
|
||||
};
|
||||
readonly icon: {};
|
||||
readonly onSurface: {};
|
||||
readonly className: {};
|
||||
readonly style: {};
|
||||
};
|
||||
};
|
||||
|
||||
// @public (undocumented)
|
||||
export type ButtonIconOwnProps = LeafSurfaceProps & {
|
||||
size?: Responsive<'small' | 'medium'>;
|
||||
variant?: Responsive<'primary' | 'secondary' | 'tertiary'>;
|
||||
icon?: ReactElement;
|
||||
loading?: boolean;
|
||||
className?: string;
|
||||
style?: CSSProperties;
|
||||
};
|
||||
|
||||
// @public
|
||||
export interface ButtonIconProps extends ButtonProps_2 {
|
||||
// (undocumented)
|
||||
icon?: ReactElement;
|
||||
// (undocumented)
|
||||
loading?: boolean;
|
||||
// (undocumented)
|
||||
size?: 'small' | 'medium' | Partial<Record<Breakpoint, 'small' | 'medium'>>;
|
||||
// (undocumented)
|
||||
variant?:
|
||||
| 'primary'
|
||||
| 'secondary'
|
||||
| 'tertiary'
|
||||
| Partial<Record<Breakpoint, 'primary' | 'secondary' | 'tertiary'>>;
|
||||
}
|
||||
export interface ButtonIconProps
|
||||
extends Omit<ButtonProps_2, 'children' | 'className' | 'style'>,
|
||||
ButtonIconOwnProps {}
|
||||
|
||||
// @public (undocumented)
|
||||
export const ButtonLink: ForwardRefExoticComponent<
|
||||
@@ -320,37 +340,55 @@ export const ButtonLink: ForwardRefExoticComponent<
|
||||
|
||||
// @public
|
||||
export const ButtonLinkDefinition: {
|
||||
readonly styles: {
|
||||
readonly [key: string]: string;
|
||||
};
|
||||
readonly classNames: {
|
||||
readonly root: 'bui-ButtonLink';
|
||||
readonly content: 'bui-ButtonLinkContent';
|
||||
};
|
||||
readonly surface: 'leaf';
|
||||
readonly propDefs: {
|
||||
readonly size: {
|
||||
readonly dataAttribute: true;
|
||||
readonly default: 'small';
|
||||
};
|
||||
readonly variant: {
|
||||
readonly dataAttribute: true;
|
||||
readonly default: 'primary';
|
||||
};
|
||||
readonly iconStart: {};
|
||||
readonly iconEnd: {};
|
||||
readonly onSurface: {};
|
||||
readonly children: {};
|
||||
readonly className: {};
|
||||
readonly style: {};
|
||||
};
|
||||
};
|
||||
|
||||
// @public
|
||||
export interface ButtonLinkProps extends LinkProps_2 {
|
||||
// (undocumented)
|
||||
children?: ReactNode;
|
||||
// (undocumented)
|
||||
iconEnd?: ReactElement;
|
||||
// (undocumented)
|
||||
// @public (undocumented)
|
||||
export type ButtonLinkOwnProps = LeafSurfaceProps & {
|
||||
size?: Responsive<'small' | 'medium'>;
|
||||
variant?: Responsive<'primary' | 'secondary' | 'tertiary'>;
|
||||
iconStart?: ReactElement;
|
||||
// (undocumented)
|
||||
size?: 'small' | 'medium' | Partial<Record<Breakpoint, 'small' | 'medium'>>;
|
||||
// (undocumented)
|
||||
variant?:
|
||||
| 'primary'
|
||||
| 'secondary'
|
||||
| 'tertiary'
|
||||
| Partial<Record<Breakpoint, 'primary' | 'secondary' | 'tertiary'>>;
|
||||
}
|
||||
iconEnd?: ReactElement;
|
||||
children?: ReactNode;
|
||||
className?: string;
|
||||
style?: CSSProperties;
|
||||
};
|
||||
|
||||
// @public
|
||||
export type ButtonOwnProps = {
|
||||
export interface ButtonLinkProps
|
||||
extends Omit<LinkProps_2, 'children' | 'className' | 'style'>,
|
||||
ButtonLinkOwnProps {}
|
||||
|
||||
// @public (undocumented)
|
||||
export type ButtonOwnProps = LeafSurfaceProps & {
|
||||
size?: Responsive<'small' | 'medium' | 'large'>;
|
||||
variant?: Responsive<'primary' | 'secondary' | 'tertiary'>;
|
||||
iconStart?: ReactElement;
|
||||
iconEnd?: ReactElement;
|
||||
loading?: boolean;
|
||||
onSurface?: Responsive<Surface>;
|
||||
children?: ReactNode;
|
||||
className?: string;
|
||||
style?: CSSProperties;
|
||||
@@ -583,6 +621,12 @@ export interface ContainerProps {
|
||||
style?: React.CSSProperties;
|
||||
}
|
||||
|
||||
// @public (undocumented)
|
||||
export interface ContainerSurfaceProps {
|
||||
// (undocumented)
|
||||
surface?: Responsive<Surface>;
|
||||
}
|
||||
|
||||
// @public (undocumented)
|
||||
export interface CursorParams<TFilter> {
|
||||
// (undocumented)
|
||||
@@ -981,6 +1025,12 @@ export type JustifyContent =
|
||||
| 'around'
|
||||
| 'between';
|
||||
|
||||
// @public (undocumented)
|
||||
export interface LeafSurfaceProps {
|
||||
// (undocumented)
|
||||
onSurface?: Responsive<Surface>;
|
||||
}
|
||||
|
||||
// @public (undocumented)
|
||||
export const Link: ForwardRefExoticComponent<
|
||||
LinkProps & RefAttributes<HTMLAnchorElement>
|
||||
|
||||
@@ -18,17 +18,205 @@
|
||||
|
||||
@layer components {
|
||||
.bui-ButtonIcon {
|
||||
--loading-duration: 200ms;
|
||||
--bg: transparent;
|
||||
--bg-hover: transparent;
|
||||
--bg-active: transparent;
|
||||
--fg: inherit;
|
||||
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: none;
|
||||
user-select: none;
|
||||
padding: 0;
|
||||
cursor: pointer;
|
||||
border-radius: var(--bui-radius-2);
|
||||
flex-shrink: 0;
|
||||
transition: background-color var(--loading-duration) ease-out,
|
||||
box-shadow var(--loading-duration) ease-out;
|
||||
|
||||
/* Apply variables */
|
||||
color: var(--fg);
|
||||
background-color: var(--bg);
|
||||
|
||||
&:hover {
|
||||
background-color: var(--bg-hover);
|
||||
transition: background-color 150ms ease;
|
||||
}
|
||||
|
||||
&:active {
|
||||
background-color: var(--bg-active);
|
||||
}
|
||||
|
||||
&[data-disabled='true'] {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
&[data-loading='true'] {
|
||||
cursor: wait;
|
||||
}
|
||||
}
|
||||
|
||||
.bui-ButtonIcon[data-variant='primary'] {
|
||||
--bg: var(--bui-bg-solid);
|
||||
--bg-hover: var(--bui-bg-solid-hover);
|
||||
--bg-active: var(--bui-bg-solid-pressed);
|
||||
--fg: var(--bui-fg-solid);
|
||||
|
||||
&[data-disabled='true'],
|
||||
&[data-loading='true'] {
|
||||
--bg: var(--bui-bg-solid-disabled);
|
||||
--bg-hover: var(--bui-bg-solid-disabled);
|
||||
--bg-active: var(--bui-bg-solid-disabled);
|
||||
--fg: var(--bui-fg-solid-disabled);
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
outline: 2px solid var(--bui-ring);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
.bui-ButtonIcon[data-variant='secondary'] {
|
||||
--bg: var(--bui-bg-neutral-on-surface-0);
|
||||
--bg-hover: var(--bui-bg-neutral-on-surface-0-hover);
|
||||
--bg-active: var(--bui-bg-neutral-on-surface-0-pressed);
|
||||
--fg: var(--bui-fg-primary);
|
||||
|
||||
&[data-on-surface='1'] {
|
||||
--bg: var(--bui-bg-neutral-on-surface-1);
|
||||
--bg-hover: var(--bui-bg-neutral-on-surface-1-hover);
|
||||
--bg-active: var(--bui-bg-neutral-on-surface-1-pressed);
|
||||
}
|
||||
|
||||
&[data-on-surface='2'] {
|
||||
--bg: var(--bui-bg-neutral-on-surface-2);
|
||||
--bg-hover: var(--bui-bg-neutral-on-surface-2-hover);
|
||||
--bg-active: var(--bui-bg-neutral-on-surface-2-pressed);
|
||||
}
|
||||
|
||||
&[data-on-surface='3'] {
|
||||
--bg: var(--bui-bg-neutral-on-surface-3);
|
||||
--bg-hover: var(--bui-bg-neutral-on-surface-3-hover);
|
||||
--bg-active: var(--bui-bg-neutral-on-surface-3-pressed);
|
||||
}
|
||||
|
||||
&[data-disabled='true'],
|
||||
&[data-loading='true'] {
|
||||
--bg-hover: var(--bg);
|
||||
--bg-active: var(--bg);
|
||||
--fg: var(--bui-fg-disabled);
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
outline: none;
|
||||
transition: none;
|
||||
box-shadow: inset 0 0 0 2px var(--bui-ring);
|
||||
}
|
||||
}
|
||||
|
||||
.bui-ButtonIcon[data-variant='tertiary'] {
|
||||
--bg-hover: var(--bui-bg-neutral-on-surface-0-hover);
|
||||
--bg-active: var(--bui-bg-neutral-on-surface-0-pressed);
|
||||
--fg: var(--bui-fg-primary);
|
||||
|
||||
&[data-on-surface='1'] {
|
||||
--bg-hover: var(--bui-bg-neutral-on-surface-1-hover);
|
||||
--bg-active: var(--bui-bg-neutral-on-surface-1-pressed);
|
||||
}
|
||||
|
||||
&[data-on-surface='2'] {
|
||||
--bg-hover: var(--bui-bg-neutral-on-surface-2-hover);
|
||||
--bg-active: var(--bui-bg-neutral-on-surface-2-pressed);
|
||||
}
|
||||
|
||||
&[data-on-surface='3'] {
|
||||
--bg-hover: var(--bui-bg-neutral-on-surface-3-hover);
|
||||
--bg-active: var(--bui-bg-neutral-on-surface-3-pressed);
|
||||
}
|
||||
|
||||
&[data-disabled='true'],
|
||||
&[data-loading='true'] {
|
||||
--bg-hover: var(--bg);
|
||||
--bg-active: var(--bg);
|
||||
--fg: var(--bui-fg-disabled);
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
outline: none;
|
||||
transition: none;
|
||||
box-shadow: inset 0 0 0 2px var(--bui-ring);
|
||||
}
|
||||
}
|
||||
|
||||
.bui-ButtonIcon[data-size='small'] {
|
||||
padding: 0;
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
|
||||
svg {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
.bui-ButtonIcon[data-size='medium'] {
|
||||
padding: 0;
|
||||
width: 2.5rem;
|
||||
height: 2.5rem;
|
||||
|
||||
svg {
|
||||
width: 1.25rem;
|
||||
height: 1.25rem;
|
||||
}
|
||||
}
|
||||
|
||||
.bui-ButtonIconContent {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
transition: opacity var(--loading-duration) ease-out;
|
||||
|
||||
.bui-ButtonIcon[data-loading='true'] & {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.bui-ButtonIconSpinner {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
display: flex;
|
||||
opacity: 0;
|
||||
transition: opacity var(--loading-duration) ease-in;
|
||||
|
||||
.bui-ButtonIcon[data-loading='true'] & {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
& svg {
|
||||
animation: bui-spin 1s linear infinite;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.bui-ButtonIcon {
|
||||
transition-duration: 50ms;
|
||||
}
|
||||
|
||||
.bui-ButtonIconContent {
|
||||
transition-duration: 50ms;
|
||||
}
|
||||
|
||||
.bui-ButtonIconSpinner {
|
||||
transition-duration: 50ms;
|
||||
}
|
||||
|
||||
.bui-ButtonIconSpinner svg {
|
||||
animation: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,71 +14,39 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import clsx from 'clsx';
|
||||
import { forwardRef, Ref } from 'react';
|
||||
import { Button as RAButton, ProgressBar } from 'react-aria-components';
|
||||
import { RiLoader4Line } from '@remixicon/react';
|
||||
import type { ButtonIconProps } from './types';
|
||||
import { useStyles } from '../../hooks/useStyles';
|
||||
import { ButtonDefinition } from '../Button/definition';
|
||||
import { useDefinition } from '../../hooks/useDefinition';
|
||||
import { ButtonIconDefinition } from './definition';
|
||||
import stylesButtonIcon from './ButtonIcon.module.css';
|
||||
import stylesButton from '../Button/Button.module.css';
|
||||
|
||||
/** @public */
|
||||
export const ButtonIcon = forwardRef(
|
||||
(props: ButtonIconProps, ref: Ref<HTMLButtonElement>) => {
|
||||
const { classNames, dataAttributes, cleanedProps } = useStyles(
|
||||
ButtonDefinition,
|
||||
{
|
||||
size: 'small',
|
||||
variant: 'primary',
|
||||
...props,
|
||||
},
|
||||
const { ownProps, restProps, dataAttributes } = useDefinition(
|
||||
ButtonIconDefinition,
|
||||
props,
|
||||
);
|
||||
|
||||
const { classNames: classNamesButtonIcon } =
|
||||
useStyles(ButtonIconDefinition);
|
||||
|
||||
const { className, icon, loading, ...rest } = cleanedProps;
|
||||
const { classes, icon, loading } = ownProps;
|
||||
|
||||
return (
|
||||
<RAButton
|
||||
className={clsx(
|
||||
classNames.root,
|
||||
classNamesButtonIcon.root,
|
||||
stylesButton[classNames.root],
|
||||
stylesButtonIcon[classNamesButtonIcon.root],
|
||||
className,
|
||||
)}
|
||||
className={classes.root}
|
||||
ref={ref}
|
||||
isPending={loading}
|
||||
{...dataAttributes}
|
||||
{...rest}
|
||||
{...restProps}
|
||||
>
|
||||
{({ isPending }) => (
|
||||
<>
|
||||
<span
|
||||
className={clsx(
|
||||
classNames.content,
|
||||
classNamesButtonIcon.content,
|
||||
stylesButton[classNames.content],
|
||||
stylesButtonIcon[classNamesButtonIcon.content],
|
||||
)}
|
||||
>
|
||||
{icon}
|
||||
</span>
|
||||
<span className={classes.content}>{icon}</span>
|
||||
|
||||
{isPending && (
|
||||
<ProgressBar
|
||||
aria-label="Loading"
|
||||
isIndeterminate
|
||||
className={clsx(
|
||||
classNames.spinner,
|
||||
classNamesButtonIcon.spinner,
|
||||
stylesButton[classNames.spinner],
|
||||
stylesButtonIcon[classNamesButtonIcon.spinner],
|
||||
)}
|
||||
className={classes.spinner}
|
||||
>
|
||||
<RiLoader4Line aria-hidden="true" />
|
||||
</ProgressBar>
|
||||
|
||||
@@ -14,16 +14,29 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type { ComponentDefinition } from '../../types';
|
||||
import { defineComponent } from '../../hooks/useDefinition';
|
||||
import type { ButtonIconOwnProps } from './types';
|
||||
import styles from './ButtonIcon.module.css';
|
||||
|
||||
/**
|
||||
* Component definition for ButtonIcon
|
||||
* @public
|
||||
*/
|
||||
export const ButtonIconDefinition = {
|
||||
export const ButtonIconDefinition = defineComponent<ButtonIconOwnProps>()({
|
||||
styles,
|
||||
classNames: {
|
||||
root: 'bui-ButtonIcon',
|
||||
content: 'bui-ButtonIconContent',
|
||||
spinner: 'bui-ButtonIconSpinner',
|
||||
},
|
||||
} as const satisfies ComponentDefinition;
|
||||
surface: 'leaf',
|
||||
propDefs: {
|
||||
size: { dataAttribute: true, default: 'small' },
|
||||
variant: { dataAttribute: true, default: 'primary' },
|
||||
loading: { dataAttribute: true },
|
||||
icon: {},
|
||||
onSurface: {},
|
||||
className: {},
|
||||
style: {},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -14,22 +14,25 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Breakpoint } from '../..';
|
||||
import { ReactElement } from 'react';
|
||||
import { ButtonProps as RAButtonProps } from 'react-aria-components';
|
||||
import type { ReactElement, CSSProperties } from 'react';
|
||||
import type { ButtonProps as RAButtonProps } from 'react-aria-components';
|
||||
import type { LeafSurfaceProps, Responsive } from '../../types';
|
||||
|
||||
/** @public */
|
||||
export type ButtonIconOwnProps = LeafSurfaceProps & {
|
||||
size?: Responsive<'small' | 'medium'>;
|
||||
variant?: Responsive<'primary' | 'secondary' | 'tertiary'>;
|
||||
icon?: ReactElement;
|
||||
loading?: boolean;
|
||||
className?: string;
|
||||
style?: CSSProperties;
|
||||
};
|
||||
|
||||
/**
|
||||
* Properties for {@link ButtonIcon}
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export interface ButtonIconProps extends RAButtonProps {
|
||||
size?: 'small' | 'medium' | Partial<Record<Breakpoint, 'small' | 'medium'>>;
|
||||
variant?:
|
||||
| 'primary'
|
||||
| 'secondary'
|
||||
| 'tertiary'
|
||||
| Partial<Record<Breakpoint, 'primary' | 'secondary' | 'tertiary'>>;
|
||||
icon?: ReactElement;
|
||||
loading?: boolean;
|
||||
}
|
||||
export interface ButtonIconProps
|
||||
extends Omit<RAButtonProps, 'children' | 'className' | 'style'>,
|
||||
ButtonIconOwnProps {}
|
||||
|
||||
@@ -0,0 +1,182 @@
|
||||
/*
|
||||
* Copyright 2026 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
@layer tokens, base, components, utilities;
|
||||
|
||||
@layer components {
|
||||
.bui-ButtonLink {
|
||||
--bg: transparent;
|
||||
--bg-hover: transparent;
|
||||
--bg-active: transparent;
|
||||
--fg: inherit;
|
||||
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
border: none;
|
||||
user-select: none;
|
||||
font-family: var(--bui-font-regular);
|
||||
font-weight: var(--bui-font-weight-bold);
|
||||
padding: 0;
|
||||
cursor: pointer;
|
||||
border-radius: var(--bui-radius-2);
|
||||
flex-shrink: 0;
|
||||
text-decoration: none;
|
||||
transition: background-color 200ms ease-out, box-shadow 200ms ease-out;
|
||||
|
||||
/* Apply variables */
|
||||
color: var(--fg);
|
||||
background-color: var(--bg);
|
||||
|
||||
&:hover {
|
||||
background-color: var(--bg-hover);
|
||||
transition: background-color 150ms ease;
|
||||
}
|
||||
|
||||
&:active {
|
||||
background-color: var(--bg-active);
|
||||
}
|
||||
|
||||
&[data-disabled='true'] {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
|
||||
.bui-ButtonLink[data-variant='primary'] {
|
||||
--bg: var(--bui-bg-solid);
|
||||
--bg-hover: var(--bui-bg-solid-hover);
|
||||
--bg-active: var(--bui-bg-solid-pressed);
|
||||
--fg: var(--bui-fg-solid);
|
||||
|
||||
&[data-disabled='true'] {
|
||||
--bg: var(--bui-bg-solid-disabled);
|
||||
--bg-hover: var(--bui-bg-solid-disabled);
|
||||
--bg-active: var(--bui-bg-solid-disabled);
|
||||
--fg: var(--bui-fg-solid-disabled);
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
outline: 2px solid var(--bui-ring);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
.bui-ButtonLink[data-variant='secondary'] {
|
||||
--bg: var(--bui-bg-neutral-on-surface-0);
|
||||
--bg-hover: var(--bui-bg-neutral-on-surface-0-hover);
|
||||
--bg-active: var(--bui-bg-neutral-on-surface-0-pressed);
|
||||
--fg: var(--bui-fg-primary);
|
||||
|
||||
&[data-on-surface='1'] {
|
||||
--bg: var(--bui-bg-neutral-on-surface-1);
|
||||
--bg-hover: var(--bui-bg-neutral-on-surface-1-hover);
|
||||
--bg-active: var(--bui-bg-neutral-on-surface-1-pressed);
|
||||
}
|
||||
|
||||
&[data-on-surface='2'] {
|
||||
--bg: var(--bui-bg-neutral-on-surface-2);
|
||||
--bg-hover: var(--bui-bg-neutral-on-surface-2-hover);
|
||||
--bg-active: var(--bui-bg-neutral-on-surface-2-pressed);
|
||||
}
|
||||
|
||||
&[data-on-surface='3'] {
|
||||
--bg: var(--bui-bg-neutral-on-surface-3);
|
||||
--bg-hover: var(--bui-bg-neutral-on-surface-3-hover);
|
||||
--bg-active: var(--bui-bg-neutral-on-surface-3-pressed);
|
||||
}
|
||||
|
||||
&[data-disabled='true'] {
|
||||
--bg-hover: var(--bg);
|
||||
--bg-active: var(--bg);
|
||||
--fg: var(--bui-fg-disabled);
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
outline: none;
|
||||
transition: none;
|
||||
box-shadow: inset 0 0 0 2px var(--bui-ring);
|
||||
}
|
||||
}
|
||||
|
||||
.bui-ButtonLink[data-variant='tertiary'] {
|
||||
--bg-hover: var(--bui-bg-neutral-on-surface-0-hover);
|
||||
--bg-active: var(--bui-bg-neutral-on-surface-0-pressed);
|
||||
--fg: var(--bui-fg-primary);
|
||||
|
||||
&[data-on-surface='1'] {
|
||||
--bg-hover: var(--bui-bg-neutral-on-surface-1-hover);
|
||||
--bg-active: var(--bui-bg-neutral-on-surface-1-pressed);
|
||||
}
|
||||
|
||||
&[data-on-surface='2'] {
|
||||
--bg-hover: var(--bui-bg-neutral-on-surface-2-hover);
|
||||
--bg-active: var(--bui-bg-neutral-on-surface-2-pressed);
|
||||
}
|
||||
|
||||
&[data-on-surface='3'] {
|
||||
--bg-hover: var(--bui-bg-neutral-on-surface-3-hover);
|
||||
--bg-active: var(--bui-bg-neutral-on-surface-3-pressed);
|
||||
}
|
||||
|
||||
&[data-disabled='true'] {
|
||||
--bg-hover: var(--bg);
|
||||
--bg-active: var(--bg);
|
||||
--fg: var(--bui-fg-disabled);
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
outline: none;
|
||||
transition: none;
|
||||
box-shadow: inset 0 0 0 2px var(--bui-ring);
|
||||
}
|
||||
}
|
||||
|
||||
.bui-ButtonLink[data-size='small'] {
|
||||
font-size: var(--bui-font-size-3);
|
||||
padding: 0 var(--bui-space-2);
|
||||
height: 2rem;
|
||||
|
||||
svg {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
.bui-ButtonLink[data-size='medium'] {
|
||||
font-size: var(--bui-font-size-4);
|
||||
padding: 0 var(--bui-space-3);
|
||||
height: 2.5rem;
|
||||
|
||||
svg {
|
||||
width: 1.25rem;
|
||||
height: 1.25rem;
|
||||
}
|
||||
}
|
||||
|
||||
.bui-ButtonLinkContent {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: var(--bui-space-1_5);
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.bui-ButtonLink {
|
||||
transition-duration: 50ms;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -14,55 +14,35 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import clsx from 'clsx';
|
||||
import { forwardRef, Ref } from 'react';
|
||||
import { Link as RALink, RouterProvider } from 'react-aria-components';
|
||||
import { useNavigate, useHref } from 'react-router-dom';
|
||||
import type { ButtonLinkProps } from './types';
|
||||
import { useStyles } from '../../hooks/useStyles';
|
||||
import { ButtonDefinition } from '../Button/definition';
|
||||
import { useDefinition } from '../../hooks/useDefinition';
|
||||
import { ButtonLinkDefinition } from './definition';
|
||||
import { isExternalLink } from '../../utils/isExternalLink';
|
||||
import stylesButton from '../Button/Button.module.css';
|
||||
|
||||
/** @public */
|
||||
export const ButtonLink = forwardRef(
|
||||
(props: ButtonLinkProps, ref: Ref<HTMLAnchorElement>) => {
|
||||
const navigate = useNavigate();
|
||||
|
||||
const { classNames, dataAttributes, cleanedProps } = useStyles(
|
||||
ButtonDefinition,
|
||||
{
|
||||
size: 'small',
|
||||
variant: 'primary',
|
||||
...props,
|
||||
},
|
||||
const { ownProps, restProps, dataAttributes } = useDefinition(
|
||||
ButtonLinkDefinition,
|
||||
props,
|
||||
);
|
||||
const { classes, iconStart, iconEnd, children } = ownProps;
|
||||
|
||||
const { classNames: classNamesButtonLink } =
|
||||
useStyles(ButtonLinkDefinition);
|
||||
|
||||
const { children, className, iconStart, iconEnd, href, ...rest } =
|
||||
cleanedProps;
|
||||
|
||||
const isExternal = isExternalLink(href);
|
||||
const isExternal = isExternalLink(restProps.href);
|
||||
|
||||
const linkButton = (
|
||||
<RALink
|
||||
className={clsx(
|
||||
classNames.root,
|
||||
classNamesButtonLink.root,
|
||||
stylesButton[classNames.root],
|
||||
className,
|
||||
)}
|
||||
className={classes.root}
|
||||
ref={ref}
|
||||
{...dataAttributes}
|
||||
href={href}
|
||||
{...rest}
|
||||
{...restProps}
|
||||
>
|
||||
<span
|
||||
className={clsx(classNames.content, stylesButton[classNames.content])}
|
||||
>
|
||||
<span className={classes.content}>
|
||||
{iconStart}
|
||||
{children}
|
||||
{iconEnd}
|
||||
|
||||
@@ -14,14 +14,29 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type { ComponentDefinition } from '../../types';
|
||||
import { defineComponent } from '../../hooks/useDefinition';
|
||||
import type { ButtonLinkOwnProps } from './types';
|
||||
import styles from './ButtonLink.module.css';
|
||||
|
||||
/**
|
||||
* Component definition for ButtonLink
|
||||
* @public
|
||||
*/
|
||||
export const ButtonLinkDefinition = {
|
||||
export const ButtonLinkDefinition = defineComponent<ButtonLinkOwnProps>()({
|
||||
styles,
|
||||
classNames: {
|
||||
root: 'bui-ButtonLink',
|
||||
content: 'bui-ButtonLinkContent',
|
||||
},
|
||||
} as const satisfies ComponentDefinition;
|
||||
surface: 'leaf',
|
||||
propDefs: {
|
||||
size: { dataAttribute: true, default: 'small' },
|
||||
variant: { dataAttribute: true, default: 'primary' },
|
||||
iconStart: {},
|
||||
iconEnd: {},
|
||||
onSurface: {},
|
||||
children: {},
|
||||
className: {},
|
||||
style: {},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -14,23 +14,26 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Breakpoint } from '../..';
|
||||
import { ReactElement, ReactNode } from 'react';
|
||||
import { LinkProps as RALinkProps } from 'react-aria-components';
|
||||
import type { ReactElement, ReactNode, CSSProperties } from 'react';
|
||||
import type { LinkProps as RALinkProps } from 'react-aria-components';
|
||||
import type { LeafSurfaceProps, Responsive } from '../../types';
|
||||
|
||||
/** @public */
|
||||
export type ButtonLinkOwnProps = LeafSurfaceProps & {
|
||||
size?: Responsive<'small' | 'medium'>;
|
||||
variant?: Responsive<'primary' | 'secondary' | 'tertiary'>;
|
||||
iconStart?: ReactElement;
|
||||
iconEnd?: ReactElement;
|
||||
children?: ReactNode;
|
||||
className?: string;
|
||||
style?: CSSProperties;
|
||||
};
|
||||
|
||||
/**
|
||||
* Properties for {@link ButtonLink}
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export interface ButtonLinkProps extends RALinkProps {
|
||||
size?: 'small' | 'medium' | Partial<Record<Breakpoint, 'small' | 'medium'>>;
|
||||
variant?:
|
||||
| 'primary'
|
||||
| 'secondary'
|
||||
| 'tertiary'
|
||||
| Partial<Record<Breakpoint, 'primary' | 'secondary' | 'tertiary'>>;
|
||||
iconStart?: ReactElement;
|
||||
iconEnd?: ReactElement;
|
||||
children?: ReactNode;
|
||||
}
|
||||
export interface ButtonLinkProps
|
||||
extends Omit<RALinkProps, 'children' | 'className' | 'style'>,
|
||||
ButtonLinkOwnProps {}
|
||||
|
||||
Reference in New Issue
Block a user