Просмотр исходного кода

Merge pull request #18369 from overleaf/ii-bs5-downshift

[web] BS5 downshift in account settings page

GitOrigin-RevId: acfbde3ec87ae8038e0b19ddaca35e6c7392743b
ilkin-overleaf 2 лет назад
Родитель
Сommit
f469d8e5e3

+ 55 - 18
services/web/frontend/js/features/settings/components/emails/add-email/country-input.tsx

@@ -5,6 +5,8 @@ import classnames from 'classnames'
 import countries, { CountryCode } from '../../../data/countries-list'
 import { bsVersion } from '@/features/utils/bootstrap-5'
 import OLFormControl from '@/features/ui/components/ol/ol-form-control'
+import { DropdownItem } from '@/features/ui/components/bootstrap-5/dropdown-menu'
+import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher'
 
 type CountryInputProps = {
   setValue: React.Dispatch<React.SetStateAction<CountryCode | null>>
@@ -46,16 +48,24 @@ function Downshift({ setValue, inputRef }: CountryInputProps) {
     },
   })
 
+  const shouldOpen = isOpen && inputItems.length
+
   return (
     <div
       className={classnames(
-        'ui-select-container ui-select-bootstrap dropdown',
-        {
-          open: isOpen && inputItems.length,
-        }
+        'dropdown',
+        bsVersion({
+          bs5: 'd-block',
+          bs3: classnames('ui-select-container ui-select-bootstrap', {
+            open: shouldOpen,
+          }),
+        })
       )}
     >
-      <div {...getComboboxProps()} className="ui-select-toggle">
+      <div
+        {...getComboboxProps()}
+        className={bsVersion({ bs3: 'ui-select-toggle' })}
+      >
         {/* eslint-disable-next-line jsx-a11y/label-has-for */}
         <label
           {...getLabelProps()}
@@ -81,25 +91,52 @@ function Downshift({ setValue, inputRef }: CountryInputProps) {
       </div>
       <ul
         {...getMenuProps()}
-        className="ui-select-choices ui-select-choices-content ui-select-dropdown dropdown-menu"
+        className={classnames(
+          'dropdown-menu',
+          bsVersion({
+            bs5: classnames('select-dropdown-menu', { show: shouldOpen }),
+            bs3: 'ui-select-choices ui-select-choices-content ui-select-dropdown',
+          })
+        )}
       >
         {inputItems.map((item, index) => (
+          // eslint-disable-next-line jsx-a11y/role-supports-aria-props
           <li
-            className="ui-select-choices-group"
+            className={bsVersion({ bs3: 'ui-select-choices-group' })}
             key={`${item.name}-${index}`}
             {...getItemProps({ item, index })}
+            aria-selected={selectedItem?.name === item.name}
           >
-            <div
-              className={classnames('ui-select-choices-row', {
-                active: selectedItem?.name === item.name,
-                'ui-select-choices-row--highlighted':
-                  highlightedIndex === index,
-              })}
-            >
-              <span className="ui-select-choices-row-inner">
-                <span>{item.name}</span>
-              </span>
-            </div>
+            <BootstrapVersionSwitcher
+              bs3={
+                <div
+                  className={classnames('ui-select-choices-row', {
+                    active: selectedItem?.name === item.name,
+                    'ui-select-choices-row--highlighted':
+                      highlightedIndex === index,
+                  })}
+                >
+                  <span className="ui-select-choices-row-inner">
+                    <span>{item.name}</span>
+                  </span>
+                </div>
+              }
+              bs5={
+                <DropdownItem
+                  as="span"
+                  role={undefined}
+                  className={classnames({
+                    active: selectedItem?.name === item.name,
+                    'dropdown-item--highlighted': highlightedIndex === index,
+                  })}
+                  trailingIcon={
+                    selectedItem?.name === item.name ? 'check' : undefined
+                  }
+                >
+                  {item.name}
+                </DropdownItem>
+              }
+            />
           </li>
         ))}
       </ul>

+ 59 - 18
services/web/frontend/js/features/settings/components/emails/downshift-input.tsx

@@ -4,6 +4,8 @@ import classnames from 'classnames'
 import { escapeRegExp } from 'lodash'
 import { bsVersion } from '@/features/utils/bootstrap-5'
 import OLFormControl from '@/features/ui/components/ol/ol-form-control'
+import { DropdownItem } from '@/features/ui/components/bootstrap-5/dropdown-menu'
+import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher'
 
 type DownshiftInputProps = {
   highlightMatches?: boolean
@@ -77,13 +79,18 @@ function Downshift({
     )
   }
 
+  const shouldOpen = isOpen && inputItems.length
+
   return (
     <div
       className={classnames(
-        'ui-select-container ui-select-bootstrap dropdown',
-        {
-          open: isOpen && inputItems.length,
-        }
+        'dropdown',
+        bsVersion({
+          bs5: 'd-block',
+          bs3: classnames('ui-select-container ui-select-bootstrap', {
+            open: shouldOpen,
+          }),
+        })
       )}
     >
       <div {...getComboboxProps()}>
@@ -116,28 +123,62 @@ function Downshift({
       </div>
       <ul
         {...getMenuProps()}
-        className="ui-select-choices ui-select-choices-content ui-select-dropdown dropdown-menu"
+        className={classnames(
+          'dropdown-menu',
+          bsVersion({
+            bs5: classnames('select-dropdown-menu', { show: shouldOpen }),
+            bs3: 'ui-select-choices ui-select-choices-content ui-select-dropdown',
+          })
+        )}
       >
         {showSuggestedText && inputItems.length && (
-          <li className="ui-select-title">{itemsTitle}</li>
+          <BootstrapVersionSwitcher
+            bs3={<li className="ui-select-title">{itemsTitle}</li>}
+            bs5={
+              <li>
+                <DropdownItem as="span" role={undefined} disabled>
+                  {itemsTitle}
+                </DropdownItem>
+              </li>
+            }
+          />
         )}
         {inputItems.map((item, index) => (
+          // eslint-disable-next-line jsx-a11y/role-supports-aria-props
           <li
-            className="ui-select-choices-group"
+            className={bsVersion({ bs3: 'ui-select-choices-group' })}
             key={`${item}${index}`}
             {...getItemProps({ item, index })}
+            aria-selected={selectedItem === item}
           >
-            <div
-              className={classnames('ui-select-choices-row', {
-                active: selectedItem === item,
-                'ui-select-choices-row--highlighted':
-                  highlightedIndex === index,
-              })}
-            >
-              <span className="ui-select-choices-row-inner">
-                <span>{highlightMatchedCharacters(item, inputValue)}</span>
-              </span>
-            </div>
+            <BootstrapVersionSwitcher
+              bs3={
+                <div
+                  className={classnames('ui-select-choices-row', {
+                    active: selectedItem === item,
+                    'ui-select-choices-row--highlighted':
+                      highlightedIndex === index,
+                  })}
+                >
+                  <span className="ui-select-choices-row-inner">
+                    <span>{highlightMatchedCharacters(item, inputValue)}</span>
+                  </span>
+                </div>
+              }
+              bs5={
+                <DropdownItem
+                  as="span"
+                  role={undefined}
+                  className={classnames({
+                    active: selectedItem === item,
+                    'dropdown-item--highlighted': highlightedIndex === index,
+                  })}
+                  trailingIcon={selectedItem === item ? 'check' : undefined}
+                >
+                  {highlightMatchedCharacters(item, inputValue)}
+                </DropdownItem>
+              }
+            />
           </li>
         ))}
       </ul>

+ 16 - 15
services/web/frontend/js/features/ui/components/bootstrap-5/dropdown-menu.tsx

@@ -1,4 +1,4 @@
-import React from 'react'
+import React, { forwardRef } from 'react'
 import {
   Dropdown as BS5Dropdown,
   DropdownToggle as BS5DropdownToggle,
@@ -18,22 +18,22 @@ export function Dropdown({ ...props }: DropdownProps) {
   return <BS5Dropdown {...props} />
 }
 
-export function DropdownItem({
-  active,
-  children,
-  description,
-  leadingIcon,
-  trailingIcon,
-  ...props
-}: DropdownItemProps) {
-  const trailingIconType = active ? 'check' : trailingIcon
-  return (
-    <li>
+export const DropdownItem = forwardRef<
+  typeof BS5DropdownItem,
+  DropdownItemProps
+>(
+  (
+    { active, children, description, leadingIcon, trailingIcon, ...props },
+    ref
+  ) => {
+    const trailingIconType = active ? 'check' : trailingIcon
+    return (
       <BS5DropdownItem
         active={active}
         className={description ? 'dropdown-item-description-container' : ''}
         role="menuitem"
         {...props}
+        ref={ref}
       >
         {leadingIcon && (
           <MaterialIcon
@@ -52,9 +52,10 @@ export function DropdownItem({
           <span className="dropdown-item-description">{description}</span>
         )}
       </BS5DropdownItem>
-    </li>
-  )
-}
+    )
+  }
+)
+DropdownItem.displayName = 'DropdownItem'
 
 export function DropdownToggle({ ...props }: DropdownToggleProps) {
   return <BS5DropdownToggle {...props} />

+ 3 - 3
services/web/frontend/js/features/ui/components/bootstrap-5/split-button.tsx

@@ -40,9 +40,9 @@ export function SplitButton({
         </DropdownToggle>
         <DropdownMenu>
           {items.map((item, index) => (
-            <DropdownItem key={index} eventKey={item.eventKey}>
-              {item.label}
-            </DropdownItem>
+            <li key={index}>
+              <DropdownItem eventKey={item.eventKey}>{item.label}</DropdownItem>
+            </li>
           ))}
         </DropdownMenu>
       </Dropdown>

+ 3 - 1
services/web/frontend/js/features/ui/components/types/dropdown-menu-props.ts

@@ -22,12 +22,14 @@ export type DropdownItemProps = PropsWithChildren<{
   as?: ElementType
   description?: string
   disabled?: boolean
-  eventKey: string | number
+  eventKey?: string | number
   href?: string
   leadingIcon?: string
   onClick?: () => void
   trailingIcon?: string
   variant?: 'default' | 'danger'
+  className?: string
+  role?: string
 }>
 
 export type DropdownToggleProps = PropsWithChildren<{

+ 109 - 66
services/web/frontend/stories/ui/dropdown-menu.stories.tsx

@@ -10,16 +10,22 @@ 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>
+      <li>
+        <DropdownItem eventKey="1" href="#/action-1">
+          Example
+        </DropdownItem>
+      </li>
+      <li>
+        <DropdownItem eventKey="2" href="#/action-2">
+          Example
+        </DropdownItem>
+      </li>
       <DropdownDivider />
-      <DropdownItem eventKey="3" disabled={args.disabled} href="#/action-3">
-        Example
-      </DropdownItem>
+      <li>
+        <DropdownItem eventKey="3" disabled={args.disabled} href="#/action-3">
+          Example
+        </DropdownItem>
+      </li>
     </DropdownMenu>
   )
 }
@@ -27,16 +33,27 @@ export const Default = (args: Args) => {
 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>
+      <li>
+        <DropdownItem eventKey="1" href="#/action-1">
+          Example
+        </DropdownItem>
+      </li>
+      <li>
+        <DropdownItem
+          eventKey="2"
+          active
+          href="#/action-2"
+          trailingIcon="check"
+        >
+          Example
+        </DropdownItem>
+      </li>
       <DropdownDivider />
-      <DropdownItem eventKey="3" disabled={args.disabled} href="#/action-3">
-        Example
-      </DropdownItem>
+      <li>
+        <DropdownItem eventKey="3" disabled={args.disabled} href="#/action-3">
+          Example
+        </DropdownItem>
+      </li>
     </DropdownMenu>
   )
 }
@@ -44,16 +61,22 @@ export const Active = (args: Args) => {
 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>
+      <li>
+        <DropdownItem eventKey="1" disabled={args.disabled} href="#/action-1">
+          Example
+        </DropdownItem>
+      </li>
+      <li>
+        <DropdownItem eventKey="2" href="#/action-2">
+          Example
+        </DropdownItem>
+      </li>
       <DropdownDivider />
-      <DropdownItem eventKey="3" href="#/action-3" variant="danger">
-        Example
-      </DropdownItem>
+      <li>
+        <DropdownItem eventKey="3" href="#/action-3" variant="danger">
+          Example
+        </DropdownItem>
+      </li>
     </DropdownMenu>
   )
 }
@@ -61,23 +84,27 @@ export const Danger = (args: Args) => {
 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>
+      <li>
+        <DropdownItem
+          disabled={args.disabled}
+          eventKey="1"
+          href="#/action-1"
+          description="Description of the menu"
+        >
+          Example
+        </DropdownItem>
+      </li>
+      <li>
+        <DropdownItem
+          active
+          eventKey="2"
+          href="#/action-2"
+          description="Description of the menu"
+          trailingIcon="check"
+        >
+          Example
+        </DropdownItem>
+      </li>
     </DropdownMenu>
   )
 }
@@ -85,28 +112,44 @@ export const Description = (args: Args) => {
 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>
+      <li>
+        <DropdownItem
+          disabled={args.disabled}
+          eventKey="1"
+          href="#/action-1"
+          leadingIcon="view_column_2"
+        >
+          Editor & PDF
+        </DropdownItem>
+      </li>
+      <li>
+        <DropdownItem
+          active
+          eventKey="2"
+          href="#/action-2"
+          leadingIcon="terminal"
+        >
+          Editor only
+        </DropdownItem>
+      </li>
+      <li>
+        <DropdownItem
+          eventKey="2"
+          href="#/action-2"
+          leadingIcon="picture_as_pdf"
+        >
+          PDF only
+        </DropdownItem>
+      </li>
+      <li>
+        <DropdownItem
+          eventKey="2"
+          href="#/action-2"
+          leadingIcon="select_window"
+        >
+          PDF in separate tab
+        </DropdownItem>
+      </li>
     </DropdownMenu>
   )
 }

+ 13 - 0
services/web/frontend/stylesheets/bootstrap-5/components/dropdown-menu.scss

@@ -113,3 +113,16 @@
     border-left: 1px solid var(--neutral-10);
   }
 }
+
+.select-dropdown-menu {
+  top: 100%;
+  margin-top: var(--spacing-04);
+  width: 100%;
+  max-height: 200px;
+  overflow: auto;
+  overflow-x: hidden;
+}
+
+.dropdown-item--highlighted {
+  background-color: var(--bg-light-secondary);
+}