1
0
Fork 0
mirror of https://github.com/overleaf/overleaf.git synced 2025-04-11 17:15:35 +00:00

Merge pull request from overleaf/rd-dropdown-bootstrap5

Bootstrap-5 Dropdown menu component

GitOrigin-RevId: 8a74f2341eebf953367ab73946d72e8aa7bd3c13
This commit is contained in:
Rebeka Dekany 2024-02-23 12:23:32 +01:00 committed by Copybot
parent 3d4f3d3d73
commit 4a1af0f057
13 changed files with 494 additions and 4 deletions

View file

@ -14,13 +14,14 @@ function Button({
disabled = false,
loading = false,
children,
className,
}: ButtonProps) {
const sizeClass = sizeClasses.get(size)
return (
<BootstrapButton
className={sizeClass + ' ' + className}
variant={variant}
className={sizeClass}
disabled={disabled}
{...(loading ? { 'data-ol-loading': true } : null)}
>

View file

@ -0,0 +1,69 @@
import React from 'react'
import {
Dropdown as B5Dropdown,
DropdownToggle as B5DropdownToggle,
DropdownMenu as B5DropdownMenu,
DropdownItem as B5DropdownItem,
DropdownDivider as B5DropdownDivider,
} from 'react-bootstrap-5'
import type {
DropdownProps,
DropdownItemProps,
DropdownToggleProps,
DropdownMenuProps,
} from '@/features/ui/components/types/dropdown-menu-props'
import MaterialIcon from '@/shared/components/material-icon'
export function Dropdown({ ...props }: DropdownProps) {
return <B5Dropdown {...props} />
}
export function DropdownItem({
active,
children,
description,
leadingIcon,
trailingIcon,
...props
}: DropdownItemProps) {
const trailingIconType = active ? 'check' : trailingIcon
return (
<li>
<B5DropdownItem
active={active}
className={description ? 'dropdown-item-description-container' : ''}
role="menuitem"
{...props}
>
{leadingIcon && (
<MaterialIcon
className="dropdown-item-leading-icon"
type={leadingIcon}
/>
)}
{children}
{trailingIconType && (
<MaterialIcon
className="dropdown-item-trailing-icon"
type={trailingIconType}
/>
)}
{description && (
<span className="dropdown-item-description">{description}</span>
)}
</B5DropdownItem>
</li>
)
}
export function DropdownToggle({ ...props }: DropdownToggleProps) {
return <B5DropdownToggle {...props} />
}
export function DropdownMenu({ as = 'ul', ...props }: DropdownMenuProps) {
return <B5DropdownMenu as={as} role="menubar" {...props} />
}
export function DropdownDivider() {
return <B5DropdownDivider aria-hidden="true" />
}

View file

@ -0,0 +1,46 @@
import classNames from 'classnames'
import Button from './button'
import {
Dropdown,
DropdownItem,
DropdownMenu,
DropdownToggle,
} from './dropdown-menu'
import MaterialIcon from '@/shared/components/material-icon'
import type { SplitButtonProps } from '@/features/ui/components/types/split-button-props'
export function SplitButton({
align,
id,
items,
text,
variant,
...props
}: SplitButtonProps) {
const buttonClassName = classNames('split-button')
return (
<div>
<Dropdown align={align}>
<Button className={buttonClassName} variant={variant} {...props}>
<span className="split-button-content">{text}</span>
</Button>
<DropdownToggle
bsPrefix="dropdown-button-toggle"
id={id}
variant={variant}
{...props}
>
<MaterialIcon className="split-button-caret" type="expand_more" />
</DropdownToggle>
<DropdownMenu>
{items.map((item, index) => (
<DropdownItem key={index} eventKey={item.eventKey}>
{item.label}
</DropdownItem>
))}
</DropdownMenu>
</Dropdown>
</div>
)
}

View file

@ -1,7 +1,7 @@
import type { ReactNode } from 'react'
export type ButtonProps = {
variant?:
variant:
| 'primary'
| 'secondary'
| 'ghost'
@ -12,4 +12,5 @@ export type ButtonProps = {
disabled?: boolean
loading?: boolean
children: ReactNode
className?: string
}

View file

@ -0,0 +1,45 @@
import type { ElementType, ReactNode, PropsWithChildren } from 'react'
import type { SplitButtonVariants } from './split-button-props'
export type DropdownProps = {
align?:
| 'start'
| 'end'
| { sm: 'start' | 'end' }
| { md: 'start' | 'end' }
| { lg: 'start' | 'end' }
| { xl: 'start' | 'end' }
| { xxl: 'start' | 'end' }
as?: ElementType
children: ReactNode
onSelect?: (eventKey: any, event: object) => any
onToggle?: (show: boolean) => void
show?: boolean
}
export type DropdownItemProps = PropsWithChildren<{
active?: boolean
as?: ElementType
description?: string
disabled?: boolean
eventKey: string | number
href?: string
leadingIcon?: string
onClick?: () => void
trailingIcon?: string
variant?: 'default' | 'danger'
}>
export type DropdownToggleProps = PropsWithChildren<{
bsPrefix?: string
disabled?: boolean
split?: boolean
id: string // necessary for assistive technologies
variant: SplitButtonVariants
}>
export type DropdownMenuProps = PropsWithChildren<{
as?: ElementType
disabled?: boolean
show?: boolean
}>

View file

@ -0,0 +1,28 @@
import { PropsWithChildren } from 'react'
import type {
DropdownItemProps,
DropdownProps,
DropdownToggleProps,
} from './dropdown-menu-props'
import type { ButtonProps } from './button-props'
type SplitButtonItemProps = Pick<
DropdownItemProps,
'eventKey' | 'leadingIcon'
> & {
label: React.ReactNode
}
export type SplitButtonVariants = Extract<
ButtonProps['variant'],
'primary' | 'secondary' | 'danger'
>
export type SplitButtonProps = PropsWithChildren<{
align?: DropdownProps['align']
disabled?: boolean
id: DropdownToggleProps['id']
items: SplitButtonItemProps[]
text: string
variant: SplitButtonVariants
}>

View file

@ -0,0 +1,132 @@
import {
DropdownMenu,
DropdownItem,
DropdownDivider,
} from '@/features/ui/components/bootstrap-5/dropdown-menu'
import type { Meta } from '@storybook/react'
type Args = React.ComponentProps<typeof DropdownMenu>
export const Default = (args: Args) => {
return (
<DropdownMenu show>
<DropdownItem eventKey="1" href="#/action-1">
Example
</DropdownItem>
<DropdownItem eventKey="2" href="#/action-2">
Example
</DropdownItem>
<DropdownDivider />
<DropdownItem eventKey="3" disabled={args.disabled} href="#/action-3">
Example
</DropdownItem>
</DropdownMenu>
)
}
export const Active = (args: Args) => {
return (
<DropdownMenu show>
<DropdownItem eventKey="1" href="#/action-1">
Example
</DropdownItem>
<DropdownItem eventKey="2" active href="#/action-2" trailingIcon="check">
Example
</DropdownItem>
<DropdownDivider />
<DropdownItem eventKey="3" disabled={args.disabled} href="#/action-3">
Example
</DropdownItem>
</DropdownMenu>
)
}
export const Danger = (args: Args) => {
return (
<DropdownMenu show>
<DropdownItem eventKey="1" disabled={args.disabled} href="#/action-1">
Example
</DropdownItem>
<DropdownItem eventKey="2" href="#/action-2">
Example
</DropdownItem>
<DropdownDivider />
<DropdownItem eventKey="3" href="#/action-3" variant="danger">
Example
</DropdownItem>
</DropdownMenu>
)
}
export const Description = (args: Args) => {
return (
<DropdownMenu show>
<DropdownItem
disabled={args.disabled}
eventKey="1"
href="#/action-1"
description="Description of the menu"
>
Example
</DropdownItem>
<DropdownItem
active
eventKey="2"
href="#/action-2"
description="Description of the menu"
trailingIcon="check"
>
Example
</DropdownItem>
</DropdownMenu>
)
}
export const Icon = (args: Args) => {
return (
<DropdownMenu show>
<DropdownItem
disabled={args.disabled}
eventKey="1"
href="#/action-1"
leadingIcon="view_column_2"
>
Editor & PDF
</DropdownItem>
<DropdownItem
active
eventKey="2"
href="#/action-2"
leadingIcon="terminal"
>
Editor only
</DropdownItem>
<DropdownItem eventKey="2" href="#/action-2" leadingIcon="picture_as_pdf">
PDF only
</DropdownItem>
<DropdownItem eventKey="2" href="#/action-2" leadingIcon="select_window">
PDF in separate tab
</DropdownItem>
</DropdownMenu>
)
}
const meta: Meta<typeof DropdownMenu> = {
title: 'Shared / Components / Bootstrap 5 / DropdownMenu',
component: DropdownMenu,
argTypes: {
disabled: {
control: 'boolean',
},
show: {
table: {
disable: true,
},
},
},
parameters: {
bootstrap5: true,
},
}
export default meta

View file

@ -0,0 +1,34 @@
import { SplitButton } from '@/features/ui/components/bootstrap-5/split-button'
import type { Meta } from '@storybook/react'
type Args = React.ComponentProps<typeof SplitButton>
export const Dropdown = (args: Args) => {
return <SplitButton {...args} />
}
const meta: Meta<typeof SplitButton> = {
title: 'Shared/Components/Bootstrap 5/SplitButton',
component: SplitButton,
args: {
align: { sm: 'start' },
id: 'split-button',
items: [
{ eventKey: '1', label: 'Action 1' },
{ eventKey: '2', label: 'Action 2' },
{ eventKey: '3', label: 'Action 3' },
],
text: 'Split Button',
},
argTypes: {
id: {
table: {
disable: true,
},
},
},
parameters: {
bootstrap5: true,
},
}
export default meta

View file

@ -0,0 +1,15 @@
// Cards
@mixin shadow-sm {
box-shadow: 0px 2px 8px rgba(30, 37, 48, 0.08);
}
// Tooltips, Callouts, Dropdowns, etc.
@mixin shadow-md {
box-shadow: 0px 4px 24px rgba(30, 37, 48, 0.12), 0px 1px 4px rgba(30, 37, 48, 0.08);
}
// Modals, drawers
@mixin shadow-lg {
box-shadow: 0px 8px 24px rgba(30, 37, 48, 0.16), 0px 4px 8px rgba(30, 37, 48, 0.16);
}

View file

@ -19,6 +19,7 @@ $is-overleaf-light: false;
@import 'foundations/spacing';
@import 'foundations/typography';
@import 'foundations/border-radius';
@import 'foundations/elevation';
// Boostrap-related
@ -33,3 +34,7 @@ $is-overleaf-light: false;
// variables in the usual CSS way, and can also refer to (but not override)
// Bootstrap Sass variables.
@import 'scss/bootstrap-rule-overrides';
// Components
@import 'scss/components/dropdown-menu';
@import 'scss/components/split-button';

View file

@ -27,7 +27,6 @@
@import 'bootstrap-5/scss/grid';
@import 'bootstrap-5/scss/helpers';
@import 'bootstrap-5/scss/buttons';
@import 'bootstrap-5/scss/dropdown';
@import 'bootstrap-5/scss/modal';
// Include utilities API last to generate classes based on the Sass map in `_utilities.scss`
@import 'bootstrap-5/scss/utilities/api';

View file

@ -0,0 +1,106 @@
.dropdown {
display: inline-flex;
}
.dropdown-menu {
@include shadow-sm;
border: none;
border-radius: var(--border-radius-base);
padding: var(--spacing-02);
transform: none;
width: 240px;
}
.dropdown-item {
@include body-sm;
border-radius: var(--border-radius-base);
color: var(--neutral-90);
display: grid;
grid-auto-flow: column;
justify-content: start;
min-height: 44px; // a minimum height of 44px to be accessible for touch screens
padding: var(--spacing-05) var(--spacing-04);
position: relative;
&:hover:not(.active) {
background-color: var(--bg-light-secondary);
cursor: pointer;
}
&:active {
background-color: var(--bg-accent-03);
color: inherit;
}
&[variant="danger"] {
color: var(--content-danger);
&:hover:not(.active) {
background-color: var(--bg-danger-03);
}
}
&.active {
background-color: var(--bg-accent-03);
color: var(--green-70);
}
}
.dropdown-divider {
border-radius: 1px;
background-color: var(--border-divider);
margin: 2px 6px;
}
.dropdown-item-description {
@include body-xs;
color: var(--content-secondary);
margin-top: var(--spacing-01);
}
.dropdown-item-description-container {
grid-auto-flow: row;
min-height: 60px;
}
.dropdown-item-trailing-icon {
position: absolute;
right: 10px;
top: 50%;
transform: translateY(-50%);
}
.dropdown-item-leading-icon {
padding-right: var(--spacing-04);
}
// description text should look disabled when the dropdown item is disabled
.dropdown-item.disabled .dropdown-item-description,
.dropdown-item[aria-disabled="true"] .dropdown-item-description {
background-color: transparent;
color: var(--content-disabled);
}
// override disabled styles when the state is active
.dropdown-item.active .dropdown-item-description {
background-color: initial;
color: var(--green-70);
}
.dropdown-button-toggle {
border-bottom-left-radius: 0;
border-top-left-radius: 0;
padding-right: var(--spacing-05);
padding-left: var(--spacing-05);
&.btn-primary, &.btn-danger {
border-left: 1px solid rgb($neutral-90, 0.16);
}
&.btn-secondary {
border-left: 1px solid var(--neutral-60);
}
&[disabled], &[aria-disabled="true"] {
border-left: 1px solid var(--neutral-10);
}
}

View file

@ -0,0 +1,9 @@
.split-button {
border-bottom-right-radius: 0;
border-top-right-radius: 0;
border-right: none;
}
.split-button-caret {
vertical-align: middle;
}