Skip to content

Commit

Permalink
refactor: move Dropdown, DropdownOverlay to floating ui
Browse files Browse the repository at this point in the history
  • Loading branch information
saurabhdaware committed Jan 20, 2025
1 parent 37653a8 commit 10076fe
Show file tree
Hide file tree
Showing 20 changed files with 942 additions and 21 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import { useFloatingTree, useListItem, useMergeRefs } from '@floating-ui/react';
import React from 'react';
import type { DropdownItemProps } from '../types';
import { useDropdown } from '../useDropdown';
import { Box } from '~components/Box';
import { BaseMenuItem } from '~components/BaseMenu';
import { ChevronRightIcon } from '~components/Icons';
import type { ActionListProps } from '~components/ActionList';

const ActionList = ({ children }: ActionListProps): React.ReactElement => {
return <Box>{children}</Box>;
};

const ActionListItem = React.forwardRef<HTMLButtonElement, DropdownItemProps>(
(
{
title,
isDisabled,
description,
leading,
trailing,
_isDropdownTrigger,
_hasFocusInside,
href,
target,
children,
as,
...props
},
forwardedRef,
) => {
const dropdown = useDropdown();
const item = useListItem({ label: isDisabled && Boolean(children) ? null : title });
const tree = useFloatingTree();

const isLink = Boolean(href);

const defaultAs = isLink ? 'a' : 'button';

return (
<BaseMenuItem
title={title}
description={description}
leading={leading}
trailing={
_isDropdownTrigger ? <ChevronRightIcon color="interactive.icon.gray.muted" /> : trailing
}
as={as ?? defaultAs}
href={href}
ref={useMergeRefs([item.ref, forwardedRef])}
isDisabled={isDisabled}
{...props}
{...(_isDropdownTrigger
? {}
: dropdown.getItemProps({
onClick(event: React.MouseEvent<HTMLButtonElement>) {
props.onClick?.(event);
tree?.events.emit('click');
},
onFocus(event: React.FocusEvent<HTMLButtonElement>) {
props.onFocus?.(event);
dropdown.setHasFocusInside(true);
},
}))}
>
{children}
</BaseMenuItem>
);
},
);

export { ActionList, ActionListItem };
14 changes: 14 additions & 0 deletions packages/blade/src/components/DropdownNew/Dropdown.native.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import type { DropdownProps } from './types';
import { Text } from '~components/Typography';
import { throwBladeError } from '~utils/logger';

const Dropdown = (_props: DropdownProps): React.ReactElement => {
throwBladeError({
message: 'Dropdown is not yet implemented for native',
moduleName: 'Dropdown',
});

return <Text>Dropdown Component is not available for Native mobile apps.</Text>;
};

export { Dropdown };
112 changes: 112 additions & 0 deletions packages/blade/src/components/DropdownNew/Dropdown.web.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import {
FloatingFocusManager,
FloatingList,
FloatingNode,
FloatingPortal,
useMergeRefs,
} from '@floating-ui/react';
import * as React from 'react';
import { DropdownContext, useFloatingDropdownSetup, useDropdown } from './useDropdown';
import type { DropdownProps } from './types';

const Dropdown = ({
children,
openInteraction = 'click',
onOpenChange,
isOpen: isOpenControlled,
}: DropdownProps): React.ReactElement => {
const [hasFocusInside, setHasFocusInside] = React.useState(false);

const elementsRef = React.useRef<(HTMLButtonElement | null)[]>([]);
const labelsRef = React.useRef<(string | null)[]>([]);
const parent = useDropdown();

const {
getReferenceProps,
getFloatingProps,
getItemProps,
item,
refs,
floatingStyles,
isOpen,
nodeId,
isNested,
context,
isMounted,
floatingTransitionStyles,
} = useFloatingDropdownSetup({
elementsRef,
openInteraction,
onOpenChange,
isOpen: isOpenControlled,
});

const referenceProps = {
ref: useMergeRefs([refs.setReference, item.ref]),
...getReferenceProps(
parent.getItemProps({
onFocus() {
setHasFocusInside(false);
parent.setHasFocusInside(true);
},
}),
),
};

const floatingProps = {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
ref: refs.setFloating as any,
style: floatingStyles,
_transitionStyle: floatingTransitionStyles,
...getFloatingProps(),
};

const [dropdownTriggerChild, dropdownOverlayChild] = React.Children.toArray(children) as [
React.ReactElement,
React.ReactElement,
];

const triggerWithReferenceProps = React.cloneElement(dropdownTriggerChild, {
...dropdownTriggerChild.props,
_referenceProps: referenceProps,
_hasFocusInside: hasFocusInside,
_isDropdownTrigger: true,
});

const overlayWithFloatingProps = React.cloneElement(dropdownOverlayChild, {
...dropdownOverlayChild.props,
...floatingProps,
});

const contextValue = React.useMemo(() => {
return {
getItemProps,
setHasFocusInside,
isOpen,
};
}, [isOpen]);

return (
<FloatingNode id={nodeId}>
<DropdownContext.Provider value={contextValue}>
{triggerWithReferenceProps}
<FloatingList elementsRef={elementsRef} labelsRef={labelsRef}>
{isMounted && (
<FloatingPortal>
<FloatingFocusManager
context={context}
modal={false}
initialFocus={0}
returnFocus={!isNested}
>
{overlayWithFloatingProps}
</FloatingFocusManager>
</FloatingPortal>
)}
</FloatingList>
</DropdownContext.Provider>
</FloatingNode>
);
};

