Skip to content

Commit

Permalink
feat: various dropdown improvements
Browse files Browse the repository at this point in the history
  • Loading branch information
jquense committed Apr 20, 2021
1 parent 310cf53 commit b00f23d
Show file tree
Hide file tree
Showing 14 changed files with 439 additions and 188 deletions.
1 change: 1 addition & 0 deletions src/Button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
37 changes: 12 additions & 25 deletions src/Dropdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
/**
Expand All @@ -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
Expand All @@ -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.
*
Expand Down Expand Up @@ -102,8 +101,7 @@ export interface ToggleMetadata {
}

export interface DropdownProps {
drop?: DropDirection;
alignEnd?: boolean;
placement?: Placement;
defaultShow?: boolean;
show?: boolean;
onSelect?: SelectCallback;
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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);
Expand Down
6 changes: 2 additions & 4 deletions src/DropdownContext.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -10,8 +9,7 @@ export type DropdownContextValue = {
setToggle: (ref: HTMLElement | null) => void;

show: boolean;
alignEnd?: boolean;
drop?: DropDirection;
placement?: Placement;
};

const DropdownContext = React.createContext<DropdownContextValue | null>(null);
Expand Down
7 changes: 5 additions & 2 deletions src/DropdownItem.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
import classNames from 'classnames';
import PropTypes from 'prop-types';
import * as React from 'react';
import { useContext } from 'react';
import useEventCallback from '@restart/hooks/useEventCallback';

import SelectableContext, { makeEventKey } from './SelectableContext';
import NavContext from './NavContext';
// import SafeAnchor from './SafeAnchor';

import {
EventKey,
DynamicRefForwardingComponent,
Expand Down Expand Up @@ -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,
Expand Down
28 changes: 11 additions & 17 deletions src/DropdownMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'];
Expand All @@ -35,7 +36,7 @@ export type UserDropdownMenuArrowProps = Record<string, any> & {

export interface UseDropdownMenuMetadata {
show: boolean;
alignEnd?: boolean;
placement?: Placement;
hasShown: boolean;
toggle?: DropdownContextValue['toggle'];
popper: UsePopperState | null;
Expand Down Expand Up @@ -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;
Expand All @@ -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,
Expand All @@ -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,
Expand Down Expand Up @@ -141,7 +136,6 @@ const propTypes = {
*
* @type {Function ({
* show: boolean,
* alignEnd: boolean,
* close: (?SyntheticEvent) => void,
* placement: Placement,
* update: () => void,
Expand All @@ -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
Expand Down Expand Up @@ -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';
Expand Down
36 changes: 12 additions & 24 deletions src/usePopper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<CSSStyleDeclaration> => ({
position,
top: '0',
left: '0',
opacity: '0',
pointerEvents: 'none',
});

const disabledApplyStylesModifier = { name: 'applyStyles', enabled: false };

// until docjs supports type exports...
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -140,7 +128,7 @@ function usePopper(
forceUpdate,
attributes: {},
styles: {
popper: initialPopperStyles(strategy),
popper: {},
arrow: {},
},
}),
Expand Down Expand Up @@ -206,7 +194,7 @@ function usePopper(
setState((s) => ({
...s,
attributes: {},
styles: { popper: initialPopperStyles(strategy) },
styles: { popper: {} },
}));
}
};
Expand Down
Loading

0 comments on commit b00f23d

Please sign in to comment.