Skip to content

Commit

Permalink
feat(Dropdown)!: migrate to restart/ui
Browse files Browse the repository at this point in the history
BREAKING CHANGE: remove `onSelect` from DropdownItem
  • Loading branch information
kyletsang committed Aug 7, 2021
1 parent 1ff7af7 commit 7030465
Show file tree
Hide file tree
Showing 8 changed files with 117 additions and 253 deletions.
115 changes: 49 additions & 66 deletions src/Dropdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,24 +2,21 @@ import classNames from 'classnames';
import PropTypes from 'prop-types';
import * as React from 'react';
import { useContext, useMemo } from 'react';
import BaseDropdown from 'react-overlays/Dropdown';
import { DropDirection } from 'react-overlays/DropdownContext';
import BaseDropdown, {
DropdownProps as BaseDropdownProps,
ToggleMetadata,
} from '@restart/ui/Dropdown';
import { useUncontrolled } from 'uncontrollable';
import useEventCallback from '@restart/hooks/useEventCallback';
import DropdownContext from './DropdownContext';
import DropdownContext, { DropDirection } from './DropdownContext';
import DropdownItem from './DropdownItem';
import DropdownMenu from './DropdownMenu';
import DropdownToggle from './DropdownToggle';
import InputGroupContext from './InputGroupContext';
import SelectableContext from './SelectableContext';
import { useBootstrapPrefix } from './ThemeProvider';
import createWithBsPrefix from './createWithBsPrefix';
import {
BsPrefixProps,
BsPrefixRefForwardingComponent,
SelectCallback,
} from './helpers';
import { AlignType, alignPropType } from './types';
import { BsPrefixProps, BsPrefixRefForwardingComponent } from './helpers';
import { AlignType, alignPropType, Placement } from './types';

