diff --git a/lerna.json b/lerna.json index ac144ec8..d525cc6b 100644 --- a/lerna.json +++ b/lerna.json @@ -1,5 +1,5 @@ { - "version": "1.4.2", + "version": "1.4.3-beta.0", "npmClient": "pnpm", "packages": ["packages/*"], "command": { diff --git a/packages/bui-core/package.json b/packages/bui-core/package.json index b349b072..358e5585 100644 --- a/packages/bui-core/package.json +++ b/packages/bui-core/package.json @@ -1,6 +1,6 @@ { "name": "@bifrostui/react", - "version": "1.4.2", + "version": "1.4.3-beta.0", "description": "React components for building mobile application", "homepage": "http://bui.taopiaopiao.com", "license": "MIT", diff --git a/packages/bui-core/src/Badge/__tests__/__snapshots__/Badge.snapshot.test.tsx.snap b/packages/bui-core/src/Badge/__tests__/__snapshots__/Badge.snapshot.test.tsx.snap index 36eb7a96..d9631ee4 100644 --- a/packages/bui-core/src/Badge/__tests__/__snapshots__/Badge.snapshot.test.tsx.snap +++ b/packages/bui-core/src/Badge/__tests__/__snapshots__/Badge.snapshot.test.tsx.snap @@ -65,13 +65,39 @@ exports[`Badge snapshot Badge demo snapshot 0 2`] = ` } >
1
+
+
+ +
+
`; diff --git a/packages/bui-core/src/Badge/index.zh-CN.md b/packages/bui-core/src/Badge/index.zh-CN.md index d28760e3..29e99db7 100644 --- a/packages/bui-core/src/Badge/index.zh-CN.md +++ b/packages/bui-core/src/Badge/index.zh-CN.md @@ -36,13 +36,15 @@ export default () => { ##### 圆形徽章 ```tsx -import { Badge, Stack } from '@bifrostui/react'; +import { Avatar, Badge, Stack } from '@bifrostui/react'; import React from 'react'; export default () => { return ( - + + + ); }; diff --git a/packages/bui-core/src/Input/Input.less b/packages/bui-core/src/Input/Input.less index dc9b7283..f4f80820 100644 --- a/packages/bui-core/src/Input/Input.less +++ b/packages/bui-core/src/Input/Input.less @@ -41,6 +41,7 @@ &-input { flex: 1; + min-width: 0; display: flex; align-items: center; padding: 0; @@ -50,7 +51,6 @@ outline: none; background-color: var(--background-color); font-size: var(--bui-text-size-2); - .ellipsis(); &::placeholder { color: var(--bui-color-fg-subtle); @@ -58,7 +58,6 @@ } &-disabled { - pointer-events: none; background-color: var(--disabled-background-color); .bui-input-input { diff --git a/packages/bui-core/src/Popover/Popover.less b/packages/bui-core/src/Popover/Popover.less index 616f3905..a9d3175d 100644 --- a/packages/bui-core/src/Popover/Popover.less +++ b/packages/bui-core/src/Popover/Popover.less @@ -40,7 +40,7 @@ --localtion-position: var(--bui-popover-localtion-position, 8PX); --max-width: var(--bui-popover-max-width, 350px); --content-min-width: var(--bui-popover-content-min-width, 30px); - --content-padding: var(--bui-popover-content-padding, 0); + --content-padding: var(--bui-popover-content-padding, 6px 8px); max-width: var(--max-width); font-size: var(--bui-text-size-1); position: absolute; diff --git a/packages/bui-core/src/Popover/Popover.tsx b/packages/bui-core/src/Popover/Popover.tsx index c18f79be..1c6d2972 100644 --- a/packages/bui-core/src/Popover/Popover.tsx +++ b/packages/bui-core/src/Popover/Popover.tsx @@ -3,6 +3,7 @@ import React, { useState, useRef, useEffect } from 'react'; import { getStylesAndLocation, triggerEventTransform, + parsePlacement, useUniqueId, throttle, } from '@bifrostui/utils'; @@ -20,6 +21,7 @@ const Popover = React.forwardRef((props, ref) => { title, content, defaultOpen, + offsetSpacing = 0, placement = 'top', trigger = 'click', onOpenChange, @@ -29,16 +31,7 @@ const Popover = React.forwardRef((props, ref) => { } = props; const controlByUser = typeof open !== 'undefined'; - const positionArr = placement.split(/([A-Z])/); - const direction = positionArr[0]; - let location; - if (positionArr.length > 1) { - positionArr.splice(0, 1); - location = positionArr.join('').toLowerCase(); - } else { - location = 'center'; - } - + const { direction, location = 'center' } = parsePlacement(placement); const childrenRef = useRef(); const [openStatus, setOpenStatus] = useState(defaultOpen); // 气泡所在位置 @@ -82,10 +75,15 @@ const Popover = React.forwardRef((props, ref) => { }; const onRootElementMouted = throttle(() => { + const { + direction: newParsedDirection, + location: newParsedLocation = 'center', + } = parsePlacement(placement); const result = getStylesAndLocation({ childrenRef, - arrowDirection, - arrowLocation, + arrowDirection: newParsedDirection, + arrowLocation: newParsedLocation, + offsetSpacing, selector: `[data-id="tt_${ttId}"]`, }); if (!result) return; diff --git a/packages/bui-core/src/Popover/Popover.types.ts b/packages/bui-core/src/Popover/Popover.types.ts index 4c6f0a7e..7f21d314 100644 --- a/packages/bui-core/src/Popover/Popover.types.ts +++ b/packages/bui-core/src/Popover/Popover.types.ts @@ -31,6 +31,10 @@ export type PopoverProps< * @default false */ hideArrow?: boolean; + /** + * 用于控制浮层和目标元素偏移量 + */ + offsetSpacing?: number; /** * 气泡框位置 * @default 'top' diff --git a/packages/bui-core/src/Popover/index.en-US.md b/packages/bui-core/src/Popover/index.en-US.md index 3dbcf201..371af831 100644 --- a/packages/bui-core/src/Popover/index.en-US.md +++ b/packages/bui-core/src/Popover/index.en-US.md @@ -20,8 +20,9 @@ import React from 'react'; export default () => { return ( This is a title} - content={
This is a content
} + title="This is a title" + content="This is a content" + placement="topLeft" > click显示
@@ -40,10 +41,7 @@ import React, { useState } from 'react'; export default () => { const [open, setOpen] = useState(true); return ( - This is a popover} - open={open} - > + setOpen(!open)}>open控制显隐 ); @@ -60,16 +58,30 @@ import React from 'react'; export default () => { return ( - This is a popover} - defaultOpen - > + defaultOpen默认显示 ); }; ``` +### OffsetSpacing is the interval between the floating layer and the target element + +OffsetSpacing can be set to control the distance from the target element + +```tsx +import { Popover } from '@bifrostui/react'; +import React from 'react'; + +export default () => { + return ( + + offsetSpacing控制目标间隔(设置20 以便观察) + + ); +}; +``` + ### HideArrow arrow display You can set 'hideArrow' to true to hide arrows @@ -80,11 +92,7 @@ import React from 'react'; export default () => { return ( - This is a popover} - defaultOpen - hideArrow - > + defaultOpen默认显示 ); @@ -93,7 +101,7 @@ export default () => { ### Placement Bubble Box Position -Placement: Set the position of the bubble float layer. The optional values are: top, left, right, bottom, topLeft, topRight, bottomLeft, bottomRight, leftTop, leftBottom, rightTop, rightBottom +Placement: Set the position of the bubble float layer. The optional values are: top left right bottom topLeft topRight bottomLeft bottomRight leftTop leftBottom rightTop rightBottom ```tsx import { Popover, Button } from '@bifrostui/react'; @@ -118,25 +126,13 @@ export default () => { justifyContent: 'space-between', }} > - This is a popover} - placement="topLeft" - > + {packageButton(topLeft)} - This is a popover} - placement="top" - > + {packageButton(top)} - This is a popover} - placement="topRight" - > + {packageButton(topRight)} @@ -148,23 +144,15 @@ export default () => { flexDirection: 'column', }} > - This is a popover} - placement="leftTop" - > + {packageButton(leftTop)} - This is a popover} - placement="left" - > + {packageButton(left)} This is a popover} + title="This is a popover" placement="leftBottom" > {packageButton(leftBottom)} @@ -178,23 +166,15 @@ export default () => { flexDirection: 'column', }} > - This is a popover} - placement="rightTop" - > + {packageButton(rightTop)} - This is a popover} - placement="right" - > + {packageButton(right)} This is a popover} + title="This is a popover" placement="rightBottom" > {packageButton(rightBottom)} @@ -211,21 +191,17 @@ export default () => { > This is a popover} + title="This is a popover" placement="bottomLeft" > {packageButton(bottomLeft)} - This is a popover} - placement="bottom" - > + {packageButton(bottom)} This is a popover} + title="This is a popover" placement="bottomRight" > {packageButton(bottomRight)} @@ -246,10 +222,7 @@ import React from 'react'; export default () => { return ( - This is a popover} - trigger="hover" - > + hover触发方式 ); @@ -268,7 +241,7 @@ export default () => { }; return ( This is a popover} + title="This is a popover" trigger="hover" onOpenChange={onOpenChange} > @@ -280,16 +253,17 @@ export default () => { ### API -| attribute | explain | type | Default value | -| ------------ | -------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------- | -| title | Title of Bubble Floating Layer Floating Layer Content | ReactNode | - | -| content | Content of Bubble Floating Layer | ReactNode | - | -| defaultOpen | Whether to hide by default | boolean | false | -| open | Used for manually controlling the appearance and concealment of bubble floating layers | boolean | - | -| hideArrow | Display arrows or not | boolean | false | -| placement | Bubble box position | String, the enumeration value is `center` `left` `leftTop` `leftBottom` `right` `rightTop` `rightBottom` `top` `topLeft` `topRight` `bottom` `bottomLeft` `bottomRight` `bottom` | 'top' | -| trigger | Trigger behavior | string \|String [], the enumeration value is' click '\|'hover' | 'click' | -| onOpenChange | The callback method for bubble floating layer manifestation and concealment | (e: React.MouseEvent,data: {open: boolean}) => void | - | +| attribute | explain | type | Default value | +| ------------- | -------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------- | +| title | Title of Bubble Floating Layer Floating Layer Content | ReactNode | - | +| content | Content of Bubble Floating Layer | ReactNode | - | +| defaultOpen | Whether to hide by default | boolean | false | +| open | Used for manually controlling the appearance and concealment of bubble floating layers | boolean | - | +| hideArrow | Display arrows or not | boolean | false | +| offsetSpacing | The offset between the floating layer and the target element | number | 0 | +| placement | Bubble box position | string, The enumeration value is `center` `left` `leftTop` `leftBottom` `right` `rightTop` `rightBottom` `top` `topLeft` `topRight` `bottom` `bottomLeft` `bottomRight` `bottom` | 'top' | +| trigger | Trigger behavior | string \|string[], The enumeration value is' click '\|'hover' | 'click' | +| onOpenChange | The callback method for bubble floating layer manifestation and concealment | (e: React.MouseEvent,data: {open: boolean}) => void | - | ### Style variables diff --git a/packages/bui-core/src/Popover/index.zh-CN.md b/packages/bui-core/src/Popover/index.zh-CN.md index 977106cc..7ffa354e 100644 --- a/packages/bui-core/src/Popover/index.zh-CN.md +++ b/packages/bui-core/src/Popover/index.zh-CN.md @@ -20,8 +20,9 @@ import React from 'react'; export default () => { return ( This is a title} - content={
This is a content
} + title="This is a title" + content="This is a content" + placement="topLeft" > click显示
@@ -40,10 +41,7 @@ import React, { useState } from 'react'; export default () => { const [open, setOpen] = useState(true); return ( - This is a popover} - open={open} - > + setOpen(!open)}>open控制显隐 ); @@ -60,16 +58,30 @@ import React from 'react'; export default () => { return ( - This is a popover} - defaultOpen - > + defaultOpen默认显示 ); }; ``` +### offsetSpacing 浮层和目标元素间隔 + +可以设置offsetSpacing来控制和目标元素的距离 + +```tsx +import { Popover } from '@bifrostui/react'; +import React from 'react'; + +export default () => { + return ( + + offsetSpacing控制目标间隔(设置20 以便观察) + + ); +}; +``` + ### hideArrow 箭头展示 可以设置 `hideArrow` 为 true 隐藏箭头 @@ -80,11 +92,7 @@ import React from 'react'; export default () => { return ( - This is a popover} - defaultOpen - hideArrow - > + defaultOpen默认显示 ); @@ -118,25 +126,13 @@ export default () => { justifyContent: 'space-between', }} > - This is a popover} - placement="topLeft" - > + {packageButton(topLeft)} - This is a popover} - placement="top" - > + {packageButton(top)} - This is a popover} - placement="topRight" - > + {packageButton(topRight)} @@ -148,23 +144,15 @@ export default () => { flexDirection: 'column', }} > - This is a popover} - placement="leftTop" - > + {packageButton(leftTop)} - This is a popover} - placement="left" - > + {packageButton(left)} This is a popover} + title="This is a popover" placement="leftBottom" > {packageButton(leftBottom)} @@ -178,23 +166,15 @@ export default () => { flexDirection: 'column', }} > - This is a popover} - placement="rightTop" - > + {packageButton(rightTop)} - This is a popover} - placement="right" - > + {packageButton(right)} This is a popover} + title="This is a popover" placement="rightBottom" > {packageButton(rightBottom)} @@ -211,21 +191,17 @@ export default () => { > This is a popover} + title="This is a popover" placement="bottomLeft" > {packageButton(bottomLeft)} - This is a popover} - placement="bottom" - > + {packageButton(bottom)} This is a popover} + title="This is a popover" placement="bottomRight" > {packageButton(bottomRight)} @@ -246,10 +222,7 @@ import React from 'react'; export default () => { return ( - This is a popover} - trigger="hover" - > + hover触发方式 ); @@ -268,7 +241,7 @@ export default () => { }; return ( This is a popover} + title="This is a popover" trigger="hover" onOpenChange={onOpenChange} > @@ -280,16 +253,17 @@ export default () => { ### API -| 属性 | 说明 | 类型 | 默认值 | -| ------------ | ------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------- | -| title | 气泡浮层的标题浮层内容 | ReactNode | - | -| content | 气泡浮层的内容 | ReactNode | - | -| defaultOpen | 默认是否显隐 | boolean | false | -| open | 用于手动控制气泡浮层显隐 | boolean | - | -| hideArrow | 是否展示箭头 | boolean | false | -| placement | 气泡框位置 | string,枚举值是 `center` `left` `leftTop` `leftBottom` `right` `rightTop` `rightBottom` `top` `topLeft` `topRight` `bottom` `bottomLeft` `bottomRight` `bottom` | 'top' | -| trigger | 触发行为 | string \| string[],枚举值是 'click' \| 'hover' | 'click' | -| onOpenChange | 气泡浮层显隐的回调方法 | (e: React.MouseEvent,data: {open: boolean}) => void | - | +| 属性 | 说明 | 类型 | 默认值 | +| ------------- | ------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------- | +| title | 气泡浮层的标题浮层内容 | ReactNode | - | +| content | 气泡浮层的内容 | ReactNode | - | +| defaultOpen | 默认是否显隐 | boolean | false | +| open | 用于手动控制气泡浮层显隐 | boolean | - | +| hideArrow | 是否展示箭头 | boolean | false | +| offsetSpacing | 浮层与目标元素的偏移量 | number | 0 | +| placement | 气泡框位置 | string,枚举值是 `center` `left` `leftTop` `leftBottom` `right` `rightTop` `rightBottom` `top` `topLeft` `topRight` `bottom` `bottomLeft` `bottomRight` `bottom` | 'top' | +| trigger | 触发行为 | string \| string[],枚举值是 'click' \| 'hover' | 'click' | +| onOpenChange | 气泡浮层显隐的回调方法 | (e: React.MouseEvent,data: {open: boolean}) => void | - | ### 样式变量 diff --git a/packages/bui-core/src/Select/Select.less b/packages/bui-core/src/Select/Select.less index 90b4dec7..d8e0e637 100644 --- a/packages/bui-core/src/Select/Select.less +++ b/packages/bui-core/src/Select/Select.less @@ -6,11 +6,6 @@ --mini-width: var(--bui-select-min-width, 100px); --font-size: var(--bui-select-font-size, var(--bui-title-size-3)); --padding: var(--bui-select-selector-container, 0 14px); - --option-container-padding: var(--bui-select-option-container-padding, 3px 0); - --option-container-margin-top: var(--bui-select-option-margin-top, 6px); - --option-padding: var(--bui-select-option-padding, 0 14px); - --option-margin: var(--bui-select-option-margin, 0 3px); - --option-height: var(--bui-select-option-height, 27px); position: relative; cursor: pointer; @@ -20,8 +15,8 @@ border-radius: 5px; background-color: var(--bui-color-bg-view); font-family: var(--bui-font-family); + -webkit-tap-highlight-color: transparent; - &:active, &-active { box-shadow: 0 0 0 2px var(--bui-color-bg-default); } @@ -38,6 +33,7 @@ height: 0; border: 0; opacity: 0; + pointer-events: none; } &-placeholder { @@ -65,22 +61,37 @@ top: 100%; left: 0; width: 100%; - z-index: var(--bui-z-index-dropdown); - margin-top: var(--option-container-margin-top); + font-size: var(--bui-select-font-size, var(--bui-title-size-3)); + z-index: var(--bui-z-index-tooltip); border-radius: 3px; background-color: var(--bui-color-bg-view); padding: 2px; overflow: hidden; + + &-top { + margin-top: -6px; + } + + &-bottom { + margin-top: 6px; + } + + &-hide { + pointer-events: none; + } } &-option-main { border-radius: 3px; - padding: var(--option-container-padding); + padding: var(--bui-select-option-container-padding, 3px 0); box-shadow: 0 0 0 2px var(--bui-color-bg-default); overflow: hidden; } &-option { + --option-padding: var(--bui-select-option-padding, 0 14px); + --option-margin: var(--bui-select-option-margin, 0 3px); + --option-height: var(--bui-select-option-height, 27px); display: flex; align-items: center; height: var(--option-height); diff --git a/packages/bui-core/src/Select/Select.tsx b/packages/bui-core/src/Select/Select.tsx index 8cba4148..7a8f940a 100644 --- a/packages/bui-core/src/Select/Select.tsx +++ b/packages/bui-core/src/Select/Select.tsx @@ -1,15 +1,24 @@ import { CaretDownIcon, CaretUpIcon } from '@bifrostui/icons'; -import { useValue } from '@bifrostui/utils'; +import { + getStylesAndLocation, + isMini, + throttle, + useForkRef, + useUniqueId, + useValue, +} from '@bifrostui/utils'; import clsx from 'clsx'; -import React, { useEffect, useMemo, useState } from 'react'; +import React, { useEffect, useMemo, useRef, useState } from 'react'; import Fade from '../Fade'; import Slide from '../Slide'; import { SelectProps } from './Select.types'; import BuiSelectContext from './selectContext'; import Backdrop from '../Backdrop'; +import Portal from '../Portal'; import './Select.less'; const prefixCls = 'bui-select'; +const defaultPlacement = 'bottom'; const Select = React.forwardRef((props, ref) => { const { @@ -25,6 +34,7 @@ const Select = React.forwardRef((props, ref) => { placeholder, icon, open, + scrollContainer, onChange, onClose, onOpen, @@ -38,24 +48,53 @@ const Select = React.forwardRef((props, ref) => { onChange, }); - // 是否展开下拉框 - const [isOpen, setIsOpen] = useState(false); + const [internalOpen, setInternalOpen] = useState(false); // 根选择器展示的内容 const [renderValue, setRenderValue] = useState(''); + const [placement, setPlacement] = useState(defaultPlacement); + const [optionStyle, setOptionStyle] = useState({}); + const isOpen = open !== undefined ? open : internalOpen; + const locatorRef = useRef(null); + const rootRef = useForkRef(ref, locatorRef); + const ttId = useUniqueId(); + const dataId = `${prefixCls}-tt-${ttId}`; - const defaultIcon = isOpen ? ( - - ) : ( - - ); + const updateOptionStyle = throttle(() => { + const curScrollRoot = scrollContainer(); + if (!isMini && curScrollRoot) { + const result = getStylesAndLocation({ + scrollRoot: curScrollRoot, + childrenRef: locatorRef, + arrowDirection: defaultPlacement, + arrowLocation: 'none', + selector: `[data-id="${dataId}"]`, + offsetSpacing: 0, + }); + if (!result) return; + const { styles, childrenStyle, newArrowDirection } = result; + setPlacement(newArrowDirection); + setOptionStyle({ ...styles, width: childrenStyle.width }); + } + }, 100); + + const changeOpen = (newOpen: boolean) => { + if (newOpen) { + updateOptionStyle(); + // 第一次超出边界变化方向时,Slide的动画方向更新时序问题 + setTimeout(() => { + setInternalOpen(newOpen); + onOpen?.(); + }, 100); + } else { + onClose?.(); + setInternalOpen(newOpen); + } + }; // 点击根选择器的回调 const handleSelectClick = (e) => { if (disabled) return; - setIsOpen(!isOpen); + changeOpen(!isOpen); if (typeof onClick === 'function') onClick(e); }; @@ -67,13 +106,11 @@ const Select = React.forwardRef((props, ref) => { } else { onChange?.(e, { value: optionValue }); } - setIsOpen(false); + changeOpen(false); }; - const handleBackdropTouchStart = () => { - if (isOpen) { - setIsOpen(false); - } + const handleBackdropClick = () => { + changeOpen(false); }; const selectContext = useMemo( @@ -81,20 +118,62 @@ const Select = React.forwardRef((props, ref) => { [selectValue, onChange, setRenderValue], ); - // 监听外部open的改变 - useEffect(() => { - if (open !== undefined) setIsOpen(open); - }, [open]); - - // 折叠/展开时的处理 + // eslint-disable-next-line consistent-return useEffect(() => { - // 非function情况,直接调用会报错 - if (isOpen) { - onOpen?.(); - } else { - onClose?.(); + if (!isMini) { + window.addEventListener('resize', updateOptionStyle); + return () => { + window.removeEventListener('resize', updateOptionStyle); + }; } - }, [isOpen]); + }, []); + + const defaultIcon = isOpen ? ( + + ) : ( + + ); + + const renderOptions = () => { + return ( + +
`${cls}-option-container`) || []), + `${prefixCls}-option-container-${placement}`, + { + [`${prefixCls}-option-container-hide`]: !isOpen, + }, + )} + data-id={dataId} + style={optionStyle} + > + +
{children}
+
+
+
+ ); + }; return ( @@ -103,7 +182,7 @@ const Select = React.forwardRef((props, ref) => { [`${prefixCls}-disabled`]: disabled, [`${prefixCls}-active`]: isOpen, })} - ref={ref} + ref={rootRef} {...others} onClick={handleSelectClick} > @@ -123,33 +202,19 @@ const Select = React.forwardRef((props, ref) => { /> {icon || defaultIcon} - {/* 选项下拉框 */} - -
- -
{children}
-
-
-
+ {isMini && renderOptions()} + {!isMini && ( + + {renderOptions()} + + )} ((props, ref) => { Select.displayName = 'BuiSelect'; Select.defaultProps = { defaultValue: '', + scrollContainer: () => { + return isMini ? null : document.body; + }, }; export default Select; diff --git a/packages/bui-core/src/Select/Select.types.ts b/packages/bui-core/src/Select/Select.types.ts index 37ba726e..689ae1a6 100644 --- a/packages/bui-core/src/Select/Select.types.ts +++ b/packages/bui-core/src/Select/Select.types.ts @@ -94,6 +94,12 @@ export type SelectProps< * 是否展开下拉框 */ open?: boolean; + /** + * 滚动容器 + * 下拉框元素和children将会被append到scrollContainer中 + * 默认是页面的根节点 + */ + scrollContainer?: () => Element | null; /** * 自定义选中后展示的内容 */ diff --git a/packages/bui-core/src/Select/__tests__/Select.test.tsx b/packages/bui-core/src/Select/__tests__/Select.test.tsx index 3b1f57eb..0b092178 100644 --- a/packages/bui-core/src/Select/__tests__/Select.test.tsx +++ b/packages/bui-core/src/Select/__tests__/Select.test.tsx @@ -6,6 +6,9 @@ import SelectOption from '../SelectOption'; const classPrefix = 'bui-select'; describe('Select', () => { + beforeEach(() => { + jest.useFakeTimers(); + }); isConformant({ Component: Select, displayName: 'BuiSelect', @@ -84,6 +87,9 @@ describe('Select', () => { await act(async () => { userEvent.click(document.querySelector('.bui-select')); }); + await act(async () => { + await jest.runAllTimers(); + }); expect(onOpen).toHaveBeenCalled(); }); it('should call onClick when click open', async () => { diff --git a/packages/bui-core/src/Select/__tests__/__snapshots__/select.snapshot.test.tsx.snap b/packages/bui-core/src/Select/__tests__/__snapshots__/select.snapshot.test.tsx.snap deleted file mode 100644 index b138ad7c..00000000 --- a/packages/bui-core/src/Select/__tests__/__snapshots__/select.snapshot.test.tsx.snap +++ /dev/null @@ -1,1161 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Select snapshot Select demo snapshot 0 1`] = ` -
-
-
-
- - ", - } - } - focusable="false" - viewBox="0 0 96 96" - /> -
-
-
-
- option 1 -
-
- option 2 -
-
- option 3 -
-
-
-
-
-
-`; - -exports[`Select snapshot Select demo snapshot 0 2`] = ` -
-
-
-
- 下拉选择 -
- - ", - } - } - focusable="false" - viewBox="0 0 96 96" - /> -
-
-
-
- option 1 -
-
- option 2 -
-
- option 3 -
-
-
-
-
-
-`; - -exports[`Select snapshot Select demo snapshot 0 3`] = ` -
-
-
-
- - ", - } - } - focusable="false" - viewBox="0 0 96 96" - /> -
-
-
-
- option 1 -
-
- option 2 -
-
- option 3 -
-
-
-
-
-
-`; - -exports[`Select snapshot Select demo snapshot 0 4`] = ` -
-
-
-
- 选择器A -
- - ", - } - } - focusable="false" - viewBox="0 0 96 96" - /> -
-
-
-
- option 1 -
-
- option 2 -
-
- option 3 -
-
-
-
-
-
-
-
- 选择器B -
- - ", - } - } - focusable="false" - viewBox="0 0 96 96" - /> -
-
-
-
- option 1 -
-
- option 2 -
-
- option 3 -
-
-
-
-
- -
-`; - -exports[`Select snapshot Select demo snapshot 0 5`] = ` -
-
-
-
- 禁用 -
- - ", - } - } - focusable="false" - viewBox="0 0 96 96" - /> -
-
-
-
- option 1 -
-
- option 2 -
-
- option 3 -
-
-
-
-
-
-
-
- - ", - } - } - focusable="false" - viewBox="0 0 96 96" - /> -
-
-
-
- option 1 -
-
- option 2 -
-
- option 3 -
-
-
-
-
-
-`; - -exports[`Select snapshot Select demo snapshot 0 6`] = ` -
-
-
-
- 自定义图标 -
- - ", - } - } - focusable="false" - viewBox="0 0 96 96" - /> -
-
-
-
- option 1 -
-
- option 2 -
-
- option 3 -
-
-
-
-
-
-`; - -exports[`Select snapshot Select demo snapshot 0 7`] = ` -
-
-
-
- - ", - } - } - focusable="false" - viewBox="0 0 96 96" - /> -
-
-
-
- 选项 - - option 1 -
-
- 选项 - - option 2 -
-
- 选项 - - option 3 -
-
-
-
-
-
-`; - -exports[`Select snapshot Select demo snapshot 0 8`] = ` -
-
-
-
-
- ", - } - } - focusable="false" - viewBox="0 0 96 96" - /> - option 2 -
-
- - ", - } - } - focusable="false" - viewBox="0 0 96 96" - /> -
-
-
-
- 选项- - option 1 -
-
- 选项- - option 2 -
-
- 选项- - option 3 -
-
-
-
-
-
-`; - -exports[`Select snapshot Select demo snapshot 0 9`] = ` -
-
-
- ", - } - } - focusable="false" - viewBox="0 0 96 96" - /> -
-
- 当前状态: - 初始化 -
-
-
-
-
- 下拉选择 -
- - ", - } - } - focusable="false" - viewBox="0 0 96 96" - /> -
-
-
-
- option 1 -
-
- option 2 -
-
- option 3 -
-
-
-
-
-
-`; diff --git a/packages/bui-core/src/Select/__tests__/select.snapshot.test.tsx b/packages/bui-core/src/Select/__tests__/select.snapshot.test.tsx deleted file mode 100644 index 33e52e5e..00000000 --- a/packages/bui-core/src/Select/__tests__/select.snapshot.test.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import { snapshotTest } from 'testing'; - -describe('Select snapshot', () => { - snapshotTest('Select'); -}); diff --git a/packages/bui-core/src/Select/index.en-US.md b/packages/bui-core/src/Select/index.en-US.md index ce4ef6ba..090ce154 100644 --- a/packages/bui-core/src/Select/index.en-US.md +++ b/packages/bui-core/src/Select/index.en-US.md @@ -82,7 +82,7 @@ export default () => { }; ``` -## Initialize default values +### Initialize default values Support initial selection value through the 'defaultValue' attribute. @@ -118,7 +118,7 @@ export default () => { }; ``` -## Uncontrolled/Uncontrolled +### controlled/Uncontrolled Distinguish whether it is a controlled component by passing in 'value': Under controlled circumstances, the business retrieves the control component value through 'onChange' callback; @@ -192,7 +192,7 @@ export default () => { }; ``` -## Disable +### Disable Provide the 'disabled' attribute to prohibit user operations. You can disable all operations by setting 'disabled' on 'Select', or disable operations on a specific option by setting 'disabled' on 'SelectOption'. @@ -243,11 +243,53 @@ export default () => { }; ``` -## customized +### controlled open -The following is an example of a customized Select component. +Provide the 'open' attribute to control open status of the selector by yourself. -#### customize icons +```tsx +import { Select, SelectOption, Stack } from '@bifrostui/react'; +import React, { useState } from 'react'; + +const options = [ + { + label: 'option 1', + value: 1, + }, + { + label: 'option 2', + value: 2, + }, + { + label: 'option 3', + value: 3, + }, +]; + +export default () => { + const [open, setOpen] = useState(true); + + const handleOpen = () => { + setOpen(true); + }; + + const handleClose = () => { + setOpen(false); + }; + + return ( + + + + ); +}; +``` + +### customize icons Provide the ability to customize icons, which can be customized through the 'icon' attribute. @@ -326,7 +368,7 @@ export default () => { }; ``` -### Customized selector displays results +### customize selector displays results Label 'supports the' ReactNode 'type. When you want to customize the display content of the selector' Select ', you can use the' SelectOption 'component properties:' label 'and' children ', and use them together to achieve customization. @@ -375,7 +417,7 @@ export default () => { }; ``` -## event +### event The Select component not only provides basic 'onChange' callbacks, but also event callbacks for options such as' unfold 'and' collapse '. @@ -422,6 +464,104 @@ export default () => { }; ``` +### customize scroll container + +Provide the ability to customize scroll container, which can be customized through the 'scrollContainer' attribute. Default value is '() => document.body'. +The select option container's display direction will be automatically calculated according to the scroll container. +Only support H5. + +```tsx +import { Select, SelectOption, Stack } from '@bifrostui/react'; +import React, { useRef } from 'react'; + +const options = [ + { + label: 'option 1', + value: 1, + }, + { + label: 'option 2', + value: 2, + }, + { + label: 'option 3', + value: 3, + }, +]; + +export default () => { + const ref = useRef(); + + return ( + +
+ +
+
+ ); +}; +``` + +### override style with `className` attribute + +Provide the ability to override style through `className` attribute. +className will be mounted on the root component, and mouted on the dropdown container as `className-option-container` + +```tsx +import { Select, SelectOption, Stack } from '@bifrostui/react'; +import React from 'react'; + +const options = [ + { + label: 'option 1', + value: 1, + }, + { + label: 'option 2', + value: 2, + }, + { + label: 'option 3', + value: 3, + }, +]; + +/** + .custom-classname { + color: red; + } + .custom-classname-option-container { + color: blue; + } +*/ + +export default () => { + return ( + + + + ); +}; +``` + ### API ##### SelectProps diff --git a/packages/bui-core/src/Select/index.zh-CN.md b/packages/bui-core/src/Select/index.zh-CN.md index 2dfa0c45..4a1eb7f2 100644 --- a/packages/bui-core/src/Select/index.zh-CN.md +++ b/packages/bui-core/src/Select/index.zh-CN.md @@ -82,7 +82,7 @@ export default () => { }; ``` -## 初始化默认值 +### 初始化默认值 支持通过 `defaultValue` 属性,初始选中值。 @@ -118,7 +118,7 @@ export default () => { }; ``` -## 非受控/非受控 +### 受控/非受控 通过是否传入`value`来区分是否为受控组件: 受控情况业务通过 `onChange` 回调控制组件 value; @@ -192,7 +192,7 @@ export default () => { }; ``` -## 禁用 +### 禁用 提供 `disabled` 属性来禁止用户操作。 您可以通过在`Select`上设置`disabled` 全部禁止操作,也可以在`SelectOption`上设置`disabled`对某个选项禁止操作。 @@ -243,11 +243,53 @@ export default () => { }; ``` -## 定制 +### 受控展开/收起 -以下是定制 Select 组件示例。 +通过受控open属性来控制选择器展开/收起 -#### 定制图标 +```tsx +import { Select, SelectOption, Stack } from '@bifrostui/react'; +import React, { useState } from 'react'; + +const options = [ + { + label: 'option 1', + value: 1, + }, + { + label: 'option 2', + value: 2, + }, + { + label: 'option 3', + value: 3, + }, +]; + +export default () => { + const [open, setOpen] = useState(true); + + const handleOpen = () => { + setOpen(true); + }; + + const handleClose = () => { + setOpen(false); + }; + + return ( + + + + ); +}; +``` + +### 定制图标 提供自定义图标能力,可以通过`icon`属性来定制图标。 @@ -375,7 +417,7 @@ export default () => { }; ``` -## 事件 +### 事件 Select 组件除了提供基础的`onChange`回调,还提供选项`展开`、`折叠`时的事件回调。 @@ -422,6 +464,104 @@ export default () => { }; ``` +### 指定滚动父容器 + +通过scrollContainer指定滚动父容器,默认是() => document.body。 +指定后,下拉框的展示方向会自动根据滚动父容器进行计算。 +仅支持H5。 + +```tsx +import { Select, SelectOption, Stack } from '@bifrostui/react'; +import React, { useRef } from 'react'; + +const options = [ + { + label: 'option 1', + value: 1, + }, + { + label: 'option 2', + value: 2, + }, + { + label: 'option 3', + value: 3, + }, +]; + +export default () => { + const ref = useRef(); + + return ( + +
+ +
+
+ ); +}; +``` + +### 通过类名复写样式 + +通过传递`className`属性,可以复写样式 +className除了会挂载在根组件上,还会以`className-option-container`的形式挂载在下拉框容器上 + +```tsx +import { Select, SelectOption, Stack } from '@bifrostui/react'; +import React from 'react'; + +const options = [ + { + label: 'option 1', + value: 1, + }, + { + label: 'option 2', + value: 2, + }, + { + label: 'option 3', + value: 3, + }, +]; + +/** + .custom-classname { + color: red; + } + .custom-classname-option-container { + color: blue; + } +*/ + +export default () => { + return ( + + + + ); +}; +``` + ### API ##### SelectProps diff --git a/packages/bui-core/src/Tabs/TabPanel.tsx b/packages/bui-core/src/Tabs/TabPanel.tsx index 5ef1de49..d0f448f6 100644 --- a/packages/bui-core/src/Tabs/TabPanel.tsx +++ b/packages/bui-core/src/Tabs/TabPanel.tsx @@ -6,7 +6,14 @@ import { TabPanelProps } from './TabPanel.types'; const prefixCls = 'bui-tabpanel'; const TabPanel = forwardRef((props, ref) => { - const { className, children, value, index, keepMounted, ...others } = props; + const { + className, + children, + value, + index, + keepMounted = false, + ...others + } = props; const keepActiveDom = keepMounted ? children : null; @@ -25,8 +32,5 @@ const TabPanel = forwardRef((props, ref) => { }); TabPanel.displayName = 'BuiTabPanel'; -TabPanel.defaultProps = { - keepMounted: false, -}; export default TabPanel; diff --git a/packages/bui-core/src/Tabs/Tabs.tsx b/packages/bui-core/src/Tabs/Tabs.tsx index 581b0cf7..a9b1fd87 100644 --- a/packages/bui-core/src/Tabs/Tabs.tsx +++ b/packages/bui-core/src/Tabs/Tabs.tsx @@ -13,10 +13,10 @@ import React, { useState, } from 'react'; import Tab from './Tab'; -import './Tabs.less'; import { TabsProps } from './Tabs.types'; import { TabsContextProvider } from './TabsContext'; import bound from './utils/bound'; +import './Tabs.less'; const prefixCls = 'bui-tabs'; @@ -40,15 +40,16 @@ const Tabs = React.forwardRef((props, ref) => { if (!container) return; const activeIndex = - !!tabs.length && - tabs.findIndex((item) => item.index === (active || tabs[0]?.index)); - + !!tabs.length && tabs.findIndex((item) => item.index === active); const activeLine = activeLineRef.current; if (!activeLine) return; let activeTab; if (tabs.length) { - activeTab = container.childNodes[activeIndex + 1] as HTMLDivElement; + activeTab = + activeIndex > -1 + ? (container.childNodes[activeIndex + 1] as HTMLDivElement) + : undefined; } else { activeTab = [...container.childNodes].find((child: any) => { if (isMini) { @@ -59,22 +60,28 @@ const Tabs = React.forwardRef((props, ref) => { return [...child.classList].includes('bui-tab-active'); }) as HTMLDivElement; } - if (!activeTab) return; - - const activeTabLeft = activeTab.offsetLeft; - const activeTabWidth = activeTab.offsetWidth; - const containerWidth = container.offsetWidth; - const containerScrollWidth = container.scrollWidth; - const activeLineWidth = activeLine.offsetWidth; - const x = activeTabLeft + (activeTabWidth - activeLineWidth) / 2; + let activeTabLeft = 0; + let activeTabWidth = 0; + let containerWidth = 0; + let containerScrollWidth = 0; + let activeLineWidth = 0; + let x = 0; + if (activeTab) { + activeTabLeft = activeTab.offsetLeft; + activeTabWidth = activeTab.offsetWidth; + containerWidth = container.offsetWidth; + containerScrollWidth = container.scrollWidth; + activeLineWidth = activeLine.offsetWidth; + x = activeTabLeft + (activeTabWidth - activeLineWidth) / 2; + } setLineData({ x, transitionInUse, }); const maxScrollDistance = containerScrollWidth - containerWidth; - if (maxScrollDistance <= 0) return; + if (maxScrollDistance <= 0 || !activeTab) return; const nextScrollLeft = bound( activeTabLeft - (containerWidth - activeTabWidth) / 2, @@ -88,8 +95,7 @@ const Tabs = React.forwardRef((props, ref) => { }; useEffect(() => { - const defaultIndex = safeValue(); - setActive(defaultIndex); + setActive(value); }, [value]); useLayoutEffect(() => { @@ -113,26 +119,6 @@ const Tabs = React.forwardRef((props, ref) => { animate({ transitionInUse: true }); }, [active, tabs, children]); - const safeValue = () => { - let defaultIndex = value; - const childs = React.Children.toArray(children); - const hasSameChild = - !!childs.length && - childs.some( - (child) => React.isValidElement(child) && child?.props?.index === value, - ); - if (!!tabs.length && !tabs.some((item) => item.index === value)) { - defaultIndex = tabs[0]?.index; - } else if (children && !hasSameChild) { - const childNode = childs[0]; - if (React.isValidElement(childNode)) { - defaultIndex = childNode.props.index; - } - } - - return defaultIndex; - }; - const updateMask = useMemo( () => throttle( @@ -178,8 +164,7 @@ const Tabs = React.forwardRef((props, ref) => { }; const providerValue = useMemo(() => { - const v = safeValue(); - return { value: v, align, triggerChange: handleClick }; + return { value, align, triggerChange: handleClick }; }, [value, align, children, handleClick]); return ( @@ -201,7 +186,7 @@ const Tabs = React.forwardRef((props, ref) => {
{ const originalModule = jest.requireActual('@bifrostui/utils'); @@ -11,6 +11,7 @@ describe('Tabs', () => { }; beforeEach(() => { + document.body.innerHTML = ''; jest.useFakeTimers(); }); @@ -353,4 +354,280 @@ describe('Tabs', () => { expect(activeTab).toHaveTextContent('蔬菜'); expect(activeTabPanel).toHaveTextContent('西红柿'); }); + + it('should no active Tab when value is invalid', () => { + function Component() { + const [value, setValue] = useState('2'); + const defultList = [ + { title: '长津湖', index: '1' }, + { title: '战狼2', index: '2' }, + { title: '你好,李焕英', index: '3' }, + { title: '哪吒之魔童降世', index: '4' }, + { title: '流浪地球', index: '5' }, + { title: '唐人街探案3', index: '6' }, + ]; + const [tabList, setTabList] = useState(defultList); + + const handleChange = (e, { index }) => { + setValue(index); + }; + + return ( + <> +
{ + setValue(''); + }} + > + 置为无效值 +
+
{ + if (tabList.length === 4) { + setTabList(defultList); + } else { + const newTabList = defultList.slice(0, 4); + setTabList(newTabList); + if (!newTabList.some((item) => item.index === value)) { + setValue('1'); + } + } + }} + > + {tabList.length === 4 ? '增加' : '减少'}TabList长度 +
+
+ + {tabList.map((item) => ( + + {item.title} + + ))} + + + {tabList.map((item) => ( + + {item.index} + + ))} +
+ + ); + } + + const { container, getByTestId } = render(); + + const testInvalidValueBtn = getByTestId('test-invalid-value'); + const [, , tab3] = container.querySelectorAll(`.bui-tab`); + const tabpanels = container.querySelectorAll(`.${rootClass.tabpanel}`); + + tabpanels.forEach((tabpanel, index) => { + if (index === 1) { + expect(tabpanel).toHaveTextContent('2'); + } else { + expect(tabpanel).toHaveTextContent(''); + } + }); + + fireEvent.click(testInvalidValueBtn); + tabpanels.forEach((tabpanel) => { + expect(tabpanel).toHaveTextContent(''); + }); + + fireEvent.click(tab3); + tabpanels.forEach((tabpanel, index) => { + if (index === 2) { + expect(tabpanel).toHaveTextContent('3'); + } else { + expect(tabpanel).toHaveTextContent(''); + } + }); + }); + + it('should no active Tab when value is invalid by use tabs', () => { + function Component() { + const [value, setValue] = useState('2'); + const defultList = [ + { title: '长津湖', index: '1' }, + { title: '战狼2', index: '2' }, + { title: '你好,李焕英', index: '3' }, + { title: '哪吒之魔童降世', index: '4' }, + { title: '流浪地球', index: '5' }, + { title: '唐人街探案3', index: '6' }, + ]; + const [tabList, setTabList] = useState(defultList); + + const handleChange = (e, { index }) => { + setValue(index); + }; + + return ( + <> +
{ + setValue(''); + }} + > + 置为无效值 +
+
{ + if (tabList.length === 4) { + setTabList(defultList); + } else { + const newTabList = defultList.slice(0, 4); + setTabList(newTabList); + if (!newTabList.some((item) => item.index === value)) { + setValue('1'); + } + } + }} + > + {tabList.length === 4 ? '增加' : '减少'}TabList长度 +
+
+ + + {tabList.map((item) => ( + + {item.index} + + ))} +
+ + ); + } + + const { container, getByTestId } = render(); + + const testInvalidValueBtn = getByTestId('test-invalid-value'); + const [, , tab3] = container.querySelectorAll(`.bui-tab`); + const tabpanels = container.querySelectorAll(`.${rootClass.tabpanel}`); + + tabpanels.forEach((tabpanel, index) => { + if (index === 1) { + expect(tabpanel).toHaveTextContent('2'); + } else { + expect(tabpanel).toHaveTextContent(''); + } + }); + + fireEvent.click(testInvalidValueBtn); + tabpanels.forEach((tabpanel) => { + expect(tabpanel).toHaveTextContent(''); + }); + + fireEvent.click(tab3); + tabpanels.forEach((tabpanel, index) => { + if (index === 2) { + expect(tabpanel).toHaveTextContent('3'); + } else { + expect(tabpanel).toHaveTextContent(''); + } + }); + }); + + it('should render correctly when TabList changed', async () => { + function Component() { + const [value, setValue] = useState('2'); + const defultList = [ + { title: '长津湖', index: '1' }, + { title: '战狼2', index: '2' }, + { title: '你好,李焕英', index: '3' }, + { title: '哪吒之魔童降世', index: '4' }, + { title: '流浪地球', index: '5' }, + { title: '唐人街探案3', index: '6' }, + ]; + const [tabList, setTabList] = useState(defultList); + + const handleChange = (e, { index }) => { + setValue(index); + }; + + return ( + <> +
{ + if (tabList.length === 4) { + setTabList(defultList); + } else { + const newTabList = defultList.slice(0, 4); + setTabList(newTabList); + if (!newTabList.some((item) => item.index === value)) { + setValue('1'); + } + } + }} + > + {tabList.length === 4 ? '增加' : '减少'}TabList长度 +
+
+ + {tabList.map((item) => ( + + {item.title} + + ))} + + + {tabList.map((item) => ( + + {item.index} + + ))} +
+ + ); + } + + const { container, getByTestId } = render(); + + const testModifyTablistBtn = getByTestId('test-modify-tablist2'); + const [tab1, , , , , tab6] = container.querySelectorAll(`.bui-tab`); + const tabpanels = container.querySelectorAll(`.${rootClass.tabpanel}`); + const [tabpanel1] = tabpanels; + + tabpanels.forEach((tabpanel, index) => { + if (index === 1) { + expect(tabpanel).toHaveTextContent('2'); + } else { + expect(tabpanel).toHaveTextContent(''); + } + }); + + fireEvent.click(tab6); + tabpanels.forEach((tabpanel, index) => { + if (index === 5) { + expect(tabpanel).toHaveTextContent('6'); + } else { + expect(tabpanel).toHaveTextContent(''); + } + }); + + fireEvent.click(testModifyTablistBtn); + expect(tab1).toHaveClass('bui-tab-active'); + expect(tabpanel1).toHaveTextContent('1'); + + fireEvent.click(testModifyTablistBtn); + expect(tab1).toHaveClass('bui-tab-active'); + expect(tabpanel1).toHaveTextContent('1'); + }); }); diff --git a/packages/bui-core/src/Tabs/__tests__/__snapshots__/Tabs.snapshot.test.tsx.snap b/packages/bui-core/src/Tabs/__tests__/__snapshots__/Tabs.snapshot.test.tsx.snap index c2c6def8..646dd6bd 100644 --- a/packages/bui-core/src/Tabs/__tests__/__snapshots__/Tabs.snapshot.test.tsx.snap +++ b/packages/bui-core/src/Tabs/__tests__/__snapshots__/Tabs.snapshot.test.tsx.snap @@ -42,7 +42,7 @@ exports[`Tabs snapshot Tabs demo snapshot 0 1`] = ` onScroll={[Function]} >
+ + +
+
+
+
+
+
+
+ 长津湖 +
+
+ 战狼2 +
+
+ 你好,李焕英 +
+
+ 哪吒之魔童降世 +
+
+ 流浪地球 +
+
+ 唐人街探案3 +
+
+
+
+
+ 2 +
+
+
+
+
+
+
+`; + +exports[`Tabs snapshot Tabs demo snapshot 0 4`] = ` +
+ + +
+
+
+
+
+
+
+ 长津湖 +
+
+ 战狼2 +
+
+ 你好,李焕英 +
+
+ 哪吒之魔童降世 +
+
+ 流浪地球 +
+
+ 唐人街探案3 +
+
+
+
+
+ 2 +
+
+
+
+
+
+
+`; + +exports[`Tabs snapshot Tabs demo snapshot 0 5`] = `
`; -exports[`Tabs snapshot Tabs demo snapshot 0 4`] = ` +exports[`Tabs snapshot Tabs demo snapshot 0 6`] = `
`; -exports[`Tabs snapshot Tabs demo snapshot 0 5`] = ` +exports[`Tabs snapshot Tabs demo snapshot 0 7`] = `
{ 使用 `tabs` 生成 Tab。 ```tsx -import { Stack, TabPanel, Tabs } from '@bifrostui/react'; +import { Stack, TabPanel, Tabs, Button } from '@bifrostui/react'; import React, { useState } from 'react'; export default () => { @@ -98,6 +98,147 @@ export default () => { }; ``` +### value值无效时不选中 + +value为无效值时不选中任何Tab。 + +```tsx +import { Stack, Tab, TabPanel, Tabs, Button } from '@bifrostui/react'; +import React, { useState } from 'react'; + +export default () => { + const [value, setValue] = useState('2'); + const defultList = [ + { title: '长津湖', index: '1' }, + { title: '战狼2', index: '2' }, + { title: '你好,李焕英', index: '3' }, + { title: '哪吒之魔童降世', index: '4' }, + { title: '流浪地球', index: '5' }, + { title: '唐人街探案3', index: '6' }, + ]; + const [tabList, setTabList] = useState(defultList); + + const handleChange = (e, { index }) => { + console.log(e, `Tab Change, value index is: ${index}`); + setValue(index); + }; + + return ( + + + +
+ + {tabList.map((item) => ( + + {item.title} + + ))} + + + {tabList.map((item) => ( + + {item.index} + + ))} +
+
+ ); +}; +``` + +### value值无效时不选中(使用tabs) + +value为无效值时不选中任何Tab。 + +```tsx +import { Stack, Tab, TabPanel, Tabs, Button } from '@bifrostui/react'; +import React, { useState } from 'react'; + +export default () => { + const [value, setValue] = useState('2'); + const defultList = [ + { title: '长津湖', index: '1' }, + { title: '战狼2', index: '2' }, + { title: '你好,李焕英', index: '3' }, + { title: '哪吒之魔童降世', index: '4' }, + { title: '流浪地球', index: '5' }, + { title: '唐人街探案3', index: '6' }, + ]; + const [tabList, setTabList] = useState(defultList); + + const handleChange = (e, { index }) => { + console.log(e, `Tab Change, value index is: ${index}`); + setValue(index); + }; + + return ( + + + +
+ + + {tabList.map((item) => ( + + {item.index} + + ))} +
+
+ ); +}; +``` + ### 禁用状态 通过 `disabled` 禁止 Tab 触发点击。 @@ -148,6 +289,8 @@ export default () => { ### 受控 tabs 组件 +可通过 `value` 属性控制Tabs组件的选中态。 + ```tsx import { Button, Stack, Tab, TabPanel, Tabs } from '@bifrostui/react'; import React, { useState } from 'react'; @@ -240,6 +383,8 @@ export default () => { ### 超出可滑动 +当Tab过多时,超出可滑动。 + ```tsx import { Stack, Tab, TabPanel, Tabs } from '@bifrostui/react'; import React, { useState } from 'react'; diff --git a/packages/bui-core/src/Toast/index.en-US.md b/packages/bui-core/src/Toast/index.en-US.md index 2be465f2..9e07cce6 100644 --- a/packages/bui-core/src/Toast/index.en-US.md +++ b/packages/bui-core/src/Toast/index.en-US.md @@ -574,12 +574,12 @@ export default () => { | Method Name | explain | parameter | Return value | | ------------- | ----------------- | --------------------- | --------------- | -| Taost | Display Tips | ToastOptions \|string | ToastReturnType | -| Taost.warning | Warning prompt | ToastOptions \|string | ToastReturnType | -| Taost.loading | Loading prompt | ToastOptions \|string | ToastReturnType | -| Taost.success | Successful prompt | ToastOptions \|string | ToastReturnType | -| Taost.fail | Failure prompt | ToastOptions \|string | ToastReturnType | -| Taost.clear | Clear prompt | - | - | +| Toast | Display Tips | ToastOptions \|string | ToastReturnType | +| Toast.warning | Warning prompt | ToastOptions \|string | ToastReturnType | +| Toast.loading | Loading prompt | ToastOptions \|string | ToastReturnType | +| Toast.success | Successful prompt | ToastOptions \|string | ToastReturnType | +| Toast.fail | Failure prompt | ToastOptions \|string | ToastReturnType | +| Toast.clear | Clear prompt | - | - | ##### ToastReturnType diff --git a/packages/bui-core/src/Toast/index.zh-CN.md b/packages/bui-core/src/Toast/index.zh-CN.md index f5e4dbdc..87766b72 100644 --- a/packages/bui-core/src/Toast/index.zh-CN.md +++ b/packages/bui-core/src/Toast/index.zh-CN.md @@ -272,7 +272,7 @@ export default () => { ### 同时存在多个Toast -使用`allowMultiple`可允许页面中同时存在多个Taost提示,默认每次只展示一个Toast。 +使用`allowMultiple`可允许页面中同时存在多个Toast提示,默认每次只展示一个Toast。 ```tsx import { @@ -574,12 +574,12 @@ export default () => { | 方法名 | 说明 | 参数 | 返回值 | | ------------- | -------- | ---------------------- | --------------- | -| Taost | 展示提示 | ToastOptions \| string | ToastReturnType | -| Taost.warning | 警告提示 | ToastOptions \| string | ToastReturnType | -| Taost.loading | 加载提示 | ToastOptions \| string | ToastReturnType | -| Taost.success | 成功提示 | ToastOptions \| string | ToastReturnType | -| Taost.fail | 失败提示 | ToastOptions \| string | ToastReturnType | -| Taost.clear | 清空提示 | - | - | +| Toast | 展示提示 | ToastOptions \| string | ToastReturnType | +| Toast.warning | 警告提示 | ToastOptions \| string | ToastReturnType | +| Toast.loading | 加载提示 | ToastOptions \| string | ToastReturnType | +| Toast.success | 成功提示 | ToastOptions \| string | ToastReturnType | +| Toast.fail | 失败提示 | ToastOptions \| string | ToastReturnType | +| Toast.clear | 清空提示 | - | - | ##### ToastReturnType diff --git a/packages/bui-core/src/Tooltip/Tooltip.tsx b/packages/bui-core/src/Tooltip/Tooltip.tsx index cad12bdb..a354b883 100644 --- a/packages/bui-core/src/Tooltip/Tooltip.tsx +++ b/packages/bui-core/src/Tooltip/Tooltip.tsx @@ -3,6 +3,7 @@ import React, { useState, useRef, useEffect } from 'react'; import { getStylesAndLocation, triggerEventTransform, + parsePlacement, useUniqueId, throttle, } from '@bifrostui/utils'; @@ -19,6 +20,7 @@ const Tooltip = React.forwardRef((props, ref) => { children, title, defaultOpen, + offsetSpacing = 0, placement = 'top', trigger = 'click', onOpenChange, @@ -27,16 +29,8 @@ const Tooltip = React.forwardRef((props, ref) => { } = props; const controlByUser = typeof open !== 'undefined'; - const positionArr = placement.split(/([A-Z])/); - const direction = positionArr[0]; - let location; - if (positionArr.length > 1) { - positionArr.splice(0, 1); - location = positionArr.join('').toLowerCase(); - } else { - location = 'center'; - } + const { direction, location = 'center' } = parsePlacement(placement); const childrenRef = useRef(); const [openStatus, setOpenStatus] = useState(defaultOpen); // 气泡所在位置 @@ -80,10 +74,15 @@ const Tooltip = React.forwardRef((props, ref) => { }; const onRootElementMouted = throttle(() => { + const { + direction: newParsedDirection, + location: newParsedLocation = 'center', + } = parsePlacement(placement); const result = getStylesAndLocation({ childrenRef, - arrowDirection, - arrowLocation, + arrowDirection: newParsedDirection, + arrowLocation: newParsedLocation, + offsetSpacing, selector: `[data-id="tt_${ttId}"]`, }); if (!result) return; diff --git a/packages/bui-core/src/Tooltip/Tooltip.types.ts b/packages/bui-core/src/Tooltip/Tooltip.types.ts index 1a924e2c..0e45dbfe 100644 --- a/packages/bui-core/src/Tooltip/Tooltip.types.ts +++ b/packages/bui-core/src/Tooltip/Tooltip.types.ts @@ -22,6 +22,10 @@ export type TooltipProps< * 用于手动控制气泡浮层显隐 */ open?: boolean; + /** + * 用于控制气泡浮层和目标元素偏移量 + */ + offsetSpacing?: number; /** * 气泡框位置 * @default 'top' diff --git a/packages/bui-core/src/Tooltip/index.en-US.md b/packages/bui-core/src/Tooltip/index.en-US.md index c7778344..52cf8888 100644 --- a/packages/bui-core/src/Tooltip/index.en-US.md +++ b/packages/bui-core/src/Tooltip/index.en-US.md @@ -61,6 +61,23 @@ export default () => { }; ``` +### OffsetSpacing is the interval between the floating layer and the target element + +OffsetSpacing can be set to control the distance from the target element + +```tsx +import { Tooltip } from '@bifrostui/react'; +import React from 'react'; + +export default () => { + return ( + + offsetSpacing控制目标间隔(设置20 以便观察) + + ); +}; +``` + ### Placement Bubble Box Position placement设置气泡浮层的位置,可选 top left right bottom topLeft topRight bottomLeft bottomRight leftTop leftBottom rightTop rightBottom @@ -184,7 +201,7 @@ import React from 'react'; export default () => { return ( - + hover触发方式 ); @@ -215,14 +232,15 @@ export default () => { ### API -| attribute | explain | type | Default value | -| ------------ | -------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------- | -| title | Bubble floating layer content | string | - | -| defaultOpen | Whether to hide by default | boolean | false | -| open | Used for manually controlling the appearance and concealment of bubble floating layers | boolean | - | -| placement | Bubble box position | String, the enumeration value is `center` `left` `leftTop` `leftBottom` `right` `rightTop` `rightBottom` `top` `topLeft` `topRight` `bottom` `bottomLeft` `bottomRight` `bottom` | 'top' | -| trigger | Trigger behavior | string \|String [], the enumeration value is' click '\|'hover' | 'click' | -| onOpenChange | The callback method for bubble floating layer manifestation and concealment | (e: React.MouseEvent,data: {open: boolean}) => void | - | +| attribute | explain | type | Default value | +| ------------- | -------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------- | +| title | Bubble floating layer content | string | - | +| defaultOpen | Whether to hide by default | boolean | false | +| open | Used for manually controlling the appearance and concealment of bubble floating layers | boolean | - | +| offsetSpacing | The offset between the floating layer and the target element | number | 0 | +| placement | Bubble box position | string, The enumeration value is `center` `left` `leftTop` `leftBottom` `right` `rightTop` `rightBottom` `top` `topLeft` `topRight` `bottom` `bottomLeft` `bottomRight` `bottom` | 'top' | +| trigger | Trigger behavior | string \|string[], The enumeration value is' click '\|'hover' | 'click' | +| onOpenChange | The callback method for bubble floating layer manifestation and concealment | (e: React.MouseEvent,data: {open: boolean}) => void | - | ### Style variables diff --git a/packages/bui-core/src/Tooltip/index.zh-CN.md b/packages/bui-core/src/Tooltip/index.zh-CN.md index 9af87b19..d83c4f87 100644 --- a/packages/bui-core/src/Tooltip/index.zh-CN.md +++ b/packages/bui-core/src/Tooltip/index.zh-CN.md @@ -61,6 +61,23 @@ export default () => { }; ``` +### offsetSpacing 浮层和目标元素间隔 + +可以设置offsetSpacing来控制和目标元素的距离 + +```tsx +import { Tooltip } from '@bifrostui/react'; +import React from 'react'; + +export default () => { + return ( + + offsetSpacing控制目标间隔(设置20 以便观察) + + ); +}; +``` + ### placement 气泡框位置 placement设置气泡浮层的位置,可选 top left right bottom topLeft topRight bottomLeft bottomRight leftTop leftBottom rightTop rightBottom @@ -215,14 +232,15 @@ export default () => { ### API -| 属性 | 说明 | 类型 | 默认值 | -| ------------ | ------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------- | -| title | 气泡浮层内容 | string | - | -| defaultOpen | 默认是否显隐 | boolean | false | -| open | 用于手动控制气泡浮层显隐 | boolean | - | -| placement | 气泡框位置 | string,枚举值是 `center` `left` `leftTop` `leftBottom` `right` `rightTop` `rightBottom` `top` `topLeft` `topRight` `bottom` `bottomLeft` `bottomRight` `bottom` | 'top' | -| trigger | 触发行为 | string \| string[],枚举值是 'click' \| 'hover' | 'click' | -| onOpenChange | 气泡浮层显隐的回调方法 | (e: React.MouseEvent,data: {open: boolean}) => void | - | +| 属性 | 说明 | 类型 | 默认值 | +| ------------- | ------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------- | +| title | 气泡浮层内容 | string | - | +| defaultOpen | 默认是否显隐 | boolean | false | +| open | 用于手动控制气泡浮层显隐 | boolean | - | +| offsetSpacing | 浮层与目标元素的偏移量 | number | 0 | +| placement | 气泡框位置 | string,枚举值是 `center` `left` `leftTop` `leftBottom` `right` `rightTop` `rightBottom` `top` `topLeft` `topRight` `bottom` `bottomLeft` `bottomRight` `bottom` | 'top' | +| trigger | 触发行为 | string \| string[],枚举值是 'click' \| 'hover' | 'click' | +| onOpenChange | 气泡浮层显隐的回调方法 | (e: React.MouseEvent,data: {open: boolean}) => void | - | ### 样式变量 diff --git a/packages/bui-icons/package.json b/packages/bui-icons/package.json index 5912a6be..80700d1b 100644 --- a/packages/bui-icons/package.json +++ b/packages/bui-icons/package.json @@ -1,6 +1,6 @@ { "name": "@bifrostui/icons", - "version": "1.4.2", + "version": "1.4.3-beta.0", "description": "SVG icon components for BUI", "homepage": "http://bui.taopiaopiao.com", "license": "MIT", diff --git a/packages/bui-styles/package.json b/packages/bui-styles/package.json index ba4bf2f6..c9e1df18 100644 --- a/packages/bui-styles/package.json +++ b/packages/bui-styles/package.json @@ -1,6 +1,7 @@ { "name": "@bifrostui/styles", - "version": "1.4.2", + "version": "1.4.3-beta.0", + "main": "dist/index.css", "description": "Common style definitions for BUI React components", "homepage": "http://bui.taopiaopiao.com", "license": "MIT", diff --git a/packages/bui-types/package.json b/packages/bui-types/package.json index 626e47df..60933469 100644 --- a/packages/bui-types/package.json +++ b/packages/bui-types/package.json @@ -1,6 +1,6 @@ { "name": "@bifrostui/types", - "version": "1.4.2", + "version": "1.4.3-beta.0", "description": "Utility types for BUI", "typings": "src/index.ts", "files": [ diff --git a/packages/bui-utils/package.json b/packages/bui-utils/package.json index f8a107fa..41426957 100644 --- a/packages/bui-utils/package.json +++ b/packages/bui-utils/package.json @@ -1,6 +1,6 @@ { "name": "@bifrostui/utils", - "version": "1.4.2", + "version": "1.4.3-beta.0", "description": "BUI React utilities for building components.", "main": "src/index.ts", "module": "src/index.ts", diff --git a/packages/bui-utils/src/directionLocationUtil.ts b/packages/bui-utils/src/directionLocationUtil.ts index bc5f7047..c28ca3c0 100644 --- a/packages/bui-utils/src/directionLocationUtil.ts +++ b/packages/bui-utils/src/directionLocationUtil.ts @@ -5,21 +5,46 @@ const directionCssMap = { bottom: 'top', }; +const isBodyScroll = (scrollRoot) => { + return scrollRoot === document.body; +}; + /** * 根据元素宽高判断是否超出边界,超出边界则重新定义方向 */ export const getNewDirectionLocation = ({ - rootOffset, + scrollRoot, + scrollRootOffset, + childrenOffset, arrowDirection, tipOffset, arrowLocation, + offsetSpacing, }) => { - const { left: cLeft, right: cRight, top: cTop, bottom: cBottom } = rootOffset; + const { + left: cLeft, + right: cRight, + top: cTop, + bottom: cBottom, + width: cWidth, + height: cHeight, + } = childrenOffset; const { width, height } = tipOffset; - const pgegWidth = + const { + top: sTop, + bottom: sBottom, + left: sLeft, + right: sRight, + } = scrollRootOffset; + + const pageWidth = document.documentElement.clientWidth || document.body.clientWidth; - const pgegHeight = + const pageHeight = document.documentElement.clientHeight || document.body.clientHeight; + const maxTop = isBodyScroll(scrollRoot) ? 0 : sTop; + const maxBottom = isBodyScroll(scrollRoot) ? pageHeight : sBottom; + const maxLeft = isBodyScroll(scrollRoot) ? 0 : sLeft; + const maxRight = isBodyScroll(scrollRoot) ? pageWidth : sRight; let newArrowDirection = arrowDirection; let newArrowLocation = arrowLocation; @@ -31,20 +56,21 @@ export const getNewDirectionLocation = ({ const isDirectionRight = arrowDirection === 'right'; if ( - (isDirectionTop && cTop - height < 0) || - (isDirectionBottom && cBottom + height > pgegHeight) || - (isDirectionLeft && cLeft - width < 0) || - (isDirectionRight && cRight + width > pgegWidth) + (isDirectionTop && cTop - height - offsetSpacing < maxTop) || + (isDirectionBottom && cBottom + height + offsetSpacing > maxBottom) || + (isDirectionLeft && cLeft - width - offsetSpacing < maxLeft) || + (isDirectionRight && cRight + width + offsetSpacing > maxRight) ) { + // 计算气泡超过编辑之后 到反方向去 newArrowDirection = directionCssMap[arrowDirection]; } // 箭头靠边的情况,是否超过边界 if ( - (arrowLocation === 'top' && cTop + height > pgegHeight) || - (arrowLocation === 'bottom' && cBottom - height < 0) || - (arrowLocation === 'left' && cLeft + width > pgegWidth) || - (arrowLocation === 'right' && cRight - width < 0) + (arrowLocation === 'top' && cTop + height > maxBottom) || + (arrowLocation === 'bottom' && cBottom - height < maxTop) || + (arrowLocation === 'left' && cLeft + width > maxRight) || + (arrowLocation === 'right' && cRight - width < maxLeft) ) { newArrowLocation = directionCssMap[arrowLocation]; } @@ -52,19 +78,25 @@ export const getNewDirectionLocation = ({ const isCenter = arrowLocation === 'center'; // 箭头在中间的情况,是否超过边界 if (isCenter && (isDirectionTop || isDirectionBottom)) { - if (cLeft + width > pgegWidth) { + // cLeft + (cWidth - width) / 2 代表浮层最左侧的坐标 + if (cLeft + (cWidth - width) / 2 + width > maxRight) { newArrowLocation = directionCssMap.left; - } else if (cRight - width < 0) { + } else if (cLeft + (cWidth - width) / 2 < maxLeft) { newArrowLocation = directionCssMap.right; } } else if (isCenter && (isDirectionLeft || isDirectionRight)) { - if (cTop + height > pgegHeight) { + // cTop + (cHeight - height) / 2 代表浮层最上侧的坐标 + if (cTop + (cHeight - height) / 2 + cHeight > maxBottom) { newArrowLocation = directionCssMap.top; - } else if (cBottom - height < 0) { + } else if (cTop + (cHeight - height) / 2 < maxTop) { newArrowLocation = directionCssMap.bottom; } } + if (arrowLocation === 'none') { + newArrowLocation = 'none'; + } + return { newArrowDirection, newArrowLocation, @@ -75,17 +107,18 @@ export const getNewDirectionLocation = ({ * 根据新的气泡位置和箭头位置 计算气泡位置以及箭头位置 */ export const getDirectionLocationStyle = ({ - rootOffset, + childrenOffset, arrowDirection, tipOffset, arrowLocation, + offsetSpacing, }) => { const scrollTop = (window.scrollY >= 0 && window.scrollY) || document.documentElement.scrollTop; const scrollLeft = - (window.screenX >= 0 && window.screenX) || + (window.scrollX >= 0 && window.scrollX) || document.documentElement.scrollLeft; const styles: any = {}; @@ -96,11 +129,15 @@ export const getDirectionLocationStyle = ({ right: cRight, top: cTop, bottom: cBottom, - } = rootOffset; + } = childrenOffset; + let childrenStyle: any = {}; + if (cWidth && cHeight) { + childrenStyle = { width: `${cWidth}px`, height: `${cHeight}px` }; + } const { width, height } = tipOffset; if (arrowDirection === 'top') { - styles.top = cTop; - styles.transform = `translateY(-100%)`; + // 浮层在上方 + styles.top = cTop - offsetSpacing - height; switch (arrowLocation) { case 'left': styles.left = cLeft; @@ -109,14 +146,17 @@ export const getDirectionLocationStyle = ({ styles.left = cLeft + (cWidth - width) / 2; break; case 'right': - styles.left = cRight; - styles.transform = `translate(-100%, -100%)`; + styles.left = cRight - width; + break; + case 'none': + styles.left = cLeft; break; default: break; } } else if (arrowDirection === 'bottom') { - styles.top = cBottom; + // 浮层在下方 + styles.top = cBottom + offsetSpacing; switch (arrowLocation) { case 'left': styles.left = cLeft; @@ -125,15 +165,17 @@ export const getDirectionLocationStyle = ({ styles.left = cLeft + (cWidth - width) / 2; break; case 'right': - styles.left = cRight; - styles.transform = `translateX(-100%)`; + styles.left = cRight - width; + break; + case 'none': + styles.left = cLeft; break; default: break; } } else if (arrowDirection === 'left') { - styles.left = cLeft; - styles.transform = `translateX(-100%)`; + // 浮层在左方 + styles.left = cLeft - offsetSpacing - width; switch (arrowLocation) { case 'top': styles.top = cTop; @@ -142,14 +184,17 @@ export const getDirectionLocationStyle = ({ styles.top = cTop + (cHeight - height) / 2; break; case 'bottom': - styles.top = cBottom; - styles.transform = `translate(-100%, -100%)`; + styles.top = cBottom - height; + break; + case 'none': + styles.top = cTop; break; default: break; } } else if (arrowDirection === 'right') { - styles.left = cRight; + // 浮层在右方 + styles.left = cRight + offsetSpacing; switch (arrowLocation) { case 'top': styles.top = cTop; @@ -158,8 +203,10 @@ export const getDirectionLocationStyle = ({ styles.top = cTop + (cHeight - height) / 2; break; case 'bottom': - styles.top = cBottom; - styles.transform = `translateY(-100%)`; + styles.top = cBottom - height; + break; + case 'none': + styles.top = cTop; break; default: break; @@ -171,16 +218,18 @@ export const getDirectionLocationStyle = ({ if (styles.left) { styles.left = `${styles.left + scrollLeft}px`; } - return styles; + return { styles, childrenStyle }; }; /** * 获取气泡位置和箭头位置 */ export const getStylesAndLocation = ({ + scrollRoot = document.body as Element, childrenRef, arrowDirection, arrowLocation, + offsetSpacing, selector, }) => { if (!childrenRef?.current) { @@ -189,27 +238,33 @@ export const getStylesAndLocation = ({ ); return null; } - const rootOffset = childrenRef.current.getBoundingClientRect(); + const childrenOffset = childrenRef.current.getBoundingClientRect(); const $rtDom = document.querySelector(selector); if (!$rtDom) return null; const tipOffset = $rtDom.getBoundingClientRect(); + const scrollRootOffset = scrollRoot.getBoundingClientRect(); const { newArrowDirection, newArrowLocation } = getNewDirectionLocation({ - rootOffset, + scrollRoot, + scrollRootOffset, + childrenOffset, arrowDirection, tipOffset, arrowLocation, + offsetSpacing, }); - const styles = getDirectionLocationStyle({ - rootOffset, + const { styles, childrenStyle } = getDirectionLocationStyle({ + childrenOffset, arrowDirection: newArrowDirection, tipOffset, arrowLocation: newArrowLocation, + offsetSpacing, }); styles.visibility = 'visible'; return { styles, + childrenStyle, newArrowDirection, newArrowLocation, }; @@ -251,3 +306,21 @@ export const triggerEventTransform = ({ trigger, click, show, hide }) => { return option; }; +/** + * for example: placement = 'topLeft', return { direction: 'top', location: 'left' } + * @param placement + * @returns + */ +export const parsePlacement = (placement) => { + const positionArr = placement.split(/([A-Z])/); + const direction = positionArr[0]; + let location; + if (positionArr.length > 1) { + positionArr.splice(0, 1); + location = positionArr.join('').toLowerCase(); + } + return { + direction, + location, + }; +}; diff --git a/packages/bui-utils/src/index.ts b/packages/bui-utils/src/index.ts index 061d66a9..262b34b1 100644 --- a/packages/bui-utils/src/index.ts +++ b/packages/bui-utils/src/index.ts @@ -2,6 +2,7 @@ export { default as debounce } from './debounce'; export { getStylesAndLocation, triggerEventTransform, + parsePlacement, } from './directionLocationUtil'; export { default as convertHexToRGBA } from './hex2rgba'; export {