Explorar o código

Merge pull request #18338 from overleaf/ii-bs5-split-badges-and-tags

[web] Split badges and tags

GitOrigin-RevId: fce5a93672f431ff74c2b63a67e249f5f7e7fecd
ilkin-overleaf %!s(int64=2) %!d(string=hai) anos
pai
achega
fe7de51827

+ 1 - 1
services/web/frontend/js/features/group-management/components/members-table/member-row.tsx

@@ -48,7 +48,7 @@ export default function MemberRow({
               >
                 <Badge
                   bsStyle={null}
-                  className="badge-bs3"
+                  className="badge-tag-bs3"
                   aria-label={t('pending_invite')}
                   data-testid="badge-pending-invite"
                 >

+ 7 - 8
services/web/frontend/js/features/history/components/change-list/tag-tooltip.tsx

@@ -3,7 +3,6 @@ import { useTranslation } from 'react-i18next'
 import { Modal } from 'react-bootstrap'
 import Icon from '../../../../shared/components/icon'
 import Tooltip from '../../../../shared/components/tooltip'
-import Badge from '../../../../shared/components/badge'
 import AccessibleModal from '../../../../shared/components/accessible-modal'
 import ModalError from './modal-error'
 import useAbortController from '../../../../shared/hooks/use-abort-controller'
@@ -16,13 +15,14 @@ import { LoadedLabel } from '../../services/types/label'
 import { debugConsole } from '@/utils/debugging'
 import { formatTimeBasedOnYear } from '@/features/utils/format-date'
 import { useEditorContext } from '@/shared/context/editor-context'
+import Tag from '@/shared/components/tag'
 
 type TagProps = {
   label: LoadedLabel
   currentUserId: string
 }
 
-function Tag({ label, currentUserId, ...props }: TagProps) {
+function ChangeTag({ label, currentUserId, ...props }: TagProps) {
   const { isProjectOwner } = useEditorContext()
 
   const { t } = useTranslation()
@@ -70,22 +70,21 @@ function Tag({ label, currentUserId, ...props }: TagProps) {
 
   return (
     <>
-      <Badge
+      <Tag
         prepend={<Icon type="tag" fw />}
         closeBtnProps={
           showCloseButton
             ? { 'aria-label': t('delete'), onClick: showConfirmationModal }
             : undefined
         }
-        bsStyle={null}
-        className="badge-bs3 history-version-badge"
+        className="history-version-badge"
         data-testid="history-version-badge"
         {...props}
       >
         {isPseudoCurrentStateLabel
           ? t('history_label_project_current_state')
           : label.comment}
-      </Badge>
+      </Tag>
       {!isPseudoCurrentStateLabel && (
         <AccessibleModal
           show={showDeleteModal}
@@ -167,10 +166,10 @@ function TagTooltip({ label, currentUserId, showTooltip }: LabelBadgesProps) {
       id={label.id}
       overlayProps={{ placement: 'left' }}
     >
-      <Tag label={label} currentUserId={currentUserId} />
+      <ChangeTag label={label} currentUserId={currentUserId} />
     </Tooltip>
   ) : (
-    <Tag label={label} currentUserId={currentUserId} />
+    <ChangeTag label={label} currentUserId={currentUserId} />
   )
 }
 

+ 4 - 9
services/web/frontend/js/features/history/components/file-tree/history-file-tree-item.tsx

@@ -1,7 +1,7 @@
 import classNames from 'classnames'
 import type { ReactNode } from 'react'
 import type { FileOperation } from '../../services/types/file-operation'
-import Badge from '../../../../shared/components/badge'
+import Tag from '@/shared/components/tag'
 
 type FileTreeItemProps = {
   name: string
@@ -25,14 +25,9 @@ export default function HistoryFileTreeItem({
         >
           {name}
         </div>
-        {operation ? (
-          <Badge
-            bsStyle={null}
-            className="badge-bs3 history-file-tree-item-badge"
-          >
-            {operation}
-          </Badge>
-        ) : null}
+        {operation && (
+          <Tag className="history-file-tree-item-badge">{operation}</Tag>
+        )}
       </div>
     </div>
   )

+ 1 - 8
services/web/frontend/js/features/ui/components/bootstrap-5/badge.tsx

@@ -1,12 +1,10 @@
 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'>
   }
 >
 
@@ -14,12 +12,7 @@ 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>
-      )}
+      <span className="badge-content">{children}</span>
     </BSBadge>
   )
 }

+ 42 - 0
services/web/frontend/js/features/ui/components/bootstrap-5/tag.tsx

@@ -0,0 +1,42 @@
+import { useTranslation } from 'react-i18next'
+import { Badge } from 'react-bootstrap-5'
+import MaterialIcon from '@/shared/components/material-icon'
+import { MergeAndOverride } from '../../../../../../types/utils'
+import classnames from 'classnames'
+
+type TagProps = MergeAndOverride<
+  React.ComponentProps<typeof Badge>,
+  {
+    prepend?: React.ReactNode
+    closeBtnProps?: React.ComponentProps<'button'>
+  }
+>
+
+function Tag({
+  prepend,
+  children,
+  closeBtnProps,
+  className,
+  ...rest
+}: TagProps) {
+  const { t } = useTranslation()
+
+  return (
+    <Badge bg="light" className={classnames('badge-tag', className)} {...rest}>
+      {prepend && <span className="badge-prepend">{prepend}</span>}
+      <span className="badge-content">{children}</span>
+      {closeBtnProps && (
+        <button
+          type="button"
+          className="badge-close"
+          aria-label={t('remove_tag', { tagName: children })}
+          {...closeBtnProps}
+        >
+          <MaterialIcon className="badge-close-icon" type="close" />
+        </button>
+      )}
+    </Badge>
+  )
+}
+
+export default Tag

+ 0 - 1
services/web/frontend/js/features/ui/components/ol/ol-badge.tsx

@@ -15,7 +15,6 @@ function OLBadge(props: OLBadgeProps) {
   let bs3BadgeProps: React.ComponentProps<typeof BS3Badge> = {
     prepend: rest.prepend,
     children: rest.children,
-    closeBtnProps: rest.closeBtnProps,
     className: rest.className,
     bsStyle: rest.bg,
   }

+ 28 - 0
services/web/frontend/js/features/ui/components/ol/ol-tag.tsx

@@ -0,0 +1,28 @@
+import Tag from '@/features/ui/components/bootstrap-5/tag'
+import BS3Tag from '@/shared/components/tag'
+import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher'
+
+type OLTagProps = React.ComponentProps<typeof Tag> & {
+  bs3Props?: React.ComponentProps<typeof BS3Tag>
+}
+
+function OLTag(props: OLTagProps) {
+  const { bs3Props, ...rest } = props
+
+  const bs3TagProps: React.ComponentProps<typeof BS3Tag> = {
+    children: rest.children,
+    prepend: rest.prepend,
+    closeBtnProps: rest.closeBtnProps,
+    className: rest.className,
+    ...bs3Props,
+  }
+
+  return (
+    <BootstrapVersionSwitcher
+      bs3={<BS3Tag {...bs3TagProps} />}
+      bs5={<Tag {...rest} />}
+    />
+  )
+}
+
+export default OLTag

+ 4 - 19
services/web/frontend/js/shared/components/badge.tsx

@@ -1,28 +1,18 @@
+import { Label } from 'react-bootstrap'
 import classnames from 'classnames'
 import { MergeAndOverride } from '../../../../types/utils'
-import OLBadge from '@/features/ui/components/ol/ol-badge'
 
 type BadgeProps = MergeAndOverride<
   React.ComponentProps<'span'>,
   {
     prepend?: React.ReactNode
     children: React.ReactNode
-    closeBtnProps?: React.ComponentProps<'button'>
     className?: string
-    bsStyle?: NonNullable<
-      React.ComponentProps<typeof OLBadge>['bs3Props']
-    >['bsStyle']
+    bsStyle?: React.ComponentProps<typeof Label>['bsStyle'] | null
   }
 >
 
-function Badge({
-  prepend,
-  children,
-  closeBtnProps,
-  bsStyle,
-  className,
-  ...rest
-}: BadgeProps) {
+function Badge({ prepend, children, bsStyle, className, ...rest }: BadgeProps) {
   const classNames =
     bsStyle === null
       ? className
@@ -30,13 +20,8 @@ function Badge({
 
   return (
     <span className={classNames} {...rest}>
-      {prepend && <span className="badge-bs3-prepend">{prepend}</span>}
+      {prepend && <span className="badge-tag-bs3-prepend">{prepend}</span>}
       {children}
-      {closeBtnProps && (
-        <button type="button" className="badge-bs3-close" {...closeBtnProps}>
-          <span aria-hidden="true">&times;</span>
-        </button>
-      )}
     </span>
   )
 }

+ 45 - 0
services/web/frontend/js/shared/components/tag.tsx

@@ -0,0 +1,45 @@
+import { useTranslation } from 'react-i18next'
+import { Label } from 'react-bootstrap'
+import { MergeAndOverride } from '../../../../types/utils'
+import classnames from 'classnames'
+
+type TagProps = MergeAndOverride<
+  React.ComponentProps<'span'>,
+  {
+    prepend?: React.ReactNode
+    children: React.ReactNode
+    closeBtnProps?: React.ComponentProps<'button'>
+    className?: string
+    bsStyle?: React.ComponentProps<typeof Label>['bsStyle'] | null
+  }
+>
+
+function Tag({
+  prepend,
+  children,
+  closeBtnProps,
+  bsStyle,
+  className,
+  ...rest
+}: TagProps) {
+  const { t } = useTranslation()
+
+  return (
+    <span className={classnames('badge-tag-bs3', className)} {...rest}>
+      {prepend && <span className="badge-tag-bs3-prepend">{prepend}</span>}
+      <span className="badge-tag-bs3-content">{children}</span>
+      {closeBtnProps && (
+        <button
+          type="button"
+          className="badge-tag-bs3-close"
+          aria-label={t('remove_tag', { tagName: children })}
+          {...closeBtnProps}
+        >
+          <span aria-hidden="true">&times;</span>
+        </button>
+      )}
+    </span>
+  )
+}
+
+export default Tag

+ 13 - 47
services/web/frontend/stories/ui/badge-bs3.stories.tsx

@@ -1,11 +1,10 @@
-import Badge from '@/shared/components/badge'
+import BS3Badge 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> = {
+const meta: Meta<typeof BS3Badge> = {
   title: 'Shared / Components / Badge / Bootstrap 3',
-  component: Badge,
+  component: BS3Badge,
   parameters: {
     bootstrap5: false,
   },
@@ -19,7 +18,7 @@ const meta: Meta<typeof Badge> = {
       },
     },
     bsStyle: {
-      options: [null, 'primary', 'warning', 'danger'],
+      options: ['info', 'primary', 'warning', 'danger'],
       control: { type: 'radio' },
     },
     className: {
@@ -27,67 +26,34 @@ const meta: Meta<typeof Badge> = {
         disable: true,
       },
     },
-    closeBtnProps: {
-      table: {
-        disable: true,
-      },
-    },
   },
 }
 export default meta
 
-type Story = StoryObj<typeof Badge>
+type Story = StoryObj<typeof BS3Badge>
 
 export const BadgeDefault: Story = {
   render: args => {
     return (
-      <Badge
-        className={classnames({ 'badge-bs3': args.bsStyle === null })}
-        {...args}
-      />
+      <div className="small">
+        <BS3Badge {...args} />
+      </div>
     )
   },
 }
 BadgeDefault.args = {
-  bsStyle: null,
+  bsStyle: meta.argTypes!.bsStyle!.options[0],
 }
 
 export const BadgePrepend: Story = {
   render: args => {
     return (
-      <Badge
-        className={classnames({ 'badge-bs3': args.bsStyle === null })}
-        prepend={<Icon type="tag" fw />}
-        {...args}
-      />
+      <div className="small">
+        <BS3Badge prepend={<Icon type="star" fw />} {...args} />
+      </div>
     )
   },
 }
 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,
-    },
-  },
+  bsStyle: meta.argTypes!.bsStyle!.options[0],
 }

+ 3 - 28
services/web/frontend/stories/ui/badge-bs5.stories.tsx

@@ -49,7 +49,7 @@ export const BadgeDefault: Story = {
   },
 }
 BadgeDefault.args = {
-  bg: 'light',
+  bg: meta.argTypes!.bg!.options[0],
 }
 
 export const BadgePrepend: Story = {
@@ -57,37 +57,12 @@ export const BadgePrepend: Story = {
     return (
       <Badge
         className={classnames({ 'text-dark': args.bg === 'light' })}
-        prepend={<Icon type="tag" fw />}
+        prepend={<Icon type="star" 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,
-    },
-  },
+  bg: meta.argTypes!.bg!.options[0],
 }

+ 70 - 0
services/web/frontend/stories/ui/tag-bs3.stories.tsx

@@ -0,0 +1,70 @@
+import Icon from '@/shared/components/icon'
+import BS3Tag from '@/shared/components/tag'
+import type { Meta, StoryObj } from '@storybook/react'
+
+const meta: Meta<typeof BS3Tag> = {
+  title: 'Shared / Components / Tag / Bootstrap 3',
+  component: BS3Tag,
+  parameters: {
+    bootstrap5: false,
+  },
+  args: {
+    children: 'Tag',
+  },
+  argTypes: {
+    prepend: {
+      table: {
+        disable: true,
+      },
+    },
+    className: {
+      table: {
+        disable: true,
+      },
+    },
+    closeBtnProps: {
+      table: {
+        disable: true,
+      },
+    },
+  },
+}
+export default meta
+
+type Story = StoryObj<typeof BS3Tag>
+
+export const TagDefault: Story = {
+  render: args => {
+    return (
+      <div className="small">
+        <BS3Tag {...args} />
+      </div>
+    )
+  },
+}
+
+export const TagPrepend: Story = {
+  render: args => {
+    return (
+      <div className="small">
+        <BS3Tag prepend={<Icon type="tag" fw />} {...args} />
+      </div>
+    )
+  },
+}
+
+export const TagWithCloseButton: Story = {
+  render: args => {
+    return (
+      <div className="small">
+        <BS3Tag
+          prepend={<Icon type="tag" fw />}
+          closeBtnProps={{
+            onClick: () => alert('Close triggered!'),
+          }}
+          {...args}
+        />
+      </div>
+    )
+  },
+}

+ 60 - 0
services/web/frontend/stories/ui/tag-bs5.stories.tsx

@@ -0,0 +1,60 @@
+import Icon from '@/shared/components/icon'
+import Tag from '@/features/ui/components/bootstrap-5/tag'
+import type { Meta, StoryObj } from '@storybook/react'
+
+const meta: Meta<typeof Tag> = {
+  title: 'Shared / Components / Tag / Bootstrap 5',
+  component: Tag,
+  parameters: {
+    bootstrap5: true,
+  },
+  args: {
+    children: 'Tag',
+  },
+  argTypes: {
+    prepend: {
+      table: {
+        disable: true,
+      },
+    },
+    className: {
+      table: {
+        disable: true,
+      },
+    },
+    closeBtnProps: {
+      table: {
+        disable: true,
+      },
+    },
+  },
+}
+export default meta
+
+type Story = StoryObj<typeof Tag>
+
+export const TagDefault: Story = {
+  render: args => {
+    return <Tag {...args} />
+  },
+}
+
+export const TagPrepend: Story = {
+  render: args => {
+    return <Tag prepend={<Icon type="tag" fw />} {...args} />
+  },
+}
+
+export const TagWithCloseButton: Story = {
+  render: args => {
+    return (
+      <Tag
+        prepend={<Icon type="tag" fw />}
+        closeBtnProps={{
+          onClick: () => alert('Close triggered!'),
+        }}
+        {...args}
+      />
+    )
+  },
+}

+ 25 - 9
services/web/frontend/stylesheets/bootstrap-5/components/badge.scss

@@ -1,13 +1,16 @@
 .badge {
   display: inline-flex;
-  align-items: center;
-  overflow: hidden;
+  align-items: stretch;
+  max-width: 100%;
   line-height: var(--line-height-01);
+  padding: 0 var(--bs-badge-padding-x);
 }
 
 .badge-prepend {
   margin-left: calc($spacing-01 / -2);
   margin-right: $spacing-01;
+  display: flex;
+  align-items: center;
 }
 
 .badge-close {
@@ -16,15 +19,14 @@
   display: flex;
   align-items: center;
   justify-content: center;
+  flex-shrink: 0;
 
-  // 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;
+  padding: 0 $spacing-02;
+  margin: 0 (-$spacing-02) 0 $spacing-02;
+  border-top-right-radius: inherit;
+  border-bottom-right-radius: inherit;
   color: inherit;
+  user-select: none;
 
   .badge-close-icon {
     font-size: $font-size-base;
@@ -34,3 +36,17 @@
     background-color: var(--neutral-40);
   }
 }
+
+.badge-content {
+  @include text-truncate;
+  padding: var(--bs-badge-padding-y) 0;
+}
+
+.badge-tag {
+  @include body-sm;
+  color: $dark;
+
+  &:hover {
+    background-color: var(--neutral-30) !important;
+  }
+}

+ 8 - 10
services/web/frontend/stylesheets/components/badge.less

@@ -1,10 +1,10 @@
-.badge-bs3 {
+.badge-tag-bs3 {
   @size: 24px;
   @padding: 4px;
   display: inline-flex;
   align-items: center;
-  overflow: hidden;
   height: @size;
+  max-width: 100%;
   min-height: @size;
   padding: 0 @padding;
   white-space: nowrap;
@@ -26,10 +26,14 @@
     margin-left: 4px;
     font-size: 20px;
     height: @size;
+    line-height: 1;
     display: flex;
     align-items: center;
     justify-content: center;
+    flex-shrink: 0;
     margin-right: -@padding;
+    border-top-right-radius: inherit;
+    border-bottom-right-radius: inherit;
     color: inherit;
 
     &:hover {
@@ -37,13 +41,7 @@
     }
   }
 
-  &-sm {
-    @size-sm: 20px;
-    height: @size-sm;
-    font-size: @font-size-extra-small;
-    .badge-bs3-close {
-      width: @size-sm;
-      font-size: @size-sm;
-    }
+  &-content {
+    .text-overflow();
   }
 }