Improve support for button links
Signed-off-by: Charles de Dreuille <charles.dedreuille@gmail.com>
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/canon': minor
|
||||
---
|
||||
|
||||
**Breaking** We are removing the render prop on the Button component. If you want to use Button as a link, simply add the href prop and we will automaticlly use it as an anchor tag underneath.
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
buttonFullWidthSnippet,
|
||||
buttonDisabledSnippet,
|
||||
buttonResponsiveSnippet,
|
||||
buttonAsLinkSnippet,
|
||||
} from './button.props';
|
||||
import { ComponentInfos } from '@/components/ComponentInfos';
|
||||
|
||||
@@ -102,3 +103,15 @@ Here's a view when buttons are disabled.
|
||||
Here's a view when buttons are responsive.
|
||||
|
||||
<CodeBlock code={buttonResponsiveSnippet} />
|
||||
|
||||
### As Link
|
||||
|
||||
Use the `href` prop to make a button act as a link.
|
||||
|
||||
<Snippet
|
||||
align="center"
|
||||
py={4}
|
||||
open
|
||||
preview={<ButtonSnippet story="AsLink" />}
|
||||
code={buttonAsLinkSnippet}
|
||||
/>
|
||||
|
||||
@@ -61,3 +61,7 @@ export const buttonDisabledSnippet = `<Flex gap="4">
|
||||
export const buttonResponsiveSnippet = `<Button variant={{ initial: 'primary', lg: 'secondary' }}>
|
||||
Responsive Button
|
||||
</Button>`;
|
||||
|
||||
export const buttonAsLinkSnippet = `<Button href="https://canon.backstage.io" target="_blank">
|
||||
I am a link
|
||||
</Button>`;
|
||||
|
||||
@@ -3,8 +3,10 @@
|
||||
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
|
||||
|
||||
```ts
|
||||
import { AnchorHTMLAttributes } from 'react';
|
||||
import { Avatar as Avatar_2 } from '@base-ui-components/react/avatar';
|
||||
import { Breakpoint as Breakpoint_2 } from '@backstage/canon';
|
||||
import { ButtonHTMLAttributes } from 'react';
|
||||
import { ChangeEvent } from 'react';
|
||||
import { Collapsible as Collapsible_2 } from '@base-ui-components/react/collapsible';
|
||||
import { ComponentProps } from 'react';
|
||||
@@ -152,37 +154,36 @@ export const breakpoints: Breakpoint[];
|
||||
|
||||
// @public (undocumented)
|
||||
export const Button: ForwardRefExoticComponent<
|
||||
Omit<ButtonProps, 'ref'> & RefAttributes<HTMLButtonElement>
|
||||
ButtonProps & RefAttributes<HTMLElement>
|
||||
>;
|
||||
|
||||
// @public (undocumented)
|
||||
export type ButtonOwnProps = GetPropDefTypes<typeof buttonPropDefs>;
|
||||
export type ButtonAnchorProps = ButtonCommonProps &
|
||||
Omit<AnchorHTMLAttributes<HTMLAnchorElement>, 'type' | 'onClick'> & {
|
||||
href: string;
|
||||
onClick?: React.MouseEventHandler<HTMLAnchorElement>;
|
||||
};
|
||||
|
||||
// @public (undocumented)
|
||||
export const buttonPropDefs: {
|
||||
variant: {
|
||||
type: 'enum';
|
||||
values: ('primary' | 'secondary')[];
|
||||
className: string;
|
||||
default: 'primary';
|
||||
responsive: true;
|
||||
};
|
||||
size: {
|
||||
type: 'enum';
|
||||
values: ('small' | 'medium')[];
|
||||
className: string;
|
||||
default: 'medium';
|
||||
responsive: true;
|
||||
};
|
||||
export type ButtonCommonProps = {
|
||||
size?: 'small' | 'medium';
|
||||
variant?: 'primary' | 'secondary';
|
||||
iconStart?: ReactElement;
|
||||
iconEnd?: ReactElement;
|
||||
className?: string;
|
||||
style?: React.CSSProperties;
|
||||
children?: React.ReactNode;
|
||||
};
|
||||
|
||||
// @public (undocumented)
|
||||
export type ButtonNativeProps = ButtonCommonProps &
|
||||
Omit<ButtonHTMLAttributes<HTMLButtonElement>, 'href'> & {
|
||||
href?: undefined;
|
||||
onClick?: React.MouseEventHandler<HTMLButtonElement>;
|
||||
};
|
||||
|
||||
// @public
|
||||
export interface ButtonProps extends useRender.ComponentProps<'button'> {
|
||||
iconEnd?: ReactElement;
|
||||
iconStart?: ReactElement;
|
||||
size?: ButtonOwnProps['size'];
|
||||
variant?: ButtonOwnProps['variant'];
|
||||
}
|
||||
export type ButtonProps = ButtonAnchorProps | ButtonNativeProps;
|
||||
|
||||
// @public (undocumented)
|
||||
export const Checkbox: ForwardRefExoticComponent<
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
/*
|
||||
* Copyright 2025 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.
|
||||
*/
|
||||
|
||||
import type { PropDef, GetPropDefTypes } from '../../props/prop-def';
|
||||
|
||||
/** @public */
|
||||
export const buttonPropDefs = {
|
||||
variant: {
|
||||
type: 'enum',
|
||||
values: ['primary', 'secondary'],
|
||||
className: 'canon-Button--variant',
|
||||
default: 'primary',
|
||||
responsive: true,
|
||||
},
|
||||
size: {
|
||||
type: 'enum',
|
||||
values: ['small', 'medium'],
|
||||
className: 'canon-Button--size',
|
||||
default: 'medium',
|
||||
responsive: true,
|
||||
},
|
||||
} satisfies {
|
||||
variant: PropDef<'primary' | 'secondary'>;
|
||||
size: PropDef<'small' | 'medium'>;
|
||||
};
|
||||
|
||||
/** @public */
|
||||
export type ButtonOwnProps = GetPropDefTypes<typeof buttonPropDefs>;
|
||||
@@ -134,13 +134,9 @@ export const Disabled: Story = {
|
||||
export const AsLink: Story = {
|
||||
args: {
|
||||
children: 'I am a link',
|
||||
href: 'https://canon.backstage.io',
|
||||
target: '_blank',
|
||||
},
|
||||
render: args => (
|
||||
<Button
|
||||
{...args}
|
||||
render={<a href="https://canon.backstage.io" target="_blank" />}
|
||||
/>
|
||||
),
|
||||
};
|
||||
|
||||
export const Responsive: Story = {
|
||||
|
||||
@@ -14,69 +14,91 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { forwardRef, useRef } from 'react';
|
||||
import { AnchorHTMLAttributes, ButtonHTMLAttributes, forwardRef } from 'react';
|
||||
import clsx from 'clsx';
|
||||
import { useResponsiveValue } from '../../hooks/useResponsiveValue';
|
||||
import { useRender } from '@base-ui-components/react/use-render';
|
||||
|
||||
import type { ButtonProps } from './types';
|
||||
|
||||
/** @public */
|
||||
export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
(props: ButtonProps, ref) => {
|
||||
const {
|
||||
size = 'small',
|
||||
variant = 'primary',
|
||||
iconStart,
|
||||
iconEnd,
|
||||
children,
|
||||
render = <button />,
|
||||
className,
|
||||
style,
|
||||
...rest
|
||||
} = props;
|
||||
export const Button = forwardRef<HTMLElement, ButtonProps>((props, ref) => {
|
||||
const {
|
||||
size = 'small',
|
||||
variant = 'primary',
|
||||
iconStart,
|
||||
iconEnd,
|
||||
children,
|
||||
href,
|
||||
className,
|
||||
style,
|
||||
...rest
|
||||
} = props;
|
||||
|
||||
// Get the responsive value for the variant
|
||||
const responsiveSize = useResponsiveValue(size);
|
||||
const responsiveVariant = useResponsiveValue(variant);
|
||||
const internalRef = useRef<HTMLElement | null>(null);
|
||||
function isAnchor(props: { href?: unknown }): props is { href: string } {
|
||||
return typeof props.href === 'string';
|
||||
}
|
||||
|
||||
const { renderElement } = useRender({
|
||||
render,
|
||||
props: {
|
||||
className: clsx('canon-Button', className),
|
||||
['data-variant']: responsiveVariant,
|
||||
['data-size']: responsiveSize,
|
||||
...rest,
|
||||
children: (
|
||||
<>
|
||||
{iconStart && (
|
||||
<span
|
||||
className="canon-ButtonIcon"
|
||||
aria-hidden="true"
|
||||
data-size={responsiveSize}
|
||||
>
|
||||
{iconStart}
|
||||
</span>
|
||||
)}
|
||||
{children}
|
||||
{iconEnd && (
|
||||
<span
|
||||
className="canon-ButtonIcon"
|
||||
aria-hidden="true"
|
||||
data-size={responsiveSize}
|
||||
>
|
||||
{iconEnd}
|
||||
</span>
|
||||
)}
|
||||
</>
|
||||
),
|
||||
},
|
||||
refs: [ref, internalRef],
|
||||
});
|
||||
const responsiveSize = useResponsiveValue(size);
|
||||
const responsiveVariant = useResponsiveValue(variant);
|
||||
|
||||
return renderElement();
|
||||
},
|
||||
);
|
||||
const content = (
|
||||
<>
|
||||
{iconStart && (
|
||||
<span
|
||||
className="canon-ButtonIcon"
|
||||
aria-hidden="true"
|
||||
data-size={responsiveSize}
|
||||
>
|
||||
{iconStart}
|
||||
</span>
|
||||
)}
|
||||
{children}
|
||||
{iconEnd && (
|
||||
<span
|
||||
className="canon-ButtonIcon"
|
||||
aria-hidden="true"
|
||||
data-size={responsiveSize}
|
||||
>
|
||||
{iconEnd}
|
||||
</span>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
if (isAnchor(props)) {
|
||||
const { onClick, ...anchorRest } =
|
||||
rest as AnchorHTMLAttributes<HTMLAnchorElement>;
|
||||
return (
|
||||
<a
|
||||
href={href}
|
||||
className={clsx('canon-Button', className)}
|
||||
data-variant={responsiveVariant}
|
||||
data-size={responsiveSize}
|
||||
style={style}
|
||||
ref={ref as React.Ref<HTMLAnchorElement>}
|
||||
onClick={onClick}
|
||||
{...anchorRest}
|
||||
>
|
||||
{content}
|
||||
</a>
|
||||
);
|
||||
} else {
|
||||
const { onClick, ...buttonRest } =
|
||||
rest as ButtonHTMLAttributes<HTMLButtonElement>;
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
className={clsx('canon-Button', className)}
|
||||
data-variant={responsiveVariant}
|
||||
data-size={responsiveSize}
|
||||
style={style}
|
||||
ref={ref as React.Ref<HTMLButtonElement>}
|
||||
onClick={onClick}
|
||||
{...buttonRest}
|
||||
>
|
||||
{content}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
export default Button;
|
||||
|
||||
+3
-4
@@ -13,7 +13,6 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
export { Button } from './Button';
|
||||
export type { ButtonProps } from './types';
|
||||
export { buttonPropDefs } from './Button.props';
|
||||
export type { ButtonOwnProps } from './Button.props';
|
||||
|
||||
export * from './Button';
|
||||
export * from './types';
|
||||
@@ -14,35 +14,40 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type { ButtonOwnProps } from './Button.props';
|
||||
import { ReactElement } from 'react';
|
||||
import type { useRender } from '@base-ui-components/react/use-render';
|
||||
import {
|
||||
ReactElement,
|
||||
AnchorHTMLAttributes,
|
||||
ButtonHTMLAttributes,
|
||||
} from 'react';
|
||||
|
||||
/** @public */
|
||||
export type ButtonCommonProps = {
|
||||
size?: 'small' | 'medium';
|
||||
variant?: 'primary' | 'secondary';
|
||||
iconStart?: ReactElement;
|
||||
iconEnd?: ReactElement;
|
||||
className?: string;
|
||||
style?: React.CSSProperties;
|
||||
children?: React.ReactNode;
|
||||
};
|
||||
|
||||
/** @public */
|
||||
export type ButtonAnchorProps = ButtonCommonProps &
|
||||
Omit<AnchorHTMLAttributes<HTMLAnchorElement>, 'type' | 'onClick'> & {
|
||||
href: string;
|
||||
onClick?: React.MouseEventHandler<HTMLAnchorElement>;
|
||||
};
|
||||
|
||||
/** @public */
|
||||
export type ButtonNativeProps = ButtonCommonProps &
|
||||
Omit<ButtonHTMLAttributes<HTMLButtonElement>, 'href'> & {
|
||||
href?: undefined;
|
||||
onClick?: React.MouseEventHandler<HTMLButtonElement>;
|
||||
};
|
||||
|
||||
/**
|
||||
* Properties for {@link Button}
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export interface ButtonProps extends useRender.ComponentProps<'button'> {
|
||||
/**
|
||||
* The size of the button
|
||||
* @defaultValue 'medium'
|
||||
*/
|
||||
size?: ButtonOwnProps['size'];
|
||||
|
||||
/**
|
||||
* The visual variant of the button
|
||||
* @defaultValue 'primary'
|
||||
*/
|
||||
variant?: ButtonOwnProps['variant'];
|
||||
|
||||
/**
|
||||
* Optional icon to display at the start of the button
|
||||
*/
|
||||
iconStart?: ReactElement;
|
||||
|
||||
/**
|
||||
* Optional icon to display at the end of the button
|
||||
*/
|
||||
iconEnd?: ReactElement;
|
||||
}
|
||||
export type ButtonProps = ButtonAnchorProps | ButtonNativeProps;
|
||||
|
||||
Reference in New Issue
Block a user