From b00f23dd1f2a6728c1979ca21c95990b3d544995 Mon Sep 17 00:00:00 2001 From: Jason Quense Date: Tue, 20 Apr 2021 16:29:33 -0400 Subject: [PATCH] feat: various dropdown improvements --- src/Button.tsx | 1 + src/Dropdown.tsx | 37 ++---- src/DropdownContext.ts | 6 +- src/DropdownItem.tsx | 7 +- src/DropdownMenu.tsx | 28 ++--- src/usePopper.ts | 36 ++---- test/DropdownSpec.js | 54 +++++--- test/TabContainerSpec.js | 21 ++-- www/docs/Dropdown.mdx | 222 +++++++++++++++++++++++++++++++-- www/docs/Nav.mdx | 76 +++++++++++ www/docs/useDropdownMenu.mdx | 37 ------ www/docs/useDropdownToggle.mdx | 25 ---- www/sidebars.js | 14 +-- www/src/Dropdown.tsx | 63 ++++++++++ 14 files changed, 439 insertions(+), 188 deletions(-) delete mode 100644 www/docs/useDropdownMenu.mdx delete mode 100644 www/docs/useDropdownToggle.mdx create mode 100644 www/src/Dropdown.tsx diff --git a/src/Button.tsx b/src/Button.tsx index 488faf0..f6ef382 100644 --- a/src/Button.tsx +++ b/src/Button.tsx @@ -2,6 +2,7 @@ import * as React from 'react'; import PropTypes from 'prop-types'; import { DynamicRefForwardingComponent } from './types'; + export type ButtonType = 'button' | 'reset' | 'submit' | string; interface UseButtonPropsOptions { diff --git a/src/Dropdown.tsx b/src/Dropdown.tsx index 5a8f820..1309452 100644 --- a/src/Dropdown.tsx +++ b/src/Dropdown.tsx @@ -15,13 +15,15 @@ import useForceUpdate from '@restart/hooks/useForceUpdate'; import useGlobalListener from '@restart/hooks/useGlobalListener'; import useEventCallback from '@restart/hooks/useEventCallback'; -import DropdownContext, { DropDirection } from './DropdownContext'; +import DropdownContext from './DropdownContext'; import DropdownMenu from './DropdownMenu'; import DropdownToggle from './DropdownToggle'; import SelectableContext from './SelectableContext'; import { SelectCallback } from './types'; import DropdownItem from './DropdownItem'; import { dataAttr } from './DataKey'; +import { Placement } from './usePopper'; +import { placements } from './popper'; const propTypes = { /** @@ -38,9 +40,11 @@ const propTypes = { children: PropTypes.node, /** - * Determines the direction and location of the Menu in relation to it's Toggle. + * The PopperJS placement for positioning the Dropdown menu in relation to it's Toggle. + * + * @default 'bottom-start' */ - drop: PropTypes.oneOf(['up', 'left', 'right', 'down']), + placement: PropTypes.oneOf(placements), /** * Controls the focus behavior for when the Dropdown is opened. Set to @@ -59,11 +63,6 @@ const propTypes = { */ itemSelector: PropTypes.string, - /** - * Align the menu to the 'end' side of the placement side of the Dropdown toggle. The default placement is `top-start` or `bottom-start`. - */ - alignEnd: PropTypes.bool, - /** * Whether or not the Dropdown is visible. * @@ -102,8 +101,7 @@ export interface ToggleMetadata { } export interface DropdownProps { - drop?: DropDirection; - alignEnd?: boolean; + placement?: Placement; defaultShow?: boolean; show?: boolean; onSelect?: SelectCallback; @@ -132,14 +130,13 @@ function useRefWithUpdate() { * @public */ function Dropdown({ - drop, - alignEnd, defaultShow, show: rawShow, onSelect, onToggle: rawOnToggle, itemSelector = `* [${dataAttr('dropdown-item')}]`, focusFirstItemOnShow, + placement = 'bottom-start', children, }: DropdownProps) { const [show, onToggle] = useUncontrolledProp( @@ -190,24 +187,14 @@ function Dropdown({ const context = useMemo( () => ({ toggle, - drop, + placement, show, - alignEnd, menuElement, toggleElement, setMenu, setToggle, }), - [ - toggle, - drop, - show, - alignEnd, - menuElement, - toggleElement, - setMenu, - setToggle, - ], + [toggle, placement, show, menuElement, toggleElement, setMenu, setToggle], ); if (menuElement && lastShow && !show) { @@ -285,7 +272,7 @@ function Dropdown({ } lastSourceEvent.current = event.type; - let meta = { originalEvent: event, source: event.type }; + const meta = { originalEvent: event, source: event.type }; switch (key) { case 'ArrowUp': { const next = getNextFocusedChild(target, -1); diff --git a/src/DropdownContext.ts b/src/DropdownContext.ts index f849491..53aa66d 100644 --- a/src/DropdownContext.ts +++ b/src/DropdownContext.ts @@ -1,6 +1,5 @@ import React from 'react'; - -export type DropDirection = 'up' | 'down' | 'left' | 'right'; +import type { Placement } from './usePopper'; export type DropdownContextValue = { toggle: (nextShow: boolean, event?: React.SyntheticEvent | Event) => void; @@ -10,8 +9,7 @@ export type DropdownContextValue = { setToggle: (ref: HTMLElement | null) => void; show: boolean; - alignEnd?: boolean; - drop?: DropDirection; + placement?: Placement; }; const DropdownContext = React.createContext(null); diff --git a/src/DropdownItem.tsx b/src/DropdownItem.tsx index 57daadf..c182411 100644 --- a/src/DropdownItem.tsx +++ b/src/DropdownItem.tsx @@ -1,4 +1,3 @@ -import classNames from 'classnames'; import PropTypes from 'prop-types'; import * as React from 'react'; import { useContext } from 'react'; @@ -6,7 +5,7 @@ import useEventCallback from '@restart/hooks/useEventCallback'; import SelectableContext, { makeEventKey } from './SelectableContext'; import NavContext from './NavContext'; -// import SafeAnchor from './SafeAnchor'; + import { EventKey, DynamicRefForwardingComponent, @@ -66,6 +65,10 @@ interface UseDropdownItemOptions { onClick?: React.MouseEventHandler; } +/** + * Create a dropdown item. Returns a set of props for the dropdown item component + * including an `onClick` handler that prevents selection when the item is disabled + */ export function useDropdownItem({ key, active, diff --git a/src/DropdownMenu.tsx b/src/DropdownMenu.tsx index d5eb004..0e0f340 100644 --- a/src/DropdownMenu.tsx +++ b/src/DropdownMenu.tsx @@ -10,12 +10,13 @@ import usePopper, { } from './usePopper'; import useRootClose, { RootCloseOptions } from './useRootClose'; import mergeOptionsWithPopperConfig from './mergeOptionsWithPopperConfig'; +import { placements } from './popper'; export interface UseDropdownMenuOptions { flip?: boolean; show?: boolean; fixed?: boolean; - alignEnd?: boolean; + placement?: Placement; usePopper?: boolean; offset?: Offset; rootCloseEvent?: RootCloseOptions['clickTrigger']; @@ -35,7 +36,7 @@ export type UserDropdownMenuArrowProps = Record & { export interface UseDropdownMenuMetadata { show: boolean; - alignEnd?: boolean; + placement?: Placement; hasShown: boolean; toggle?: DropdownContextValue['toggle']; popper: UsePopperState | null; @@ -66,13 +67,12 @@ export function useDropdownMenu(options: UseDropdownMenuOptions = {}) { offset, rootCloseEvent, fixed = false, + placement: placementOverride, popperConfig = {}, usePopper: shouldUsePopper = !!context, } = options; const show = context?.show == null ? !!options.show : context.show; - const alignEnd = - context?.alignEnd == null ? options.alignEnd : context.alignEnd; if (show && !hasShownRef.current) { hasShownRef.current = true; @@ -82,19 +82,14 @@ export function useDropdownMenu(options: UseDropdownMenuOptions = {}) { context?.toggle(false, e); }; - const { drop, setMenu, menuElement, toggleElement } = context || {}; - - let placement: Placement = alignEnd ? 'bottom-end' : 'bottom-start'; - if (drop === 'up') placement = alignEnd ? 'top-end' : 'top-start'; - else if (drop === 'right') placement = alignEnd ? 'right-end' : 'right-start'; - else if (drop === 'left') placement = alignEnd ? 'left-end' : 'left-start'; + const { placement, setMenu, menuElement, toggleElement } = context || {}; const popper = usePopper( toggleElement, menuElement, mergeOptionsWithPopperConfig({ - placement, - enabled: !!(shouldUsePopper && show), + placement: placementOverride || placement || 'bottom-start', + enabled: shouldUsePopper, enableEvents: show, offset, flip, @@ -113,7 +108,7 @@ export function useDropdownMenu(options: UseDropdownMenuOptions = {}) { const metadata: UseDropdownMenuMetadata = { show, - alignEnd, + placement, hasShown: hasShownRef.current, toggle: context?.toggle, popper: shouldUsePopper ? popper : null, @@ -141,7 +136,6 @@ const propTypes = { * * @type {Function ({ * show: boolean, - * alignEnd: boolean, * close: (?SyntheticEvent) => void, * placement: Placement, * update: () => void, @@ -167,11 +161,11 @@ const propTypes = { show: PropTypes.bool, /** - * Aligns the dropdown menu to the 'end' of it's placement position. + * The PopperJS placement for positioning the Dropdown menu in relation to it's Toggle. * Generally this is provided by the parent `Dropdown` component, * but may also be specified as a prop directly. */ - alignEnd: PropTypes.bool, + placement: PropTypes.oneOf(placements), /** * Enables the Popper.js `flip` modifier, allowing the Dropdown to @@ -213,7 +207,7 @@ export interface DropdownMenuProps extends UseDropdownMenuOptions { function DropdownMenu({ children, ...options }: DropdownMenuProps) { const [props, meta] = useDropdownMenu(options); - return <>{meta.hasShown ? children(props, meta) : null}; + return <>{children(props, meta)}; } DropdownMenu.displayName = 'DropdownMenu'; diff --git a/src/usePopper.ts b/src/usePopper.ts index 45f1eb5..7cb60cf 100644 --- a/src/usePopper.ts +++ b/src/usePopper.ts @@ -3,16 +3,6 @@ import useSafeState from '@restart/hooks/useSafeState'; import * as Popper from '@popperjs/core'; import { createPopper } from './popper'; -const initialPopperStyles = ( - position: string, -): Partial => ({ - position, - top: '0', - left: '0', - opacity: '0', - pointerEvents: 'none', -}); - const disabledApplyStylesModifier = { name: 'applyStyles', enabled: false }; // until docjs supports type exports... @@ -63,18 +53,16 @@ const ariaDescribedByModifier: Modifier<'ariaDescribedBy', undefined> = { name: 'ariaDescribedBy', enabled: true, phase: 'afterWrite', - effect: ({ state }) => { - return () => { - const { reference, popper } = state.elements; - if ('removeAttribute' in reference) { - const ids = (reference.getAttribute('aria-describedby') || '') - .split(',') - .filter((id) => id.trim() !== popper.id); - - if (!ids.length) reference.removeAttribute('aria-describedby'); - else reference.setAttribute('aria-describedby', ids.join(',')); - } - }; + effect: ({ state }) => () => { + const { reference, popper } = state.elements; + if ('removeAttribute' in reference) { + const ids = (reference.getAttribute('aria-describedby') || '') + .split(',') + .filter((id) => id.trim() !== popper.id); + + if (!ids.length) reference.removeAttribute('aria-describedby'); + else reference.setAttribute('aria-describedby', ids.join(',')); + } }, fn: ({ state }) => { const { popper, reference } = state.elements; @@ -140,7 +128,7 @@ function usePopper( forceUpdate, attributes: {}, styles: { - popper: initialPopperStyles(strategy), + popper: {}, arrow: {}, }, }), @@ -206,7 +194,7 @@ function usePopper( setState((s) => ({ ...s, attributes: {}, - styles: { popper: initialPopperStyles(strategy) }, + styles: { popper: {} }, })); } }; diff --git a/test/DropdownSpec.js b/test/DropdownSpec.js index 996c35e..507297e 100644 --- a/test/DropdownSpec.js +++ b/test/DropdownSpec.js @@ -4,6 +4,7 @@ import ReactDOM from 'react-dom'; import { act } from 'react-dom/test-utils'; import simulant from 'simulant'; import Dropdown from '../src/Dropdown'; +import DropdownItem from '../src/DropdownItem'; describe('', () => { const Menu = ({ @@ -20,7 +21,7 @@ describe('', () => { rootCloseEvent={rootCloseEvent} > {(menuProps, meta) => { - const { show, toggle } = meta; + const { show } = meta; renderSpy && renderSpy(meta); return ( @@ -90,13 +91,18 @@ describe('', () => { buttonNode.getAttribute('id').should.be.ok; }); - it('forwards alignEnd to menu', () => { + it('forwards placement to menu', () => { const renderSpy = sinon.spy((meta) => { - meta.alignEnd.should.equal(true); + meta.placement.should.equal('bottom-end'); }); mount( - , + , ); renderSpy.should.have.been.called; @@ -109,7 +115,7 @@ describe('', () => { const wrapper = mount(); wrapper.assertNone('.show'); - wrapper.assertNone('DropdownMenu > *'); + wrapper.assertSingle('button[aria-expanded=false]').simulate('click'); wrapper.assertSingle('Dropdown'); @@ -190,7 +196,7 @@ describe('', () => { onToggle.should.have.been.calledWith(false); }); - it.only('closes when child Dropdown.Item is selected', () => { + it('closes when child Dropdown.Item is selected', () => { const onToggle = sinon.spy(); const wrapper = mount(); @@ -206,7 +212,8 @@ describe('', () => { const wrapper = mount(); wrapper.find('.toggle').simulate('click'); - wrapper.find('.menu > button').first().simulate('click'); + + wrapper.find('.menu button').first().simulate('click'); onToggle.should.have.been.calledWith(false); wrapper.find('Dropdown').prop('show').should.equal(true); @@ -245,14 +252,14 @@ describe('', () => { document.activeElement.should.equal(wrapper.find('.toggle').getDOMNode()); }); - it('when focused and closed sets focus on first menu item when the key "down" is pressed for role="menu"', () => { + it('when focused and closed sets focus on first menu item when the key "down" is pressed for role="menu"', (done) => { const wrapper = mount(
Child Title, - - + Item 1 + Item 2
, @@ -266,9 +273,12 @@ describe('', () => { key: 'ArrowDown', }); - document.activeElement.should.equal( - wrapper.update().find('.menu > button').first().getDOMNode(), - ); + setTimeout(() => { + document.activeElement.should.equal( + wrapper.update().find('.menu button').first().getDOMNode(), + ); + done(); + }); }); it('when focused and closed sets focus on first menu item when the focusFirstItemOnShow is true', () => { @@ -277,8 +287,8 @@ describe('', () => {
Child Title, - - + Item 1 + Item 2
, @@ -289,9 +299,11 @@ describe('', () => { wrapper.find('.toggle').simulate('click'); - document.activeElement.should.equal( - wrapper.find('.menu > button').first().getDOMNode(), - ); + return Promise.resolve().then(() => { + document.activeElement.should.equal( + wrapper.find('.menu button').first().getDOMNode(), + ); + }); }); it('when open and the key "Escape" is pressed the menu is closed and focus is returned to the button', () => { @@ -299,7 +311,7 @@ describe('', () => { attachTo: focusableContainer, }); - const firstItem = wrapper.find('.menu > button').first().getDOMNode(); + const firstItem = wrapper.find('.menu button').first().getDOMNode(); firstItem.focus(); document.activeElement.should.equal(firstItem); @@ -310,7 +322,6 @@ describe('', () => { }); }); - console.log(document.activeElement); document.activeElement.should.equal( wrapper.update().find('.toggle').getDOMNode(), ); @@ -332,6 +343,9 @@ describe('', () => { simulant.fire(toggle, 'keydown', { key: 'Tab', }); + simulant.fire(document, 'keyup', { + key: 'Tab', + }); toggle.getAttribute('aria-expanded').should.equal('false'); diff --git a/test/TabContainerSpec.js b/test/TabContainerSpec.js index 346b2a4..77dc8bf 100644 --- a/test/TabContainerSpec.js +++ b/test/TabContainerSpec.js @@ -24,11 +24,11 @@ describe('', () => { , ); - instance.find('TabPanel Nav a').simulate('click'); + instance.find('TabPanel Nav button').simulate('click'); onSelect.should.not.have.been.called; - instance.find('Nav a').first().simulate('click'); + instance.find('Nav button').first().simulate('click'); onSelect.should.have.been.calledOnce; }); @@ -49,7 +49,7 @@ describe('', () => { , ); - instance.assertSingle(`a[id="test-id"]`); + instance.assertSingle(`button[id="test-id"]`); }); it('should match up ids', () => { @@ -66,14 +66,14 @@ describe('', () => { , ); - let tabId = instance.find('NavItem a').first().prop('id'); + let tabId = instance.find('NavItem button').first().prop('id'); let paneId = instance.find('TabPanel div').first().prop('id'); expect(tabId).to.exist; expect(paneId).to.exist; - instance.assertSingle(`a[aria-controls="${paneId}"]`); + instance.assertSingle(`button[aria-controls="${paneId}"]`); instance.assertSingle(`div[aria-labelledby="${tabId}"]`); }); @@ -91,7 +91,7 @@ describe('', () => { instance.assertSingle('div[role="tablist"]'); instance - .find('NavItem a') + .find('NavItem button') .first() .getDOMNode() .getAttribute('role') @@ -114,8 +114,9 @@ describe('', () => { instance.assertSingle('div[role="navigation"]'); // make sure its not passed to the NavItem - expect(instance.find('NavItem a').first().getDOMNode().getAttribute('role')) - .to.not.exist; + expect( + instance.find('NavItem button').first().getDOMNode().getAttribute('role'), + ).to.not.exist; }); it('Should show the correct tab when selected', () => { @@ -139,7 +140,7 @@ describe('', () => { .text() .should.equal('Tab 1'); - wrapper.find('a[role="tab"]').last().simulate('click'); + wrapper.find('button[role="tab"]').last().simulate('click'); wrapper .find('div[aria-hidden=false]') @@ -169,7 +170,7 @@ describe('', () => { .text() .should.equal('Tab 1'); - wrapper.find('a[role="tab"]').last().simulate('click'); + wrapper.find('button[role="tab"]').last().simulate('click'); wrapper .find('div[role="tabpanel"]') diff --git a/www/docs/Dropdown.mdx b/www/docs/Dropdown.mdx index 5b22e99..b0422ab 100644 --- a/www/docs/Dropdown.mdx +++ b/www/docs/Dropdown.mdx @@ -1,6 +1,5 @@ ---- -title: Introduction ---- +import Tabs from "@theme/Tabs"; +import TabItem from "@theme/TabItem"; `Dropdown` is set of structural components for building, accessible dropdown menus with close-on-click, keyboard navigation, and correct focus handling. As with all the react-overlay's @@ -22,10 +21,16 @@ import { } from "@restart/ui"; const DropdownMenu = ({ role }) => { - const [props, { toggle, show }] = useDropdownMenu({ - flip: true, - offset: [0, 8], - }); + const [props, { toggle, show, popper }] = useDropdownMenu( + { + flip: true, + offset: [0, 8], + } + ); + + useLayoutEffect(() => { + if (show) popper.update(); + }, [show]); return (
setShow(nextShow)} title={`${show ? "Close" : "Open"} Dropdown`} /> - - - -
); } ``` +## Components + +Dropdowns are made up of a wrapping `Dropdown` components, a `Toggle` that +triggers that menu visibility, a `Menu` and the `Item`s in that menu. With the exception +of the outer `Dropdown` component each sub component can be constructed with a component +render prop API or hook depending on preference. + +### Dropdown + +The `Dropdown` component is the non presentational orchestrator of the dropdown state. +It handles menu visibility, keyboard navigation and focus management. As a convenience a +few `Menu` specific props can be provided to `Dropdown` directly, such as `placement`. It +is sometimes helpful to also render a wrapping DOM element around your Toggle and Menu, but not +required. + +```jsx +import { Dropdown } from '@restart/ui'; + + + + + {/* Toggle */} + + +``` + +### Toggle + +The `Toggle` is an interactive element, usually a button, that opens the Dropdown Menu. +The toggle props contain a `ref` that must be passed to a valid DOM element, you +can pass `disabled` and `onClick` props as well and they will be composed into the returned +props automatically. + + + + +```jsx +import { useDropdownToggle } from "@restart/ui"; +import { Button } from "@restart/ui"; + +const DropdownToggle = (props) => { + const [toggleProps] = useDropdownToggle(props); + + return + )} + + + {(menuProps, meta) => ( +
    +

    Hey there

    +
+ )} +
+
; +``` + +If the above isn't feasible and you must use `display: none` to hide the menu, you +may need to manually trigger a position recalculation on show, when popper is able to +measure the element. + +```jsx +import { useDropdownMenu } from "@restart/ui"; + +function Menu() { + const [menuProps, meta] = useDropdownMenu(); + + useLayoutEffect(() => { + meta.update() + }, [meta.show]) + + return ( +
    + {...} +
+ ); +} +``` + ## Different containers Dropdowns use `PopperJS` by default to position Menu's to Toggles. PopperJS is a diff --git a/www/docs/Nav.mdx b/www/docs/Nav.mdx index 8e30e62..acdc9f8 100644 --- a/www/docs/Nav.mdx +++ b/www/docs/Nav.mdx @@ -76,6 +76,82 @@ function Example() { } ``` +## Dropdowns + +Dropdown components can be used in `Nav`'s as well: + +```jsx live +import clsx from "clsx"; +import { + Nav, + useNavItem, + useDropdownToggle, +} from "@restart/ui"; + +import Dropdown from "../src/Dropdown"; + +const NavLink = React.forwardRef( + ({ href, disabled, children, onClick }, ref) => { + const [navItemProps, meta] = useNavItem({ + key: href, + onClick, + disabled, + }); + + return ( + + {children} + + ); + } +); + +function Example() { + const [activeKey, setActiveKey] = useState("/home"); + + return ( + + ); +} +``` + ## Tabs Create dynamic tabbed interfaces from a `Nav`, as described in the WAI ARIA Authoring Practices. diff --git a/www/docs/useDropdownMenu.mdx b/www/docs/useDropdownMenu.mdx deleted file mode 100644 index 277435e..0000000 --- a/www/docs/useDropdownMenu.mdx +++ /dev/null @@ -1,37 +0,0 @@ -Create custom dropdown menus without the extra component with `useDropdownMenu`. -Make sure to spread through the returned props. If creating a navigation menu -(as described [here](https://www.w3.org/TR/wai-aria-practices/#menu)), make sure -to provide the `role='menu'` prop to automatically get correct keyboard focus handling. - -```jsx live showImports -import { useDropdownMenu } from "@restart/ui"; - -const MenuItem = ({ children, href }) => ( -
  • - - {children} - -
  • -); - -const NavMenu = () => { - const { props } = useDropdownMenu(); - - return ( -
      - Home - Docs - About -
    - ); -}; - -; -``` diff --git a/www/docs/useDropdownToggle.mdx b/www/docs/useDropdownToggle.mdx deleted file mode 100644 index 060afbc..0000000 --- a/www/docs/useDropdownToggle.mdx +++ /dev/null @@ -1,25 +0,0 @@ -Create custom dropdown toggles without the extra component with `useDropdownToggle`. -Make sure to spread through the returned props. Accessible `Toggle`s should -also provide an `id` to the HTML element that is rendered. - -```jsx live showImports -import { useDropdownToggle } from "@restart/ui"; - -const Toggle = ({ id, children }) => { - const [props, { show, toggle }] = useDropdownToggle(); - - return ( - - ); -}; - -; -``` diff --git a/www/sidebars.js b/www/sidebars.js index b2fe293..b51f161 100644 --- a/www/sidebars.js +++ b/www/sidebars.js @@ -7,19 +7,7 @@ module.exports = { type: 'category', label: 'API', collapsed: false, - items: [ - 'Button', - { - type: 'category', - label: 'Dropdown', - collapsed: false, - items: ['Dropdown', 'useDropdownMenu', 'useDropdownToggle'], - }, - 'Modal', - 'Nav', - 'Overlay', - 'Portal', - ], + items: ['Button', 'Dropdown', 'Modal', 'Nav', 'Overlay', 'Portal'], }, { type: 'category', diff --git a/www/src/Dropdown.tsx b/www/src/Dropdown.tsx new file mode 100644 index 0000000..2c6a1be --- /dev/null +++ b/www/src/Dropdown.tsx @@ -0,0 +1,63 @@ +import clsx from 'clsx'; +import React from 'react'; +import { + Button, + useDropdownMenu, + useDropdownToggle, + useDropdownItem, + Dropdown as BaseDropdown, + DropdownProps, +} from '@restart/ui'; +import StyledButton from './Button'; + +const DropdownMenu = ({ role, ...props }) => { + const [menuProps, { show }] = useDropdownMenu({ + flip: true, + offset: [0, 8], + }); + + return ( +
    + ); +}; + +const DropdownItem = ({ eventKey, ...props }) => { + const [itemProps, { active }] = useDropdownItem({ + key: eventKey, + ...props, + }); + + return ( +