Merge pull request #17908 from overleaf/ii-bs5-badge

[web] Create Bootstrap 5 badges

GitOrigin-RevId: 72355c7cf7dca2a5d16bc890d7cfa4a432dd15ba
This commit is contained in:
ilkin-overleaf 2024-04-19 15:30:23 +03:00 committed by Copybot
parent 774b300e17
commit cccd0f06d7
21 changed files with 409 additions and 108 deletions

View file

@ -46,7 +46,12 @@ export default function MemberRow({
id={`pending-invite-symbol-${user._id}`}
description={t('pending_invite')}
>
<Badge aria-label={t('pending_invite')}>
<Badge
bsStyle={null}
className="badge-bs3"
aria-label={t('pending_invite')}
data-testid="badge-pending-invite"
>
{t('pending_invite')}
</Badge>
</Tooltip>

View file

@ -64,16 +64,21 @@ function Tag({ label, currentUserId, ...props }: TagProps) {
}
}
const showCloseButton = Boolean(
(isOwnedByCurrentUser || isProjectOwner) && !isPseudoCurrentStateLabel
)
return (
<>
<Badge
prepend={<Icon type="tag" fw />}
onClose={showConfirmationModal}
closeButton={Boolean(
(isOwnedByCurrentUser || isProjectOwner) && !isPseudoCurrentStateLabel
)}
closeBtnProps={{ 'aria-label': t('delete') }}
className="history-version-badge"
closeBtnProps={
showCloseButton
? { 'aria-label': t('delete'), onClick: showConfirmationModal }
: undefined
}
bsStyle={null}
className="badge-bs3 history-version-badge"
data-testid="history-version-badge"
{...props}
>

View file

@ -26,7 +26,10 @@ export default function HistoryFileTreeItem({
{name}
</div>
{operation ? (
<Badge className="history-file-tree-item-badge" size="sm">
<Badge
bsStyle={null}
className="badge-bs3 history-file-tree-item-badge"
>
{operation}
</Badge>
) : null}

View file

@ -2,6 +2,9 @@ import { useTranslation } from 'react-i18next'
import { UserEmailData } from '../../../../../../types/user-email'
import ResendConfirmationEmailButton from './resend-confirmation-email-button'
import { ssoAvailableForInstitution } from '../../utils/sso'
import BadgeWrapper from '@/features/ui/components/bootstrap-5/wrappers/badge-wrapper'
import { isBootstrap5 } from '@/features/utils/bootstrap-5'
import classnames from 'classnames'
type EmailProps = {
userEmailData: UserEmailData
@ -37,14 +40,14 @@ function Email({ userEmailData }: EmailProps) {
</div>
)}
{hasBadges && (
<div className="small">
<div className={classnames({ small: !isBootstrap5 })}>
{isPrimary && (
<>
<span className="label label-info">Primary</span>{' '}
<BadgeWrapper bg="info">Primary</BadgeWrapper>{' '}
</>
)}
{isProfessional && (
<span className="label label-primary">{t('professional')}</span>
<BadgeWrapper bg="primary">{t('professional')}</BadgeWrapper>
)}
</div>
)}

View file

@ -2,6 +2,7 @@ import { ReactNode } from 'react'
import { useTranslation } from 'react-i18next'
import { Button } from 'react-bootstrap'
import { sendMB } from '@/infrastructure/event-tracking'
import BadgeWrapper from '@/features/ui/components/bootstrap-5/wrappers/badge-wrapper'
function trackUpgradeClick() {
sendMB('settings-upgrade-click')
@ -48,7 +49,7 @@ export function EnableWidget({
<div className="title-row">
<h4>{title}</h4>
{!hasFeature && isPremiumFeature && (
<span className="label label-info">{t('premium_feature')}</span>
<BadgeWrapper bg="info">{t('premium_feature')}</BadgeWrapper>
)}
</div>
<p className="small">

View file

@ -1,6 +1,7 @@
import { useCallback, useState, ReactNode } from 'react'
import { useTranslation } from 'react-i18next'
import AccessibleModal from '../../../../shared/components/accessible-modal'
import BadgeWrapper from '@/features/ui/components/bootstrap-5/wrappers/badge-wrapper'
import { Button, Modal } from 'react-bootstrap'
import getMeta from '../../../../utils/meta'
import { sendMB } from '../../../../infrastructure/event-tracking'
@ -57,7 +58,7 @@ export function IntegrationLinkingWidget({
<div className="title-row">
<h4>{title}</h4>
{!hasFeature && (
<span className="label label-info">{t('premium_feature')}</span>
<BadgeWrapper bg="info">{t('premium_feature')}</BadgeWrapper>
)}
</div>
<p className="small">

View file

@ -0,0 +1,27 @@
import { Badge as BSBadge } from 'react-bootstrap-5'
import { MergeAndOverride } from '../../../../../../types/utils'
import MaterialIcon from '@/shared/components/material-icon'
type BadgeProps = MergeAndOverride<
React.ComponentProps<typeof BSBadge>,
{
prepend?: React.ReactNode
closeBtnProps?: React.ComponentProps<'button'>
}
>
function Badge({ prepend, children, closeBtnProps, ...rest }: BadgeProps) {
return (
<BSBadge {...rest}>
{prepend && <span className="badge-prepend">{prepend}</span>}
{children}
{closeBtnProps && (
<button type="button" className="badge-close" {...closeBtnProps}>
<MaterialIcon className="badge-close-icon" type="close" />
</button>
)}
</BSBadge>
)
}
export default Badge

View file

@ -0,0 +1,41 @@
import { Label } from 'react-bootstrap'
import Badge from '@/features/ui/components/bootstrap-5/badge'
import BS3Badge from '@/shared/components/badge'
import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher'
type BadgeWrapperProps = React.ComponentProps<typeof Badge> & {
bs3Props?: {
bsStyle?: React.ComponentProps<typeof Label>['bsStyle'] | null
}
}
function BadgeWrapper(props: BadgeWrapperProps) {
const { bs3Props, ...rest } = props
let bs3BadgeProps: React.ComponentProps<typeof BS3Badge> = {
prepend: rest.prepend,
children: rest.children,
closeBtnProps: rest.closeBtnProps,
className: rest.className,
bsStyle: rest.bg,
}
if (bs3Props) {
const { bsStyle, ...restBs3Props } = bs3Props
bs3BadgeProps = {
...bs3BadgeProps,
...restBs3Props,
bsStyle: 'bsStyle' in bs3Props ? bsStyle : rest.bg,
}
}
return (
<BootstrapVersionSwitcher
bs3={<BS3Badge {...bs3BadgeProps} />}
bs5={<Badge {...rest} />}
/>
)
}
export default BadgeWrapper

View file

@ -1,40 +1,39 @@
import classnames from 'classnames'
import { MergeAndOverride } from '../../../../types/utils'
import BadgeWrapper from '@/features/ui/components/bootstrap-5/wrappers/badge-wrapper'
type BadgeProps = MergeAndOverride<
React.ComponentProps<'span'>,
{
prepend?: React.ReactNode
children: React.ReactNode
className?: string
closeButton?: boolean
onClose?: (e: React.MouseEvent<HTMLButtonElement>) => void
closeBtnProps?: React.ComponentProps<'button'>
size?: 'sm'
className?: string
bsStyle?: NonNullable<
React.ComponentProps<typeof BadgeWrapper>['bs3Props']
>['bsStyle']
}
>
function Badge({
prepend,
children,
className,
closeButton = false,
onClose,
closeBtnProps,
size,
bsStyle,
className,
...rest
}: BadgeProps) {
const classNames =
bsStyle === null
? className
: classnames('label', `label-${bsStyle}`, className)
return (
<span className={classnames('badge-new', className)} {...rest}>
{prepend}
<span className="badge-new-comment">{children}</span>
{closeButton && (
<button
type="button"
className="badge-new-close"
onClick={onClose}
{...closeBtnProps}
>
<span className={classNames} {...rest}>
{prepend && <span className="badge-bs3-prepend">{prepend}</span>}
{children}
{closeBtnProps && (
<button type="button" className="badge-bs3-close" {...closeBtnProps}>
<span aria-hidden="true">&times;</span>
</button>
)}

View file

@ -1,53 +0,0 @@
import Badge from '../js/shared/components/badge'
import Icon from '../js/shared/components/icon'
type Args = React.ComponentProps<typeof Badge>
export const NewBadge = (args: Args) => {
return <Badge {...args} />
}
export const NewBadgePrepend = (args: Args) => {
return <Badge prepend={<Icon type="tag" fw />} {...args} />
}
export const NewBadgeWithCloseButton = (args: Args) => {
return (
<Badge
prepend={<Icon type="tag" fw />}
closeButton
onClose={() => alert('Close triggered!')}
{...args}
/>
)
}
export default {
title: 'Shared / Components / Badge',
component: Badge,
args: {
children: 'content',
},
argTypes: {
prepend: {
table: {
disable: true,
},
},
closeButton: {
table: {
disable: true,
},
},
onClose: {
table: {
disable: true,
},
},
closeBtnProps: {
table: {
disable: true,
},
},
},
}

View file

@ -0,0 +1,93 @@
import Badge from '@/shared/components/badge'
import Icon from '@/shared/components/icon'
import type { Meta, StoryObj } from '@storybook/react'
import classnames from 'classnames'
const meta: Meta<typeof Badge> = {
title: 'Shared / Components / Badge / Bootstrap 3',
component: Badge,
parameters: {
bootstrap5: false,
},
args: {
children: 'Badge',
},
argTypes: {
prepend: {
table: {
disable: true,
},
},
bsStyle: {
options: [null, 'primary', 'warning', 'danger'],
control: { type: 'radio' },
},
className: {
table: {
disable: true,
},
},
closeBtnProps: {
table: {
disable: true,
},
},
},
}
export default meta
type Story = StoryObj<typeof Badge>
export const BadgeDefault: Story = {
render: args => {
return (
<Badge
className={classnames({ 'badge-bs3': args.bsStyle === null })}
{...args}
/>
)
},
}
BadgeDefault.args = {
bsStyle: null,
}
export const BadgePrepend: Story = {
render: args => {
return (
<Badge
className={classnames({ 'badge-bs3': args.bsStyle === null })}
prepend={<Icon type="tag" fw />}
{...args}
/>
)
},
}
BadgePrepend.args = {
bsStyle: null,
}
export const BadgeWithCloseButton: Story = {
render: args => {
return (
<Badge
className={classnames({ 'badge-bs3': args.bsStyle === null })}
prepend={<Icon type="tag" fw />}
closeBtnProps={{
onClick: () => alert('Close triggered!'),
}}
{...args}
/>
)
},
}
BadgeWithCloseButton.args = {
bsStyle: null,
}
BadgeWithCloseButton.argTypes = {
bsStyle: {
table: {
disable: true,
},
},
}

View file

@ -0,0 +1,93 @@
import Badge from '@/features/ui/components/bootstrap-5/badge'
import Icon from '@/shared/components/icon'
import type { Meta, StoryObj } from '@storybook/react'
import classnames from 'classnames'
const meta: Meta<typeof Badge> = {
title: 'Shared / Components / Badge / Bootstrap 5',
component: Badge,
parameters: {
bootstrap5: true,
},
args: {
children: 'Badge',
},
argTypes: {
bg: {
options: ['light', 'info', 'primary', 'warning', 'danger'],
control: { type: 'radio' },
},
prepend: {
table: {
disable: true,
},
},
className: {
table: {
disable: true,
},
},
closeBtnProps: {
table: {
disable: true,
},
},
},
}
export default meta
type Story = StoryObj<typeof Badge>
export const BadgeDefault: Story = {
render: args => {
return (
<Badge
className={classnames({ 'text-dark': args.bg === 'light' })}
{...args}
/>
)
},
}
BadgeDefault.args = {
bg: 'light',
}
export const BadgePrepend: Story = {
render: args => {
return (
<Badge
className={classnames({ 'text-dark': args.bg === 'light' })}
prepend={<Icon type="tag" fw />}
{...args}
/>
)
},
}
BadgePrepend.args = {
bg: 'light',
}
export const BadgeWithCloseButton: Story = {
render: args => {
return (
<Badge
className={classnames({ 'text-dark': args.bg === 'light' })}
prepend={<Icon type="tag" fw />}
closeBtnProps={{
onClick: () => alert('Close triggered!'),
}}
{...args}
/>
)
},
}
BadgeWithCloseButton.args = {
bg: 'light',
}
BadgeWithCloseButton.argTypes = {
bg: {
table: {
disable: true,
},
},
}

View file

@ -44,3 +44,11 @@
border-color: var(--bs-btn-border-color);
}
}
@mixin reset-button() {
padding: 0;
cursor: pointer;
background: transparent;
border: 0;
-webkit-appearance: none;
}

View file

@ -1,5 +1,7 @@
// Overrides for Bootstrap 5's default Sass variables
$prefix: bs-;
// Fonts
$font-family-sans-serif: 'Noto Sans', sans-serif;
$font-family-serif: 'Merriweather', serif;
@ -21,6 +23,18 @@ $btn-border-radius-sm: $border-radius-full;
// Colors
$primary: $bg-accent-01;
$secondary: $bg-light-primary;
$info: $bg-info-01;
$warning: $bg-warning-01;
$danger: $bg-danger-01;
$light: $bg-light-tertiary;
$dark: $neutral-90;
// Badges
$badge-font-size: var(--font-size-01);
$badge-font-weight: var(--bs-body-font-weight);
$badge-padding-y: $spacing-01;
$badge-padding-x: $spacing-02;
$badge-border-radius: $border-radius-base;
// Tooltips
$tooltip-max-width: 320px;

View file

@ -28,6 +28,7 @@
@import 'bootstrap-5/scss/grid';
@import 'bootstrap-5/scss/buttons';
@import 'bootstrap-5/scss/dropdown';
@import 'bootstrap-5/scss/badge';
@import 'bootstrap-5/scss/modal';
@import 'bootstrap-5/scss/tooltip';
@import 'bootstrap-5/scss/spinners';

View file

@ -4,3 +4,4 @@
@import 'notifications';
@import 'tooltip';
@import 'card';
@import 'badge';

View file

@ -0,0 +1,35 @@
.badge {
display: inline-flex;
align-items: center;
overflow: hidden;
line-height: var(--line-height-01);
}
.badge-prepend {
margin-left: calc($spacing-01 / -2);
margin-right: $spacing-01;
}
.badge-close {
@include reset-button();
display: flex;
align-items: center;
justify-content: center;
// a random number that would cause the close button to expand enough
// so that it won't be affected by badge's padding
$expand: 100px;
padding: $expand $spacing-01;
margin: (-$expand) (-$spacing-02) (-$expand) $spacing-02;
user-select: none;
color: inherit;
.badge-close-icon {
font-size: $font-size-base;
}
&:hover {
background-color: var(--neutral-40);
}
}

View file

@ -1,4 +1,4 @@
.badge-new {
.badge-bs3 {
@size: 24px;
@padding: 4px;
display: inline-flex;
@ -20,10 +20,6 @@
margin-right: 2px;
}
&-comment {
flex: 1;
}
&-close {
.reset-button;
width: @size;
@ -34,6 +30,7 @@
align-items: center;
justify-content: center;
margin-right: -@padding;
color: inherit;
&:hover {
background-color: @neutral-40;
@ -44,7 +41,7 @@
@size-sm: 20px;
height: @size-sm;
font-size: @font-size-extra-small;
.badge-new-close {
.badge-bs3-close {
width: @size-sm;
font-size: @size-sm;
}

View file

@ -61,14 +61,17 @@ describe('GroupMembers', function () {
cy.contains('john.doe@test.com')
cy.contains('John Doe')
cy.contains('15th Jan 2023')
cy.get('.badge-new-comment').contains('Pending invite')
cy.findByTestId('badge-pending-invite').should(
'have.text',
'Pending invite'
)
})
cy.get('tr:nth-child(2)').within(() => {
cy.contains('bobby.lapointe@test.com')
cy.contains('Bobby Lapointe')
cy.contains('2nd Jan 2023')
cy.get('.badge-new-comment').should('not.exist')
cy.findByTestId('badge-pending-invite').should('not.exist')
})
})
})
@ -91,7 +94,10 @@ describe('GroupMembers', function () {
cy.get('tr:nth-child(3)').within(() => {
cy.contains('someone.else@test.com')
cy.contains('N/A')
cy.get('.badge-new-comment').contains('Pending invite')
cy.findByTestId('badge-pending-invite').should(
'have.text',
'Pending invite'
)
})
})
})
@ -236,10 +242,10 @@ describe('GroupMembers', function () {
cy.contains('John Doe')
cy.contains('15th Jan 2023')
cy.get('.sr-only').contains('Pending invite')
cy.get('.badge-new-comment').contains('Pending invite')
cy.get(`.security-state-invite-pending`).should('exist')
cy.get('.badge-new-comment').contains('Pending invite')
cy.findByTestId('badge-pending-invite').should(
'have.text',
'Pending invite'
)
cy.get(`.security-state-invite-pending`).should('exist')
})
@ -247,7 +253,7 @@ describe('GroupMembers', function () {
cy.contains('bobby.lapointe@test.com')
cy.contains('Bobby Lapointe')
cy.contains('2nd Jan 2023')
cy.get('.badge-new-comment').should('not.exist')
cy.findByTestId('badge-pending-invite').should('not.exist')
cy.get('.sr-only').contains('Not managed')
})
@ -255,7 +261,7 @@ describe('GroupMembers', function () {
cy.contains('claire.jennings@test.com')
cy.contains('Claire Jennings')
cy.contains('3rd Jan 2023')
cy.get('.badge-new-comment').should('not.exist')
cy.findByTestId('badge-pending-invite').should('not.exist')
cy.get('.sr-only').contains('Managed')
})
})
@ -280,7 +286,10 @@ describe('GroupMembers', function () {
cy.contains('someone.else@test.com')
cy.contains('N/A')
cy.get('.sr-only').contains('Pending invite')
cy.get('.badge-new-comment').contains('Pending invite')
cy.findByTestId('badge-pending-invite').should(
'have.text',
'Pending invite'
)
cy.get(`.security-state-invite-pending`).should('exist')
})
})

View file

@ -90,7 +90,10 @@ describe('group members, with managed users', function () {
cy.contains('15th Jan 2023')
cy.get('.sr-only').contains('Pending invite')
cy.get('.badge-new-comment').contains('Pending invite')
cy.findByTestId('badge-pending-invite').should(
'have.text',
'Pending invite'
)
cy.get(`.security-state-invite-pending`).should('exist')
})
@ -98,7 +101,7 @@ describe('group members, with managed users', function () {
cy.contains('bobby.lapointe@test.com')
cy.contains('Bobby Lapointe')
cy.contains('2nd Jan 2023')
cy.get('.badge-new-comment').should('not.exist')
cy.findByTestId('badge-pending-invite').should('not.exist')
cy.get('.sr-only').contains('Not managed')
})
@ -106,7 +109,7 @@ describe('group members, with managed users', function () {
cy.contains('claire.jennings@test.com')
cy.contains('Claire Jennings')
cy.contains('3rd Jan 2023')
cy.get('.badge-new-comment').should('not.exist')
cy.findByTestId('badge-pending-invite').should('not.exist')
cy.get('.sr-only').contains('Managed')
})
})
@ -131,7 +134,10 @@ describe('group members, with managed users', function () {
cy.contains('someone.else@test.com')
cy.contains('N/A')
cy.get('.sr-only').contains('Pending invite')
cy.get('.badge-new-comment').contains('Pending invite')
cy.findByTestId('badge-pending-invite').should(
'have.text',
'Pending invite'
)
cy.get(`.security-state-invite-pending`).should('exist')
})
})

View file

@ -97,7 +97,10 @@ describe('MemberRow', function () {
})
it('should render a "Pending invite" badge', function () {
cy.get('.badge-new-comment').contains('Pending invite')
cy.findByTestId('badge-pending-invite').should(
'have.text',
'Pending invite'
)
})
})
@ -269,7 +272,10 @@ describe('MemberRow', function () {
})
it('should render a "Pending invite" badge', function () {
cy.get('.badge-new-comment').contains('Pending invite')
cy.findByTestId('badge-pending-invite').should(
'have.text',
'Pending invite'
)
})
})
@ -443,7 +449,10 @@ describe('MemberRow', function () {
})
it('should render a "Pending invite" badge', function () {
cy.get('.badge-new-comment').contains('Pending invite')
cy.findByTestId('badge-pending-invite').should(
'have.text',
'Pending invite'
)
})
})
@ -618,7 +627,10 @@ describe('MemberRow', function () {
})
it('should render a "Pending invite" badge', function () {
cy.get('.badge-new-comment').contains('Pending invite')
cy.findByTestId('badge-pending-invite').should(
'have.text',
'Pending invite'
)
})
})