diff --git a/.dumi/theme/common/ThemeSwitch/index.tsx b/.dumi/theme/common/ThemeSwitch/index.tsx index 9f55bc8aef..ecdcbbef0f 100644 --- a/.dumi/theme/common/ThemeSwitch/index.tsx +++ b/.dumi/theme/common/ThemeSwitch/index.tsx @@ -1,7 +1,6 @@ import React from 'react'; import { BgColorsOutlined, SmileOutlined } from '@ant-design/icons'; import { FloatButton } from 'antd'; -import { useTheme } from 'antd-style'; import { CompactTheme, DarkTheme } from 'antd-token-previewer/es/icons'; // import { Motion } from 'antd-token-previewer/es/icons'; import { FormattedMessage, useLocation } from 'dumi'; @@ -20,7 +19,6 @@ export interface ThemeSwitchProps { const ThemeSwitch: React.FC = (props) => { const { value = ['light'], onChange } = props; - const token = useTheme(); const { pathname, search } = useLocation(); // const isMotionOff = value.includes('motion-off'); @@ -38,7 +36,7 @@ const ThemeSwitch: React.FC = (props) => { > } diff --git a/components/float-button/FloatButtonGroup.tsx b/components/float-button/FloatButtonGroup.tsx index 574c2a37e5..2cac8128e8 100644 --- a/components/float-button/FloatButtonGroup.tsx +++ b/components/float-button/FloatButtonGroup.tsx @@ -22,6 +22,7 @@ const FloatButtonGroup: React.FC = (props) => { style, shape = 'circle', type = 'default', + placement = 'top', icon = , closeIcon, description, @@ -40,12 +41,17 @@ const FloatButtonGroup: React.FC = (props) => { const prefixCls = getPrefixCls(floatButtonPrefixCls, customizePrefixCls); const rootCls = useCSSVarCls(prefixCls); const [wrapCSSVar, hashId, cssVarCls] = useStyle(prefixCls, rootCls); + const groupPrefixCls = `${prefixCls}-group`; + const isMenuMode = trigger && ['click', 'hover'].includes(trigger); + const isValidPlacement = placement && ['top', 'left', 'right', 'bottom'].includes(placement); + const groupCls = classNames(groupPrefixCls, hashId, cssVarCls, rootCls, className, { [`${groupPrefixCls}-rtl`]: direction === 'rtl', [`${groupPrefixCls}-${shape}`]: shape, - [`${groupPrefixCls}-${shape}-shadow`]: !trigger, + [`${groupPrefixCls}-${shape}-shadow`]: !isMenuMode, + [`${groupPrefixCls}-${placement}`]: isMenuMode && isValidPlacement, // 只有菜单模式才支持弹出方向 }); // ============================ zIndex ============================ @@ -119,7 +125,7 @@ const FloatButtonGroup: React.FC = (props) => { return wrapCSSVar(
- {trigger && ['click', 'hover'].includes(trigger) ? ( + {isMenuMode ? ( <> {({ className: motionClassName }) => ( diff --git a/components/float-button/__tests__/__snapshots__/demo-extend.test.ts.snap b/components/float-button/__tests__/__snapshots__/demo-extend.test.ts.snap index e97c410f8c..889b6db2f8 100644 --- a/components/float-button/__tests__/__snapshots__/demo-extend.test.ts.snap +++ b/components/float-button/__tests__/__snapshots__/demo-extend.test.ts.snap @@ -574,7 +574,7 @@ Array [ ,
,
,
+
+
+ +
+
+ +
+
+ +
+
+
+`; + +exports[`renders components/float-button/demo/placement.tsx extend context correctly 2`] = `[]`; + exports[`renders components/float-button/demo/render-panel.tsx extend context correctly 1`] = `
,
,
,
+
+
+ +
+
+ +
+
+ +
+
+
+`; + exports[`renders components/float-button/demo/render-panel.tsx correctly 1`] = `
{ expect(container.querySelector('.ant-badge')).toBeTruthy(); }); + + it('FloatButton.Group should support placement', () => { + (['bottom', 'left', 'right', 'top'] as const).forEach((placement) => { + const { container } = render( + + + , + ); + const element = container.querySelector('.ant-float-btn-group'); + expect(element).toHaveClass(`ant-float-btn-group-${placement}`); + }); + }); }); diff --git a/components/float-button/demo/placement.md b/components/float-button/demo/placement.md new file mode 100644 index 0000000000..9f01830e85 --- /dev/null +++ b/components/float-button/demo/placement.md @@ -0,0 +1,7 @@ +## zh-CN + +自定义弹出位置,提供了四个预设值:`top`、`right`、`bottom`、`left`,默认值为 `top`。 + +## en-US + +Customize animation placement, providing four preset placement: `top`, `right`, `bottom`, `left`, the `top` position by default. diff --git a/components/float-button/demo/placement.tsx b/components/float-button/demo/placement.tsx new file mode 100644 index 0000000000..f6bd59a859 --- /dev/null +++ b/components/float-button/demo/placement.tsx @@ -0,0 +1,74 @@ +import React from 'react'; +import { + CommentOutlined, + DownOutlined, + LeftOutlined, + RightOutlined, + UpOutlined, +} from '@ant-design/icons'; +import { Flex, FloatButton } from 'antd'; + +const BOX_SIZE = 100; +const BUTTON_SIZE = 40; + +const wrapperStyle: React.CSSProperties = { + width: '100%', + height: '100vh', + overflow: 'hidden', + position: 'relative', +}; + +const boxStyle: React.CSSProperties = { + width: BOX_SIZE, + height: BOX_SIZE, + position: 'relative', +}; + +const insetInlineEnd: React.CSSProperties['insetInlineEnd'][] = [ + (BOX_SIZE - BUTTON_SIZE) / 2, + -(BUTTON_SIZE / 2), + (BOX_SIZE - BUTTON_SIZE) / 2, + BOX_SIZE - BUTTON_SIZE / 2, +]; + +const bottom: React.CSSProperties['bottom'][] = [ + BOX_SIZE - BUTTON_SIZE / 2, + (BOX_SIZE - BUTTON_SIZE) / 2, + -BUTTON_SIZE / 2, + (BOX_SIZE - BUTTON_SIZE) / 2, +]; + +const icons = [ + , + , + , + , +]; + +const App: React.FC = () => ( + +
+ {(['top', 'right', 'bottom', 'left'] as const).map((placement, i) => { + const style: React.CSSProperties = { + position: 'absolute', + insetInlineEnd: insetInlineEnd[i], + bottom: bottom[i], + }; + return ( + + + } /> + + ); + })} +
+
+); + +export default App; diff --git a/components/float-button/index.en-US.md b/components/float-button/index.en-US.md index faa175c123..a07e5ae998 100644 --- a/components/float-button/index.en-US.md +++ b/components/float-button/index.en-US.md @@ -26,6 +26,7 @@ tag: 5.0.0 FloatButton Group Menu mode Controlled mode +placement BackTop badge debug dot @@ -59,6 +60,7 @@ Common props ref:[Common props](/docs/react/common-props) | trigger | Which action can trigger menu open/close | `click` \| `hover` | - | | | open | Whether the menu is visible or not, use it with trigger | boolean | - | | | closeIcon | Customize close button icon | React.ReactNode | `` | | +| placement | Customize menu animation placement | `top` \| `left` \| `right` \| `bottom` | `top` | 5.21.0 | | onOpenChange | Callback executed when active menu is changed, use it with trigger | (open: boolean) => void | - | | ### FloatButton.BackTop diff --git a/components/float-button/index.zh-CN.md b/components/float-button/index.zh-CN.md index 63295a742d..69a0597d3c 100644 --- a/components/float-button/index.zh-CN.md +++ b/components/float-button/index.zh-CN.md @@ -27,6 +27,7 @@ tag: 5.0.0 浮动按钮组 菜单模式 受控模式 +弹出方向 回到顶部 徽标数 调试小圆点使用 @@ -60,6 +61,7 @@ tag: 5.0.0 | trigger | 触发方式(有触发方式为菜单模式) | `click` \| `hover` | - | | | open | 受控展开,需配合 trigger 一起使用 | boolean | - | | | closeIcon | 自定义关闭按钮 | React.ReactNode | `` | | +| placement | 自定义菜单弹出位置 | `top` \| `left` \| `right` \| `bottom` | `top` | 5.21.0 | | onOpenChange | 展开收起时的回调,需配合 trigger 一起使用 | (open: boolean) => void | - | | ### FloatButton.BackTop diff --git a/components/float-button/interface.ts b/components/float-button/interface.ts index d7e339cc84..eb7175494f 100644 --- a/components/float-button/interface.ts +++ b/components/float-button/interface.ts @@ -49,6 +49,8 @@ export interface FloatButtonGroupProps extends FloatButtonProps { open?: boolean; // 关闭按钮自定义图标 closeIcon?: React.ReactNode; + // 菜单弹出方向 + placement?: 'top' | 'left' | 'right' | 'bottom'; // 展开收起的回调 onOpenChange?: (open: boolean) => void; } diff --git a/components/float-button/style/index.ts b/components/float-button/style/index.ts index f94e4acd81..175b708b02 100644 --- a/components/float-button/style/index.ts +++ b/components/float-button/style/index.ts @@ -1,12 +1,12 @@ import type { CSSObject } from '@ant-design/cssinjs'; -import { Keyframes, unit } from '@ant-design/cssinjs'; +import { unit } from '@ant-design/cssinjs'; import { resetComponent } from '../../style'; import { initFadeMotion } from '../../style/motion/fade'; -import { initMotion } from '../../style/motion/motion'; import type { FullToken, GenerateStyle, GetDefaultToken } from '../../theme/internal'; import { genStyleHooks, mergeToken } from '../../theme/internal'; import getOffset from '../util'; +import floatButtonGroupMotion from './keyframes'; /** Component only token. Which will handle additional calculation of alias token */ export interface ComponentToken { @@ -26,7 +26,7 @@ export interface ComponentToken { * @desc FloatButton 组件的 Token * @descEN Token for FloatButton component */ -type FloatButtonToken = FullToken<'FloatButton'> & { +export type FloatButtonToken = FullToken<'FloatButton'> & { /** * @desc FloatButton 颜色 * @descEN Color of FloatButton @@ -86,58 +86,6 @@ type FloatButtonToken = FullToken<'FloatButton'> & { 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, ${unit(floatButtonSize)}, 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, ${unit(floatButtonSize)}, 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 { @@ -157,22 +105,25 @@ const floatButtonGroupStyle: GenerateStyle = (token [groupPrefixCls]: { ...resetComponent(token), zIndex: zIndexPopupBase, - display: 'block', + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + justifyContent: 'center', border: 'none', position: 'fixed', - width: floatButtonSize, height: 'auto', boxShadow: 'none', + minWidth: floatButtonSize, minHeight: floatButtonSize, insetInlineEnd: token.floatButtonInsetInlineEnd, - insetBlockEnd: token.floatButtonInsetBlockEnd, + bottom: token.floatButtonInsetBlockEnd, borderRadius: borderRadiusLG, - [`${groupPrefixCls}-wrap`]: { zIndex: -1, - display: 'block', - position: 'relative', - marginBottom: margin, + display: 'flex', + justifyContent: 'center', + alignItems: 'center', + position: 'absolute', }, [`&${groupPrefixCls}-rtl`]: { direction: 'rtl', @@ -181,14 +132,30 @@ const floatButtonGroupStyle: GenerateStyle = (token position: 'static', }, }, + [`${groupPrefixCls}-top > ${groupPrefixCls}-wrap`]: { + flexDirection: 'column', + top: 'auto', + bottom: calc(floatButtonSize).add(margin).equal(), + }, + [`${groupPrefixCls}-bottom > ${groupPrefixCls}-wrap`]: { + flexDirection: 'column', + top: calc(floatButtonSize).add(margin).equal(), + bottom: 'auto', + }, + [`${groupPrefixCls}-right > ${groupPrefixCls}-wrap`]: { + flexDirection: 'row', + left: { _skip_check_: true, value: calc(floatButtonSize).add(margin).equal() }, + right: { _skip_check_: true, value: 'auto' }, + }, + [`${groupPrefixCls}-left > ${groupPrefixCls}-wrap`]: { + flexDirection: 'row', + left: { _skip_check_: true, value: 'auto' }, + right: { _skip_check_: true, value: calc(floatButtonSize).add(margin).equal() }, + }, [`${groupPrefixCls}-circle`]: { - [`${componentCls}-circle:not(:last-child)`]: { - marginBottom: token.margin, - [`${componentCls}-body`]: { - width: floatButtonSize, - height: floatButtonSize, - borderRadius: '50%', - }, + gap: margin, + [`${groupPrefixCls}-wrap`]: { + gap: margin, }, }, [`${groupPrefixCls}-square`]: { @@ -217,14 +184,22 @@ const floatButtonGroupStyle: GenerateStyle = (token }, }, [`${groupPrefixCls}-wrap`]: { - display: 'block', borderRadius: borderRadiusLG, boxShadow: token.boxShadowSecondary, [`${componentCls}-square`]: { boxShadow: 'none', - marginTop: 0, borderRadius: 0, padding: floatButtonBodyPadding, + [`${componentCls}-body`]: { + width: token.floatButtonBodySize, + height: token.floatButtonBodySize, + }, + }, + }, + }, + [`${groupPrefixCls}-top > ${groupPrefixCls}-wrap, ${groupPrefixCls}-bottom > ${groupPrefixCls}-wrap`]: + { + [`> ${componentCls}-square`]: { '&:first-child': { borderStartStartRadius: borderRadiusLG, borderStartEndRadius: borderRadiusLG, @@ -236,13 +211,25 @@ const floatButtonGroupStyle: GenerateStyle = (token '&:not(:last-child)': { borderBottom: `${unit(token.lineWidth)} ${token.lineType} ${token.colorSplit}`, }, - [`${componentCls}-body`]: { - width: token.floatButtonBodySize, - height: token.floatButtonBodySize, + }, + }, + [`${groupPrefixCls}-left > ${groupPrefixCls}-wrap, ${groupPrefixCls}-right > ${groupPrefixCls}-wrap`]: + { + [`> ${componentCls}-square`]: { + '&:first-child': { + borderStartStartRadius: borderRadiusLG, + borderEndStartRadius: borderRadiusLG, + }, + '&:last-child': { + borderStartEndRadius: borderRadiusLG, + borderEndEndRadius: borderRadiusLG, + }, + '&:not(:last-child)': { + borderInlineEnd: `${unit(token.lineWidth)} ${token.lineType} ${token.colorSplit}`, }, }, }, - }, + [`${groupPrefixCls}-circle-shadow`]: { boxShadow: 'none', }, @@ -290,7 +277,7 @@ const sharedFloatButtonStyle: GenerateStyle = (toke width: floatButtonSize, height: floatButtonSize, insetInlineEnd: token.floatButtonInsetInlineEnd, - insetBlockEnd: token.floatButtonInsetBlockEnd, + bottom: token.floatButtonInsetBlockEnd, boxShadow: token.boxShadowSecondary, // Pure Panel '&-pure': { @@ -454,12 +441,11 @@ export default genStyleHooks( floatButtonBodyPadding: paddingXXS, badgeOffset: calc(paddingXXS).mul(1.5).equal(), }); - return [ floatButtonGroupStyle(floatButtonToken), sharedFloatButtonStyle(floatButtonToken), initFadeMotion(token), - initFloatButtonGroupMotion(floatButtonToken), + floatButtonGroupMotion(floatButtonToken), ]; }, prepareComponentToken, diff --git a/components/float-button/style/keyframes.ts b/components/float-button/style/keyframes.ts new file mode 100644 index 0000000000..7623f4c865 --- /dev/null +++ b/components/float-button/style/keyframes.ts @@ -0,0 +1,153 @@ +import { Keyframes, unit } from '@ant-design/cssinjs'; + +import type { FloatButtonToken } from '.'; +import { initMotion } from '../../style/motion/motion'; + +const floatButtonGroupMotion = (token: FloatButtonToken) => { + const { componentCls, floatButtonSize, motionDurationSlow, motionEaseInOutCirc, calc } = token; + const moveTopIn = new Keyframes('antFloatButtonMoveTopIn', { + '0%': { + transform: `translate3d(0, ${unit(floatButtonSize)}, 0)`, + transformOrigin: '0 0', + opacity: 0, + }, + '100%': { + transform: 'translate3d(0, 0, 0)', + transformOrigin: '0 0', + opacity: 1, + }, + }); + const moveTopOut = new Keyframes('antFloatButtonMoveTopOut', { + '0%': { + transform: 'translate3d(0, 0, 0)', + transformOrigin: '0 0', + opacity: 1, + }, + '100%': { + transform: `translate3d(0, ${unit(floatButtonSize)}, 0)`, + transformOrigin: '0 0', + opacity: 0, + }, + }); + const moveRightIn = new Keyframes('antFloatButtonMoveRightIn', { + '0%': { + transform: `translate3d(${calc(floatButtonSize).mul(-1).equal()}, 0, 0)`, + transformOrigin: '0 0', + opacity: 0, + }, + '100%': { + transform: 'translate3d(0, 0, 0)', + transformOrigin: '0 0', + opacity: 1, + }, + }); + const moveRightOut = new Keyframes('antFloatButtonMoveRightOut', { + '0%': { + transform: 'translate3d(0, 0, 0)', + transformOrigin: '0 0', + opacity: 1, + }, + '100%': { + transform: `translate3d(${calc(floatButtonSize).mul(-1).equal()}, 0, 0)`, + transformOrigin: '0 0', + opacity: 0, + }, + }); + const moveBottomIn = new Keyframes('antFloatButtonMoveBottomIn', { + '0%': { + transform: `translate3d(0, ${calc(floatButtonSize).mul(-1).equal()}, 0)`, + transformOrigin: '0 0', + opacity: 0, + }, + '100%': { + transform: 'translate3d(0, 0, 0)', + transformOrigin: '0 0', + opacity: 1, + }, + }); + const moveBottomOut = new Keyframes('antFloatButtonMoveBottomOut', { + '0%': { + transform: 'translate3d(0, 0, 0)', + transformOrigin: '0 0', + opacity: 1, + }, + '100%': { + transform: `translate3d(0, ${calc(floatButtonSize).mul(-1).equal()}, 0)`, + transformOrigin: '0 0', + opacity: 0, + }, + }); + const moveLeftIn = new Keyframes('antFloatButtonMoveLeftIn', { + '0%': { + transform: `translate3d(${unit(floatButtonSize)}, 0, 0)`, + transformOrigin: '0 0', + opacity: 0, + }, + '100%': { + transform: 'translate3d(0, 0, 0)', + transformOrigin: '0 0', + opacity: 1, + }, + }); + const moveLeftOut = new Keyframes('antFloatButtonMoveLeftOut', { + '0%': { + transform: 'translate3d(0, 0, 0)', + transformOrigin: '0 0', + opacity: 1, + }, + '100%': { + transform: `translate3d(${unit(floatButtonSize)}, 0, 0)`, + transformOrigin: '0 0', + opacity: 0, + }, + }); + const groupPrefixCls = `${componentCls}-group`; + return [ + { + [groupPrefixCls]: { + [`&${groupPrefixCls}-top ${groupPrefixCls}-wrap`]: initMotion( + `${groupPrefixCls}-wrap`, + moveTopIn, + moveTopOut, + motionDurationSlow, + true, + ), + [`&${groupPrefixCls}-bottom ${groupPrefixCls}-wrap`]: initMotion( + `${groupPrefixCls}-wrap`, + moveBottomIn, + moveBottomOut, + motionDurationSlow, + true, + ), + [`&${groupPrefixCls}-left ${groupPrefixCls}-wrap`]: initMotion( + `${groupPrefixCls}-wrap`, + moveLeftIn, + moveLeftOut, + motionDurationSlow, + true, + ), + [`&${groupPrefixCls}-right ${groupPrefixCls}-wrap`]: initMotion( + `${groupPrefixCls}-wrap`, + moveRightIn, + moveRightOut, + motionDurationSlow, + true, + ), + }, + }, + { + [`${groupPrefixCls}-wrap`]: { + [`&${groupPrefixCls}-wrap-enter, &${groupPrefixCls}-wrap-appear`]: { + opacity: 0, + animationTimingFunction: motionEaseInOutCirc, + }, + [`&${groupPrefixCls}-wrap-leave`]: { + opacity: 1, + animationTimingFunction: motionEaseInOutCirc, + }, + }, + }, + ]; +}; + +export default floatButtonGroupMotion;