@spaced-out/ui-design-system
Version:
Sense UI components library
167 lines (155 loc) • 5.37 kB
Flow
// @flow strict
import * as React from 'react';
import {
// $FlowFixMe[untyped-import]
autoUpdate,
// $FlowFixMe[untyped-import]
flip,
// $FlowFixMe[untyped-import]
FloatingFocusManager,
// $FlowFixMe[untyped-import]
FloatingPortal,
// $FlowFixMe[untyped-import]
offset,
// $FlowFixMe[untyped-import]
shift,
// $FlowFixMe[untyped-import]
useFloating,
} from '@floating-ui/react';
import {useReferenceElementWidth} from '../../hooks';
import {spaceNone, spaceXXSmall} from '../../styles/variables/_space';
import type {ClickAwayRefType} from '../../utils';
import {ClickAway, mergeRefs} from '../../utils';
import classify from '../../utils/classify';
import type {ButtonProps, ButtonSize} from '../Button';
import {Button} from '../Button';
import type {AnchorType, Strategy} from '../ButtonDropdown';
import {ANCHOR_POSITION_TYPE, STRATEGY_TYPE} from '../ButtonDropdown';
import type {ElevationType} from '../Tooltip';
import {getElevationValue} from '../Tooltip';
import css from './FilterButtonOverlay.module.css';
type ClassNames = $ReadOnly<{wrapper?: string, overlayContainer?: string}>;
export type FilterButtonOverlaySizeTypes = 'medium' | 'small';
export type NewButtonProps = $Diff<ButtonProps, {children?: React.Node}>;
export type FilterButtonOverlayProps = {
...NewButtonProps,
classNames?: ClassNames,
children: React.Node,
positionStrategy?: Strategy,
anchorPosition?: AnchorType,
clickAwayRef?: ClickAwayRefType,
isFluid?: boolean,
size?: FilterButtonOverlaySizeTypes,
buttonLabel?: React.Node,
buttonSize?: ButtonSize,
elevation?: ElevationType,
buttonIsFluid?: boolean,
...
};
export const FilterButtonOverlay: React$AbstractComponent<
FilterButtonOverlayProps,
HTMLDivElement,
> = React.forwardRef<FilterButtonOverlayProps, HTMLDivElement>(
(
{
classNames,
anchorPosition = ANCHOR_POSITION_TYPE.bottomStart,
positionStrategy = STRATEGY_TYPE.absolute,
clickAwayRef,
size = 'medium',
children,
isFluid,
buttonLabel,
buttonIsFluid,
buttonSize,
elevation = 'modal',
...restProps
}: FilterButtonOverlayProps,
ref,
) => {
const {x, y, refs, strategy, context} = useFloating({
open: true,
strategy: positionStrategy,
placement: anchorPosition,
whileElementsMounted: autoUpdate,
middleware: [shift(), flip(), offset(parseInt(spaceXXSmall))],
});
const dropdownWidth = useReferenceElementWidth(refs.reference?.current);
return (
<ClickAway
clickAwayRef={clickAwayRef}
closeOnEscapeKeypress
containsNestedFloatingPortals
>
{({isOpen, onOpen, boundaryRef, triggerRef}) => (
<div
data-testid="FilterButtonOverlay"
className={classify(
{
[css.isFluid]: buttonIsFluid,
},
classNames?.wrapper,
)}
ref={ref}
>
<Button
{...restProps}
ref={mergeRefs([refs.setReference, triggerRef])}
onClick={(e) => {
e.stopPropagation();
onOpen();
}}
isFluid={buttonIsFluid}
size={buttonSize}
>
{buttonLabel}
</Button>
{isOpen && (
<FloatingPortal>
<FloatingFocusManager
modal={false}
context={context}
initialFocus={refs.reference}
>
<div
ref={mergeRefs([refs.setFloating, boundaryRef])}
className={css.overlayContainer}
style={{
display: 'flex',
position: strategy,
top: y ?? spaceNone,
left: x ?? spaceNone,
/* NOTE(Sharad): The FloatingPortal renders the menu outside the normal DOM structure,
so its parent is effectively the <body> element. This means the menu
would otherwise default to the body's width. To support fluid width,
we must manually set the dropdown width here; otherwise, it uses a fixed width.
Also, Only treat menu as non-fluid if isFluid is strictly false, since default is true in menu and undefined means fluid. */
...(isFluid !== false && {
'--dropdown-width': dropdownWidth,
}),
'--menu-elevation': getElevationValue(elevation),
}}
>
<div
className={classify(
css.overlayWrapper,
{
[css.fluid]: isFluid,
[css.small]: size === 'small',
[css.medium]: size === 'medium',
},
classNames?.overlayContainer,
)}
>
{children}
</div>
</div>
</FloatingFocusManager>
</FloatingPortal>
)}
</div>
)}
</ClickAway>
);
},
);