Skip to content

Commit 3f5f3ec

Browse files
authored
Feat v4 floatbutton (#6294)
* feat: add float-button components * fix type & demo display * fix components entry * fix review bug * fix bug * fix .value
1 parent 41a455f commit 3f5f3ec

28 files changed

+1989
-4
lines changed

components/components.ts

+3
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,9 @@ export { default as Drawer } from './drawer';
7676
export type { EmptyProps } from './empty';
7777
export { default as Empty } from './empty';
7878

79+
export type { FloatButtonProps, FloatButtonGroupProps } from './float-button/interface';
80+
export { default as FloatButton, FloatButtonGroup } from './float-button';
81+
7982
export type { FormProps, FormItemProps, FormInstance, FormItemInstance } from './form';
8083
export { default as Form, FormItem, FormItemRest } from './form';
8184

components/float-button/BackTop.tsx

+143
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
import VerticalAlignTopOutlined from '@ant-design/icons-vue/VerticalAlignTopOutlined';
2+
import { getTransitionProps, Transition } from '../_util/transition';
3+
import {
4+
defineComponent,
5+
nextTick,
6+
onActivated,
7+
onBeforeUnmount,
8+
onMounted,
9+
reactive,
10+
ref,
11+
watch,
12+
onDeactivated,
13+
} from 'vue';
14+
import FloatButton from './FloatButton';
15+
import useConfigInject from '../config-provider/hooks/useConfigInject';
16+
import getScroll from '../_util/getScroll';
17+
import scrollTo from '../_util/scrollTo';
18+
import throttleByAnimationFrame from '../_util/throttleByAnimationFrame';
19+
import { initDefaultProps } from '../_util/props-util';
20+
import { backTopProps } from './interface';
21+
import { floatButtonPrefixCls } from './FloatButton';
22+
23+
import useStyle from './style';
24+
25+
const BackTop = defineComponent({
26+
compatConfig: { MODE: 3 },
27+
name: 'ABackTop',
28+
inheritAttrs: false,
29+
props: initDefaultProps(backTopProps(), {
30+
visibilityHeight: 400,
31+
target: () => window,
32+
duration: 450,
33+
}),
34+
// emits: ['click'],
35+
setup(props, { slots, attrs, emit }) {
36+
const { prefixCls, direction } = useConfigInject(floatButtonPrefixCls, props);
37+
38+
const [wrapSSR] = useStyle(prefixCls);
39+
40+
const domRef = ref();
41+
const state = reactive({
42+
visible: false,
43+
scrollEvent: null,
44+
});
45+
46+
const getDefaultTarget = () =>
47+
domRef.value && domRef.value.ownerDocument ? domRef.value.ownerDocument : window;
48+
49+
const scrollToTop = (e: Event) => {
50+
const { target = getDefaultTarget, duration } = props;
51+
scrollTo(0, {
52+
getContainer: target,
53+
duration,
54+
});
55+
emit('click', e);
56+
};
57+
58+
const handleScroll = throttleByAnimationFrame((e: Event | { target: any }) => {
59+
const { visibilityHeight } = props;
60+
const scrollTop = getScroll(e.target, true);
61+
state.visible = scrollTop >= visibilityHeight;
62+
});
63+
64+
const bindScrollEvent = () => {
65+
const { target } = props;
66+
const getTarget = target || getDefaultTarget;
67+
const container = getTarget();
68+
handleScroll({ target: container });
69+
container?.addEventListener('scroll', handleScroll);
70+
};
71+
72+
const scrollRemove = () => {
73+
const { target } = props;
74+
const getTarget = target || getDefaultTarget;
75+
const container = getTarget();
76+
handleScroll.cancel();
77+
container?.removeEventListener('scroll', handleScroll);
78+
};
79+
80+
watch(
81+
() => props.target,
82+
() => {
83+
scrollRemove();
84+
nextTick(() => {
85+
bindScrollEvent();
86+
});
87+
},
88+
);
89+
90+
onMounted(() => {
91+
nextTick(() => {
92+
bindScrollEvent();
93+
});
94+
});
95+
96+
onActivated(() => {
97+
nextTick(() => {
98+
bindScrollEvent();
99+
});
100+
});
101+
102+
onDeactivated(() => {
103+
scrollRemove();
104+
});
105+
106+
onBeforeUnmount(() => {
107+
scrollRemove();
108+
});
109+
110+
return () => {
111+
const defaultElement = (
112+
<div class={`${prefixCls.value}-content`}>
113+
<div class={`${prefixCls.value}-icon`}>
114+
<VerticalAlignTopOutlined />
115+
</div>
116+
</div>
117+
);
118+
const divProps = {
119+
...attrs,
120+
onClick: scrollToTop,
121+
class: {
122+
[`${prefixCls.value}`]: true,
123+
[`${attrs.class}`]: attrs.class,
124+
[`${prefixCls.value}-rtl`]: direction.value === 'rtl',
125+
},
126+
};
127+
128+
const transitionProps = getTransitionProps('fade');
129+
return wrapSSR(
130+
<Transition {...transitionProps}>
131+
<FloatButton v-show={state.visible} {...divProps} ref={domRef}>
132+
{{
133+
icon: () => <VerticalAlignTopOutlined />,
134+
default: () => slots.default?.() || defaultElement,
135+
}}
136+
</FloatButton>
137+
</Transition>,
138+
);
139+
};
140+
},
141+
});
142+
143+
export default BackTop;
+121
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
import classNames from '../_util/classNames';
2+
import { defineComponent, computed, CSSProperties, ref } from 'vue';
3+
import Tooltip from '../tooltip';
4+
import Content from './FloatButtonContent';
5+
import type { FloatButtonContentProps } from './interface';
6+
import useConfigInject from '../config-provider/hooks/useConfigInject';
7+
import FloatButtonGroupContext from './context';
8+
import warning from '../_util/warning';
9+
import { initDefaultProps } from '../_util/props-util';
10+
import { floatButtonProps } from './interface';
11+
// import { useCompactItemContext } from '../space/Compact';
12+
13+
// CSSINJS
14+
import useStyle from './style';
15+
16+
export const floatButtonPrefixCls = 'float-btn';
17+
18+
const FloatButton = defineComponent({
19+
compatConfig: { MODE: 3 },
20+
name: 'AFloatButton',
21+
inheritAttrs: false,
22+
props: initDefaultProps(floatButtonProps(), { type: 'default', shape: 'circle' }),
23+
setup(props, { attrs, slots, expose }) {
24+
const { prefixCls, direction } = useConfigInject(floatButtonPrefixCls, props);
25+
const [wrapSSR, hashId] = useStyle(prefixCls);
26+
const { shape: groupShape } = FloatButtonGroupContext.useInject();
27+
28+
const floatButtonRef = ref(null);
29+
30+
const mergeShape = computed(() => {
31+
return groupShape?.value || props.shape;
32+
});
33+
34+
expose({
35+
floatButtonEl: floatButtonRef,
36+
});
37+
38+
return () => {
39+
const {
40+
prefixCls: customPrefixCls,
41+
type = 'default',
42+
shape = 'circle',
43+
description,
44+
tooltip,
45+
...restProps
46+
} = props;
47+
48+
const contentProps: FloatButtonContentProps = {
49+
prefixCls: prefixCls.value,
50+
description,
51+
};
52+
53+
const classString = classNames(
54+
prefixCls.value,
55+
`${prefixCls.value}-${props.type}`,
56+
`${prefixCls.value}-${mergeShape.value}`,
57+
{
58+
[`${prefixCls.value}-rtl`]: direction.value === 'rtl',
59+
},
60+
attrs.class,
61+
hashId.value,
62+
);
63+
64+
const buttonNode = (
65+
<Tooltip placement="left">
66+
{{
67+
title:
68+
slots.tooltip || tooltip
69+
? () => (slots.tooltip && slots.tooltip()) || tooltip
70+
: undefined,
71+
default: () => (
72+
<div class={`${prefixCls.value}-body`}>
73+
<Content {...contentProps}>
74+
{{
75+
icon: slots.icon,
76+
description: slots.description,
77+
}}
78+
</Content>
79+
</div>
80+
),
81+
}}
82+
</Tooltip>
83+
);
84+
85+
if (process.env.NODE_ENV !== 'production') {
86+
warning(
87+
!(shape === 'circle' && description),
88+
'FloatButton',
89+
'supported only when `shape` is `square`. Due to narrow space for text, short sentence is recommended.',
90+
);
91+
}
92+
93+
return wrapSSR(
94+
props.href ? (
95+
<a
96+
ref={floatButtonRef}
97+
{...attrs}
98+
{...(restProps as any)}
99+
class={classString}
100+
style={attrs.style as CSSProperties}
101+
>
102+
{buttonNode}
103+
</a>
104+
) : (
105+
<button
106+
ref={floatButtonRef}
107+
{...attrs}
108+
{...restProps}
109+
class={classString}
110+
style={attrs.style as CSSProperties}
111+
type="button"
112+
>
113+
{buttonNode}
114+
</button>
115+
),
116+
);
117+
};
118+
},
119+
});
120+
121+
export default FloatButton;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import { defineComponent } from 'vue';
2+
import FileTextOutlined from '@ant-design/icons-vue/FileTextOutlined';
3+
import classNames from '../_util/classNames';
4+
import { floatButtonContentProps } from './interface';
5+
6+
const FloatButtonContent = defineComponent({
7+
compatConfig: { MODE: 3 },
8+
name: 'AFloatButtonContent',
9+
inheritAttrs: false,
10+
props: floatButtonContentProps(),
11+
setup(props, { attrs, slots }) {
12+
return () => {
13+
const { description, prefixCls } = props;
14+
15+
const defaultElement = (
16+
<div class={`${prefixCls}-icon`}>
17+
<FileTextOutlined />
18+
</div>
19+
);
20+
21+
return (
22+
<div {...attrs} class={classNames(attrs.class, `${prefixCls}-content`)}>
23+
{slots.icon || description ? (
24+
<>
25+
{slots.icon && <div class={`${prefixCls}-icon`}>{slots.icon()}</div>}
26+
{(slots.description || description) && (
27+
<div class={`${prefixCls}-description`}>
28+
{(slots.description && slots.description()) || description}
29+
</div>
30+
)}
31+
</>
32+
) : (
33+
defaultElement
34+
)}
35+
</div>
36+
);
37+
};
38+
},
39+
});
40+
41+
export default FloatButtonContent;

0 commit comments

Comments
 (0)