From e18f0817c671fe459717ef90787dc9453fc29343 Mon Sep 17 00:00:00 2001 From: Rebeka Dekany <50901361+rebekadekany@users.noreply.github.com> Date: Wed, 6 Mar 2024 12:31:25 +0100 Subject: [PATCH] Merge pull request #17216 from overleaf/rd-bootstrap5-buttons Bootstrap-5 Button component GitOrigin-RevId: 1fb13b7ab2b71403b0236f1f85aec7b9545b34f1 --- .../ui/components/bootstrap-5/button.tsx | 48 ++++++++++------ .../ui/components/bootstrap-5/icon-button.tsx | 33 +++++++++++ .../bootstrap-5/icon-text-button.tsx | 29 ++++++++++ .../ui/components/types/button-props.ts | 17 +++--- .../ui/components/types/icon-button-props.ts | 6 ++ .../types/icon-text-button-props.ts | 6 ++ .../frontend/stories/ui/button.stories.tsx | 23 +++++++- .../stories/ui/icon-button.stories.tsx | 40 +++++++++++++ .../stories/ui/icon-text-button.stories.tsx | 42 ++++++++++++++ .../stylesheets/bootstrap-5/main-style.scss | 1 + .../bootstrap-5/scss/bootstrap.scss | 1 + .../bootstrap-5/scss/components/button.scss | 57 +++++++++++++++++++ 12 files changed, 277 insertions(+), 26 deletions(-) create mode 100644 services/web/frontend/js/features/ui/components/bootstrap-5/icon-button.tsx create mode 100644 services/web/frontend/js/features/ui/components/bootstrap-5/icon-text-button.tsx create mode 100644 services/web/frontend/js/features/ui/components/types/icon-button-props.ts create mode 100644 services/web/frontend/js/features/ui/components/types/icon-text-button-props.ts create mode 100644 services/web/frontend/stories/ui/icon-button.stories.tsx create mode 100644 services/web/frontend/stories/ui/icon-text-button.stories.tsx create mode 100644 services/web/frontend/stylesheets/bootstrap-5/scss/components/button.scss diff --git a/services/web/frontend/js/features/ui/components/bootstrap-5/button.tsx b/services/web/frontend/js/features/ui/components/bootstrap-5/button.tsx index b64b390144..8a375c6a59 100644 --- a/services/web/frontend/js/features/ui/components/bootstrap-5/button.tsx +++ b/services/web/frontend/js/features/ui/components/bootstrap-5/button.tsx @@ -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([ ['small', 'btn-sm'], @@ -7,27 +9,39 @@ const sizeClasses = new Map([ ['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 ( - - {children} - + + {isLoading && ( + + + )} + + {children} + + ) } - -export default Button diff --git a/services/web/frontend/js/features/ui/components/bootstrap-5/icon-button.tsx b/services/web/frontend/js/features/ui/components/bootstrap-5/icon-button.tsx new file mode 100644 index 0000000000..e78194374e --- /dev/null +++ b/services/web/frontend/js/features/ui/components/bootstrap-5/icon-button.tsx @@ -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 ( + + ) +} diff --git a/services/web/frontend/js/features/ui/components/bootstrap-5/icon-text-button.tsx b/services/web/frontend/js/features/ui/components/bootstrap-5/icon-text-button.tsx new file mode 100644 index 0000000000..973b6c52d2 --- /dev/null +++ b/services/web/frontend/js/features/ui/components/bootstrap-5/icon-text-button.tsx @@ -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 ( + + ) +} diff --git a/services/web/frontend/js/features/ui/components/types/button-props.ts b/services/web/frontend/js/features/ui/components/types/button-props.ts index 611cd2843b..7c54ce809f 100644 --- a/services/web/frontend/js/features/ui/components/types/button-props.ts +++ b/services/web/frontend/js/features/ui/components/types/button-props.ts @@ -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 + 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 } diff --git a/services/web/frontend/js/features/ui/components/types/icon-button-props.ts b/services/web/frontend/js/features/ui/components/types/icon-button-props.ts new file mode 100644 index 0000000000..7a29f052bf --- /dev/null +++ b/services/web/frontend/js/features/ui/components/types/icon-button-props.ts @@ -0,0 +1,6 @@ +import { ButtonProps } from './button-props' + +export type IconButtonProps = ButtonProps & { + icon: string + type?: 'button' +} diff --git a/services/web/frontend/js/features/ui/components/types/icon-text-button-props.ts b/services/web/frontend/js/features/ui/components/types/icon-text-button-props.ts new file mode 100644 index 0000000000..c27781c1e4 --- /dev/null +++ b/services/web/frontend/js/features/ui/components/types/icon-text-button-props.ts @@ -0,0 +1,6 @@ +import { ButtonProps } from './button-props' + +export type IconTextButtonProps = ButtonProps & { + leadingIcon?: string + trailingIcon?: string +} diff --git a/services/web/frontend/stories/ui/button.stories.tsx b/services/web/frontend/stories/ui/button.stories.tsx index 1c460547d1..7c2c05f7b1 100644 --- a/services/web/frontend/stories/ui/button.stories.tsx +++ b/services/web/frontend/stories/ui/button.stories.tsx @@ -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 @@ -11,7 +11,26 @@ const meta: Meta = { 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, diff --git a/services/web/frontend/stories/ui/icon-button.stories.tsx b/services/web/frontend/stories/ui/icon-button.stories.tsx new file mode 100644 index 0000000000..3b6ee70c58 --- /dev/null +++ b/services/web/frontend/stories/ui/icon-button.stories.tsx @@ -0,0 +1,40 @@ +import IconButton from '@/features/ui/components/bootstrap-5/icon-button' +import type { Meta } from '@storybook/react' + +type Args = React.ComponentProps + +export const Icon = (args: Args) => { + return +} + +const meta: Meta = { + 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 diff --git a/services/web/frontend/stories/ui/icon-text-button.stories.tsx b/services/web/frontend/stories/ui/icon-text-button.stories.tsx new file mode 100644 index 0000000000..6a901ebb87 --- /dev/null +++ b/services/web/frontend/stories/ui/icon-text-button.stories.tsx @@ -0,0 +1,42 @@ +import IconTextButton from '@/features/ui/components/bootstrap-5/icon-text-button' +import { Meta } from '@storybook/react' + +type Args = React.ComponentProps + +export const IconText = (args: Args) => { + return +} + +const meta: Meta = { + 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 diff --git a/services/web/frontend/stylesheets/bootstrap-5/main-style.scss b/services/web/frontend/stylesheets/bootstrap-5/main-style.scss index f6da49926e..b259356282 100644 --- a/services/web/frontend/stylesheets/bootstrap-5/main-style.scss +++ b/services/web/frontend/stylesheets/bootstrap-5/main-style.scss @@ -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'; diff --git a/services/web/frontend/stylesheets/bootstrap-5/scss/bootstrap.scss b/services/web/frontend/stylesheets/bootstrap-5/scss/bootstrap.scss index 294543f4f0..b1d8bab869 100644 --- a/services/web/frontend/stylesheets/bootstrap-5/scss/bootstrap.scss +++ b/services/web/frontend/stylesheets/bootstrap-5/scss/bootstrap.scss @@ -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'; diff --git a/services/web/frontend/stylesheets/bootstrap-5/scss/components/button.scss b/services/web/frontend/stylesheets/bootstrap-5/scss/components/button.scss new file mode 100644 index 0000000000..4635fc6fda --- /dev/null +++ b/services/web/frontend/stylesheets/bootstrap-5/scss/components/button.scss @@ -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); +}