Skip to content

Commit

Permalink
feat(DropdownMenu): Add responsive menu alignment (#5307)
Browse files Browse the repository at this point in the history
* feat(DropdownMenu): Add responsive menu alignment

* Apply suggestions

* Allow "left" and "right" to be used in the align prop
Add deprecation notice for alignRight in DropdownMenu

* Remove DEVICE_SIZES in favor of Object.keys

* Add comment to clarify responsive left align requires dropdown-menu-right class

* Fix so that only 1 breakpoint is allowed when using responsive align

* Fix menuAlign types

* Fix TS types
  • Loading branch information
kyletsang committed Aug 31, 2020
1 parent f493a15 commit b5ec39e
Show file tree
Hide file tree
Showing 8 changed files with 186 additions and 9 deletions.
16 changes: 14 additions & 2 deletions src/DropdownButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,14 @@ import PropTypes from 'prop-types';

import Dropdown, { DropdownProps } from './Dropdown';
import DropdownToggle, { PropsFromToggle } from './DropdownToggle';
import DropdownMenu from './DropdownMenu';
import DropdownMenu, { alignPropType, AlignType } from './DropdownMenu';

export interface DropdownButtonProps
extends DropdownProps,
Omit<React.HTMLAttributes<HTMLElement>, 'onSelect' | 'title'>,
React.PropsWithChildren<PropsFromToggle> {
title: React.ReactNode;
menuAlign?: AlignType;
menuRole?: string;
renderMenuOnMount?: boolean;
rootCloseEvent?: 'click' | 'mousedown';
Expand All @@ -36,6 +37,15 @@ const propTypes = {
/** Disables both Buttons */
disabled: PropTypes.bool,

/**
* Aligns the dropdown menu responsively.
*
* _see [DropdownMenu](#dropdown-menu-props) for more details_
*
* @type {"left"|"right"|{ sm: "left"|"right" }|{ md: "left"|"right" }|{ lg: "left"|"right" }|{ xl: "left"|"right"} }
*/
menuAlign: alignPropType,

/** An ARIA accessible role applied to the Menu component. When set to 'menu', The dropdown */
menuRole: PropTypes.string,

Expand All @@ -45,7 +55,7 @@ const propTypes = {
/**
* Which event when fired outside the component will cause it to be closed.
*
* _see [DropdownMenu](#menu-props) for more details_
* _see [DropdownMenu](#dropdown-menu-props) for more details_
*/
rootCloseEvent: PropTypes.string,

Expand Down Expand Up @@ -74,6 +84,7 @@ const DropdownButton = React.forwardRef<HTMLDivElement, DropdownButtonProps>(
rootCloseEvent,
variant,
size,
menuAlign,
menuRole,
renderMenuOnMount,
disabled,
Expand All @@ -95,6 +106,7 @@ const DropdownButton = React.forwardRef<HTMLDivElement, DropdownButtonProps>(
{title}
</DropdownToggle>
<DropdownMenu
align={menuAlign}
role={menuRole}
renderOnMount={renderMenuOnMount}
rootCloseEvent={rootCloseEvent}
Expand Down
75 changes: 72 additions & 3 deletions src/DropdownMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
UseDropdownMenuOptions,
} from 'react-overlays/DropdownMenu';
import useMergedRefs from '@restart/hooks/useMergedRefs';
import warning from 'warning';
import NavbarContext from './NavbarContext';
import { useBootstrapPrefix } from './ThemeProvider';
import useWrappedRefWithWarning from './useWrappedRefWithWarning';
Expand All @@ -17,10 +18,21 @@ import {
SelectCallback,
} from './helpers';

export type AlignDirection = 'left' | 'right';

export type ResponsiveAlignProp =
| { sm: AlignDirection }
| { md: AlignDirection }
| { lg: AlignDirection }
| { xl: AlignDirection };

export type AlignType = AlignDirection | ResponsiveAlignProp;

export interface DropdownMenuProps extends BsPrefixPropsWithChildren {
show?: boolean;
renderOnMount?: boolean;
flip?: boolean;
align?: AlignType;
alignRight?: boolean;
onSelect?: SelectCallback;
rootCloseEvent?: 'click' | 'mousedown';
Expand All @@ -29,6 +41,16 @@ export interface DropdownMenuProps extends BsPrefixPropsWithChildren {

type DropdownMenu = BsPrefixRefForwardingComponent<'div', DropdownMenuProps>;

const alignDirection = PropTypes.oneOf(['left', 'right']);

export const alignPropType = PropTypes.oneOfType([
alignDirection,
PropTypes.shape({ sm: alignDirection }),
PropTypes.shape({ md: alignDirection }),
PropTypes.shape({ lg: alignDirection }),
PropTypes.shape({ xl: alignDirection }),
]);

const propTypes = {
/**
* @default 'dropdown-menu'
Expand All @@ -44,7 +66,22 @@ const propTypes = {
/** Have the dropdown switch to it's opposite placement when necessary to stay on screen. */
flip: PropTypes.bool,

/** Aligns the Dropdown menu to the right of it's container. */
/**
* Aligns the dropdown menu to the specified side of the container. You can also align
* the menu responsively for breakpoints starting at `sm` and up. The alignment
* direction will affect the specified breakpoint or larger.
*
* *Note: Using responsive alignment will disable Popper usage for positioning.*
*
* @type {"left"|"right"|{ sm: "left"|"right" }|{ md: "left"|"right" }|{ lg: "left"|"right" }|{ xl: "left"|"right"} }
*/
align: alignPropType,

/**
* Aligns the Dropdown menu to the right of it's container.
*
* @deprecated Use align="right"
*/
alignRight: PropTypes.bool,

onSelect: PropTypes.func,
Expand Down Expand Up @@ -73,7 +110,8 @@ const propTypes = {
popperConfig: PropTypes.object,
};

const defaultProps = {
const defaultProps: Partial<DropdownMenuProps> = {
align: 'left',
alignRight: false,
flip: true,
};
Expand All @@ -86,6 +124,9 @@ const DropdownMenu: DropdownMenu = React.forwardRef(
{
bsPrefix,
className,
align,
// When we remove alignRight from API, use the var locally to toggle
// .dropdown-menu-right class below.
alignRight,
rootCloseEvent,
flip,
Expand All @@ -102,6 +143,31 @@ const DropdownMenu: DropdownMenu = React.forwardRef(
const prefix = useBootstrapPrefix(bsPrefix, 'dropdown-menu');
const [popperRef, marginModifiers] = usePopperMarginModifiers();

const alignClasses: string[] = [];
if (align) {
if (typeof align === 'object') {
const keys = Object.keys(align);

warning(
keys.length === 1,
'There should only be 1 breakpoint when passing an object to `align`',
);

if (keys.length) {
const brkPoint = keys[0];
const direction = align[brkPoint];

// .dropdown-menu-right is required for responsively aligning
// left in addition to align left classes.
// Reuse alignRight to toggle the class below.
alignRight = direction === 'left';
alignClasses.push(`${prefix}-${brkPoint}-${direction}`);
}
} else if (align === 'right') {
alignRight = true;
}
}

const {
hasShown,
placement,
Expand All @@ -114,7 +180,7 @@ const DropdownMenu: DropdownMenu = React.forwardRef(
rootCloseEvent,
show: showProps,
alignEnd: alignRight,
usePopper: !isNavbar,
usePopper: !isNavbar && alignClasses.length === 0,
popperConfig: {
...popperConfig,
modifiers: marginModifiers.concat(popperConfig?.modifiers || []),
Expand All @@ -137,12 +203,14 @@ const DropdownMenu: DropdownMenu = React.forwardRef(
(menuProps as any).close = close;
(menuProps as any).alignRight = alignEnd;
}

if (placement) {
// we don't need the default popper style,
// menus are display: none when not shown.
(props as any).style = { ...(props as any).style, ...menuProps.style };
props['x-placement'] = placement;
}

return (
<Component
{...props}
Expand All @@ -152,6 +220,7 @@ const DropdownMenu: DropdownMenu = React.forwardRef(
prefix,
show && 'show',
alignEnd && `${prefix}-right`,
...alignClasses,
)}
/>
);
Expand Down
15 changes: 14 additions & 1 deletion src/SplitButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import PropTypes from 'prop-types';
import Button, { ButtonType } from './Button';
import ButtonGroup from './ButtonGroup';
import Dropdown from './Dropdown';
import { alignPropType, AlignType } from './DropdownMenu';
import { PropsFromToggle } from './DropdownToggle';
import {
BsPrefixPropsWithChildren,
Expand All @@ -14,6 +15,7 @@ export interface SplitButtonProps
extends PropsFromToggle,
BsPrefixPropsWithChildren {
id: string | number;
menuAlign?: AlignType;
menuRole?: string;
onClick?: React.MouseEventHandler<this>;
renderMenuOnMount?: boolean;
Expand Down Expand Up @@ -57,6 +59,15 @@ const propTypes = {
/** Disables both Buttons */
disabled: PropTypes.bool,

/**
* Aligns the dropdown menu responsively.
*
* _see [DropdownMenu](#dropdown-menu-props) for more details_
*
* @type {"left"|"right"|{ sm: "left"|"right" }|{ md: "left"|"right" }|{ lg: "left"|"right" }|{ xl: "left"|"right"} }
*/
menuAlign: alignPropType,

/** An ARIA accessible role applied to the Menu component. When set to 'menu', The dropdown */
menuRole: PropTypes.string,

Expand All @@ -66,7 +77,7 @@ const propTypes = {
/**
* Which event when fired outside the component will cause it to be closed.
*
* _see [DropdownMenu](#menu-props) for more details_
* _see [DropdownMenu](#dropdown-menu-props) for more details_
*/
rootCloseEvent: PropTypes.string,

Expand Down Expand Up @@ -97,6 +108,7 @@ const SplitButton: SplitButton = React.forwardRef(
onClick,
href,
target,
menuAlign,
menuRole,
renderMenuOnMount,
rootCloseEvent,
Expand Down Expand Up @@ -129,6 +141,7 @@ const SplitButton: SplitButton = React.forwardRef(
</Dropdown.Toggle>

<Dropdown.Menu
align={menuAlign}
role={menuRole}
renderOnMount={renderMenuOnMount}
rootCloseEvent={rootCloseEvent}
Expand Down
36 changes: 36 additions & 0 deletions test/DropdownMenuSpec.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,42 @@ describe('<Dropdown.Menu>', () => {
).assertSingle('div.dropdown-menu');
});

it('does not add any extra classes when align="left"', () => {
const wrapper = mount(
<DropdownMenu show align="left">
<DropdownItem>Item</DropdownItem>
</DropdownMenu>,
).find('DropdownMenu');

expect(wrapper.getDOMNode().className).to.equal('dropdown-menu show');
});

it('adds right align class when align="right"', () => {
mount(
<DropdownMenu show align="right">
<DropdownItem>Item</DropdownItem>
</DropdownMenu>,
).assertSingle('.dropdown-menu-right');
});

it('adds responsive left alignment classes', () => {
mount(
<DropdownMenu show align={{ lg: 'left' }}>
<DropdownItem>Item</DropdownItem>
</DropdownMenu>,
)
.assertSingle('.dropdown-menu-right')
.assertSingle('.dropdown-menu-lg-left');
});

it('adds responsive right alignment classes', () => {
mount(
<DropdownMenu show align={{ lg: 'right' }}>
<DropdownItem>Item</DropdownItem>
</DropdownMenu>,
).assertSingle('.dropdown-menu-lg-right');
});

// it.only('warns about bad refs', () => {
// class Parent extends React.Component {
// componentDidCatch() {}
Expand Down
5 changes: 5 additions & 0 deletions tests/simple-types-test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -315,6 +315,7 @@ const MegaComponent = () => (
show
bsPrefix="dropdownmenu"
style={style}
align={{ sm: 'left' }}
>
<Dropdown.Item
active
Expand All @@ -335,6 +336,8 @@ const MegaComponent = () => (
<Dropdown.Divider as="div" bsPrefix="dropdowndivider" style={style} />
<Dropdown.Divider as="div" bsPrefix="prefix" style={style} />
</Dropdown.Menu>
<Dropdown.Menu align="left" />
<Dropdown.Menu align="right" />
</Dropdown>
<DropdownButton
disabled
Expand All @@ -349,6 +352,7 @@ const MegaComponent = () => (
variant="primary"
bsPrefix="dropdownbtn"
style={style}
menuAlign={{ sm: 'left' }}
>
<Dropdown.Item href="#/action-1">Action</Dropdown.Item>
<Dropdown.Item href="#/action-2">Another action</Dropdown.Item>
Expand Down Expand Up @@ -932,6 +936,7 @@ const MegaComponent = () => (
variant="primary"
bsPrefix="splitbutton"
style={style}
menuAlign={{ sm: 'left' }}
/>
<Table
id="id"
Expand Down
23 changes: 23 additions & 0 deletions www/src/examples/Dropdown/MenuAlignResponsive.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<>
<div>
<DropdownButton
as={ButtonGroup}
menuAlign={{ lg: 'right' }}
title="Left-aligned but right aligned when large screen"
id="dropdown-menu-align-responsive-1"
>
<Dropdown.Item eventKey="1">Action 1</Dropdown.Item>
<Dropdown.Item eventKey="2">Action 2</Dropdown.Item>
</DropdownButton>
</div>
<div className="mt-2">
<SplitButton
menuAlign={{ lg: 'left' }}
title="Right-aligned but left aligned when large screen"
id="dropdown-menu-align-responsive-2"
>
<Dropdown.Item eventKey="1">Action 1</Dropdown.Item>
<Dropdown.Item eventKey="2">Action 2</Dropdown.Item>
</SplitButton>
</div>
</>;
2 changes: 1 addition & 1 deletion www/src/examples/Dropdown/MenuAlignRight.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<DropdownButton
alignRight
menuAlign="right"
title="Dropdown right"
id="dropdown-menu-align-right"
>
Expand Down

0 comments on commit b5ec39e

Please sign in to comment.