mirror of
https://github.com/overleaf/overleaf.git
synced 2024-11-21 20:47:08 -05:00
Merge pull request #17216 from overleaf/rd-bootstrap5-buttons
Bootstrap-5 Button component GitOrigin-RevId: 1fb13b7ab2b71403b0236f1f85aec7b9545b34f1
This commit is contained in:
parent
5415aafaf8
commit
e18f0817c6
12 changed files with 277 additions and 26 deletions
|
@ -1,5 +1,7 @@
|
|||
import { Button as BootstrapButton } from 'react-bootstrap-5'
|
||||
import { Button as B5Button, Spinner } from 'react-bootstrap-5'
|
||||
import type { ButtonProps } from '@/features/ui/components/types/button-props'
|
||||
import classNames from 'classnames'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
const sizeClasses = new Map<ButtonProps['size'], string>([
|
||||
['small', 'btn-sm'],
|
||||
|
@ -7,27 +9,39 @@ const sizeClasses = new Map<ButtonProps['size'], string>([
|
|||
['large', 'btn-lg'],
|
||||
])
|
||||
|
||||
// TODO: Display a spinner when `loading` is true
|
||||
function Button({
|
||||
variant = 'primary',
|
||||
size = 'default',
|
||||
disabled = false,
|
||||
loading = false,
|
||||
export default function Button({
|
||||
children,
|
||||
className,
|
||||
isLoading = false,
|
||||
size = 'default',
|
||||
...props
|
||||
}: ButtonProps) {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const sizeClass = sizeClasses.get(size)
|
||||
const buttonClassName = classNames('d-inline-grid', sizeClass, className, {
|
||||
'button-loading': isLoading,
|
||||
})
|
||||
const loadingSpinnerClassName =
|
||||
size === 'large' ? 'loading-spinner-large' : 'loading-spinner-small'
|
||||
|
||||
return (
|
||||
<BootstrapButton
|
||||
className={sizeClass + ' ' + className}
|
||||
variant={variant}
|
||||
disabled={disabled}
|
||||
{...(loading ? { 'data-ol-loading': true } : null)}
|
||||
>
|
||||
{children}
|
||||
</BootstrapButton>
|
||||
<B5Button className={buttonClassName} {...props}>
|
||||
{isLoading && (
|
||||
<span className="spinner-container">
|
||||
<Spinner
|
||||
animation="border"
|
||||
aria-hidden="true"
|
||||
as="span"
|
||||
className={loadingSpinnerClassName}
|
||||
role="status"
|
||||
/>
|
||||
<span className="sr-only">{t('loading')}</span>
|
||||
</span>
|
||||
)}
|
||||
<span className="button-content" aria-hidden={isLoading}>
|
||||
{children}
|
||||
</span>
|
||||
</B5Button>
|
||||
)
|
||||
}
|
||||
|
||||
export default Button
|
||||
|
|
|
@ -0,0 +1,33 @@
|
|||
import { useTranslation } from 'react-i18next'
|
||||
import MaterialIcon from '@/shared/components/material-icon'
|
||||
import Button from './button'
|
||||
import type { IconButtonProps } from '@/features/ui/components/types/icon-button-props'
|
||||
import classNames from 'classnames'
|
||||
|
||||
export default function IconButton({
|
||||
icon,
|
||||
isLoading = false,
|
||||
size = 'default',
|
||||
...props
|
||||
}: IconButtonProps) {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const iconButtonClassName = `icon-button-${size}`
|
||||
const iconSizeClassName =
|
||||
size === 'large'
|
||||
? 'leading-trailing-icon-large'
|
||||
: 'leading-trailing-icon-small'
|
||||
const materialIconClassName = classNames(iconSizeClassName, {
|
||||
'button-content-hidden': isLoading,
|
||||
})
|
||||
|
||||
return (
|
||||
<Button className={iconButtonClassName} isLoading={isLoading} {...props}>
|
||||
<MaterialIcon
|
||||
accessibilityLabel={t('add')}
|
||||
className={materialIconClassName}
|
||||
type={icon}
|
||||
/>
|
||||
</Button>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
import MaterialIcon from '@/shared/components/material-icon'
|
||||
import { IconTextButtonProps } from '../types/icon-text-button-props'
|
||||
import Button from './button'
|
||||
|
||||
export default function IconTextButton({
|
||||
children,
|
||||
className,
|
||||
leadingIcon,
|
||||
size = 'default',
|
||||
trailingIcon,
|
||||
...props
|
||||
}: IconTextButtonProps) {
|
||||
const materialIconClassName =
|
||||
size === 'large'
|
||||
? 'leading-trailing-icon-large'
|
||||
: 'leading-trailing-icon-small'
|
||||
|
||||
return (
|
||||
<Button size={size} {...props}>
|
||||
{leadingIcon && (
|
||||
<MaterialIcon type={leadingIcon} className={materialIconClassName} />
|
||||
)}
|
||||
{children}
|
||||
{trailingIcon && (
|
||||
<MaterialIcon type={trailingIcon} className={materialIconClassName} />
|
||||
)}
|
||||
</Button>
|
||||
)
|
||||
}
|
|
@ -1,16 +1,19 @@
|
|||
import type { ReactNode } from 'react'
|
||||
import type { MouseEventHandler, ReactNode } from 'react'
|
||||
|
||||
export type ButtonProps = {
|
||||
variant:
|
||||
children?: ReactNode
|
||||
className?: string
|
||||
disabled?: boolean
|
||||
href?: string
|
||||
isLoading?: boolean
|
||||
onClick?: MouseEventHandler<HTMLButtonElement>
|
||||
size?: 'small' | 'default' | 'large'
|
||||
type?: 'button' | 'reset' | 'submit'
|
||||
variant?:
|
||||
| 'primary'
|
||||
| 'secondary'
|
||||
| 'ghost'
|
||||
| 'danger'
|
||||
| 'danger-ghost'
|
||||
| 'premium'
|
||||
size?: 'small' | 'default' | 'large'
|
||||
disabled?: boolean
|
||||
loading?: boolean
|
||||
children: ReactNode
|
||||
className?: string
|
||||
}
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
import { ButtonProps } from './button-props'
|
||||
|
||||
export type IconButtonProps = ButtonProps & {
|
||||
icon: string
|
||||
type?: 'button'
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
import { ButtonProps } from './button-props'
|
||||
|
||||
export type IconTextButtonProps = ButtonProps & {
|
||||
leadingIcon?: string
|
||||
trailingIcon?: string
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
import Button from '@/features/ui/components/bootstrap-5/button'
|
||||
import type { Meta } from '@storybook/react'
|
||||
import { Meta } from '@storybook/react'
|
||||
|
||||
type Args = React.ComponentProps<typeof Button>
|
||||
|
||||
|
@ -11,7 +11,26 @@ const meta: Meta<typeof Button> = {
|
|||
title: 'Shared / Components / Bootstrap 5 / Button',
|
||||
component: Button,
|
||||
args: {
|
||||
children: 'A Bootstrap 5 button',
|
||||
children: 'A Bootstrap 5 Button',
|
||||
disabled: false,
|
||||
isLoading: false,
|
||||
},
|
||||
argTypes: {
|
||||
size: {
|
||||
control: 'radio',
|
||||
options: ['small', 'default', 'large'],
|
||||
},
|
||||
variant: {
|
||||
control: 'radio',
|
||||
options: [
|
||||
'primary',
|
||||
'secondary',
|
||||
'ghost',
|
||||
'danger',
|
||||
'danger-ghost',
|
||||
'premium',
|
||||
],
|
||||
},
|
||||
},
|
||||
parameters: {
|
||||
bootstrap5: true,
|
||||
|
|
40
services/web/frontend/stories/ui/icon-button.stories.tsx
Normal file
40
services/web/frontend/stories/ui/icon-button.stories.tsx
Normal file
|
@ -0,0 +1,40 @@
|
|||
import IconButton from '@/features/ui/components/bootstrap-5/icon-button'
|
||||
import type { Meta } from '@storybook/react'
|
||||
|
||||
type Args = React.ComponentProps<typeof IconButton>
|
||||
|
||||
export const Icon = (args: Args) => {
|
||||
return <IconButton disabled {...args} />
|
||||
}
|
||||
|
||||
const meta: Meta<typeof IconButton> = {
|
||||
title: 'Shared / Components / Bootstrap 5 / IconButton',
|
||||
component: IconButton,
|
||||
args: {
|
||||
disabled: false,
|
||||
icon: 'add',
|
||||
isLoading: false,
|
||||
},
|
||||
argTypes: {
|
||||
size: {
|
||||
control: 'radio',
|
||||
options: ['small', 'default', 'large'],
|
||||
},
|
||||
variant: {
|
||||
control: 'radio',
|
||||
options: [
|
||||
'primary',
|
||||
'secondary',
|
||||
'ghost',
|
||||
'danger',
|
||||
'danger-ghost',
|
||||
'premium',
|
||||
],
|
||||
},
|
||||
},
|
||||
parameters: {
|
||||
bootstrap5: true,
|
||||
},
|
||||
}
|
||||
|
||||
export default meta
|
|
@ -0,0 +1,42 @@
|
|||
import IconTextButton from '@/features/ui/components/bootstrap-5/icon-text-button'
|
||||
import { Meta } from '@storybook/react'
|
||||
|
||||
type Args = React.ComponentProps<typeof IconTextButton>
|
||||
|
||||
export const IconText = (args: Args) => {
|
||||
return <IconTextButton {...args} />
|
||||
}
|
||||
|
||||
const meta: Meta<typeof IconTextButton> = {
|
||||
title: 'Shared / Components / Bootstrap 5 / IconTextButton',
|
||||
component: IconTextButton,
|
||||
args: {
|
||||
children: 'IconTextButton',
|
||||
disabled: false,
|
||||
isLoading: false,
|
||||
leadingIcon: 'add',
|
||||
trailingIcon: 'expand_more',
|
||||
},
|
||||
argTypes: {
|
||||
size: {
|
||||
control: 'radio',
|
||||
options: ['small', 'default', 'large'],
|
||||
},
|
||||
variant: {
|
||||
control: 'radio',
|
||||
options: [
|
||||
'primary',
|
||||
'secondary',
|
||||
'ghost',
|
||||
'danger',
|
||||
'danger-ghost',
|
||||
'premium',
|
||||
],
|
||||
},
|
||||
},
|
||||
parameters: {
|
||||
bootstrap5: true,
|
||||
},
|
||||
}
|
||||
|
||||
export default meta
|
|
@ -36,5 +36,6 @@ $is-overleaf-light: false;
|
|||
@import 'scss/bootstrap-rule-overrides';
|
||||
|
||||
// Components
|
||||
@import 'scss/components/button';
|
||||
@import 'scss/components/dropdown-menu';
|
||||
@import 'scss/components/split-button';
|
||||
|
|
|
@ -30,3 +30,4 @@
|
|||
@import 'bootstrap-5/scss/dropdown';
|
||||
@import 'bootstrap-5/scss/modal';
|
||||
@import 'bootstrap-5/scss/utilities/api';
|
||||
@import 'bootstrap-5/scss/spinners';
|
||||
|
|
|
@ -0,0 +1,57 @@
|
|||
.button-loading {
|
||||
align-items: center;
|
||||
display: inline-grid;
|
||||
grid-template-areas: 'container'; // Define a single grid area
|
||||
}
|
||||
|
||||
.button-loading > * {
|
||||
grid-area: container; // Position all the direct children within the single grid area
|
||||
}
|
||||
|
||||
.button-loading .spinner-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
.loading-spinner-small {
|
||||
border-width: 0.2em;
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
}
|
||||
.loading-spinner-large {
|
||||
border-width: 0.2em;
|
||||
height: 24px;
|
||||
width: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
// Hide the text when the spinner is visible
|
||||
.button-loading > [aria-hidden='true'] {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.button-content {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-04); // Add gap between text and icons
|
||||
|
||||
.leading-trailing-icon-small {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.leading-trailing-icon-large {
|
||||
font-size: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
.icon-button-small {
|
||||
padding: var(--spacing-01);
|
||||
}
|
||||
|
||||
.icon-button-default {
|
||||
padding: var(--spacing-04);
|
||||
}
|
||||
|
||||
.icon-button-large {
|
||||
padding: var(--spacing-05);
|
||||
}
|
Loading…
Reference in a new issue