const DropdownHeader = createWithBsPrefix('dropdown-header', {
defaultProps: { role: 'heading' },
Expand All @@ -33,19 +30,13 @@ const DropdownItemText = createWithBsPrefix('dropdown-item-text', {
});

export interface DropdownProps
extends BsPrefixProps,
Omit<React.HTMLAttributes<HTMLElement>, 'onSelect'> {
drop?: 'up' | 'start' | 'end' | 'down';
extends BaseDropdownProps,
BsPrefixProps,
Omit<React.HTMLAttributes<HTMLElement>, 'onSelect' | 'children'> {
drop?: DropDirection;
align?: AlignType;
show?: boolean;
flip?: boolean;
onToggle?: (
isOpen: boolean,
event: React.SyntheticEvent,
metadata: { source: 'select' | 'click' | 'rootClose' | 'keydown' },
) => void;
focusFirstItemOnShow?: boolean | 'keyboard';
onSelect?: SelectCallback;
navbar?: boolean;
autoClose?: boolean | 'outside' | 'inside';
}
Expand Down Expand Up @@ -156,7 +147,6 @@ const Dropdown: BsPrefixRefForwardingComponent<'div', DropdownProps> =
...props
} = useUncontrolled(pProps, { show: 'onToggle' });

const onSelectCtx = useContext(SelectableContext);
const isInputGroup = useContext(InputGroupContext);
const prefix = useBootstrapPrefix(bsPrefix, 'dropdown');

Expand All @@ -174,67 +164,60 @@ const Dropdown: BsPrefixRefForwardingComponent<'div', DropdownProps> =
};

const handleToggle = useEventCallback(
(nextShow, event, source = event.type) => {
(nextShow: boolean, meta: ToggleMetadata) => {
if (
event.currentTarget === document &&
(source !== 'keydown' || event.key === 'Escape')
meta.originalEvent!.currentTarget === document &&
(meta.source !== 'keydown' ||
(meta.originalEvent as any).key === 'Escape')
)
source = 'rootClose';
meta.source = 'rootClose';

if (isClosingPermitted(source)) onToggle?.(nextShow, event, { source });
if (isClosingPermitted(meta.source!)) onToggle?.(nextShow, meta);
},
);

const handleSelect = useEventCallback((key, event) => {
onSelectCtx?.(key, event);
onSelect?.(key, event);
handleToggle(false, event, 'select');
});

// TODO RTL: Flip directions based on RTL setting.
let direction: DropDirection = drop as DropDirection;
if (drop === 'start') {
direction = 'left';
} else if (drop === 'end') {
direction = 'right';
}
const alignEnd = align === 'end';
let placement: Placement = alignEnd ? 'bottom-end' : 'bottom-start';
if (drop === 'up') placement = alignEnd ? 'top-end' : 'top-start';
else if (drop === 'end') placement = alignEnd ? 'right-end' : 'right-start';
else if (drop === 'start') placement = alignEnd ? 'left-end' : 'left-start';

const contextValue = useMemo(
() => ({
align,
drop,
}),
[align],
[align, drop],
);

return (
<DropdownContext.Provider value={contextValue}>
<SelectableContext.Provider value={handleSelect}>
<BaseDropdown
drop={direction}
show={show}
alignEnd={align === 'end'}
onToggle={handleToggle}
focusFirstItemOnShow={focusFirstItemOnShow}
itemSelector={`.${prefix}-item:not(.disabled):not(:disabled)`}
>
{isInputGroup ? (
props.children
) : (
<Component
{...props}
ref={ref}
className={classNames(
className,
show && 'show',
(!drop || drop === 'down') && prefix,
drop === 'up' && 'dropup',
drop === 'end' && 'dropend',
drop === 'start' && 'dropstart',
)}
/>
)}
</BaseDropdown>
</SelectableContext.Provider>
<BaseDropdown
placement={placement}
show={show}
onSelect={onSelect}
onToggle={handleToggle}
focusFirstItemOnShow={focusFirstItemOnShow}
itemSelector={`.${prefix}-item:not(.disabled):not(:disabled)`}
>
{isInputGroup ? (
props.children
) : (
<Component
{...props}
ref={ref}
className={classNames(
className,
show && 'show',
(!drop || drop === 'down') && prefix,
drop === 'up' && 'dropup',
drop === 'end' && 'dropend',
drop === 'start' && 'dropstart',
)}
/>
)}
</BaseDropdown>
</DropdownContext.Provider>
);
});
Expand Down
5 changes: 2 additions & 3 deletions src/DropdownButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,9 @@ export interface DropdownButtonProps
const propTypes = {
/**
* An html id attribute for the Toggle button, necessary for assistive technologies, such as screen readers.
* @type {string|number}
* @required
* @type {string}
*/
id: PropTypes.any,
id: PropTypes.string,

/** An `href` passed to the Toggle component */
href: PropTypes.string,
Expand Down
3 changes: 3 additions & 0 deletions src/DropdownContext.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import * as React from 'react';
import { AlignType } from './types';

export type DropDirection = 'up' | 'start' | 'end' | 'down';

export type DropdownContextValue = {
align?: AlignType;
drop?: DropDirection;
};

const DropdownContext = React.createContext<DropdownContextValue>({});
Expand Down
87 changes: 21 additions & 66 deletions src/DropdownItem.tsx
Original file line number Diff line number Diff line change
@@ -1,29 +1,17 @@
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 BaseDropdownItem, {
useDropdownItem,
DropdownItemProps as BaseDropdownItemProps,
} from '@restart/ui/DropdownItem';
import Anchor from '@restart/ui/Anchor';
import { useBootstrapPrefix } from './ThemeProvider';
import NavContext from './NavContext';
import SafeAnchor from './SafeAnchor';
import {
BsPrefixProps,
BsPrefixRefForwardingComponent,
SelectCallback,
} from './helpers';
import { EventKey } from './types';
import { BsPrefixProps, BsPrefixRefForwardingComponent } from './helpers';

export interface DropdownItemProps
extends BsPrefixProps,
Omit<React.HTMLAttributes<HTMLElement>, 'onSelect'> {
active?: boolean;
disabled?: boolean;
eventKey?: EventKey;
href?: string;
onSelect?: SelectCallback;
}
extends BaseDropdownItemProps,
BsPrefixProps {}

const propTypes = {
/** @default 'dropdown-item' */
Expand Down Expand Up @@ -54,85 +42,52 @@ const propTypes = {
*/
onClick: PropTypes.func,

/**
* Callback fired when the menu item is selected.
*
* ```js
* (eventKey: any, event: Object) => any
* ```
*/
onSelect: PropTypes.func,

as: PropTypes.elementType,
};

const defaultProps = {
as: SafeAnchor,
disabled: false,
};

const DropdownItem: BsPrefixRefForwardingComponent<
typeof SafeAnchor,
typeof BaseDropdownItem,
DropdownItemProps
> = React.forwardRef(
(
{
bsPrefix,
className,
eventKey,
disabled,
href,
disabled = false,
onClick,
onSelect,
active: propActive,
as: Component,
active,
as: Component = Anchor,
...props
}: DropdownItemProps,
},
ref,
) => {
const prefix = useBootstrapPrefix(bsPrefix, 'dropdown-item');
const onSelectCtx = useContext(SelectableContext);
const navContext = useContext(NavContext);

const { activeKey } = navContext || {};
const key = makeEventKey(eventKey, href);

const active =
propActive == null && key != null
? makeEventKey(activeKey) === key
: propActive;

const handleClick = useEventCallback((event) => {
// SafeAnchor handles the disabled case, but we handle it here
// for other components
if (disabled) return;
onClick?.(event);
onSelectCtx?.(key, event);
onSelect?.(key, event);
const [dropdownItemProps, meta] = useDropdownItem({
key: eventKey,
href: props.href,
disabled,
onClick,
active,
});

return (
// "TS2604: JSX element type 'Component' does not have any construct or call signatures."
// @ts-ignore
<Component
{...props}
{...dropdownItemProps}
ref={ref}
href={href}
disabled={disabled}
className={classNames(
className,
prefix,
active && 'active',
meta.isActive && 'active',
disabled && 'disabled',
)}
onClick={handleClick}
/>
);
},
);

DropdownItem.displayName = 'DropdownItem';
DropdownItem.propTypes = propTypes;
DropdownItem.defaultProps = defaultProps;

export default DropdownItem;
Loading

0 comments on commit 7030465

Please sign in to comment.