export { Dropdown };
50 changes: 50 additions & 0 deletions packages/blade/src/components/DropdownNew/DropdownNew.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import React from 'react';
import type { Meta } from '@storybook/react';
import { ActionList, ActionListItem } from './ActionList/ActionList';
import { Dropdown, DropdownOverlay } from './index';

import { SelectInput } from '~components/Input/DropdownInputTriggers';
import { Box } from '~components/Box';
import { Button } from '~components/Button';
import { TextInput } from '~components/Input/TextInput';

const DropdownStoryMeta: Meta = {
title: 'Components/DropdownNew',
component: Dropdown,
parameters: {
viewMode: 'story',
options: {
showPanel: false,
},
previewTabs: {
'storybook/docs/panel': {
hidden: true,
},
},
chromatic: { disableSnapshot: true },
},
};

export const InternalSelect = (): React.ReactElement => {
return (
<Box
padding="spacing.5"
backgroundColor="surface.background.gray.moderate"
width="100%"
minHeight="100px"
overflow="scroll"
>
<Dropdown selectionType="multiple">
<SelectInput label="Search" />
<DropdownOverlay>
<ActionList>
<ActionListItem title="Apples" value="Apples" />
<ActionListItem title="Appricots" value="Appricots" />
</ActionList>
</DropdownOverlay>
</Dropdown>
</Box>
);
};

export default DropdownStoryMeta;
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import type { DropdownOverlayProps } from './types';
import { Text } from '~components/Typography';
import { throwBladeError } from '~utils/logger';

const DropdownOverlay = (_props: DropdownOverlayProps): React.ReactElement => {
throwBladeError({
message: 'DropdownOverlay is not yet implemented for native',
moduleName: 'DropdownOverlay',
});

return <Text>DropdownOverlay Component is not available for Native mobile apps.</Text>;
};

export { DropdownOverlay };
64 changes: 64 additions & 0 deletions packages/blade/src/components/DropdownNew/DropdownOverlay.web.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import React from 'react';
import styled from 'styled-components';
import type { DropdownOverlayProps } from './types';
import { MENU_MIN_WIDTH, overlayPaddingX, overlayPaddingY } from './tokens';
import BaseBox from '~components/Box/BaseBox';
import { componentZIndices } from '~utils/componentZIndices';
import type { BladeElementRef } from '~utils/types';
import { metaAttribute, MetaConstants } from '~utils/metaAttribute';

const UnfocussableOverlay = styled(BaseBox)((_props) => {
return {
'&:focus-visible': {
outline: 'none',
},
};
});

const _DropdownOverlay: React.ForwardRefRenderFunction<BladeElementRef, DropdownOverlayProps> = (
{
children,
zIndex = componentZIndices.dropdownOverlay,
_transitionStyle,
minWidth,
maxWidth,
width,
testID,
...props
},
ref,
): React.ReactElement => {
return (
<UnfocussableOverlay
ref={ref as never}
{...props}
zIndex={zIndex}
{...metaAttribute({ name: MetaConstants.Dropdown, testID })}
minWidth={minWidth ?? MENU_MIN_WIDTH}
width={width}
maxWidth={maxWidth}
>
{/*
Requires another nested div since floatingStyles clash with floatingTransitionStyles
https://floating-ui.com/docs/usetransition#usetransitionstyles
*/}
<BaseBox
backgroundColor="popup.background.subtle"
paddingX={overlayPaddingX}
paddingY={overlayPaddingY}
elevation="midRaised"
borderWidth="thin"
borderColor="surface.border.gray.muted"
borderRadius="medium"
style={_transitionStyle}
>
{children}
</BaseBox>
</UnfocussableOverlay>
);
};

const DropdownOverlay = React.forwardRef(_DropdownOverlay);

export { DropdownOverlay };
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import type { StyledPropsBlade } from '~components/Box/styledProps';
import { Text } from '~components/Typography';
import { throwBladeError } from '~utils/logger';

const DropdownDivider = (_styledProps: StyledPropsBlade): React.ReactElement => {
throwBladeError({
message: 'DropdownDivider is not yet implemented for native',
moduleName: 'DropdownDivider',
});

return <Text>DropdownDivider Component is not available for Native mobile apps.</Text>;
};

export { DropdownDivider };
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { getDividerMarginTokens } from '../tokens';
import type { StyledPropsBlade } from '~components/Box/styledProps';
import { Divider } from '~components/Divider';
import { useTheme } from '~utils';

const DropdownDivider = (styledProps: StyledPropsBlade): React.ReactElement => {
const { theme } = useTheme();
return <Divider {...getDividerMarginTokens(theme)} {...styledProps} />;
};

export { DropdownDivider };
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import type { DropdownHeaderProps, DropdownFooterProps } from '../types';
import { Text } from '~components/Typography';
import { throwBladeError } from '~utils/logger';

const DropdownHeader = (_props: DropdownHeaderProps): React.ReactElement => {
throwBladeError({
message: 'DropdownHeader is not yet implemented for native',
moduleName: 'DropdownHeader',
});

return <Text>DropdownHeader Component is not available for Native mobile apps.</Text>;
};

const DropdownFooter = (_props: DropdownFooterProps): React.ReactElement => {
throwBladeError({
message: 'DropdownFooter is not yet implemented for native',
moduleName: 'DropdownFooter',
});

return <Text>DropdownFooter Component is not available for Native mobile apps.</Text>;
};

export { DropdownHeader, DropdownFooter };
Loading

0 comments on commit 10076fe

Please sign in to comment.