mirror of
https://gitee.com/vant-contrib/vant.git
synced 2026-07-12 08:54:23 +08:00
Compare commits
2 Commits
2cc4ca63b5
...
b49c640789
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b49c640789 | ||
|
|
5bc2f815af |
@ -49,13 +49,37 @@ const initMarkdownIt = () => {
|
||||
|
||||
const md = initMarkdownIt();
|
||||
|
||||
const markdownToVue = (raw) => {
|
||||
const markdownToJs = (raw) => {
|
||||
let html = md.render(raw);
|
||||
html = `<div class="van-doc-markdown-body">${html}</div>`;
|
||||
html = markdownCardWrapper(html);
|
||||
// escape curly brackets
|
||||
html = html.replace(/<code(.*?)>/g, '<code$1 v-pre>');
|
||||
return `<template>${html}</template>`;
|
||||
|
||||
return `
|
||||
import { openBlock, createElementBlock } from 'vue';
|
||||
|
||||
const _hoisted_1 = ['innerHTML'];
|
||||
const html = ${JSON.stringify(html)};
|
||||
|
||||
export default {
|
||||
setup() {
|
||||
return { html: '' };
|
||||
},
|
||||
render() {
|
||||
return (
|
||||
openBlock(),
|
||||
createElementBlock(
|
||||
'div',
|
||||
{
|
||||
class: 'van-doc-markdown-body',
|
||||
innerHTML: html,
|
||||
},
|
||||
null,
|
||||
8 /* PROPS */,
|
||||
_hoisted_1,
|
||||
)
|
||||
);
|
||||
},
|
||||
};
|
||||
`
|
||||
};
|
||||
|
||||
// add target="_blank" to all links
|
||||
@ -78,5 +102,5 @@ function markdownLinkOpen(md) {
|
||||
}
|
||||
|
||||
module.exports = function (raw) {
|
||||
return markdownToVue(raw);
|
||||
return markdownToJs(raw);
|
||||
};
|
||||
|
||||
@ -79,22 +79,8 @@ export async function compileSite(isProd = false) {
|
||||
templateParameters: getTemplateParams(),
|
||||
},
|
||||
tools: {
|
||||
bundlerChain(chain, { CHAIN_ID }) {
|
||||
const vueRule = chain.module.rules
|
||||
.get(CHAIN_ID.RULE.VUE)
|
||||
.use(CHAIN_ID.USE.VUE);
|
||||
const vueLoader = vueRule.get('loader');
|
||||
const vueOptions = vueRule.get('options');
|
||||
|
||||
chain.module
|
||||
.rule('md')
|
||||
.test(/\.md$/)
|
||||
.use('vue')
|
||||
.loader(vueLoader)
|
||||
.options(vueOptions)
|
||||
.end()
|
||||
.use('md')
|
||||
.loader(MD_LOADER);
|
||||
bundlerChain(chain) {
|
||||
chain.module.rule('md').test(/\.md$/).use('md').loader(MD_LOADER);
|
||||
},
|
||||
rspack: {
|
||||
plugins: [
|
||||
|
||||
@ -2,6 +2,7 @@ import {
|
||||
reactive,
|
||||
Teleport,
|
||||
defineComponent,
|
||||
ref,
|
||||
type PropType,
|
||||
type TeleportProps,
|
||||
type CSSProperties,
|
||||
@ -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
|
||||
@ -59,6 +61,7 @@ export default defineComponent({
|
||||
transition: true,
|
||||
showWrapper: false,
|
||||
});
|
||||
const wrapperRef = ref<HTMLElement>();
|
||||
|
||||
const { parent, index } = useParent(DROPDOWN_KEY);
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -283,7 +284,7 @@ dropdownItemRef.value?.toggle();
|
||||
|
||||
### 父元素设置 transform 后,下拉菜单的位置错误?
|
||||
|
||||
把 `DropdownMenu` 嵌套在 `Tabs` 等组件内部使用时,可能会遇到下拉菜单位置错误的问题。这是因为在 Chrome 浏览器中,transform 元素内部的 fixed 布局会降级成 absolute 布局,导致下拉菜单的布局异常。
|
||||
把 `DropdownMenu` 嵌套在 `Tabs` 等组件内部使用时,可能会遇到下拉菜单位置错误的问题。这是因为 transform 元素内部的 fixed 定位会相对于该元素进行计算,而不是相对于整个文档,从而导致下拉菜单的布局异常。
|
||||
|
||||
将 `DropdownItem` 的 `teleport` 属性设置为 `body` 即可避免此问题:
|
||||
|
||||
@ -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>
|
||||
```
|
||||
|
||||
@ -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: {
|
||||
@ -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');
|
||||
});
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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';
|
||||
|
||||
@ -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();
|
||||
});
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user