Skip to content

Commit

Permalink
feat(DropdownMenu): add auto-locate prop (youzan#12251)
Browse files Browse the repository at this point in the history
  • Loading branch information
inottn authored and CatsAndMice committed Apr 8, 2024
1 parent a2e02ae commit f362df8
Show file tree
Hide file tree
Showing 7 changed files with 136 additions and 7 deletions.
30 changes: 24 additions & 6 deletions packages/vant/src/dropdown-item/DropdownItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import {
reactive,
Teleport,
defineComponent,
ref,
type PropType,
type TeleportProps,
type CSSProperties,
Expand All @@ -15,11 +16,12 @@ import {
getZIndexStyle,
createNamespace,
makeArrayProp,
getContainingBlock,
} from '../utils';
import { DROPDOWN_KEY } from '../dropdown-menu/DropdownMenu';

// Composables
import { useParent } from '@vant/use';
import { useParent, useRect } from '@vant/use';
import { useExpose } from '../composables/use-expose';

// Components
Expand Down Expand Up @@ -59,6 +61,7 @@ export default defineComponent({
transition: true,
showWrapper: false,
});
const wrapperRef = ref<HTMLElement>();

const { parent, index } = useParent(DROPDOWN_KEY);

Expand Down Expand Up @@ -160,20 +163,35 @@ export default defineComponent({

const renderContent = () => {
const { offset } = parent;
const { zIndex, overlay, duration, direction, closeOnClickOverlay } =
parent.props;

const {
autoLocate,
zIndex,
overlay,
duration,
direction,
closeOnClickOverlay,
} = parent.props;
const style: CSSProperties = getZIndexStyle(zIndex);
let offsetValue = offset.value;

if (autoLocate && wrapperRef.value) {
const offsetParent = getContainingBlock(wrapperRef.value);

if (offsetParent) {
offsetValue -= useRect(offsetParent).top;
}
}

if (direction === 'down') {
style.top = `${offset.value}px`;
style.top = `${offsetValue}px`;
} else {
style.bottom = `${offset.value}px`;
style.bottom = `${offsetValue}px`;
}

return (
<div
v-show={state.showWrapper}
ref={wrapperRef}
style={style}
class={bem([direction])}
onClick={onClickWrapper}
Expand Down
1 change: 1 addition & 0 deletions packages/vant/src/dropdown-menu/DropdownMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ export const dropdownMenuProps = {
duration: makeNumericProp(0.2),
direction: makeStringProp<DropdownMenuDirection>('down'),
activeColor: String,
autoLocate: Boolean,
closeOnClickOutside: truthProp,
closeOnClickOverlay: truthProp,
swipeThreshold: numericProp,
Expand Down
1 change: 1 addition & 0 deletions packages/vant/src/dropdown-menu/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,7 @@ You can set `swipe-threshold` prop to customize threshold number.
| close-on-click-overlay | Whether to close when overlay is clicked | _boolean_ | `true` |
| close-on-click-outside | Whether to close when outside is clicked | _boolean_ | `true` |
| swipe-threshold | Horizontal scrolling is allowed when the number of items exceeds the threshold and the total width exceeds the width of the menu. | _number \| string_ | - |
| auto-locate | When the ancestor element is set with a transform, the position of the dropdown menu will be automatically adjusted. | _boolean_ | `false` |

### DropdownItem Props

Expand Down
12 changes: 11 additions & 1 deletion packages/vant/src/dropdown-menu/README.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,7 @@ export default {
| close-on-click-overlay | 是否在点击遮罩层后关闭菜单 | _boolean_ | `true` |
| close-on-click-outside | 是否在点击外部元素后关闭菜单 | _boolean_ | `true` |
| swipe-threshold | 滚动阈值,选项数量超过阈值且总宽度超过菜单栏宽度时,可以横向滚动 | _number \| string_ | - |
| auto-locate | 当祖先元素设置了 transform 时,自动调整下拉菜单的位置 | _boolean_ | `false` |

### DropdownItem Props

Expand Down Expand Up @@ -283,7 +284,7 @@ dropdownItemRef.value?.toggle();

### 父元素设置 transform 后,下拉菜单的位置错误?

`DropdownMenu` 嵌套在 `Tabs` 等组件内部使用时,可能会遇到下拉菜单位置错误的问题。这是因为在 Chrome 浏览器中,transform 元素内部的 fixed 布局会降级成 absolute 布局,导致下拉菜单的布局异常
`DropdownMenu` 嵌套在 `Tabs` 等组件内部使用时,可能会遇到下拉菜单位置错误的问题。这是因为 transform 元素内部的 fixed 定位会相对于该元素进行计算,而不是相对于整个文档,从而导致下拉菜单的布局异常

`DropdownItem``teleport` 属性设置为 `body` 即可避免此问题:

Expand All @@ -293,3 +294,12 @@ dropdownItemRef.value?.toggle();
<van-dropdown-item teleport="body" />
</van-dropdown-menu>
```

也可以将 `DropdownMenu``auto-locate` 属性设置为 `true`

```html
<van-dropdown-menu auto-locate>
<van-dropdown-item />
<van-dropdown-item />
</van-dropdown-menu>
```
42 changes: 42 additions & 0 deletions packages/vant/src/dropdown-menu/test/index.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ import { later, mount } from '../../../test';
import { reactive, ref, onMounted, computed } from 'vue';
import DropdownItem from '../../dropdown-item';
import DropdownMenu, { DropdownMenuDirection } from '..';
import { getContainingBlock } from '../../utils/dom';

vi.mock('../../utils/dom');

function renderWrapper(
options: {
Expand Down Expand Up @@ -325,3 +328,42 @@ test('scrolling is allowed when the number of items exceeds the threshold', asyn
await later();
expect(bar.classes()).toContain('van-dropdown-menu__bar--scrollable');
});

test('auto-locate prop', async () => {
const mockedFn = vi.mocked(getContainingBlock);
const autoLocate = ref(false);
const wrapper = mount({
setup() {
const options = [
{ text: 'A', value: 0 },
{ text: 'B', value: 1 },
];

return () => (
<DropdownMenu autoLocate={autoLocate.value}>
<DropdownItem modelValue={0} options={options} />
</DropdownMenu>
);
},
});

const item = wrapper.find('.van-dropdown-item');
const offsetParent = {
getBoundingClientRect() {
return {
top: 10,
};
},
} as HTMLElement;
expect(mockedFn).not.toHaveBeenCalled();
expect(item.style.top).toEqual('0px');

mockedFn.mockReturnValue(offsetParent);
autoLocate.value = true;
await later();
expect(mockedFn).toHaveBeenCalled();
expect(mockedFn.mock.calls[0]).toEqual([item.element]);
expect(item.style.top).toEqual('-10px');

vi.doUnmock('../../utils/dom');
});
31 changes: 31 additions & 0 deletions packages/vant/src/utils/dom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,3 +86,34 @@ export function isHidden(
}

export const { width: windowWidth, height: windowHeight } = useWindowSize();

function isContainingBlock(el: Element) {
const css = window.getComputedStyle(el);

return (
css.transform !== 'none' ||
css.perspective !== 'none' ||
['transform', 'perspective', 'filter'].some((value) =>
(css.willChange || '').includes(value),
)
);
}

export function getContainingBlock(el: Element) {
let node = el.parentElement;

while (node) {
if (
node &&
node.tagName !== 'HTML' &&
node.tagName !== 'BODY' &&
isContainingBlock(node)
) {
return node;
}

node = node.parentElement;
}

return null;
}
26 changes: 26 additions & 0 deletions packages/vant/src/utils/test/index.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { get, noop, isDef, isMobile, isNumeric } from '../basic';
import { deepClone } from '../deep-clone';
import { deepAssign } from '../deep-assign';
import { getContainingBlock } from '../dom';
import { addUnit, unitToPx, camelize, formatNumber } from '../format';
import { trigger } from '../../../test';

Expand Down Expand Up @@ -120,3 +121,28 @@ test('unitToPx', () => {

spy.mockRestore();
});

test('getContainingBlock', () => {
const root = document.createElement('div');
const parent = document.createElement('div');
const child = document.createElement('div');

root.appendChild(parent);
parent.appendChild(child);

const spy = vi.spyOn(window, 'getComputedStyle').mockImplementation((el) => {
if (el === root)
return {
transform: 'matrix(1, 1, -1, 1, 0, 0)',
} as CSSStyleDeclaration;

return {
transform: 'none',
perspective: 'none',
} as CSSStyleDeclaration;
});

expect(getContainingBlock(child)).toEqual(root);

spy.mockRestore();
});

0 comments on commit f362df8

Please sign in to comment.