diff --git a/components/components.ts b/components/components.ts
index 286904c7b..0d7040516 100644
--- a/components/components.ts
+++ b/components/components.ts
@@ -76,6 +76,9 @@ export { default as Drawer } from './drawer';
export type { EmptyProps } from './empty';
export { default as Empty } from './empty';
+export type { FloatButtonProps, FloatButtonGroupProps } from './float-button/interface';
+export { default as FloatButton, FloatButtonGroup } from './float-button';
+
export type { FormProps, FormItemProps, FormInstance, FormItemInstance } from './form';
export { default as Form, FormItem, FormItemRest } from './form';
diff --git a/components/float-button/BackTop.tsx b/components/float-button/BackTop.tsx
new file mode 100644
index 000000000..3e231cec2
--- /dev/null
+++ b/components/float-button/BackTop.tsx
@@ -0,0 +1,143 @@
+import VerticalAlignTopOutlined from '@ant-design/icons-vue/VerticalAlignTopOutlined';
+import { getTransitionProps, Transition } from '../_util/transition';
+import {
+ defineComponent,
+ nextTick,
+ onActivated,
+ onBeforeUnmount,
+ onMounted,
+ reactive,
+ ref,
+ watch,
+ onDeactivated,
+} from 'vue';
+import FloatButton from './FloatButton';
+import useConfigInject from '../config-provider/hooks/useConfigInject';
+import getScroll from '../_util/getScroll';
+import scrollTo from '../_util/scrollTo';
+import throttleByAnimationFrame from '../_util/throttleByAnimationFrame';
+import { initDefaultProps } from '../_util/props-util';
+import { backTopProps } from './interface';
+import { floatButtonPrefixCls } from './FloatButton';
+
+import useStyle from './style';
+
+const BackTop = defineComponent({
+ compatConfig: { MODE: 3 },
+ name: 'ABackTop',
+ inheritAttrs: false,
+ props: initDefaultProps(backTopProps(), {
+ visibilityHeight: 400,
+ target: () => window,
+ duration: 450,
+ }),
+ // emits: ['click'],
+ setup(props, { slots, attrs, emit }) {
+ const { prefixCls, direction } = useConfigInject(floatButtonPrefixCls, props);
+
+ const [wrapSSR] = useStyle(prefixCls);
+
+ const domRef = ref();
+ const state = reactive({
+ visible: false,
+ scrollEvent: null,
+ });
+
+ const getDefaultTarget = () =>
+ domRef.value && domRef.value.ownerDocument ? domRef.value.ownerDocument : window;
+
+ const scrollToTop = (e: Event) => {
+ const { target = getDefaultTarget, duration } = props;
+ scrollTo(0, {
+ getContainer: target,
+ duration,
+ });
+ emit('click', e);
+ };
+
+ const handleScroll = throttleByAnimationFrame((e: Event | { target: any }) => {
+ const { visibilityHeight } = props;
+ const scrollTop = getScroll(e.target, true);
+ state.visible = scrollTop >= visibilityHeight;
+ });
+
+ const bindScrollEvent = () => {
+ const { target } = props;
+ const getTarget = target || getDefaultTarget;
+ const container = getTarget();
+ handleScroll({ target: container });
+ container?.addEventListener('scroll', handleScroll);
+ };
+
+ const scrollRemove = () => {
+ const { target } = props;
+ const getTarget = target || getDefaultTarget;
+ const container = getTarget();
+ handleScroll.cancel();
+ container?.removeEventListener('scroll', handleScroll);
+ };
+
+ watch(
+ () => props.target,
+ () => {
+ scrollRemove();
+ nextTick(() => {
+ bindScrollEvent();
+ });
+ },
+ );
+
+ onMounted(() => {
+ nextTick(() => {
+ bindScrollEvent();
+ });
+ });
+
+ onActivated(() => {
+ nextTick(() => {
+ bindScrollEvent();
+ });
+ });
+
+ onDeactivated(() => {
+ scrollRemove();
+ });
+
+ onBeforeUnmount(() => {
+ scrollRemove();
+ });
+
+ return () => {
+ const defaultElement = (
+
+ );
+ const divProps = {
+ ...attrs,
+ onClick: scrollToTop,
+ class: {
+ [`${prefixCls.value}`]: true,
+ [`${attrs.class}`]: attrs.class,
+ [`${prefixCls.value}-rtl`]: direction.value === 'rtl',
+ },
+ };
+
+ const transitionProps = getTransitionProps('fade');
+ return wrapSSR(
+
+
+ {{
+ icon: () => ,
+ default: () => slots.default?.() || defaultElement,
+ }}
+
+ ,
+ );
+ };
+ },
+});
+
+export default BackTop;
diff --git a/components/float-button/FloatButton.tsx b/components/float-button/FloatButton.tsx
new file mode 100644
index 000000000..a0f440452
--- /dev/null
+++ b/components/float-button/FloatButton.tsx
@@ -0,0 +1,121 @@
+import classNames from '../_util/classNames';
+import { defineComponent, computed, CSSProperties, ref } from 'vue';
+import Tooltip from '../tooltip';
+import Content from './FloatButtonContent';
+import type { FloatButtonContentProps } from './interface';
+import useConfigInject from '../config-provider/hooks/useConfigInject';
+import FloatButtonGroupContext from './context';
+import warning from '../_util/warning';
+import { initDefaultProps } from '../_util/props-util';
+import { floatButtonProps } from './interface';
+// import { useCompactItemContext } from '../space/Compact';
+
+// CSSINJS
+import useStyle from './style';
+
+export const floatButtonPrefixCls = 'float-btn';
+
+const FloatButton = defineComponent({
+ compatConfig: { MODE: 3 },
+ name: 'AFloatButton',
+ inheritAttrs: false,
+ props: initDefaultProps(floatButtonProps(), { type: 'default', shape: 'circle' }),
+ setup(props, { attrs, slots, expose }) {
+ const { prefixCls, direction } = useConfigInject(floatButtonPrefixCls, props);
+ const [wrapSSR, hashId] = useStyle(prefixCls);
+ const { shape: groupShape } = FloatButtonGroupContext.useInject();
+
+ const floatButtonRef = ref(null);
+
+ const mergeShape = computed(() => {
+ return groupShape?.value || props.shape;
+ });
+
+ expose({
+ floatButtonEl: floatButtonRef,
+ });
+
+ return () => {
+ const {
+ prefixCls: customPrefixCls,
+ type = 'default',
+ shape = 'circle',
+ description,
+ tooltip,
+ ...restProps
+ } = props;
+
+ const contentProps: FloatButtonContentProps = {
+ prefixCls: prefixCls.value,
+ description,
+ };
+
+ const classString = classNames(
+ prefixCls.value,
+ `${prefixCls.value}-${props.type}`,
+ `${prefixCls.value}-${mergeShape.value}`,
+ {
+ [`${prefixCls.value}-rtl`]: direction.value === 'rtl',
+ },
+ attrs.class,
+ hashId.value,
+ );
+
+ const buttonNode = (
+
+ {{
+ title:
+ slots.tooltip || tooltip
+ ? () => (slots.tooltip && slots.tooltip()) || tooltip
+ : undefined,
+ default: () => (
+
+
+ {{
+ icon: slots.icon,
+ description: slots.description,
+ }}
+
+
+ ),
+ }}
+
+ );
+
+ if (process.env.NODE_ENV !== 'production') {
+ warning(
+ !(shape === 'circle' && description),
+ 'FloatButton',
+ 'supported only when `shape` is `square`. Due to narrow space for text, short sentence is recommended.',
+ );
+ }
+
+ return wrapSSR(
+ props.href ? (
+
+ {buttonNode}
+
+ ) : (
+
+ ),
+ );
+ };
+ },
+});
+
+export default FloatButton;
diff --git a/components/float-button/FloatButtonContent.tsx b/components/float-button/FloatButtonContent.tsx
new file mode 100644
index 000000000..67a5591ac
--- /dev/null
+++ b/components/float-button/FloatButtonContent.tsx
@@ -0,0 +1,41 @@
+import { defineComponent } from 'vue';
+import FileTextOutlined from '@ant-design/icons-vue/FileTextOutlined';
+import classNames from '../_util/classNames';
+import { floatButtonContentProps } from './interface';
+
+const FloatButtonContent = defineComponent({
+ compatConfig: { MODE: 3 },
+ name: 'AFloatButtonContent',
+ inheritAttrs: false,
+ props: floatButtonContentProps(),
+ setup(props, { attrs, slots }) {
+ return () => {
+ const { description, prefixCls } = props;
+
+ const defaultElement = (
+
+
+
+ );
+
+ return (
+
+ {slots.icon || description ? (
+ <>
+ {slots.icon &&
{slots.icon()}
}
+ {(slots.description || description) && (
+
+ {(slots.description && slots.description()) || description}
+
+ )}
+ >
+ ) : (
+ defaultElement
+ )}
+
+ );
+ };
+ },
+});
+
+export default FloatButtonContent;
diff --git a/components/float-button/FloatButtonGroup.tsx b/components/float-button/FloatButtonGroup.tsx
new file mode 100644
index 000000000..f80c6bc94
--- /dev/null
+++ b/components/float-button/FloatButtonGroup.tsx
@@ -0,0 +1,132 @@
+import { defineComponent, ref, computed, watch } from 'vue';
+import CloseOutlined from '@ant-design/icons-vue/CloseOutlined';
+import FileTextOutlined from '@ant-design/icons-vue/FileTextOutlined';
+import classNames from '../_util/classNames';
+import { getTransitionProps, Transition } from '../_util/transition';
+import FloatButton, { floatButtonPrefixCls } from './FloatButton';
+import useConfigInject from '../config-provider/hooks/useConfigInject';
+import FloatButtonGroupContext from './context';
+import { initDefaultProps } from '../_util/props-util';
+import { floatButtonGroupProps } from './interface';
+import type { FloatButtonGroupProps } from './interface';
+
+// CSSINJS
+import useStyle from './style';
+
+const FloatButtonGroup = defineComponent({
+ compatConfig: { MODE: 3 },
+ name: 'AFloatButtonGroup',
+ inheritAttrs: false,
+ props: initDefaultProps(floatButtonGroupProps(), {
+ type: 'default',
+ shape: 'circle',
+ } as FloatButtonGroupProps),
+ setup(props, { attrs, slots }) {
+ const { prefixCls, direction } = useConfigInject(floatButtonPrefixCls, props);
+
+ // style
+ const [wrapSSR, hashId] = useStyle(prefixCls);
+
+ const open = ref(props.open);
+
+ const floatButtonGroupRef = ref(null);
+ const floatButtonRef = ref(null);
+
+ FloatButtonGroupContext.useProvide({
+ shape: computed(() => props.shape),
+ });
+
+ const hoverAction = computed(() => {
+ const hoverTypeAction = {
+ onMouseenter() {
+ open.value = true;
+ props.onOpenChange?.(true);
+ },
+ onMouseleave() {
+ open.value = false;
+ props.onOpenChange?.(false);
+ },
+ };
+ return props.trigger === 'hover' ? hoverTypeAction : {};
+ });
+
+ const handleOpenChange = () => {
+ open.value = !open.value;
+ props.onOpenChange?.(!open.value);
+ };
+
+ const onClick = (e: MouseEvent) => {
+ if (floatButtonGroupRef.value?.contains(e.target as Node)) {
+ if ((floatButtonRef.value as any)?.floatButtonEl?.contains(e.target as Node)) {
+ handleOpenChange();
+ }
+ return;
+ }
+ open.value = false;
+ props.onOpenChange?.(false);
+ };
+
+ watch(
+ computed(() => props.trigger),
+ value => {
+ if (value === 'click') {
+ document.addEventListener('click', onClick);
+ return () => {
+ document.removeEventListener('click', onClick);
+ };
+ }
+ },
+ { immediate: true },
+ );
+
+ return () => {
+ const { shape = 'circle', type = 'default', tooltip, description, trigger } = props;
+
+ const groupPrefixCls = `${prefixCls.value}-group`;
+
+ const groupCls = classNames(groupPrefixCls, hashId.value, attrs.class, {
+ [`${groupPrefixCls}-rtl`]: direction.value === 'rtl',
+ [`${groupPrefixCls}-${shape}`]: shape,
+ [`${groupPrefixCls}-${shape}-shadow`]: !trigger,
+ });
+
+ const wrapperCls = classNames(hashId.value, `${groupPrefixCls}-wrap`);
+
+ const transitionProps = getTransitionProps(`${groupPrefixCls}-wrap`);
+
+ return wrapSSR(
+
+ {trigger && ['click', 'hover'].includes(trigger) ? (
+ <>
+
+
+ {slots.default && slots.default()}
+
+
+
+ {{
+ icon: () =>
+ open.value
+ ? (slots.closeIcon && slots.closeIcon()) ||
+ : (slots.icon && slots.icon()) || ,
+ tooltip: slots.tooltip,
+ description: slots.description,
+ }}
+
+ >
+ ) : (
+ slots.default && slots.default()
+ )}
+
,
+ );
+ };
+ },
+});
+
+export default FloatButtonGroup;
diff --git a/components/float-button/__tests__/__snapshots__/demo.test.js.snap b/components/float-button/__tests__/__snapshots__/demo.test.js.snap
new file mode 100644
index 000000000..25dbe2644
--- /dev/null
+++ b/components/float-button/__tests__/__snapshots__/demo.test.js.snap
@@ -0,0 +1,256 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`renders ./components/button/demo/basic.vue correctly 1`] = `
+
+
+
+
+
+`;
+
+exports[`renders ./components/button/demo/block.vue correctly 1`] = `
+
+
+
+
+
+`;
+
+exports[`renders ./components/button/demo/button-group.vue correctly 1`] = `
+
+`;
+
+exports[`renders ./components/button/demo/danger.vue correctly 1`] = `
+
+`;
+
+exports[`renders ./components/button/demo/disabled.vue correctly 1`] = `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+`;
+
+exports[`renders ./components/button/demo/ghost.vue correctly 1`] = `
+
+`;
+
+exports[`renders ./components/button/demo/icon.vue correctly 1`] = `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+`;
+
+exports[`renders ./components/button/demo/loading.vue correctly 1`] = `
+
+
+
+
+
+
+
+
+`;
+
+exports[`renders ./components/button/demo/multiple.vue correctly 1`] = `
+
+
+
+`;
+
+exports[`renders ./components/button/demo/size.vue correctly 1`] = `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+`;
diff --git a/components/float-button/__tests__/__snapshots__/index.test.js.snap b/components/float-button/__tests__/__snapshots__/index.test.js.snap
new file mode 100644
index 000000000..00b3398d8
--- /dev/null
+++ b/components/float-button/__tests__/__snapshots__/index.test.js.snap
@@ -0,0 +1,65 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Button fixbug renders {0} , 0 and {false} 1`] = `
+
+`;
+
+exports[`Button fixbug renders {0} , 0 and {false} 2`] = `
+
+`;
+
+exports[`Button fixbug renders {0} , 0 and {false} 3`] = `
+
+`;
+
+exports[`Button renders Chinese characters correctly 1`] = `
+
+`;
+
+exports[`Button renders Chinese characters correctly 2`] = `
+
+`;
+
+exports[`Button renders Chinese characters correctly 3`] = `
+
+`;
+
+exports[`Button renders Chinese characters correctly 4`] = ``;
+
+exports[`Button renders Chinese characters correctly 5`] = ``;
+
+exports[`Button renders Chinese characters correctly 6`] = `
+
+`;
+
+exports[`Button renders correctly 1`] = `
+
+`;
+
+exports[`Button should not render as link button when href is undefined 1`] = `
+
+`;
+
+exports[`Button should support link button 1`] = `
+
+ link button
+
+`;
diff --git a/components/float-button/__tests__/demo.test.js b/components/float-button/__tests__/demo.test.js
new file mode 100644
index 000000000..fdcb0c780
--- /dev/null
+++ b/components/float-button/__tests__/demo.test.js
@@ -0,0 +1,3 @@
+import demoTest from '../../../tests/shared/demoTest';
+
+demoTest('float-button');
diff --git a/components/float-button/__tests__/index.test.js b/components/float-button/__tests__/index.test.js
new file mode 100644
index 000000000..d5bc3c4dc
--- /dev/null
+++ b/components/float-button/__tests__/index.test.js
@@ -0,0 +1,48 @@
+import FloatButton from '../index';
+import { mount } from '@vue/test-utils';
+import mountTest from '../../../tests/shared/mountTest';
+
+describe('FloatButton', () => {
+ mountTest(FloatButton);
+ mountTest(FloatButton.Group);
+ it('renders correctly', () => {
+ const wrapper = mount({
+ render() {
+ return ;
+ },
+ });
+ expect(wrapper.html()).toMatchSnapshot();
+ });
+
+ it('create primary button', () => {
+ const wrapper = mount({
+ render() {
+ return 按钮;
+ },
+ });
+ expect(wrapper.find('.ant-float-btn-primary').exists()).toBe(true);
+ });
+
+ it('fixbug renders {0} , 0 and {false}', () => {
+ const wrapper = mount({
+ render() {
+ return {0};
+ },
+ });
+ expect(wrapper.html()).toMatchSnapshot();
+
+ const wrapper1 = mount({
+ render() {
+ return 0;
+ },
+ });
+ expect(wrapper1.html()).toMatchSnapshot();
+
+ const wrapper2 = mount({
+ render() {
+ return {false};
+ },
+ });
+ expect(wrapper2.html()).toMatchSnapshot();
+ });
+});
diff --git a/components/float-button/__tests__/wave.test.js b/components/float-button/__tests__/wave.test.js
new file mode 100644
index 000000000..3102ccf3d
--- /dev/null
+++ b/components/float-button/__tests__/wave.test.js
@@ -0,0 +1,79 @@
+import FloatButton from '../index';
+import { mount } from '@vue/test-utils';
+import { asyncExpect, sleep } from '../../../tests/utils';
+
+describe('click wave effect', () => {
+ async function clickFloatButton(wrapper) {
+ await asyncExpect(() => {
+ wrapper.find('.ant-float-btn').trigger('click');
+ });
+ wrapper.find('.ant-float-btn').element.dispatchEvent(new Event('transitionstart'));
+ await sleep(20);
+ wrapper.find('.ant-float-btn').element.dispatchEvent(new Event('animationend'));
+ await sleep(20);
+ }
+
+ it('should have click wave effect for primary button', async () => {
+ const wrapper = mount({
+ render() {
+ return ;
+ },
+ });
+ await clickFloatButton(wrapper);
+ expect(
+ wrapper.find('.ant-float-btn').attributes('ant-click-animating-without-extra-node'),
+ ).toBe('true');
+ });
+
+ it('should have click wave effect for default button', async () => {
+ const wrapper = mount({
+ render() {
+ return button;
+ },
+ });
+ await clickFloatButton(wrapper);
+ expect(
+ wrapper.find('.ant-float-btn').attributes('ant-click-animating-without-extra-node'),
+ ).toBe('true');
+ });
+
+ it('should not have click wave effect for link type button', async () => {
+ const wrapper = mount({
+ render() {
+ return button;
+ },
+ });
+ await clickFloatButton(wrapper);
+ expect(
+ wrapper.find('.ant-float-btn').attributes('ant-click-animating-without-extra-node'),
+ ).toBe(undefined);
+ });
+
+ it('should not have click wave effect for text type button', async () => {
+ const wrapper = mount({
+ render() {
+ return button;
+ },
+ });
+ await clickFloatButton(wrapper);
+ expect(
+ wrapper.find('.ant-float-btn').attributes('ant-click-animating-without-extra-node'),
+ ).toBe(undefined);
+ });
+
+ it('should handle transitionstart', async () => {
+ const wrapper = mount({
+ render() {
+ return button;
+ },
+ });
+ await clickFloatButton(wrapper);
+ const buttonNode = wrapper.find('.ant-float-btn').element;
+ buttonNode.dispatchEvent(new Event('transitionstart'));
+ expect(
+ wrapper.find('.ant-float-btn').attributes('ant-click-animating-without-extra-node'),
+ ).toBe('true');
+ wrapper.unmount();
+ buttonNode.dispatchEvent(new Event('transitionstart'));
+ });
+});
diff --git a/components/float-button/context.ts b/components/float-button/context.ts
new file mode 100644
index 000000000..ef9aeeeb1
--- /dev/null
+++ b/components/float-button/context.ts
@@ -0,0 +1,29 @@
+import type { Ref } from 'vue';
+import { inject, provide } from 'vue';
+
+import type { FloatButtonShape } from './interface';
+
+function createContext>(defaultValue?: T) {
+ const contextKey = Symbol('floatButtonGroupContext');
+
+ const useProvide = (props: T) => {
+ provide(contextKey, props);
+
+ return props;
+ };
+
+ const useInject = () => {
+ return inject(contextKey, defaultValue as T) || ({} as T);
+ };
+
+ return {
+ useProvide,
+ useInject,
+ };
+}
+
+const FloatButtonGroupContext = createContext<{ shape: Ref } | undefined>(
+ undefined,
+);
+
+export default FloatButtonGroupContext;
diff --git a/components/float-button/demo/back-top.vue b/components/float-button/demo/back-top.vue
new file mode 100644
index 000000000..f0befadd5
--- /dev/null
+++ b/components/float-button/demo/back-top.vue
@@ -0,0 +1,43 @@
+
+---
+order: 7
+iframe: 360
+title:
+ zh-CN: 回到顶部
+ en-US: BackTop
+---
+
+## zh-CN
+
+返回页面顶部的操作按钮。
+
+## en-US
+
+`BackTop` makes it easy to go back to the top of the page.
+
+
+
+
+
+
Scroll to bottom
+
Scroll to bottom
+
Scroll to bottom
+
Scroll to bottom
+
Scroll to bottom
+
Scroll to bottom
+
Scroll to bottom
+
+
+
+
+
diff --git a/components/float-button/demo/basic.vue b/components/float-button/demo/basic.vue
new file mode 100644
index 000000000..eadc0b307
--- /dev/null
+++ b/components/float-button/demo/basic.vue
@@ -0,0 +1,36 @@
+
+---
+order: 0
+iframe: 360
+title:
+ zh-CN: 基本
+ en-US: Basic Usage
+---
+
+## zh-CN
+
+最简单的用法。
+
+## en-US
+
+The most basic usage.
+
+
+
+
+
+
+
+
diff --git a/components/float-button/demo/description.vue b/components/float-button/demo/description.vue
new file mode 100644
index 000000000..c8ab9936a
--- /dev/null
+++ b/components/float-button/demo/description.vue
@@ -0,0 +1,69 @@
+
+---
+order: 3
+iframe: 360
+title:
+ zh-CN: 描述
+ en-US: Description
+---
+
+## zh-CN
+
+可以通过 `description` 设置文字内容。
+
+> 仅当 `shape` 属性为 `square` 时支持。由于空间较小,推荐使用比较精简的双数文字。
+
+## en-US
+
+Setting `description` prop to show FloatButton with description.
+
+> supported only when `shape` is `square`. Due to narrow space for text, short sentence is recommended.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/components/float-button/demo/group-menu.vue b/components/float-button/demo/group-menu.vue
new file mode 100644
index 000000000..7ebc58c96
--- /dev/null
+++ b/components/float-button/demo/group-menu.vue
@@ -0,0 +1,56 @@
+
+---
+order: 6
+iframe: 360
+title:
+ zh-CN: 菜单模式
+ en-US: Menu mode
+---
+
+## zh-CN
+
+设置 `trigger` 属性即可开启菜单模式。提供 `hover` 和 `click` 两种触发方式。
+
+## en-US
+
+Open menu mode with `trigger`, which could be `hover` or `click`.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/components/float-button/demo/group.vue b/components/float-button/demo/group.vue
new file mode 100644
index 000000000..fddf87098
--- /dev/null
+++ b/components/float-button/demo/group.vue
@@ -0,0 +1,57 @@
+
+---
+order: 5
+iframe: 360
+title:
+ zh-CN: 浮动按钮组
+ en-US: FloatButton Group
+---
+
+## zh-CN
+
+按钮组合使用时,推荐使用 ``,并通过设置 `shape` 属性改变悬浮按钮组的形状。悬浮按钮组的 `shape` 会覆盖内部 FloatButton 的 `shape` 属性。
+
+## en-US
+
+When multiple buttons are used together, `` is recommended. By setting `shape` of FloatButton.Group, you can change the shape of group. `shape` of FloatButton.Group will override `shape` of FloatButton inside.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/components/float-button/demo/index.vue b/components/float-button/demo/index.vue
new file mode 100644
index 000000000..caa7ba782
--- /dev/null
+++ b/components/float-button/demo/index.vue
@@ -0,0 +1,86 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/components/float-button/demo/shape.vue b/components/float-button/demo/shape.vue
new file mode 100644
index 000000000..5797ad97f
--- /dev/null
+++ b/components/float-button/demo/shape.vue
@@ -0,0 +1,62 @@
+
+---
+order: 2
+iframe: 360
+title:
+ zh-CN: 形状
+ en-US: Shape
+---
+
+## zh-CN
+
+最简单的用法。
+
+## en-US
+
+The most basic usage.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/components/float-button/demo/tooltip.vue b/components/float-button/demo/tooltip.vue
new file mode 100644
index 000000000..ae05d4648
--- /dev/null
+++ b/components/float-button/demo/tooltip.vue
@@ -0,0 +1,47 @@
+
+---
+order: 4
+iframe: 360
+title:
+ zh-CN: 含有气泡卡片的悬浮按钮
+ en-US: FloatButton with tooltip
+---
+
+## zh-CN
+
+设置 tooltip 属性,即可开启气泡卡片。
+
+## en-US
+
+Setting `tooltip` prop to show FloatButton with tooltip.
+
+
+
+
+
+
+
+
+ Documents
+
+
+
+
+
diff --git a/components/float-button/demo/type.vue b/components/float-button/demo/type.vue
new file mode 100644
index 000000000..8523180f3
--- /dev/null
+++ b/components/float-button/demo/type.vue
@@ -0,0 +1,54 @@
+
+---
+order: 1
+iframe: 360
+title:
+ zh-CN: 类型
+ en-US: Type
+---
+
+## zh-CN
+
+通过 `type` 改变悬浮按钮的类型。
+
+## en-US
+
+Change the type of the FloatButton with `type`.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/components/float-button/index.en-US.md b/components/float-button/index.en-US.md
new file mode 100644
index 000000000..7c2226d3a
--- /dev/null
+++ b/components/float-button/index.en-US.md
@@ -0,0 +1,48 @@
+---
+category: Components
+type: Other
+title: FloatButton
+cover: https://mdn.alipayobjects.com/huamei_7uahnr/afts/img/A*HS-wTIIwu0kAAAAAAAAAAAAADrJ8AQ/original
+---
+
+FloatButton. Available since `4.0.0`.
+
+## When To Use
+
+- For global functionality on the site.
+- Buttons that can be seen wherever you browse.
+
+## API
+
+> This component is available since `ant-design-vue@4.0.0`.
+
+### common API
+
+| Property | Description | Type | Default | Version |
+| --- | --- | --- | --- | --- |
+| icon | Set the icon component of button | slot | - | |
+| description | Text and other | string \| slot | - | |
+| tooltip | The text shown in the tooltip | string \| slot | | |
+| type | Setting button type | `default` \| `primary` | `default` | |
+| shape | Setting button shape | `circle` \| `square` | `circle` | |
+| onClick | Set the handler to handle `click` event | (event) => void | - | |
+| href | The target of hyperlink | string | - | |
+| target | Specifies where to display the linked URL | string | - | |
+
+### FloatButton.Group
+
+| Property | Description | Type | Default | Version |
+| --- | --- | --- | --- | --- |
+| shape | Setting button shape of children | `circle` \| `square` | `circle` | |
+| trigger | Which action can trigger menu open/close | `click` \| `hover` | - | |
+| open | Whether the menu is visible or not | boolean | - | |
+| onOpenChange | Callback executed when active menu is changed | (open: boolean) => void | - | |
+
+### FloatButton.BackTop
+
+| Property | Description | Type | Default | Version |
+| --- | --- | --- | --- | --- |
+| duration | Time to return to top(ms) | number | 450 | |
+| target | Specifies the scrollable area dom node | () => HTMLElement | () => window | |
+| visibilityHeight | The BackTop button will not show until the scroll height reaches this value | number | 400 | |
+| onClick | A callback function, which can be executed when you click the button | () => void | - | |
diff --git a/components/float-button/index.ts b/components/float-button/index.ts
new file mode 100644
index 000000000..be156477e
--- /dev/null
+++ b/components/float-button/index.ts
@@ -0,0 +1,42 @@
+import type { App, Plugin } from 'vue';
+import FloatButton from './FloatButton';
+import FloatButtonGroup from './FloatButtonGroup';
+import BackTop from './BackTop';
+
+import type {
+ FloatButtonProps,
+ FloatButtonShape,
+ FloatButtonType,
+ FloatButtonGroupProps,
+ BackTopProps,
+} from './interface';
+
+import type { SizeType as FloatButtonSize } from '../config-provider';
+
+export type {
+ FloatButtonProps,
+ FloatButtonShape,
+ FloatButtonType,
+ FloatButtonGroupProps,
+ BackTopProps,
+ FloatButtonSize,
+};
+
+FloatButton.Group = FloatButtonGroup;
+FloatButton.BackTop = BackTop;
+
+/* istanbul ignore next */
+FloatButton.install = function (app: App) {
+ app.component(FloatButton.name, FloatButton);
+ app.component(FloatButtonGroup.name, FloatButtonGroup);
+ app.component(BackTop.name, BackTop);
+ return app;
+};
+
+export { FloatButtonGroup };
+
+export default FloatButton as typeof FloatButton &
+ Plugin & {
+ readonly Group: typeof FloatButtonGroup;
+ readonly BackTop: typeof BackTop;
+ };
diff --git a/components/float-button/index.zh-CN.md b/components/float-button/index.zh-CN.md
new file mode 100644
index 000000000..bd00f5eab
--- /dev/null
+++ b/components/float-button/index.zh-CN.md
@@ -0,0 +1,49 @@
+---
+category: Components
+subtitle: 悬浮按钮
+type: 其他
+title: FloatButton
+cover: https://mdn.alipayobjects.com/huamei_7uahnr/afts/img/A*HS-wTIIwu0kAAAAAAAAAAAAADrJ8AQ/original
+---
+
+悬浮按钮。自 `4.0.0` 版本开始提供该组件。
+
+## 何时使用
+
+- 用于网站上的全局功能;
+- 无论浏览到何处都可以看见的按钮。
+
+## API
+
+> 自 `ant-design-vue@4.0.0` 版本开始提供该组件。
+
+### 共同的 API
+
+| 参数 | 说明 | 类型 | 默认值 | 版本 |
+| --- | --- | --- | --- | --- |
+| icon | 自定义图标 | slot | - | |
+| description | 文字及其它内容 | string \| slot | - | |
+| tooltip | 气泡卡片的内容 | string \| slot | - | |
+| type | 设置按钮类型 | `default` \| `primary` | `default` | |
+| shape | 设置按钮形状 | `circle` \| `square` | `circle` | |
+| onClick | 点击按钮时的回调 | (event) => void | - | |
+| href | 点击跳转的地址,指定此属性 button 的行为和 a 链接一致 | string | - | |
+| target | 相当于 a 标签的 target 属性,href 存在时生效 | string | - | |
+
+### FloatButton.Group
+
+| 参数 | 说明 | 类型 | 默认值 | 版本 |
+| ------------ | -------------------------------- | ----------------------- | -------- | ---- |
+| shape | 设置包含的 FloatButton 按钮形状 | `circle` \| `square` | `circle` | |
+| trigger | 触发方式(有触发方式为菜单模式) | `click` \| `hover` | - | |
+| open | 受控展开 | boolean | - | |
+| onOpenChange | 展开收起时的回调 | (open: boolean) => void | - | |
+
+### FloatButton.BackTop
+
+| 参数 | 说明 | 类型 | 默认值 | 版本 |
+| ---------------- | ---------------------------------- | ----------------- | ------------ | ---- |
+| duration | 回到顶部所需时间(ms) | number | 450 | |
+| target | 设置需要监听其滚动事件的元素 | () => HTMLElement | () => window | |
+| visibilityHeight | 滚动高度达到此参数值才出现 BackTop | number | 400 | |
+| onClick | 点击按钮的回调函数 | () => void | - | |
diff --git a/components/float-button/interface.ts b/components/float-button/interface.ts
new file mode 100644
index 000000000..ceffd260f
--- /dev/null
+++ b/components/float-button/interface.ts
@@ -0,0 +1,66 @@
+import type { ExtractPropTypes } from 'vue';
+import PropTypes from '../_util/vue-types';
+import type { MouseEventHandler } from '../_util/EventInterface';
+import { stringType, booleanType, functionType } from '../_util/type';
+
+export type FloatButtonType = 'default' | 'primary';
+
+export type FloatButtonShape = 'circle' | 'square';
+
+export type FloatButtonGroupTrigger = 'click' | 'hover';
+
+export const floatButtonProps = () => {
+ return {
+ prefixCls: String,
+ description: PropTypes.any,
+ type: stringType('default'),
+ shape: stringType('circle'),
+ tooltip: PropTypes.any,
+ href: String,
+ target: functionType<() => Window | HTMLElement | null>(),
+ onClick: functionType(),
+ };
+};
+
+export type FloatButtonProps = Partial>>;
+
+export const floatButtonContentProps = () => {
+ return {
+ prefixCls: stringType(),
+ description: PropTypes.any,
+ };
+};
+
+export type FloatButtonContentProps = Partial<
+ ExtractPropTypes>
+>;
+
+export const floatButtonGroupProps = () => {
+ return {
+ ...floatButtonProps(),
+ // 包含的 Float Button
+ // 触发方式 (有触发方式为菜单模式)
+ trigger: stringType(),
+ // 受控展开
+ open: booleanType(false),
+ // 展开收起的回调
+ onOpenChange: functionType<(open: boolean) => void>(),
+ };
+};
+
+export type FloatButtonGroupProps = Partial<
+ ExtractPropTypes>
+>;
+
+export const backTopProps = () => {
+ return {
+ ...floatButtonProps(),
+ prefixCls: String,
+ duration: Number,
+ target: functionType<() => HTMLElement | Window | Document>(),
+ visibilityHeight: Number,
+ onClick: functionType(),
+ };
+};
+
+export type BackTopProps = Partial>>;
diff --git a/components/float-button/style/index.ts b/components/float-button/style/index.ts
new file mode 100644
index 000000000..86d786ef3
--- /dev/null
+++ b/components/float-button/style/index.ts
@@ -0,0 +1,333 @@
+import type { CSSObject } from '../../_util/cssinjs';
+import { Keyframes } from '../../_util/cssinjs';
+import type { FullToken, GenerateStyle } from '../../theme/internal';
+import { genComponentStyleHook, mergeToken } from '../../theme/internal';
+import { initFadeMotion } from '../../style/motion/fade';
+import { resetComponent } from '../../style';
+import { initMotion } from '../../style/motion/motion';
+
+/** Component only token. Which will handle additional calculation of alias token */
+export interface ComponentToken {
+ zIndexPopup: number;
+}
+
+type FloatButtonToken = FullToken<'FloatButton'> & {
+ floatButtonColor: string;
+ floatButtonBackgroundColor: string;
+ floatButtonHoverBackgroundColor: string;
+ floatButtonFontSize: number;
+ floatButtonSize: number;
+ floatButtonIconSize: number;
+
+ // Position
+ floatButtonInsetBlockEnd: number;
+ floatButtonInsetInlineEnd: number;
+};
+
+const initFloatButtonGroupMotion = (token: FloatButtonToken) => {
+ const { componentCls, floatButtonSize, motionDurationSlow, motionEaseInOutCirc } = token;
+ const groupPrefixCls = `${componentCls}-group`;
+ const moveDownIn = new Keyframes('antFloatButtonMoveDownIn', {
+ '0%': {
+ transform: `translate3d(0, ${floatButtonSize}px, 0)`,
+ transformOrigin: '0 0',
+ opacity: 0,
+ },
+
+ '100%': {
+ transform: 'translate3d(0, 0, 0)',
+ transformOrigin: '0 0',
+ opacity: 1,
+ },
+ });
+ const moveDownOut = new Keyframes('antFloatButtonMoveDownOut', {
+ '0%': {
+ transform: 'translate3d(0, 0, 0)',
+ transformOrigin: '0 0',
+ opacity: 1,
+ },
+
+ '100%': {
+ transform: `translate3d(0, ${floatButtonSize}px, 0)`,
+ transformOrigin: '0 0',
+ opacity: 0,
+ },
+ });
+
+ return [
+ {
+ [`${groupPrefixCls}-wrap`]: {
+ ...initMotion(`${groupPrefixCls}-wrap`, moveDownIn, moveDownOut, motionDurationSlow, true),
+ },
+ },
+ {
+ [`${groupPrefixCls}-wrap`]: {
+ [`
+ &${groupPrefixCls}-wrap-enter,
+ &${groupPrefixCls}-wrap-appear
+ `]: {
+ opacity: 0,
+ animationTimingFunction: motionEaseInOutCirc,
+ },
+
+ [`&${groupPrefixCls}-wrap-leave`]: {
+ animationTimingFunction: motionEaseInOutCirc,
+ },
+ },
+ },
+ ];
+};
+
+// ============================== Group ==============================
+const floatButtonGroupStyle: GenerateStyle = token => {
+ const { componentCls, floatButtonSize, margin, borderRadiusLG } = token;
+ const groupPrefixCls = `${componentCls}-group`;
+ return {
+ [groupPrefixCls]: {
+ ...resetComponent(token),
+ zIndex: 99,
+ display: 'block',
+ border: 'none',
+ position: 'fixed',
+ width: floatButtonSize,
+ height: 'auto',
+ boxShadow: 'none',
+ minHeight: floatButtonSize,
+ insetInlineEnd: token.floatButtonInsetInlineEnd,
+ insetBlockEnd: token.floatButtonInsetBlockEnd,
+ borderRadius: borderRadiusLG,
+
+ [`${groupPrefixCls}-wrap`]: {
+ zIndex: -1,
+ display: 'block',
+ position: 'relative',
+ marginBottom: margin,
+ },
+ [`&${groupPrefixCls}-rtl`]: {
+ direction: 'rtl',
+ },
+ [componentCls]: {
+ position: 'static',
+ },
+ },
+ [`${groupPrefixCls}-circle`]: {
+ [`${componentCls}-circle:not(:last-child)`]: {
+ marginBottom: token.margin,
+ [`${componentCls}-body`]: {
+ width: floatButtonSize,
+ height: floatButtonSize,
+ },
+ },
+ },
+ [`${groupPrefixCls}-square`]: {
+ [`${componentCls}-square`]: {
+ borderRadius: 0,
+ padding: 0,
+ '&:first-child': {
+ borderStartStartRadius: borderRadiusLG,
+ borderStartEndRadius: borderRadiusLG,
+ },
+ '&:last-child': {
+ borderEndStartRadius: borderRadiusLG,
+ borderEndEndRadius: borderRadiusLG,
+ },
+ '&:not(:last-child)': {
+ borderBottom: `${token.lineWidth}px ${token.lineType} ${token.colorSplit}`,
+ },
+ },
+ [`${groupPrefixCls}-wrap`]: {
+ display: 'block',
+ borderRadius: borderRadiusLG,
+ boxShadow: token.boxShadowSecondary,
+ overflow: 'hidden',
+ [`${componentCls}-square`]: {
+ boxShadow: 'none',
+ marginTop: 0,
+ borderRadius: 0,
+ padding: token.paddingXXS,
+ '&:first-child': {
+ borderStartStartRadius: borderRadiusLG,
+ borderStartEndRadius: borderRadiusLG,
+ },
+ '&:last-child': {
+ borderEndStartRadius: borderRadiusLG,
+ borderEndEndRadius: borderRadiusLG,
+ },
+ '&:not(:last-child)': {
+ borderBottom: `${token.lineWidth}px ${token.lineType} ${token.colorSplit}`,
+ },
+ [`${componentCls}-body`]: {
+ width: floatButtonSize - token.paddingXXS * 2,
+ height: floatButtonSize - token.paddingXXS * 2,
+ },
+ },
+ },
+ },
+
+ [`${groupPrefixCls}-circle-shadow`]: {
+ boxShadow: 'none',
+ },
+ [`${groupPrefixCls}-square-shadow`]: {
+ boxShadow: token.boxShadowSecondary,
+ [`${componentCls}-square`]: {
+ boxShadow: 'none',
+ padding: token.paddingXXS,
+ [`${componentCls}-body`]: {
+ width: floatButtonSize - token.paddingXXS * 2,
+ height: floatButtonSize - token.paddingXXS * 2,
+ },
+ },
+ },
+ };
+};
+
+// ============================== Shared ==============================
+const sharedFloatButtonStyle: GenerateStyle = token => {
+ const { componentCls, floatButtonIconSize, floatButtonSize, borderRadiusLG } = token;
+ return {
+ [componentCls]: {
+ ...resetComponent(token),
+ border: 'none',
+ position: 'fixed',
+ cursor: 'pointer',
+ overflow: 'hidden',
+ zIndex: 99,
+ display: 'block',
+ justifyContent: 'center',
+ alignItems: 'center',
+ width: floatButtonSize,
+ height: floatButtonSize,
+ insetInlineEnd: token.floatButtonInsetInlineEnd,
+ insetBlockEnd: token.floatButtonInsetBlockEnd,
+ boxShadow: token.boxShadowSecondary,
+
+ // Pure Panel
+ '&-pure': {
+ position: 'relative',
+ inset: 'auto',
+ },
+
+ '&:empty': {
+ display: 'none',
+ },
+
+ [`${componentCls}-body`]: {
+ width: '100%',
+ height: '100%',
+ display: 'flex',
+ justifyContent: 'center',
+ alignItems: 'center',
+ transition: `all ${token.motionDurationMid}`,
+ [`${componentCls}-content`]: {
+ overflow: 'hidden',
+ textAlign: 'center',
+ minHeight: floatButtonSize,
+ display: 'flex',
+ flexDirection: 'column',
+ justifyContent: 'center',
+ alignItems: 'center',
+ padding: `2px 4px`,
+ [`${componentCls}-icon`]: {
+ textAlign: 'center',
+ margin: 'auto',
+ width: floatButtonIconSize,
+ fontSize: floatButtonIconSize,
+ lineHeight: 1,
+ },
+ },
+ },
+ },
+ [`${componentCls}-circle`]: {
+ height: floatButtonSize,
+ borderRadius: '50%',
+ [`${componentCls}-body`]: {
+ borderRadius: '50%',
+ },
+ },
+ [`${componentCls}-square`]: {
+ height: 'auto',
+ minHeight: floatButtonSize,
+ borderRadius: borderRadiusLG,
+ [`${componentCls}-body`]: {
+ height: 'auto',
+ borderRadius: token.borderRadiusSM,
+ },
+ },
+ [`${componentCls}-default`]: {
+ backgroundColor: token.floatButtonBackgroundColor,
+ transition: `background-color ${token.motionDurationMid}`,
+ [`${componentCls}-body`]: {
+ backgroundColor: token.floatButtonBackgroundColor,
+ transition: `background-color ${token.motionDurationMid}`,
+ '&:hover': {
+ backgroundColor: token.colorFillContent,
+ },
+ [`${componentCls}-content`]: {
+ [`${componentCls}-icon`]: {
+ color: token.colorText,
+ },
+ [`${componentCls}-description`]: {
+ display: 'flex',
+ alignItems: 'center',
+ lineHeight: `${token.fontSizeLG}px`,
+ color: token.colorText,
+ fontSize: token.fontSizeSM,
+ },
+ },
+ },
+ },
+ [`${componentCls}-primary`]: {
+ backgroundColor: token.colorPrimary,
+ [`${componentCls}-body`]: {
+ backgroundColor: token.colorPrimary,
+ transition: `background-color ${token.motionDurationMid}`,
+ '&:hover': {
+ backgroundColor: token.colorPrimaryHover,
+ },
+ [`${componentCls}-content`]: {
+ [`${componentCls}-icon`]: {
+ color: token.colorTextLightSolid,
+ },
+ [`${componentCls}-description`]: {
+ display: 'flex',
+ alignItems: 'center',
+ lineHeight: `${token.fontSizeLG}px`,
+ color: token.colorTextLightSolid,
+ fontSize: token.fontSizeSM,
+ },
+ },
+ },
+ },
+ };
+};
+
+// ============================== Export ==============================
+export default genComponentStyleHook<'FloatButton'>('FloatButton', token => {
+ const {
+ colorTextLightSolid,
+ colorBgElevated,
+ controlHeightLG,
+ marginXXL,
+ marginLG,
+ fontSize,
+ fontSizeIcon,
+ controlItemBgHover,
+ } = token;
+ const floatButtonToken = mergeToken(token, {
+ floatButtonBackgroundColor: colorBgElevated,
+ floatButtonColor: colorTextLightSolid,
+ floatButtonHoverBackgroundColor: controlItemBgHover,
+ floatButtonFontSize: fontSize,
+ floatButtonIconSize: fontSizeIcon * 1.5,
+ floatButtonSize: controlHeightLG,
+
+ floatButtonInsetBlockEnd: marginXXL,
+ floatButtonInsetInlineEnd: marginLG,
+ });
+ return [
+ floatButtonGroupStyle(floatButtonToken),
+ sharedFloatButtonStyle(floatButtonToken),
+ initFadeMotion(token),
+ initFloatButtonGroupMotion(floatButtonToken),
+ ];
+});
diff --git a/components/theme/interface/components.ts b/components/theme/interface/components.ts
index f682f43d0..20508335f 100644
--- a/components/theme/interface/components.ts
+++ b/components/theme/interface/components.ts
@@ -3,7 +3,7 @@ import type { ComponentToken as AnchorComponentToken } from '../../anchor/style'
import type { ComponentToken as AvatarComponentToken } from '../../avatar/style';
import type { ComponentToken as BackTopComponentToken } from '../../back-top/style';
import type { ComponentToken as ButtonComponentToken } from '../../button/style';
-// import type { ComponentToken as FloatButtonComponentToken } from '../../float-button/style';
+import type { ComponentToken as FloatButtonComponentToken } from '../../float-button/style';
import type { ComponentToken as CalendarComponentToken } from '../../calendar/style';
import type { ComponentToken as CardComponentToken } from '../../card/style';
import type { ComponentToken as CarouselComponentToken } from '../../carousel/style';
@@ -71,7 +71,7 @@ export interface ComponentTokenMap {
Drawer?: DrawerComponentToken;
Dropdown?: DropdownComponentToken;
Empty?: EmptyComponentToken;
- // FloatButton?: FloatButtonComponentToken;
+ FloatButton?: FloatButtonComponentToken;
Form?: {};
Grid?: {};
Image?: ImageComponentToken;
diff --git a/site/index.html b/site/index.html
index dd15d468f..8e5cf5c59 100644
--- a/site/index.html
+++ b/site/index.html
@@ -63,7 +63,7 @@
gtag('config', 'UA-151755889-1');
-
+ > -->