mirror of
https://github.com/yu-i-i/overleaf-cep.git
synced 2026-11-15 06:33:55 +01:00
Merge intermediate component wrappers into OL-prefixed components (#34996)
* Merge intermediate component wrappers into OL-prefixed components * Replace all intermediate wrapper consumers with OL-prefixed components directly * Fix jsx/js files still importing deleted shim components * Remove duplicate OLNotification imports left by first pass * Rename OLTagIcon to TagIcon, remove OL prefix * Rename OLDropdownMenuItem to DropdownMenuItem, remove OL prefix * Remove OLNotification, use Notification directly * Move Notification to the main shared component folder * Update tag-icon import path out of ol/ * Move TagIcon to the main shared component folder * Update dropdown-menu-item import path out of ol/ * Move DropdownMenuItem to the main shared component folder * Fix dropdown-menu-item import path to dropdown/ subfolder * Remove BS5 alias prefixes from shared components * Restore notification-list wrapper in Notification component * Add translation for loading button * Remove redundant import aliasing * Resolve conflicts * Restore notification container `notification-list` GitOrigin-RevId: 24a5af18599a3e02ac6fd61c09cf5a49ef6e0020
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import classNames from 'classnames'
|
||||
import type { MergeAndOverride } from '../../../../../types/utils'
|
||||
import Badge, { type BadgeProps } from './badge'
|
||||
import Badge, { type OLBadgeProps as BadgeProps } from '../ol/ol-badge'
|
||||
|
||||
export type BadgeLinkProps = MergeAndOverride<
|
||||
BadgeProps,
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
import { Badge as BSBadge, BadgeProps as BSBadgeProps } from 'react-bootstrap'
|
||||
import { MergeAndOverride } from '../../../../../types/utils'
|
||||
|
||||
export type BadgeProps = MergeAndOverride<
|
||||
BSBadgeProps,
|
||||
{
|
||||
prepend?: React.ReactNode
|
||||
badgeContentRef?: React.RefObject<HTMLElement>
|
||||
}
|
||||
>
|
||||
|
||||
function Badge({ prepend, children, badgeContentRef, ...rest }: BadgeProps) {
|
||||
return (
|
||||
<BSBadge {...rest}>
|
||||
{prepend && <span className="badge-prepend">{prepend}</span>}
|
||||
<span className="badge-content" ref={badgeContentRef}>
|
||||
{children}
|
||||
</span>
|
||||
</BSBadge>
|
||||
)
|
||||
}
|
||||
|
||||
export default Badge
|
||||
@@ -1,77 +0,0 @@
|
||||
import { forwardRef } from 'react'
|
||||
import { Button as BS5Button } from 'react-bootstrap'
|
||||
import type { ButtonProps } from '@/shared/components/types/button-props'
|
||||
import classNames from 'classnames'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import MaterialIcon from '@/shared/components/material-icon'
|
||||
import OLSpinner from '../ol/ol-spinner'
|
||||
|
||||
const Button = forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
(
|
||||
{
|
||||
children,
|
||||
className,
|
||||
leadingIcon,
|
||||
isLoading = false,
|
||||
loadingLabel,
|
||||
trailingIcon,
|
||||
variant = 'primary',
|
||||
...props
|
||||
},
|
||||
ref
|
||||
) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const buttonClassName = classNames('d-inline-grid', className, {
|
||||
'button-loading': isLoading,
|
||||
})
|
||||
|
||||
const loadingSpinnerClassName =
|
||||
props.size === 'lg' ? 'loading-spinner-large' : 'loading-spinner-small'
|
||||
const materialIconClassName =
|
||||
props.size === 'lg' ? 'icon-large' : 'icon-small'
|
||||
|
||||
const leadingIconComponent =
|
||||
leadingIcon && typeof leadingIcon === 'string' ? (
|
||||
<MaterialIcon type={leadingIcon} className={materialIconClassName} />
|
||||
) : (
|
||||
leadingIcon
|
||||
)
|
||||
|
||||
const trailingIconComponent =
|
||||
trailingIcon && typeof trailingIcon === 'string' ? (
|
||||
<MaterialIcon type={trailingIcon} className={materialIconClassName} />
|
||||
) : (
|
||||
trailingIcon
|
||||
)
|
||||
|
||||
return (
|
||||
<BS5Button
|
||||
className={buttonClassName}
|
||||
variant={variant}
|
||||
{...props}
|
||||
ref={ref}
|
||||
disabled={isLoading || props.disabled}
|
||||
data-ol-loading={isLoading}
|
||||
role={undefined}
|
||||
>
|
||||
{isLoading && (
|
||||
<span className="spinner-container">
|
||||
<OLSpinner size="sm" className={loadingSpinnerClassName} />
|
||||
<span className="visually-hidden">
|
||||
{loadingLabel ?? t('loading')}
|
||||
</span>
|
||||
</span>
|
||||
)}
|
||||
<span className="button-content" aria-hidden={isLoading}>
|
||||
{leadingIconComponent}
|
||||
{children}
|
||||
{trailingIconComponent}
|
||||
</span>
|
||||
</BS5Button>
|
||||
)
|
||||
}
|
||||
)
|
||||
Button.displayName = 'Button'
|
||||
|
||||
export default Button
|
||||
@@ -1,46 +0,0 @@
|
||||
import { forwardRef } from 'react'
|
||||
import classNames from 'classnames'
|
||||
import MaterialIcon from '@/shared/components/material-icon'
|
||||
import Button from './button'
|
||||
import type { IconButtonProps } from '@/shared/components/types/icon-button-props'
|
||||
|
||||
const IconButton = forwardRef<HTMLButtonElement, IconButtonProps>(
|
||||
(
|
||||
{
|
||||
accessibilityLabel,
|
||||
icon,
|
||||
isLoading = false,
|
||||
size,
|
||||
className,
|
||||
unfilled,
|
||||
...props
|
||||
},
|
||||
ref
|
||||
) => {
|
||||
const iconButtonClassName = classNames(className, {
|
||||
'icon-button': !size,
|
||||
'icon-button-small': size === 'sm',
|
||||
'icon-button-large': size === 'lg',
|
||||
})
|
||||
const iconSizeClassName = size === 'lg' ? 'icon-large' : 'icon-small'
|
||||
const materialIconClassName = classNames(iconSizeClassName, {
|
||||
'button-content-hidden': isLoading,
|
||||
unfilled,
|
||||
})
|
||||
|
||||
return (
|
||||
<Button
|
||||
className={iconButtonClassName}
|
||||
isLoading={isLoading}
|
||||
aria-label={accessibilityLabel}
|
||||
{...props}
|
||||
ref={ref}
|
||||
>
|
||||
<MaterialIcon className={materialIconClassName} type={icon} />
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
)
|
||||
IconButton.displayName = 'IconButton'
|
||||
|
||||
export default IconButton
|
||||
+2
-2
@@ -3,7 +3,7 @@ import { DropdownItemProps } from '@/shared/components/types/dropdown-menu-props
|
||||
import DropdownListItem from '@/shared/components/dropdown/dropdown-list-item'
|
||||
|
||||
// This represents a menu item. It wraps the item within an <li> element.
|
||||
function OLDropdownMenuItem(props: DropdownItemProps) {
|
||||
function DropdownMenuItem(props: DropdownItemProps) {
|
||||
return (
|
||||
<DropdownListItem>
|
||||
<DropdownItem {...props} />
|
||||
@@ -11,4 +11,4 @@ function OLDropdownMenuItem(props: DropdownItemProps) {
|
||||
)
|
||||
}
|
||||
|
||||
export default OLDropdownMenuItem
|
||||
export default DropdownMenuItem
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Form } from 'react-bootstrap'
|
||||
import FormText from '@/shared/components/form/form-text'
|
||||
import OLFormText from '@/shared/components/ol/ol-form-text'
|
||||
import { ComponentProps } from 'react'
|
||||
|
||||
export type FormFeedbackProps = Pick<
|
||||
@@ -10,9 +10,9 @@ export type FormFeedbackProps = Pick<
|
||||
function DSFormFeedback(props: FormFeedbackProps) {
|
||||
return (
|
||||
<Form.Control.Feedback {...props}>
|
||||
<FormText type={props.type === 'invalid' ? 'error' : 'success'}>
|
||||
<OLFormText type={props.type === 'invalid' ? 'error' : 'success'}>
|
||||
{props.children}
|
||||
</FormText>
|
||||
</OLFormText>
|
||||
</Form.Control.Feedback>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
import { FC, ReactNode } from 'react'
|
||||
import { DefaultMessage } from './default-message'
|
||||
import OLNotification from '@/shared/components/ol/ol-notification'
|
||||
import Notification from '@/shared/components/notification'
|
||||
|
||||
export const ErrorBoundaryFallback: FC<
|
||||
React.PropsWithChildren<{ modal?: ReactNode }>
|
||||
> = ({ children, modal }) => {
|
||||
return (
|
||||
<div className="error-boundary-alert">
|
||||
<OLNotification type="error" content={children || <DefaultMessage />} />
|
||||
<div className="notification-list">
|
||||
<Notification type="error" content={children || <DefaultMessage />} />
|
||||
</div>
|
||||
{modal}
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -1,45 +0,0 @@
|
||||
import React, { forwardRef } from 'react'
|
||||
import { Form, FormControlProps as BS5FormControlProps } from 'react-bootstrap'
|
||||
import classnames from 'classnames'
|
||||
|
||||
export type OLBS5FormControlProps = BS5FormControlProps & {
|
||||
prepend?: React.ReactNode
|
||||
append?: React.ReactNode
|
||||
rows?: number
|
||||
}
|
||||
|
||||
const FormControl = forwardRef<HTMLInputElement, OLBS5FormControlProps>(
|
||||
({ prepend, append, className, ...props }, ref) => {
|
||||
if (prepend || append) {
|
||||
const wrapperClassNames = classnames('form-control-wrapper', {
|
||||
'form-control-wrapper-sm': props.size === 'sm',
|
||||
'form-control-wrapper-lg': props.size === 'lg',
|
||||
'form-control-wrapper-disabled': props.disabled,
|
||||
})
|
||||
|
||||
const formControlClassNames = classnames(className, {
|
||||
'form-control-offset-start': prepend,
|
||||
'form-control-offset-end': append,
|
||||
})
|
||||
|
||||
return (
|
||||
<div className={wrapperClassNames}>
|
||||
{prepend && (
|
||||
<span className="form-control-start-icon">{prepend}</span>
|
||||
)}
|
||||
<Form.Control
|
||||
{...props}
|
||||
className={formControlClassNames}
|
||||
ref={ref}
|
||||
/>
|
||||
{append && <span className="form-control-end-icon">{append}</span>}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return <Form.Control ref={ref} className={className} {...props} />
|
||||
}
|
||||
)
|
||||
FormControl.displayName = 'FormControl'
|
||||
|
||||
export default FormControl
|
||||
@@ -1,23 +0,0 @@
|
||||
import { Form } from 'react-bootstrap'
|
||||
import FormText from '@/shared/components/form/form-text'
|
||||
import { ComponentProps } from 'react'
|
||||
|
||||
export type FormFeedbackProps = Pick<
|
||||
ComponentProps<typeof Form.Control.Feedback>,
|
||||
'type' | 'className' | 'children'
|
||||
> & { unfilled?: boolean }
|
||||
|
||||
function FormFeedback({ unfilled, ...props }: FormFeedbackProps) {
|
||||
return (
|
||||
<Form.Control.Feedback {...props}>
|
||||
<FormText
|
||||
type={props.type === 'invalid' ? 'error' : 'success'}
|
||||
unfilled={unfilled}
|
||||
>
|
||||
{props.children}
|
||||
</FormText>
|
||||
</Form.Control.Feedback>
|
||||
)
|
||||
}
|
||||
|
||||
export default FormFeedback
|
||||
@@ -1,14 +0,0 @@
|
||||
import { forwardRef } from 'react'
|
||||
import { FormGroup as BS5FormGroup, FormGroupProps } from 'react-bootstrap'
|
||||
import classnames from 'classnames'
|
||||
|
||||
const FormGroup = forwardRef<typeof BS5FormGroup, FormGroupProps>(
|
||||
({ className, ...props }, ref) => {
|
||||
const classNames = classnames('form-group', className)
|
||||
|
||||
return <BS5FormGroup className={classNames} {...props} ref={ref} />
|
||||
}
|
||||
)
|
||||
FormGroup.displayName = 'FormGroup'
|
||||
|
||||
export default FormGroup
|
||||
@@ -1,81 +0,0 @@
|
||||
import { Form, FormTextProps as BS5FormTextProps } from 'react-bootstrap'
|
||||
import MaterialIcon from '@/shared/components/material-icon'
|
||||
import classnames from 'classnames'
|
||||
import { MergeAndOverride } from '../../../../../types/utils'
|
||||
|
||||
type TextType = 'default' | 'info' | 'success' | 'warning' | 'error'
|
||||
|
||||
export type FormTextProps = MergeAndOverride<
|
||||
BS5FormTextProps,
|
||||
{
|
||||
type?: TextType
|
||||
unfilled?: boolean
|
||||
marginless?: boolean
|
||||
}
|
||||
>
|
||||
|
||||
const typeClassMap: Partial<Record<TextType, string>> = {
|
||||
error: 'text-danger',
|
||||
success: 'text-success',
|
||||
warning: 'text-warning',
|
||||
}
|
||||
|
||||
export const getFormTextClass = (type?: TextType) =>
|
||||
typeClassMap[type || 'default']
|
||||
|
||||
function FormTextIcon({
|
||||
type,
|
||||
unfilled,
|
||||
}: Pick<FormTextProps, 'type' | 'unfilled'>) {
|
||||
switch (type) {
|
||||
case 'info':
|
||||
return unfilled ? (
|
||||
<MaterialIcon type="info" className="text-info" unfilled />
|
||||
) : (
|
||||
<MaterialIcon type="info" className="text-info" />
|
||||
)
|
||||
case 'success':
|
||||
return unfilled ? (
|
||||
<MaterialIcon type="check_circle" unfilled />
|
||||
) : (
|
||||
<MaterialIcon type="check_circle" />
|
||||
)
|
||||
case 'warning':
|
||||
return unfilled ? (
|
||||
<MaterialIcon type="warning" unfilled />
|
||||
) : (
|
||||
<MaterialIcon type="warning" />
|
||||
)
|
||||
case 'error':
|
||||
return unfilled ? (
|
||||
<MaterialIcon type="error" unfilled />
|
||||
) : (
|
||||
<MaterialIcon type="error" />
|
||||
)
|
||||
default:
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
function FormText({
|
||||
type = 'default',
|
||||
marginless,
|
||||
children,
|
||||
className,
|
||||
unfilled,
|
||||
...rest
|
||||
}: FormTextProps) {
|
||||
return (
|
||||
<Form.Text
|
||||
className={classnames(className, getFormTextClass(type), { marginless })}
|
||||
{...rest}
|
||||
>
|
||||
<span className="form-text-inner">
|
||||
<FormTextIcon type={type} unfilled={unfilled} />
|
||||
<span>{children}</span>
|
||||
</span>
|
||||
</Form.Text>
|
||||
)
|
||||
}
|
||||
|
||||
export default FormText
|
||||
@@ -4,7 +4,7 @@ import OLBadge from '@/shared/components/ol/ol-badge'
|
||||
import { postJSON } from '@/infrastructure/fetch-json'
|
||||
import OLButton from '@/shared/components/ol/ol-button'
|
||||
import getMeta from '@/utils/meta'
|
||||
import Notification from '../notification'
|
||||
import Notification from '@/shared/components/notification'
|
||||
import { LabsEnableButton } from '@/shared/components/labs/labs-enable-button'
|
||||
|
||||
export type LabsExperimentWidgetProps = {
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import classNames from 'classnames'
|
||||
import React, { ReactElement, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import MaterialIcon from './material-icon'
|
||||
import MaterialIcon from '@/shared/components/material-icon'
|
||||
|
||||
export type NotificationType =
|
||||
| 'info'
|
||||
@@ -62,7 +62,7 @@ export function NotificationIcon({
|
||||
)
|
||||
}
|
||||
|
||||
function Notification({
|
||||
export default function Notification({
|
||||
action,
|
||||
ariaLive,
|
||||
className = '',
|
||||
@@ -140,5 +140,3 @@ function Notification({
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Notification
|
||||
|
||||
@@ -1,7 +1,16 @@
|
||||
import Badge from '@/shared/components/badge/badge'
|
||||
import { Badge as BSBadge, BadgeProps as BSBadgeProps } from 'react-bootstrap'
|
||||
import { MergeAndOverride } from '../../../../../types/utils'
|
||||
|
||||
function OLBadge(props: React.ComponentProps<typeof Badge>) {
|
||||
let { bg, text, ...rest } = props
|
||||
export type OLBadgeProps = MergeAndOverride<
|
||||
BSBadgeProps,
|
||||
{
|
||||
prepend?: React.ReactNode
|
||||
badgeContentRef?: React.RefObject<HTMLElement>
|
||||
}
|
||||
>
|
||||
|
||||
function OLBadge(props: OLBadgeProps) {
|
||||
let { bg, text, prepend, children, badgeContentRef, ...rest } = props
|
||||
|
||||
// For warning badges, use a light background by default. We still want the
|
||||
// Bootstrap warning colour to be dark for text though, so make an
|
||||
@@ -11,7 +20,14 @@ function OLBadge(props: React.ComponentProps<typeof Badge>) {
|
||||
text = 'warning'
|
||||
}
|
||||
|
||||
return <Badge bg={bg} text={text} {...rest} />
|
||||
return (
|
||||
<BSBadge bg={bg} text={text} {...rest}>
|
||||
{prepend && <span className="badge-prepend">{prepend}</span>}
|
||||
<span className="badge-content" ref={badgeContentRef}>
|
||||
{children}
|
||||
</span>
|
||||
</BSBadge>
|
||||
)
|
||||
}
|
||||
|
||||
export default OLBadge
|
||||
|
||||
@@ -1,12 +1,79 @@
|
||||
import { forwardRef } from 'react'
|
||||
import { Button } from 'react-bootstrap'
|
||||
import type { ButtonProps } from '@/shared/components/types/button-props'
|
||||
import Button from '../button/button'
|
||||
import classNames from 'classnames'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import MaterialIcon from '@/shared/components/material-icon'
|
||||
import OLSpinner from './ol-spinner'
|
||||
|
||||
export type OLButtonProps = ButtonProps
|
||||
|
||||
const OLButton = forwardRef<HTMLButtonElement, OLButtonProps>((props, ref) => {
|
||||
return <Button {...props} ref={ref} />
|
||||
})
|
||||
const OLButton = forwardRef<HTMLButtonElement, OLButtonProps>(
|
||||
(
|
||||
{
|
||||
children,
|
||||
className,
|
||||
leadingIcon,
|
||||
isLoading = false,
|
||||
loadingLabel,
|
||||
trailingIcon,
|
||||
variant = 'primary',
|
||||
...props
|
||||
},
|
||||
ref
|
||||
) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const buttonClassName = classNames('d-inline-grid', className, {
|
||||
'button-loading': isLoading,
|
||||
})
|
||||
|
||||
const loadingSpinnerClassName =
|
||||
props.size === 'lg' ? 'loading-spinner-large' : 'loading-spinner-small'
|
||||
const materialIconClassName =
|
||||
props.size === 'lg' ? 'icon-large' : 'icon-small'
|
||||
|
||||
const leadingIconComponent =
|
||||
leadingIcon && typeof leadingIcon === 'string' ? (
|
||||
<MaterialIcon type={leadingIcon} className={materialIconClassName} />
|
||||
) : (
|
||||
leadingIcon
|
||||
)
|
||||
|
||||
const trailingIconComponent =
|
||||
trailingIcon && typeof trailingIcon === 'string' ? (
|
||||
<MaterialIcon type={trailingIcon} className={materialIconClassName} />
|
||||
) : (
|
||||
trailingIcon
|
||||
)
|
||||
|
||||
return (
|
||||
<Button
|
||||
className={buttonClassName}
|
||||
variant={variant}
|
||||
{...props}
|
||||
ref={ref}
|
||||
disabled={isLoading || props.disabled}
|
||||
data-ol-loading={isLoading}
|
||||
role={undefined}
|
||||
>
|
||||
{isLoading && (
|
||||
<span className="spinner-container">
|
||||
<OLSpinner size="sm" className={loadingSpinnerClassName} />
|
||||
<span className="visually-hidden">
|
||||
{loadingLabel ?? t('loading')}
|
||||
</span>
|
||||
</span>
|
||||
)}
|
||||
<span className="button-content" aria-hidden={isLoading}>
|
||||
{leadingIconComponent}
|
||||
{children}
|
||||
{trailingIconComponent}
|
||||
</span>
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
OLButton.displayName = 'OLButton'
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Form, FormCheckProps } from 'react-bootstrap'
|
||||
import { MergeAndOverride } from '../../../../../types/utils'
|
||||
import FormText from '../form/form-text'
|
||||
import { FormText } from './ol-form-text'
|
||||
|
||||
type OLFormCheckboxProps = MergeAndOverride<
|
||||
FormCheckProps,
|
||||
|
||||
@@ -1,26 +1,51 @@
|
||||
import { forwardRef } from 'react'
|
||||
import FormControl, {
|
||||
type OLBS5FormControlProps,
|
||||
} from '@/shared/components/form/form-control'
|
||||
import OLSpinner from '@/shared/components/ol/ol-spinner'
|
||||
import React, { forwardRef } from 'react'
|
||||
import { Form, FormControlProps as FormControlProps } from 'react-bootstrap'
|
||||
import classnames from 'classnames'
|
||||
import OLSpinner from './ol-spinner'
|
||||
|
||||
type OLFormControlProps = OLBS5FormControlProps & {
|
||||
export type OLFormControlProps = FormControlProps & {
|
||||
prepend?: React.ReactNode
|
||||
append?: React.ReactNode
|
||||
rows?: number
|
||||
'data-ol-dirty'?: unknown
|
||||
'main-field'?: any // For the CM6's benefit in the editor search panel
|
||||
loading?: boolean
|
||||
}
|
||||
|
||||
const OLFormControl = forwardRef<HTMLInputElement, OLFormControlProps>(
|
||||
(props, ref) => {
|
||||
const { append, ...rest } = props
|
||||
({ prepend, append, loading, className, ...props }, ref) => {
|
||||
const resolvedAppend = loading ? <OLSpinner size="sm" /> : append
|
||||
|
||||
return (
|
||||
<FormControl
|
||||
ref={ref}
|
||||
{...rest}
|
||||
append={rest.loading ? <OLSpinner size="sm" /> : append}
|
||||
/>
|
||||
)
|
||||
if (prepend || resolvedAppend) {
|
||||
const wrapperClassNames = classnames('form-control-wrapper', {
|
||||
'form-control-wrapper-sm': props.size === 'sm',
|
||||
'form-control-wrapper-lg': props.size === 'lg',
|
||||
'form-control-wrapper-disabled': props.disabled,
|
||||
})
|
||||
|
||||
const formControlClassNames = classnames(className, {
|
||||
'form-control-offset-start': prepend,
|
||||
'form-control-offset-end': resolvedAppend,
|
||||
})
|
||||
|
||||
return (
|
||||
<div className={wrapperClassNames}>
|
||||
{prepend && (
|
||||
<span className="form-control-start-icon">{prepend}</span>
|
||||
)}
|
||||
<Form.Control
|
||||
{...props}
|
||||
className={formControlClassNames}
|
||||
ref={ref}
|
||||
/>
|
||||
{resolvedAppend && (
|
||||
<span className="form-control-end-icon">{resolvedAppend}</span>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return <Form.Control ref={ref} className={className} {...props} />
|
||||
}
|
||||
)
|
||||
OLFormControl.displayName = 'OLFormControl'
|
||||
|
||||
@@ -1,9 +1,23 @@
|
||||
import FormFeedback, {
|
||||
FormFeedbackProps,
|
||||
} from '@/shared/components/form/form-feedback'
|
||||
import { Form } from 'react-bootstrap'
|
||||
import { FormText } from './ol-form-text'
|
||||
import { ComponentProps } from 'react'
|
||||
|
||||
function OLFormFeedback(props: FormFeedbackProps) {
|
||||
return <FormFeedback {...props} />
|
||||
export type OLFormFeedbackProps = Pick<
|
||||
ComponentProps<typeof Form.Control.Feedback>,
|
||||
'type' | 'className' | 'children'
|
||||
> & { unfilled?: boolean }
|
||||
|
||||
function OLFormFeedback({ unfilled, ...props }: OLFormFeedbackProps) {
|
||||
return (
|
||||
<Form.Control.Feedback {...props}>
|
||||
<FormText
|
||||
type={props.type === 'invalid' ? 'error' : 'success'}
|
||||
unfilled={unfilled}
|
||||
>
|
||||
{props.children}
|
||||
</FormText>
|
||||
</Form.Control.Feedback>
|
||||
)
|
||||
}
|
||||
|
||||
export default OLFormFeedback
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { FormGroupProps } from 'react-bootstrap'
|
||||
import FormGroup from '@/shared/components/form/form-group'
|
||||
import { FormGroup, FormGroupProps } from 'react-bootstrap'
|
||||
import classnames from 'classnames'
|
||||
|
||||
function OLFormGroup(props: FormGroupProps) {
|
||||
return <FormGroup {...props} />
|
||||
function OLFormGroup({ className, ...props }: FormGroupProps) {
|
||||
return (
|
||||
<FormGroup className={classnames('form-group', className)} {...props} />
|
||||
)
|
||||
}
|
||||
|
||||
export default OLFormGroup
|
||||
|
||||
@@ -1,7 +1,87 @@
|
||||
import FormText, { FormTextProps } from '@/shared/components/form/form-text'
|
||||
import { Form, FormTextProps as FormTextProps } from 'react-bootstrap'
|
||||
import MaterialIcon from '@/shared/components/material-icon'
|
||||
import classnames from 'classnames'
|
||||
import { MergeAndOverride } from '../../../../../types/utils'
|
||||
|
||||
function OLFormText({ as = 'div', ...rest }: FormTextProps) {
|
||||
return <FormText {...rest} as={as} />
|
||||
type TextType = 'default' | 'info' | 'success' | 'warning' | 'error'
|
||||
|
||||
export type OLFormTextProps = MergeAndOverride<
|
||||
FormTextProps,
|
||||
{
|
||||
type?: TextType
|
||||
unfilled?: boolean
|
||||
marginless?: boolean
|
||||
}
|
||||
>
|
||||
|
||||
const typeClassMap: Partial<Record<TextType, string>> = {
|
||||
error: 'text-danger',
|
||||
success: 'text-success',
|
||||
warning: 'text-warning',
|
||||
}
|
||||
|
||||
export const getFormTextClass = (type?: TextType) =>
|
||||
typeClassMap[type || 'default']
|
||||
|
||||
function FormTextIcon({
|
||||
type,
|
||||
unfilled,
|
||||
}: Pick<OLFormTextProps, 'type' | 'unfilled'>) {
|
||||
switch (type) {
|
||||
case 'info':
|
||||
return unfilled ? (
|
||||
<MaterialIcon type="info" className="text-info" unfilled />
|
||||
) : (
|
||||
<MaterialIcon type="info" className="text-info" />
|
||||
)
|
||||
case 'success':
|
||||
return unfilled ? (
|
||||
<MaterialIcon type="check_circle" unfilled />
|
||||
) : (
|
||||
<MaterialIcon type="check_circle" />
|
||||
)
|
||||
case 'warning':
|
||||
return unfilled ? (
|
||||
<MaterialIcon type="warning" unfilled />
|
||||
) : (
|
||||
<MaterialIcon type="warning" />
|
||||
)
|
||||
case 'error':
|
||||
return unfilled ? (
|
||||
<MaterialIcon type="error" unfilled />
|
||||
) : (
|
||||
<MaterialIcon type="error" />
|
||||
)
|
||||
default:
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
// Base component without a default `as` — exported for use by the backward-compat shim
|
||||
export function FormText({
|
||||
type = 'default',
|
||||
marginless,
|
||||
children,
|
||||
className,
|
||||
unfilled,
|
||||
...rest
|
||||
}: OLFormTextProps) {
|
||||
return (
|
||||
<Form.Text
|
||||
className={classnames(className, getFormTextClass(type), { marginless })}
|
||||
{...rest}
|
||||
>
|
||||
<span className="form-text-inner">
|
||||
<FormTextIcon type={type} unfilled={unfilled} />
|
||||
<span>{children}</span>
|
||||
</span>
|
||||
</Form.Text>
|
||||
)
|
||||
}
|
||||
|
||||
// OL component defaults to rendering as a <div>
|
||||
function OLFormText({ as = 'div', ...rest }: OLFormTextProps) {
|
||||
return <FormText as={as} {...rest} />
|
||||
}
|
||||
|
||||
export default OLFormText
|
||||
|
||||
@@ -1,13 +1,48 @@
|
||||
import { forwardRef } from 'react'
|
||||
import classNames from 'classnames'
|
||||
import MaterialIcon from '@/shared/components/material-icon'
|
||||
import OLButton from './ol-button'
|
||||
import type { IconButtonProps } from '@/shared/components/types/icon-button-props'
|
||||
import IconButton from '../button/icon-button'
|
||||
|
||||
export type OLIconButtonProps = IconButtonProps
|
||||
|
||||
const OLIconButton = forwardRef<HTMLButtonElement, OLIconButtonProps>(
|
||||
(props, ref) => {
|
||||
// BS5 tooltip relies on the ref
|
||||
return <IconButton {...props} ref={ref} />
|
||||
(
|
||||
{
|
||||
accessibilityLabel,
|
||||
icon,
|
||||
isLoading = false,
|
||||
loadingLabel,
|
||||
size,
|
||||
className,
|
||||
unfilled,
|
||||
...props
|
||||
},
|
||||
ref
|
||||
) => {
|
||||
const iconButtonClassName = classNames(className, {
|
||||
'icon-button': !size,
|
||||
'icon-button-small': size === 'sm',
|
||||
'icon-button-large': size === 'lg',
|
||||
})
|
||||
const iconSizeClassName = size === 'lg' ? 'icon-large' : 'icon-small'
|
||||
const materialIconClassName = classNames(iconSizeClassName, {
|
||||
'button-content-hidden': isLoading,
|
||||
unfilled,
|
||||
})
|
||||
|
||||
return (
|
||||
<OLButton
|
||||
className={iconButtonClassName}
|
||||
isLoading={isLoading}
|
||||
loadingLabel={loadingLabel}
|
||||
aria-label={accessibilityLabel}
|
||||
{...props}
|
||||
ref={ref}
|
||||
>
|
||||
<MaterialIcon className={materialIconClassName} type={icon} />
|
||||
</OLButton>
|
||||
)
|
||||
}
|
||||
)
|
||||
OLIconButton.displayName = 'OLIconButton'
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
import Notification from '@/shared/components/notification'
|
||||
|
||||
function OLNotification(props: React.ComponentProps<typeof Notification>) {
|
||||
return (
|
||||
<div className="notification-list">
|
||||
<Notification {...props} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default OLNotification
|
||||
@@ -1,7 +1,43 @@
|
||||
import Table from '@/shared/components/table'
|
||||
import { Table } from 'react-bootstrap'
|
||||
import classnames from 'classnames'
|
||||
|
||||
function OLTable(props: React.ComponentProps<typeof Table>) {
|
||||
return <Table {...props} />
|
||||
export function TableContainer({
|
||||
responsive,
|
||||
bordered,
|
||||
striped,
|
||||
children,
|
||||
}: React.ComponentProps<typeof Table>) {
|
||||
return (
|
||||
<div
|
||||
className={classnames('table-container', {
|
||||
'table-container-bordered': bordered,
|
||||
'table-responsive': responsive,
|
||||
'table-striped': striped,
|
||||
})}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
type OLTableProps = React.ComponentProps<typeof Table> & {
|
||||
container?: boolean
|
||||
}
|
||||
|
||||
function OLTable({
|
||||
container = true,
|
||||
responsive,
|
||||
bordered,
|
||||
striped,
|
||||
...rest
|
||||
}: OLTableProps) {
|
||||
return container ? (
|
||||
<TableContainer responsive={responsive} bordered={bordered}>
|
||||
<Table striped={striped} {...rest} />
|
||||
</TableContainer>
|
||||
) : (
|
||||
<Table {...rest} />
|
||||
)
|
||||
}
|
||||
|
||||
export default OLTable
|
||||
|
||||
@@ -1,11 +1,73 @@
|
||||
import Tag from '@/shared/components/tag'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Badge, BadgeProps } from 'react-bootstrap'
|
||||
import MaterialIcon from '@/shared/components/material-icon'
|
||||
import { MergeAndOverride } from '../../../../../types/utils'
|
||||
import classnames from 'classnames'
|
||||
import { forwardRef } from 'react'
|
||||
|
||||
type OLTagProps = React.ComponentProps<typeof Tag>
|
||||
type OLTagProps = MergeAndOverride<
|
||||
BadgeProps,
|
||||
{
|
||||
prepend?: React.ReactNode
|
||||
contentProps?: React.ComponentProps<'button'>
|
||||
closeBtnProps?: React.ComponentProps<'button'>
|
||||
}
|
||||
>
|
||||
|
||||
const OLTag = forwardRef<HTMLElement, OLTagProps>((props: OLTagProps, ref) => {
|
||||
return <Tag ref={ref} {...props} />
|
||||
})
|
||||
const OLTag = forwardRef<HTMLElement, OLTagProps>(
|
||||
(
|
||||
{ prepend, children, contentProps, closeBtnProps, className, ...rest },
|
||||
ref
|
||||
) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const content = (
|
||||
<>
|
||||
{prepend && <span className="badge-prepend">{prepend}</span>}
|
||||
<span className="badge-content">{children}</span>
|
||||
</>
|
||||
)
|
||||
|
||||
return (
|
||||
<Badge
|
||||
ref={ref}
|
||||
bg="light"
|
||||
className={classnames('badge-tag', className)}
|
||||
{...rest}
|
||||
>
|
||||
{contentProps?.onClick ? (
|
||||
<button
|
||||
type="button"
|
||||
{...contentProps}
|
||||
className={classnames(
|
||||
'badge-tag-content badge-tag-content-btn',
|
||||
contentProps.className
|
||||
)}
|
||||
>
|
||||
{content}
|
||||
</button>
|
||||
) : (
|
||||
<span
|
||||
{...contentProps}
|
||||
className={classnames('badge-tag-content', contentProps?.className)}
|
||||
>
|
||||
{content}
|
||||
</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>
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
OLTag.displayName = 'OLTag'
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { CSSProperties, FC } from 'react'
|
||||
import { ToastContainer as BS5ToastContainer } from 'react-bootstrap'
|
||||
import { ToastContainer } from 'react-bootstrap'
|
||||
|
||||
type OLToastContainerProps = {
|
||||
style?: CSSProperties
|
||||
@@ -10,8 +10,8 @@ export const OLToastContainer: FC<
|
||||
React.PropsWithChildren<OLToastContainerProps>
|
||||
> = ({ children, className, style }) => {
|
||||
return (
|
||||
<BS5ToastContainer className={className} style={style}>
|
||||
<ToastContainer className={className} style={style}>
|
||||
{children}
|
||||
</BS5ToastContainer>
|
||||
</ToastContainer>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import classNames from 'classnames'
|
||||
import { Toast as BS5Toast } from 'react-bootstrap'
|
||||
import { NotificationIcon, NotificationType } from '../notification'
|
||||
import { Toast } from 'react-bootstrap'
|
||||
import {
|
||||
NotificationIcon,
|
||||
NotificationType,
|
||||
} from '@/shared/components/notification'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import MaterialIcon from '../material-icon'
|
||||
import { ReactNode, useCallback, useState } from 'react'
|
||||
@@ -73,7 +76,7 @@ export const OLToast = ({
|
||||
</div>
|
||||
)
|
||||
return (
|
||||
<BS5Toast
|
||||
<Toast
|
||||
onClose={handleClose}
|
||||
autohide={autoHide}
|
||||
onExited={handleOnHidden}
|
||||
@@ -81,6 +84,6 @@ export const OLToast = ({
|
||||
show={show}
|
||||
>
|
||||
{toastElement}
|
||||
</BS5Toast>
|
||||
</Toast>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,7 +1,118 @@
|
||||
import Tooltip from '@/shared/components/tooltip'
|
||||
import {
|
||||
cloneElement,
|
||||
useEffect,
|
||||
forwardRef,
|
||||
useState,
|
||||
useCallback,
|
||||
} from 'react'
|
||||
import {
|
||||
OverlayTrigger,
|
||||
OverlayTriggerProps,
|
||||
Tooltip as BSTooltip,
|
||||
TooltipProps as BSTooltipProps,
|
||||
} from 'react-bootstrap'
|
||||
import { callFnsInSequence } from '@/utils/functions'
|
||||
|
||||
function OLTooltip(props: React.ComponentProps<typeof Tooltip>) {
|
||||
return <Tooltip {...props} />
|
||||
const DEFAULT_DELAY_SHOW = 300
|
||||
// Slightly lower value avoids flickering when an adjacent tooltip is shown before the previous one hides
|
||||
const DEFAULT_DELAY_HIDE = 290
|
||||
|
||||
type OverlayProps = Omit<OverlayTriggerProps, 'overlay' | 'children'>
|
||||
|
||||
const UpdatingTooltip = forwardRef<HTMLDivElement, BSTooltipProps>(
|
||||
({ popper, children, show: _, ...props }, ref) => {
|
||||
useEffect(() => {
|
||||
popper?.scheduleUpdate?.()
|
||||
}, [children, popper])
|
||||
|
||||
return (
|
||||
<BSTooltip ref={ref} {...props}>
|
||||
{children}
|
||||
</BSTooltip>
|
||||
)
|
||||
}
|
||||
)
|
||||
UpdatingTooltip.displayName = 'UpdatingTooltip'
|
||||
|
||||
export type OLTooltipProps = {
|
||||
description: React.ReactNode
|
||||
id: string
|
||||
overlayProps?: OverlayProps
|
||||
tooltipProps?: BSTooltipProps
|
||||
hidden?: boolean
|
||||
children: React.ReactElement
|
||||
}
|
||||
|
||||
function OLTooltip({
|
||||
id,
|
||||
description,
|
||||
children,
|
||||
tooltipProps,
|
||||
overlayProps,
|
||||
hidden,
|
||||
}: OLTooltipProps) {
|
||||
const [show, setShow] = useState(false)
|
||||
|
||||
const handleKeyDown = useCallback(
|
||||
(e: KeyboardEvent) => {
|
||||
if (show && e.key === 'Escape') {
|
||||
setShow(false)
|
||||
e.stopPropagation()
|
||||
}
|
||||
},
|
||||
[show, setShow]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (hidden) {
|
||||
setShow(false)
|
||||
}
|
||||
}, [hidden])
|
||||
|
||||
useEffect(() => {
|
||||
document.addEventListener('keydown', handleKeyDown, true)
|
||||
return () => document.removeEventListener('keydown', handleKeyDown, true)
|
||||
}, [handleKeyDown])
|
||||
|
||||
const hideTooltip = (e: React.MouseEvent) => {
|
||||
if (e.currentTarget instanceof HTMLElement) {
|
||||
e.currentTarget.blur()
|
||||
}
|
||||
setShow(false)
|
||||
}
|
||||
|
||||
const delay = overlayProps?.delay
|
||||
let delayShow = DEFAULT_DELAY_SHOW
|
||||
let delayHide = DEFAULT_DELAY_HIDE
|
||||
if (delay !== undefined) {
|
||||
delayShow = typeof delay === 'number' ? delay : delay.show
|
||||
delayHide = typeof delay === 'number' ? Math.max(delay - 10, 0) : delay.hide
|
||||
}
|
||||
|
||||
return (
|
||||
<OverlayTrigger
|
||||
overlay={
|
||||
<UpdatingTooltip
|
||||
id={`${id}-tooltip`}
|
||||
{...tooltipProps}
|
||||
style={{ display: hidden ? 'none' : 'block' }}
|
||||
>
|
||||
{description}
|
||||
</UpdatingTooltip>
|
||||
}
|
||||
{...overlayProps}
|
||||
delay={{ show: delayShow, hide: delayHide }}
|
||||
placement={overlayProps?.placement || 'top'}
|
||||
show={hidden ? false : show}
|
||||
onToggle={hidden ? undefined : setShow}
|
||||
>
|
||||
{overlayProps?.trigger === 'click'
|
||||
? children
|
||||
: cloneElement(children, {
|
||||
onClick: callFnsInSequence(children.props.onClick, hideTooltip),
|
||||
})}
|
||||
</OverlayTrigger>
|
||||
)
|
||||
}
|
||||
|
||||
export default OLTooltip
|
||||
|
||||
@@ -11,7 +11,7 @@ import classNames from 'classnames'
|
||||
import { useSelect } from 'downshift'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Form, OverlayProps } from 'react-bootstrap'
|
||||
import FormControl from '@/shared/components/form/form-control'
|
||||
import OLFormControl from '@/shared/components/ol/ol-form-control'
|
||||
import MaterialIcon from '@/shared/components/material-icon'
|
||||
import { CaretUp, CaretDown, Check } from '@phosphor-icons/react'
|
||||
import { DropdownItem } from '@/shared/components/dropdown/dropdown-menu'
|
||||
@@ -101,7 +101,7 @@ export type SelectProps<T> = {
|
||||
dataTestId?: string
|
||||
// CIAM-specific layout
|
||||
isCiam?: boolean
|
||||
size?: React.ComponentProps<typeof FormControl>['size']
|
||||
size?: React.ComponentProps<typeof OLFormControl>['size']
|
||||
// Renders the menu in a portal so it escapes overflow-clipping ancestors.
|
||||
portal?: boolean
|
||||
// Element the portaled menu is rendered into (only used with `portal`).
|
||||
@@ -312,7 +312,7 @@ export const Select = <T,>({
|
||||
{loading && <OLSpinner size="sm" />}
|
||||
</Form.Label>
|
||||
) : null}
|
||||
<FormControl
|
||||
<OLFormControl
|
||||
data-testid={dataTestId}
|
||||
{...getToggleButtonProps({
|
||||
disabled,
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
import { Table as BS5Table } from 'react-bootstrap'
|
||||
import classnames from 'classnames'
|
||||
|
||||
export function TableContainer({
|
||||
responsive,
|
||||
bordered,
|
||||
striped,
|
||||
children,
|
||||
}: React.ComponentProps<typeof BS5Table>) {
|
||||
return (
|
||||
<div
|
||||
className={classnames('table-container', {
|
||||
'table-container-bordered': bordered,
|
||||
'table-responsive': responsive,
|
||||
'table-striped': striped,
|
||||
})}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
type TableProps = React.ComponentProps<typeof BS5Table> & {
|
||||
container?: boolean
|
||||
}
|
||||
|
||||
function Table({
|
||||
container = true,
|
||||
responsive,
|
||||
bordered,
|
||||
striped,
|
||||
...rest
|
||||
}: TableProps) {
|
||||
return container ? (
|
||||
<TableContainer responsive={responsive} bordered={bordered}>
|
||||
<BS5Table striped={striped} {...rest} />
|
||||
</TableContainer>
|
||||
) : (
|
||||
<BS5Table {...rest} />
|
||||
)
|
||||
}
|
||||
|
||||
export default Table
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
import MaterialIcon from '@/shared/components/material-icon'
|
||||
|
||||
export default function OLTagIcon() {
|
||||
export default function TagIcon() {
|
||||
return <MaterialIcon type="sell" />
|
||||
}
|
||||
@@ -1,74 +0,0 @@
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Badge, BadgeProps } from 'react-bootstrap'
|
||||
import MaterialIcon from '@/shared/components/material-icon'
|
||||
import { MergeAndOverride } from '../../../../types/utils'
|
||||
import classnames from 'classnames'
|
||||
import { forwardRef } from 'react'
|
||||
|
||||
type TagProps = MergeAndOverride<
|
||||
BadgeProps,
|
||||
{
|
||||
prepend?: React.ReactNode
|
||||
contentProps?: React.ComponentProps<'button'>
|
||||
closeBtnProps?: React.ComponentProps<'button'>
|
||||
}
|
||||
>
|
||||
|
||||
const Tag = forwardRef<HTMLElement, TagProps>(
|
||||
(
|
||||
{ prepend, children, contentProps, closeBtnProps, className, ...rest },
|
||||
ref
|
||||
) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const content = (
|
||||
<>
|
||||
{prepend && <span className="badge-prepend">{prepend}</span>}
|
||||
<span className="badge-content">{children}</span>
|
||||
</>
|
||||
)
|
||||
|
||||
return (
|
||||
<Badge
|
||||
ref={ref}
|
||||
bg="light"
|
||||
className={classnames('badge-tag', className)}
|
||||
{...rest}
|
||||
>
|
||||
{contentProps?.onClick ? (
|
||||
<button
|
||||
type="button"
|
||||
{...contentProps}
|
||||
className={classnames(
|
||||
'badge-tag-content badge-tag-content-btn',
|
||||
contentProps.className
|
||||
)}
|
||||
>
|
||||
{content}
|
||||
</button>
|
||||
) : (
|
||||
<span
|
||||
{...contentProps}
|
||||
className={classnames('badge-tag-content', contentProps?.className)}
|
||||
>
|
||||
{content}
|
||||
</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>
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
Tag.displayName = 'Tag'
|
||||
|
||||
export default Tag
|
||||
@@ -1,118 +0,0 @@
|
||||
import {
|
||||
cloneElement,
|
||||
useEffect,
|
||||
forwardRef,
|
||||
useState,
|
||||
useCallback,
|
||||
} from 'react'
|
||||
import {
|
||||
OverlayTrigger,
|
||||
OverlayTriggerProps,
|
||||
Tooltip as BSTooltip,
|
||||
TooltipProps as BSTooltipProps,
|
||||
} from 'react-bootstrap'
|
||||
import { callFnsInSequence } from '@/utils/functions'
|
||||
|
||||
const DEFAULT_DELAY_SHOW = 300
|
||||
// Slightly lower value avoids flickering when an adjacent tooltip is shown before the previous one hides
|
||||
const DEFAULT_DELAY_HIDE = 290
|
||||
|
||||
type OverlayProps = Omit<OverlayTriggerProps, 'overlay' | 'children'>
|
||||
|
||||
const UpdatingTooltip = forwardRef<HTMLDivElement, BSTooltipProps>(
|
||||
({ popper, children, show: _, ...props }, ref) => {
|
||||
useEffect(() => {
|
||||
popper?.scheduleUpdate?.()
|
||||
}, [children, popper])
|
||||
|
||||
return (
|
||||
<BSTooltip ref={ref} {...props}>
|
||||
{children}
|
||||
</BSTooltip>
|
||||
)
|
||||
}
|
||||
)
|
||||
UpdatingTooltip.displayName = 'UpdatingTooltip'
|
||||
|
||||
export type TooltipProps = {
|
||||
description: React.ReactNode
|
||||
id: string
|
||||
overlayProps?: OverlayProps
|
||||
tooltipProps?: BSTooltipProps
|
||||
hidden?: boolean
|
||||
children: React.ReactElement
|
||||
}
|
||||
|
||||
function Tooltip({
|
||||
id,
|
||||
description,
|
||||
children,
|
||||
tooltipProps,
|
||||
overlayProps,
|
||||
hidden,
|
||||
}: TooltipProps) {
|
||||
const [show, setShow] = useState(false)
|
||||
|
||||
const handleKeyDown = useCallback(
|
||||
(e: KeyboardEvent) => {
|
||||
if (show && e.key === 'Escape') {
|
||||
setShow(false)
|
||||
e.stopPropagation()
|
||||
}
|
||||
},
|
||||
[show, setShow]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (hidden) {
|
||||
setShow(false)
|
||||
}
|
||||
}, [hidden])
|
||||
|
||||
useEffect(() => {
|
||||
document.addEventListener('keydown', handleKeyDown, true)
|
||||
return () => document.removeEventListener('keydown', handleKeyDown, true)
|
||||
}, [handleKeyDown])
|
||||
|
||||
const hideTooltip = (e: React.MouseEvent) => {
|
||||
if (e.currentTarget instanceof HTMLElement) {
|
||||
e.currentTarget.blur()
|
||||
}
|
||||
setShow(false)
|
||||
}
|
||||
|
||||
const delay = overlayProps?.delay
|
||||
let delayShow = DEFAULT_DELAY_SHOW
|
||||
let delayHide = DEFAULT_DELAY_HIDE
|
||||
if (delay !== undefined) {
|
||||
delayShow = typeof delay === 'number' ? delay : delay.show
|
||||
delayHide = typeof delay === 'number' ? Math.max(delay - 10, 0) : delay.hide
|
||||
}
|
||||
|
||||
return (
|
||||
<OverlayTrigger
|
||||
overlay={
|
||||
<UpdatingTooltip
|
||||
id={`${id}-tooltip`}
|
||||
{...tooltipProps}
|
||||
style={{ display: hidden ? 'none' : 'block' }}
|
||||
>
|
||||
{description}
|
||||
</UpdatingTooltip>
|
||||
}
|
||||
{...overlayProps}
|
||||
delay={{ show: delayShow, hide: delayHide }}
|
||||
placement={overlayProps?.placement || 'top'}
|
||||
show={hidden ? false : show}
|
||||
onToggle={hidden ? undefined : setShow}
|
||||
>
|
||||
{overlayProps?.trigger === 'click'
|
||||
? children
|
||||
: cloneElement(children, {
|
||||
onClick: callFnsInSequence(children.props.onClick, hideTooltip),
|
||||
})}
|
||||
</OverlayTrigger>
|
||||
)
|
||||
}
|
||||
|
||||
export default Tooltip
|
||||
Reference in New Issue
Block a user