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

[web] BS5 downshift in account settings page

GitOrigin-RevId: acfbde3ec87ae8038e0b19ddaca35e6c7392743b
This commit is contained in:
ilkin-overleaf 2024-05-20 15:50:31 +03:00 committed by Copybot
parent 2d75652f61
commit f469d8e5e3
7 changed files with 258 additions and 121 deletions

View file

@ -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>

View file

@ -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>

View file

@ -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} />

View file

@ -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>

View file

@ -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<{

View file

@ -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>
)
}

View file

@ -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);
}