Skip to content

Commit

Permalink
feat(Dropdown): add RTL support
Browse files Browse the repository at this point in the history
  • Loading branch information
kyletsang committed Aug 11, 2021
1 parent c82e133 commit 22d8d84
Show file tree
Hide file tree
Showing 6 changed files with 92 additions and 25 deletions.
16 changes: 7 additions & 9 deletions src/Dropdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,13 @@ import { useUncontrolled } from 'uncontrollable';
import useEventCallback from '@restart/hooks/useEventCallback';
import DropdownContext, { DropDirection } from './DropdownContext';
import DropdownItem from './DropdownItem';
import DropdownMenu from './DropdownMenu';
import DropdownMenu, { getDropdownMenuPlacement } from './DropdownMenu';
import DropdownToggle from './DropdownToggle';
import InputGroupContext from './InputGroupContext';
import { useBootstrapPrefix } from './ThemeProvider';
import { useBootstrapPrefix, useIsRTL } from './ThemeProvider';
import createWithBsPrefix from './createWithBsPrefix';
import { BsPrefixProps, BsPrefixRefForwardingComponent } from './helpers';
import { AlignType, alignPropType, Placement } from './types';
import { AlignType, alignPropType } from './types';

const DropdownHeader = createWithBsPrefix('dropdown-header', {
defaultProps: { role: 'heading' },
Expand Down Expand Up @@ -149,6 +149,7 @@ const Dropdown: BsPrefixRefForwardingComponent<'div', DropdownProps> =

const isInputGroup = useContext(InputGroupContext);
const prefix = useBootstrapPrefix(bsPrefix, 'dropdown');
const isRTL = useIsRTL();

const isClosingPermitted = (source: string): boolean => {
// autoClose=false only permits close on button click
Expand Down Expand Up @@ -176,19 +177,16 @@ const Dropdown: BsPrefixRefForwardingComponent<'div', DropdownProps> =
},
);

// TODO RTL: Flip directions based on RTL setting.
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 placement = getDropdownMenuPlacement(alignEnd, drop, isRTL);

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

return (
Expand Down
1 change: 1 addition & 0 deletions src/DropdownContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export type DropDirection = 'up' | 'start' | 'end' | 'down';
export type DropdownContextValue = {
align?: AlignType;
drop?: DropDirection;
isRTL?: boolean;
};

const DropdownContext = React.createContext<DropdownContextValue>({});
Expand Down
34 changes: 26 additions & 8 deletions src/DropdownMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {
import useMergedRefs from '@restart/hooks/useMergedRefs';
import { SelectCallback } from '@restart/ui/types';
import warning from 'warning';
import DropdownContext from './DropdownContext';
import DropdownContext, { DropDirection } from './DropdownContext';
import InputGroupContext from './InputGroupContext';
import NavbarContext from './NavbarContext';
import { useBootstrapPrefix } from './ThemeProvider';
Expand Down Expand Up @@ -95,6 +95,29 @@ const defaultProps: Partial<DropdownMenuProps> = {
flip: true,
};

export function getDropdownMenuPlacement(
alignEnd: boolean,
dropDirection?: DropDirection,
isRTL?: boolean,
) {
const topStart = isRTL ? 'top-end' : 'top-start';
const topEnd = isRTL ? 'top-start' : 'top-end';
const bottomStart = isRTL ? 'bottom-end' : 'bottom-start';
const bottomEnd = isRTL ? 'bottom-start' : 'bottom-end';
const leftStart = isRTL ? 'right-start' : 'left-start';
const leftEnd = isRTL ? 'right-end' : 'left-end';
const rightStart = isRTL ? 'left-start' : 'right-start';
const rightEnd = isRTL ? 'left-end' : 'right-end';

let placement: Placement = alignEnd ? bottomEnd : bottomStart;
if (dropDirection === 'up') placement = alignEnd ? topEnd : topStart;
else if (dropDirection === 'end')
placement = alignEnd ? rightEnd : rightStart;
else if (dropDirection === 'start')
placement = alignEnd ? leftEnd : leftStart;
return placement;
}

const DropdownMenu: BsPrefixRefForwardingComponent<'div', DropdownMenuProps> =
React.forwardRef<HTMLElement, DropdownMenuProps>(
(
Expand All @@ -117,7 +140,7 @@ const DropdownMenu: BsPrefixRefForwardingComponent<'div', DropdownMenuProps> =
let alignEnd = false;
const isNavbar = useContext(NavbarContext);
const prefix = useBootstrapPrefix(bsPrefix, 'dropdown-menu');
const { align: contextAlign, drop } = useContext(DropdownContext);
const { align: contextAlign, drop, isRTL } = useContext(DropdownContext);
align = align || contextAlign;
const isInputGroup = useContext(InputGroupContext);

Expand Down Expand Up @@ -145,12 +168,7 @@ const DropdownMenu: BsPrefixRefForwardingComponent<'div', DropdownMenuProps> =
}
}

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 placement = getDropdownMenuPlacement(alignEnd, drop, isRTL);

const [menuProps, { hasShown, popper, show, toggle }] = useDropdownMenu({
flip,
Expand Down
56 changes: 55 additions & 1 deletion test/DropdownMenuSpec.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { mount } from 'enzyme';
import DropdownItem from '../src/DropdownItem';
import DropdownMenu from '../src/DropdownMenu';
import DropdownMenu, { getDropdownMenuPlacement } from '../src/DropdownMenu';

describe('<Dropdown.Menu>', () => {
const simpleMenu = (
Expand Down Expand Up @@ -86,6 +86,60 @@ describe('<Dropdown.Menu>', () => {
).assertSingle('.dropdown-menu.dropdown-menu-dark');
});

describe('getDropdownMenuPlacement', () => {
it('should return top placement', () => {
getDropdownMenuPlacement(false, 'up', false).should.equal('top-start');
getDropdownMenuPlacement(true, 'up', false).should.equal('top-end');
});

it('should return top placement for RTL', () => {
getDropdownMenuPlacement(false, 'up', true).should.equal('top-end');
getDropdownMenuPlacement(true, 'up', true).should.equal('top-start');
});

it('should return end placement', () => {
getDropdownMenuPlacement(false, 'end', false).should.equal('right-start');
getDropdownMenuPlacement(true, 'end', false).should.equal('right-end');
});

it('should return end placement for RTL', () => {
getDropdownMenuPlacement(false, 'end', true).should.equal('left-start');
getDropdownMenuPlacement(true, 'end', true).should.equal('left-end');
});

it('should return bottom placement', () => {
getDropdownMenuPlacement(false, 'bottom', false).should.equal(
'bottom-start',
);
getDropdownMenuPlacement(true, 'bottom', false).should.equal(
'bottom-end',
);
});

it('should return bottom placement for RTL', () => {
getDropdownMenuPlacement(false, 'bottom', true).should.equal(
'bottom-end',
);
getDropdownMenuPlacement(true, 'bottom', true).should.equal(
'bottom-start',
);
});

it('should return start placement', () => {
getDropdownMenuPlacement(false, 'start', false).should.equal(
'left-start',
);
getDropdownMenuPlacement(true, 'start', false).should.equal('left-end');
});

it('should return start placement for RTL', () => {
getDropdownMenuPlacement(false, 'start', true).should.equal(
'right-start',
);
getDropdownMenuPlacement(true, 'start', true).should.equal('right-end');
});
});

// it.only('warns about bad refs', () => {
// class Parent extends React.Component {
// componentDidCatch() {}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,4 @@
<DropdownButton
align="end"
title="Dropdown right"
id="dropdown-menu-align-right"
>
<DropdownButton align="end" title="Dropdown end" id="dropdown-menu-align-end">
<Dropdown.Item eventKey="1">Action</Dropdown.Item>
<Dropdown.Item eventKey="2">Another action</Dropdown.Item>
<Dropdown.Item eventKey="3">Something else here</Dropdown.Item>
Expand Down
4 changes: 2 additions & 2 deletions www/src/pages/components/dropdowns.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import DropdownButtonCustomMenu from '../../examples/Dropdown/ButtonCustomMenu';
import DropdownButtonSizes from '../../examples/Dropdown/ButtonSizes';
import DropDirections from '../../examples/Dropdown/DropDirections';
import DropdownItemTags from '../../examples/Dropdown/DropdownItemTags';
import MenuAlignRight from '../../examples/Dropdown/MenuAlignRight';
import MenuAlignEnd from '../../examples/Dropdown/MenuAlignEnd';
import MenuAlignResponsive from '../../examples/Dropdown/MenuAlignResponsive';
import MenuDividers from '../../examples/Dropdown/MenuDividers';
import MenuHeaders from '../../examples/Dropdown/MenuHeaders';
Expand Down Expand Up @@ -131,7 +131,7 @@ Feel free to style further with custom CSS or text utilities.
By default, a dropdown menu is aligned to the left, but you can switch
it by passing `align="end"` to a `<Dropdown>`, `<DropdownButton>`, or `<SplitButton>`.

<ReactPlayground codeText={MenuAlignRight} />
<ReactPlayground codeText={MenuAlignEnd} />

### Responsive alignment

Expand Down

0 comments on commit 22d8d84

Please sign in to comment.