Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: Dropdown will jump #6715 #6723

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions src/Dropdown.tsx
Expand Up @@ -29,6 +29,7 @@ export interface DropdownProps
focusFirstItemOnShow?: boolean | 'keyboard';
navbar?: boolean;
autoClose?: boolean | 'outside' | 'inside';
lockPlacement?: boolean;
}

const propTypes = {
Expand Down
4 changes: 4 additions & 0 deletions src/DropdownButton.tsx
Expand Up @@ -17,6 +17,7 @@ export interface DropdownButtonProps
rootCloseEvent?: 'click' | 'mousedown';
menuVariant?: DropdownMenuVariant;
flip?: boolean;
lockPlacement?: boolean;
}

const propTypes = {
Expand Down Expand Up @@ -80,6 +81,7 @@ const propTypes = {
variant: PropTypes.string,
/** @ignore */
size: PropTypes.string,
lockPlacement: PropTypes.bool,
};

/**
Expand Down Expand Up @@ -110,6 +112,7 @@ const DropdownButton: BsPrefixRefForwardingComponent<
id,
menuVariant,
flip,
lockPlacement,
...props
},
ref,
Expand All @@ -126,6 +129,7 @@ const DropdownButton: BsPrefixRefForwardingComponent<
{title}
</DropdownToggle>
<DropdownMenu
lockPlacement={lockPlacement}
role={menuRole}
renderOnMount={renderMenuOnMount}
rootCloseEvent={rootCloseEvent}
Expand Down
26 changes: 25 additions & 1 deletion src/DropdownMenu.tsx
Expand Up @@ -25,6 +25,7 @@ export interface DropdownMenuProps
show?: boolean;
renderOnMount?: boolean;
flip?: boolean;
lockPlacement?: boolean;
align?: AlignType;
rootCloseEvent?: 'click' | 'mousedown';
popperConfig?: UseDropdownMenuOptions['popperConfig'];
Expand Down Expand Up @@ -86,6 +87,13 @@ const propTypes = {
* Omitting this will use the default light color.
*/
variant: PropTypes.string,

/**
*Lock the drop down menu such that when scroll down outside
* or within the viewport, the dropdown will not jump
*
*/
lockPlacement: PropTypes.bool, // New prop to lock the placement
};

export function getDropdownMenuPlacement(
Expand Down Expand Up @@ -128,6 +136,7 @@ const DropdownMenu: BsPrefixRefForwardingComponent<'div', DropdownMenuProps> =
as: Component = 'div',
popperConfig,
variant,
lockPlacement,
...props
},
ref,
Expand Down Expand Up @@ -165,13 +174,28 @@ const DropdownMenu: BsPrefixRefForwardingComponent<'div', DropdownMenuProps> =

const placement = getDropdownMenuPlacement(alignEnd, drop, isRTL);

const mergedPopperConfig = {
...popperConfig,
modifiers: [
...(popperConfig?.modifiers || []),
{
name: 'flip',
enabled: !lockPlacement, // Disable flip modifier when lockPlacement is true
},
{
name: 'preventOverflow',
enabled: !lockPlacement, // Disable preventOverflow modifier when lockPlacement is true
},
],
};

const [menuProps, { hasShown, popper, show, toggle }] = useDropdownMenu({
flip,
rootCloseEvent,
show: showProps,
usePopper: !isNavbar && alignClasses.length === 0,
offset: [0, 2],
popperConfig,
popperConfig: mergedPopperConfig, // Use the merged popperConfig here
placement,
});

Expand Down
46 changes: 46 additions & 0 deletions test/DropdownButtonSpec.tsx
@@ -1,5 +1,6 @@
import { render, fireEvent } from '@testing-library/react';
import sinon from 'sinon';
import { expect } from 'chai';
import DropdownButton from '../src/DropdownButton';
import DropdownItem from '../src/DropdownItem';

Expand Down Expand Up @@ -140,4 +141,49 @@ describe('<DropdownButton>', () => {
const button = getByTestId('test-id').firstElementChild!;
button.classList.contains('my-button-primary').should.be.true;
});

it('maintains its placement when lockPlacement is set to true', () => {
const { container, getByTestId } = render(
<DropdownButton
title="title"
data-testid="test-id"
bsPrefix="my-button"
lockPlacement
align="start"
>
<DropdownItem eventKey="1">Item 1</DropdownItem>
<DropdownItem eventKey="2">Item 2</DropdownItem>
<DropdownItem eventKey="3">Item 3</DropdownItem>
<DropdownItem eventKey="4">Item 4</DropdownItem>
<DropdownItem eventKey="5">Item 5</DropdownItem>
<DropdownItem eventKey="6">Item 6</DropdownItem>
<DropdownItem eventKey="7">Item 7</DropdownItem>
<DropdownItem eventKey="8">Item 8</DropdownItem>
<DropdownItem eventKey="9">Item 9</DropdownItem>
<DropdownItem eventKey="10">Item 10</DropdownItem>
<DropdownItem eventKey="11">Item 11</DropdownItem>
<DropdownItem eventKey="12">Item 12</DropdownItem>
<DropdownItem eventKey="13">Item 13</DropdownItem>
<DropdownItem eventKey="14">Item 14</DropdownItem>
<DropdownItem eventKey="15">Item 15</DropdownItem>
</DropdownButton>,
);
console.log({ container });

fireEvent.click(getByTestId('test-id').firstElementChild!);
const initialPlacement = container
.querySelector('div[x-placement]')!
.getAttribute('x-placement');

// Simulate a scroll event
// Note: You may need to dispatch this event on the window or another scrollable parent,
// depending on where you have attached your scroll listener.
fireEvent.scroll(window, { target: { scrollY: 100 } });

const finalPlacement = container
.querySelector('div[x-placement]')!
.getAttribute('x-placement');

expect(initialPlacement).equal(finalPlacement);
});
});