diff --git a/packages/site/src/app/App.tsx b/packages/site/src/app/App.tsx index 06a6f694..9bfe0164 100644 --- a/packages/site/src/app/App.tsx +++ b/packages/site/src/app/App.tsx @@ -71,7 +71,7 @@ export function App() { const rootContext = useMemo( () => ({ i18n: { lang: i18n.language as DLang }, - updatePosition: { scroll: ['main.app-main'], resize: ['article.app-route-article'] }, + layout: { scrollEl: 'main.app-main', resizeEl: 'article.app-route-article' }, }), [i18n.language] ); diff --git a/packages/site/src/app/components/layout/sidebar/Sidebar.tsx b/packages/site/src/app/components/layout/sidebar/Sidebar.tsx index 3138fca7..bfb37bcd 100644 --- a/packages/site/src/app/components/layout/sidebar/Sidebar.tsx +++ b/packages/site/src/app/components/layout/sidebar/Sidebar.tsx @@ -43,29 +43,19 @@ export function AppSidebar(props: { aMenuOpen: boolean; onMenuOpenChange: (open: id: group.title, title: t(`menu-group.${group.title}`), type: 'group', - children: - group.title === 'Other' - ? [ - { - id: 'Interface', - title: ( - - Interface{i18n.language !== 'en-US' && {t(`Documentation.Interface`)}} - - ), - type: 'item', - }, - ] - : group.children.map((child) => ({ - id: child.title, - title: ( - - {child.title} - {i18n.language !== 'en-US' && {t(`menu.${child.title}`)}} - - ), - type: 'item', - })), + children: (group.title === 'Other' + ? group.children.concat([{ title: 'Interface', to: '/components/Interface' }]) + : group.children + ).map((child) => ({ + id: child.title, + title: ( + + {child.title} + {i18n.language !== 'en-US' && {t(`menu.${child.title}`)}} + + ), + type: 'item', + })), }))} dActive={activeId} onActiveChange={(id) => { diff --git a/packages/ui/src/components/_date-input/DateInput.tsx b/packages/ui/src/components/_date-input/DateInput.tsx index b1f82f3e..25889f5d 100644 --- a/packages/ui/src/components/_date-input/DateInput.tsx +++ b/packages/ui/src/components/_date-input/DateInput.tsx @@ -4,16 +4,7 @@ import type { DFormControl } from '../form'; import React, { useEffect, useId, useImperativeHandle, useRef, useState } from 'react'; import ReactDOM from 'react-dom'; -import { - usePrefixConfig, - useTranslation, - useAsync, - useForkRef, - useEventCallback, - useMaxIndex, - useElement, - useUpdatePosition, -} from '../../hooks'; +import { usePrefixConfig, useTranslation, useAsync, useForkRef, useEventCallback, useMaxIndex, useElement, useLayout } from '../../hooks'; import { CloseCircleFilled, SwapRightOutlined } from '../../icons'; import { checkNodeExist, getClassName, getNoTransformSize, getVerticalSidePosition } from '../../utils'; import { TTANSITION_DURING_POPUP } from '../../utils/global'; @@ -75,6 +66,7 @@ function DateInput(props: DDateInputProps, ref: React.ForwardedRef { let el = document.getElementById(`${prefix}-root`); if (!el) { @@ -145,8 +139,6 @@ function DateInput(props: DDateInputProps, ref: React.ForwardedRef { if (dVisible) { const [asyncGroup, asyncId] = asyncCapture.createGroup(); @@ -169,6 +161,36 @@ function DateInput(props: DDateInputProps, ref: React.ForwardedRef { + if (dVisible && scrollEl) { + const [asyncGroup, asyncId] = asyncCapture.createGroup(); + + asyncGroup.fromEvent(scrollEl, 'scroll', { passive: true }).subscribe({ + next: () => { + updatePosition(); + }, + }); + + return () => { + asyncCapture.deleteGroup(asyncId); + }; + } + }, [asyncCapture, dVisible, scrollEl, updatePosition]); + + useEffect(() => { + if (dVisible && resizeEl) { + const [asyncGroup, asyncId] = asyncCapture.createGroup(); + + asyncGroup.onResize(resizeEl, () => { + updatePosition(); + }); + + return () => { + asyncCapture.deleteGroup(asyncId); + }; + } + }, [asyncCapture, dVisible, resizeEl, updatePosition]); + const preventBlur: React.MouseEventHandler = (e) => { if (e.target !== inputRefLeft.current && e.target !== inputRefRight.current && e.button === 0) { e.preventDefault(); diff --git a/packages/ui/src/components/_popup/Popup.tsx b/packages/ui/src/components/_popup/Popup.tsx index 579e7868..67aab8fa 100644 --- a/packages/ui/src/components/_popup/Popup.tsx +++ b/packages/ui/src/components/_popup/Popup.tsx @@ -3,7 +3,7 @@ import { useEffect, useId, useRef } from 'react'; import ReactDOM from 'react-dom'; import { filter } from 'rxjs'; -import { useAsync, useElement, useEventCallback, usePrefixConfig, useUpdatePosition } from '../../hooks'; +import { useAsync, useElement, useEventCallback, useLayout, usePrefixConfig } from '../../hooks'; export interface DPopupPopupRenderProps { 'data-popup-popupid': string; @@ -50,6 +50,7 @@ export function DPopup(props: DPopupProps): JSX.Element | null { //#region Context const dPrefix = usePrefixConfig(); + const { dScrollEl, dResizeEl } = useLayout(); //#endregion const dataRef = useRef<{ @@ -60,19 +61,21 @@ export function DPopup(props: DPopupProps): JSX.Element | null { const uniqueId = useId(); - const containerEl = useElement( - isUndefined(dContainer) - ? () => { - let el = document.getElementById(`${dPrefix}popup-root`); - if (!el) { - el = document.createElement('div'); - el.id = `${dPrefix}popup-root`; - document.body.appendChild(el); - } - return el; - } - : dContainer - ); + const scrollEl = useElement(dScrollEl); + const resizeEl = useElement(dResizeEl); + const containerEl = useElement(() => { + if (isUndefined(dContainer)) { + let el = document.getElementById(`${dPrefix}popup-root`); + if (!el) { + el = document.createElement('div'); + el.id = `${dPrefix}popup-root`; + document.body.appendChild(el); + } + return el; + } + + return dContainer; + }); const changeVisible = (visible?: boolean) => { if (isUndefined(visible)) { @@ -127,7 +130,7 @@ export function DPopup(props: DPopupProps): JSX.Element | null { asyncCapture.deleteGroup(asyncId); }; } - }, [asyncCapture, handleTrigger, dTrigger, dVisible, dDisabled]); + }, [asyncCapture, dDisabled, dTrigger, dVisible, handleTrigger]); useEffect(() => { if (!dDisabled && dVisible && dEscClosable) { @@ -146,11 +149,7 @@ export function DPopup(props: DPopupProps): JSX.Element | null { asyncCapture.deleteGroup(asyncId); }; } - }, [asyncCapture, handleTrigger, dEscClosable, dVisible, dDisabled]); - - useUpdatePosition(() => { - dUpdatePosition?.(); - }, !dDisabled && dVisible); + }, [asyncCapture, dDisabled, dEscClosable, dVisible, handleTrigger]); useEffect(() => { if (!dDisabled && dVisible) { @@ -174,8 +173,7 @@ export function DPopup(props: DPopupProps): JSX.Element | null { asyncCapture.deleteGroup(asyncId); }; } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [asyncCapture, dVisible, uniqueId, dDisabled]); + }, [asyncCapture, dDisabled, dUpdatePosition, dVisible, uniqueId]); useEffect(() => { if (!dDisabled && dVisible && dContainer) { @@ -195,8 +193,37 @@ export function DPopup(props: DPopupProps): JSX.Element | null { asyncCapture.deleteGroup(asyncId); }; } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [asyncCapture, dContainer, dDisabled, dVisible]); + }, [asyncCapture, dContainer, dDisabled, dUpdatePosition, dVisible]); + + useEffect(() => { + if (!dDisabled && dVisible && scrollEl) { + const [asyncGroup, asyncId] = asyncCapture.createGroup(); + + asyncGroup.fromEvent(scrollEl, 'scroll', { passive: true }).subscribe({ + next: () => { + dUpdatePosition?.(); + }, + }); + + return () => { + asyncCapture.deleteGroup(asyncId); + }; + } + }, [asyncCapture, dDisabled, dUpdatePosition, dVisible, scrollEl]); + + useEffect(() => { + if (!dDisabled && dVisible && resizeEl) { + const [asyncGroup, asyncId] = asyncCapture.createGroup(); + + asyncGroup.onResize(resizeEl, () => { + dUpdatePosition?.(); + }); + + return () => { + asyncCapture.deleteGroup(asyncId); + }; + } + }, [asyncCapture, dDisabled, dUpdatePosition, dVisible, resizeEl]); const childProps: DPopupRenderProps = { 'data-popup-triggerid': uniqueId }; if (!dDisabled) { diff --git a/packages/ui/src/components/_selectbox/Selectbox.tsx b/packages/ui/src/components/_selectbox/Selectbox.tsx index 4382d976..79678be3 100644 --- a/packages/ui/src/components/_selectbox/Selectbox.tsx +++ b/packages/ui/src/components/_selectbox/Selectbox.tsx @@ -5,16 +5,7 @@ import React, { useImperativeHandle, useRef, useState } from 'react'; import { useEffect } from 'react'; import ReactDOM from 'react-dom'; -import { - useAsync, - usePrefixConfig, - useTranslation, - useEventCallback, - useElement, - useMaxIndex, - useUpdatePosition, - useForkRef, -} from '../../hooks'; +import { useAsync, usePrefixConfig, useTranslation, useEventCallback, useElement, useMaxIndex, useForkRef, useLayout } from '../../hooks'; import { CloseCircleFilled, DownOutlined, LoadingOutlined, SearchOutlined } from '../../icons'; import { checkNodeExist, getClassName } from '../../utils'; import { TTANSITION_DURING_POPUP } from '../../utils/global'; @@ -87,6 +78,7 @@ function Selectbox(props: DSelectboxProps, ref: React.ForwardedRef { let el = document.getElementById(`${prefix}-root`); if (!el) { @@ -134,8 +128,6 @@ function Selectbox(props: DSelectboxProps, ref: React.ForwardedRef { if (dVisible) { const [asyncGroup, asyncId] = asyncCapture.createGroup(); @@ -158,6 +150,36 @@ function Selectbox(props: DSelectboxProps, ref: React.ForwardedRef { + if (dVisible && scrollEl) { + const [asyncGroup, asyncId] = asyncCapture.createGroup(); + + asyncGroup.fromEvent(scrollEl, 'scroll', { passive: true }).subscribe({ + next: () => { + updatePosition(); + }, + }); + + return () => { + asyncCapture.deleteGroup(asyncId); + }; + } + }, [asyncCapture, dVisible, scrollEl, updatePosition]); + + useEffect(() => { + if (dVisible && resizeEl) { + const [asyncGroup, asyncId] = asyncCapture.createGroup(); + + asyncGroup.onResize(resizeEl, () => { + updatePosition(); + }); + + return () => { + asyncCapture.deleteGroup(asyncId); + }; + } + }, [asyncCapture, dVisible, resizeEl, updatePosition]); + const preventBlur: React.MouseEventHandler = (e) => { if (e.target !== inputRef.current && e.button === 0) { e.preventDefault(); diff --git a/packages/ui/src/components/affix/Affix.tsx b/packages/ui/src/components/affix/Affix.tsx index 7176d3e6..4373b3ea 100644 --- a/packages/ui/src/components/affix/Affix.tsx +++ b/packages/ui/src/components/affix/Affix.tsx @@ -10,7 +10,7 @@ import { useElement, useIsomorphicLayoutEffect, useEventCallback, - useUpdatePosition, + useLayout, } from '../../hooks'; import { getClassName, registerComponentMate, toPx } from '../../utils'; @@ -40,6 +40,7 @@ function Affix(props: DAffixProps, ref: React.ForwardedRef): JSX.Elem //#region Context const dPrefix = usePrefixConfig(); + const { dScrollEl, dResizeEl } = useLayout(); //#endregion //#region Ref @@ -49,22 +50,23 @@ function Affix(props: DAffixProps, ref: React.ForwardedRef): JSX.Elem const asyncCapture = useAsync(); - const targetEl = useElement(dTarget ?? null); + const targetEl = useElement(dTarget ?? dScrollEl); + const resizeEl = useElement(dResizeEl); const [fixedStyle, setFixedStyle] = useState(); const [referenceStyle, setReferenceStyle] = useState(); const [fixed, setFixed] = useState(false); const updatePosition = useEventCallback(() => { - if (isUndefined(dTarget) || targetEl) { + if (targetEl) { const offsetEl = fixed ? referenceRef.current : affixRef.current; if (offsetEl) { - let targetRect = { - top: 0, - bottom: window.innerHeight, - }; - if (targetEl) { - targetRect = targetEl.getBoundingClientRect(); + let targetRect: { top: number; bottom: number } = targetEl.getBoundingClientRect(); + if (targetEl === document.documentElement) { + targetRect = { + top: 0, + bottom: window.innerHeight, + }; } const offsetRect = offsetEl.getBoundingClientRect(); @@ -101,8 +103,6 @@ function Affix(props: DAffixProps, ref: React.ForwardedRef): JSX.Elem // eslint-disable-next-line react-hooks/exhaustive-deps }, []); - useUpdatePosition(updatePosition); - useEffect(() => { if (targetEl) { const [asyncGroup, asyncId] = asyncCapture.createGroup(); @@ -119,6 +119,20 @@ function Affix(props: DAffixProps, ref: React.ForwardedRef): JSX.Elem } }, [asyncCapture, targetEl, updatePosition]); + useEffect(() => { + if (resizeEl) { + const [asyncGroup, asyncId] = asyncCapture.createGroup(); + + asyncGroup.onResize(resizeEl, () => { + updatePosition(); + }); + + return () => { + asyncCapture.deleteGroup(asyncId); + }; + } + }, [asyncCapture, resizeEl, updatePosition]); + useImperativeHandle( ref, () => ({ diff --git a/packages/ui/src/components/anchor/Anchor.tsx b/packages/ui/src/components/anchor/Anchor.tsx index b1ec45f6..f5d3349a 100644 --- a/packages/ui/src/components/anchor/Anchor.tsx +++ b/packages/ui/src/components/anchor/Anchor.tsx @@ -4,7 +4,15 @@ import type { DNestedChildren } from '../../utils/global'; import { isArray, isUndefined } from 'lodash'; import React, { useEffect, useImperativeHandle, useRef, useState } from 'react'; -import { usePrefixConfig, useComponentConfig, useElement, useAsync, useIsomorphicLayoutEffect, useEventCallback } from '../../hooks'; +import { + usePrefixConfig, + useComponentConfig, + useElement, + useAsync, + useIsomorphicLayoutEffect, + useEventCallback, + useLayout, +} from '../../hooks'; import { getClassName, registerComponentMate, scrollTo } from '../../utils'; export interface DAnchorOption { @@ -45,6 +53,7 @@ function Anchor(props: DAnchorProps, ref: React.Forw //#region Context const dPrefix = usePrefixConfig(); + const { dScrollEl, dResizeEl } = useLayout(); //#endregion //#region Ref @@ -58,20 +67,17 @@ function Anchor(props: DAnchorProps, ref: React.Forw const asyncCapture = useAsync(); - const pageEl = useElement(dPage ?? null); + const pageEl = useElement(dPage ?? dScrollEl); + const resizeEl = useElement(dResizeEl); const [activeHref, setActiveHref] = useState(null); const updateAnchor = useEventCallback(() => { - let pageTop = 0; - if (!isUndefined(dPage)) { - if (pageEl) { - pageTop = pageEl.getBoundingClientRect().top; - } else { - return; - } + if (!pageEl) { + return; } + const pageTop = pageEl.getBoundingClientRect().top; let nearestEl: [string, number] | undefined; const reduceLinks = (arr: DNestedChildren[]) => { arr.forEach(({ href, children }) => { @@ -118,6 +124,20 @@ function Anchor(props: DAnchorProps, ref: React.Forw } }, [asyncCapture, pageEl, updateAnchor]); + useEffect(() => { + if (resizeEl) { + const [asyncGroup, asyncId] = asyncCapture.createGroup(); + + asyncGroup.onResize(resizeEl, () => { + updateAnchor(); + }); + + return () => { + asyncCapture.deleteGroup(asyncId); + }; + } + }, [asyncCapture, resizeEl, updateAnchor]); + useEffect(() => { if (anchorRef.current && indicatorRef.current) { if (activeHref) { @@ -143,27 +163,22 @@ function Anchor(props: DAnchorProps, ref: React.Forw ); const handleLinkClick = (href: string) => { - let pageTop = 0; - let targetEl: HTMLElement = document.documentElement; - if (!isUndefined(dPage)) { - if (pageEl) { - pageTop = pageEl.getBoundingClientRect().top; - targetEl = pageEl; - } else { - return; - } + if (!pageEl) { + return; } - const scrollTop = targetEl.scrollTop; + const pageTop = pageEl.getBoundingClientRect().top; + + const scrollTop = pageEl.scrollTop; window.location.hash = href; - targetEl.scrollTop = scrollTop; + pageEl.scrollTop = scrollTop; const el = document.querySelector(href); if (el) { const top = el.getBoundingClientRect().top; - const scrollTop = top - pageTop + targetEl.scrollTop - dDistance; + const scrollTop = top - pageTop + pageEl.scrollTop - dDistance; dataRef.current.clearTid?.(); - dataRef.current.clearTid = scrollTo(targetEl, { + dataRef.current.clearTid = scrollTo(pageEl, { top: scrollTop, behavior: dScrollBehavior, }); diff --git a/packages/ui/src/components/anchor/demos/1.Basic.md b/packages/ui/src/components/anchor/demos/1.Basic.md index 45d172d7..c72fa982 100644 --- a/packages/ui/src/components/anchor/demos/1.Basic.md +++ b/packages/ui/src/components/anchor/demos/1.Basic.md @@ -31,7 +31,6 @@ export default function Demo() { }, { title: 'API', href: '#API' }, ]} - dPage=".app-main" /> ); diff --git a/packages/ui/src/components/anchor/demos/2.Indicator.md b/packages/ui/src/components/anchor/demos/2.Indicator.md index 6bb9ffe5..0ef7a586 100644 --- a/packages/ui/src/components/anchor/demos/2.Indicator.md +++ b/packages/ui/src/components/anchor/demos/2.Indicator.md @@ -30,7 +30,6 @@ export default function Demo() { }, { title: 'API', href: '#API' }, ]} - dPage=".app-main" dIndicator={DAnchor.LINE_INDICATOR} /> ); diff --git a/packages/ui/src/components/anchor/demos/3.Scroll.md b/packages/ui/src/components/anchor/demos/3.Scroll.md index d453e3fa..62755764 100644 --- a/packages/ui/src/components/anchor/demos/3.Scroll.md +++ b/packages/ui/src/components/anchor/demos/3.Scroll.md @@ -6,11 +6,11 @@ title: # en-US -Customize scrolling behavior through `dScrollBehavior`. +Customize scrolling behavior through `dScrollBehavior` and `dDistance`. # zh-Hant -通过 `dScrollBehavior` 自定义滚动行为。 +通过 `dScrollBehavior` 和 `dDistance` 自定义滚动行为。 ```tsx import { DAnchor } from '@react-devui/ui'; @@ -30,7 +30,6 @@ export default function Demo() { }, { title: 'API', href: '#API' }, ]} - dPage=".app-main" dDistance={window.innerHeight / 2} dScrollBehavior="smooth" /> diff --git a/packages/ui/src/components/auto-complete/AutoComplete.tsx b/packages/ui/src/components/auto-complete/AutoComplete.tsx index 72f2904e..76c61c40 100644 --- a/packages/ui/src/components/auto-complete/AutoComplete.tsx +++ b/packages/ui/src/components/auto-complete/AutoComplete.tsx @@ -13,9 +13,9 @@ import { useEventCallback, useElement, useMaxIndex, - useUpdatePosition, useAsync, useDValue, + useLayout, } from '../../hooks'; import { LoadingOutlined } from '../../icons'; import { findNested, registerComponentMate, getClassName, getNoTransformSize, getVerticalSidePosition } from '../../utils'; @@ -66,6 +66,7 @@ function AutoComplete( //#region Context const dPrefix = usePrefixConfig(); + const { dScrollEl, dResizeEl } = useLayout(); //#endregion //#region Ref @@ -81,6 +82,8 @@ function AutoComplete( const getOptionId = (val: string) => `${dPrefix}auto-complete-option-${val}-${uniqueId}`; const getGroupId = (val: string) => `${dPrefix}auto-complete-group-${val}-${uniqueId}`; + const scrollEl = useElement(dScrollEl); + const resizeEl = useElement(dResizeEl); const containerEl = useElement(() => { let el = document.getElementById(`${dPrefix}auto-complete-root`); if (!el) { @@ -122,8 +125,6 @@ function AutoComplete( } }); - useUpdatePosition(updatePosition, visible); - const [_focusOption, setFocusOption] = useState | undefined>(); const focusOption = (() => { if (_focusOption && findNested(dOptions, (o) => canSelectOption(o) && o.value === _focusOption.value)) { @@ -162,6 +163,36 @@ function AutoComplete( } }, [asyncCapture, uniqueId, updatePosition, visible]); + useEffect(() => { + if (visible && scrollEl) { + const [asyncGroup, asyncId] = asyncCapture.createGroup(); + + asyncGroup.fromEvent(scrollEl, 'scroll', { passive: true }).subscribe({ + next: () => { + updatePosition(); + }, + }); + + return () => { + asyncCapture.deleteGroup(asyncId); + }; + } + }, [asyncCapture, visible, scrollEl, updatePosition]); + + useEffect(() => { + if (visible && resizeEl) { + const [asyncGroup, asyncId] = asyncCapture.createGroup(); + + asyncGroup.onResize(resizeEl, () => { + updatePosition(); + }); + + return () => { + asyncCapture.deleteGroup(asyncId); + }; + } + }, [asyncCapture, visible, resizeEl, updatePosition]); + const preventBlur: React.MouseEventHandler = (e) => { if (e.button === 0) { e.preventDefault(); diff --git a/packages/ui/src/components/back-top/BackTop.tsx b/packages/ui/src/components/back-top/BackTop.tsx new file mode 100644 index 00000000..82d12582 --- /dev/null +++ b/packages/ui/src/components/back-top/BackTop.tsx @@ -0,0 +1,134 @@ +import type { DElementSelector } from '../../hooks/ui/useElement'; +import type { DTransitionState } from '../_transition'; + +import React, { useEffect, useRef, useState } from 'react'; + +import { + usePrefixConfig, + useComponentConfig, + useElement, + useAsync, + useEventCallback, + useIsomorphicLayoutEffect, + useLayout, +} from '../../hooks'; +import { VerticalAlignTopOutlined } from '../../icons'; +import { registerComponentMate, getClassName, checkNodeExist, scrollTo } from '../../utils'; +import { TTANSITION_DURING_BASE } from '../../utils/global'; +import { DTransition } from '../_transition'; + +export interface DBackTopProps extends React.ButtonHTMLAttributes { + dPage?: DElementSelector; + dDistance?: number; + dScrollBehavior?: 'instant' | 'smooth'; +} + +const { COMPONENT_NAME } = registerComponentMate({ COMPONENT_NAME: 'DBackTop' }); +export function DBackTop(props: DBackTopProps): JSX.Element | null { + const { + children, + dPage, + dDistance = 400, + dScrollBehavior = 'instant', + + ...restProps + } = useComponentConfig(COMPONENT_NAME, props); + + //#region Context + const dPrefix = usePrefixConfig(); + const { dScrollEl, dResizeEl } = useLayout(); + //#endregion + + const dataRef = useRef<{ + clearTid?: () => void; + }>({}); + + const asyncCapture = useAsync(); + + const pageEl = useElement(dPage ?? dScrollEl); + const resizeEl = useElement(dResizeEl); + + const [visible, setVisible] = useState(false); + + const transitionStyles: Partial> = { + enter: { opacity: 0 }, + entering: { + transition: ['opacity'].map((attr) => `${attr} ${TTANSITION_DURING_BASE}ms linear`).join(', '), + }, + leaving: { + opacity: 0, + transition: ['opacity'].map((attr) => `${attr} ${TTANSITION_DURING_BASE}ms linear`).join(', '), + }, + leaved: { display: 'none' }, + }; + + const updateBackTop = useEventCallback(() => { + if (!pageEl) { + return; + } + + setVisible(pageEl.scrollTop >= dDistance); + }); + useIsomorphicLayoutEffect(() => { + updateBackTop(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + useEffect(() => { + if (pageEl) { + const [asyncGroup, asyncId] = asyncCapture.createGroup(); + + asyncGroup.fromEvent(pageEl, 'scroll', { passive: true }).subscribe({ + next: () => { + updateBackTop(); + }, + }); + + return () => { + asyncCapture.deleteGroup(asyncId); + }; + } + }, [asyncCapture, pageEl, updateBackTop]); + + useEffect(() => { + if (resizeEl) { + const [asyncGroup, asyncId] = asyncCapture.createGroup(); + + asyncGroup.onResize(resizeEl, () => { + updateBackTop(); + }); + + return () => { + asyncCapture.deleteGroup(asyncId); + }; + } + }, [asyncCapture, resizeEl, updateBackTop]); + + return ( + + {(state) => ( + + )} + + ); +} diff --git a/packages/ui/src/components/back-top/README.md b/packages/ui/src/components/back-top/README.md new file mode 100644 index 00000000..62b11c5b --- /dev/null +++ b/packages/ui/src/components/back-top/README.md @@ -0,0 +1,6 @@ +--- +group: Other +title: BackTop +--- + +## API diff --git a/packages/ui/src/components/back-top/README.zh-Hant.md b/packages/ui/src/components/back-top/README.zh-Hant.md new file mode 100644 index 00000000..19e6503a --- /dev/null +++ b/packages/ui/src/components/back-top/README.zh-Hant.md @@ -0,0 +1,5 @@ +--- +title: 回到顶部 +--- + +## API diff --git a/packages/ui/src/components/back-top/demos/1.Basic.md b/packages/ui/src/components/back-top/demos/1.Basic.md new file mode 100644 index 00000000..22ead06d --- /dev/null +++ b/packages/ui/src/components/back-top/demos/1.Basic.md @@ -0,0 +1,40 @@ +--- +title: + en-US: Basic + zh-Hant: 基本 +--- + +# en-US + +The simplest usage. + +# zh-Hant + +最简单的用法。 + +```tsx +import { DBackTop } from '@react-devui/ui'; + +export default function Demo() { + return ( +
+
+
+
+ +
+ ); +} +``` + +```scss +#auto-place-container-1 { + height: 200px; + overflow: auto; + background-color: var(--d-background-color-primary); + + > .overflow { + height: 1000px; + } +} +``` diff --git a/packages/ui/src/components/back-top/demos/2.Icon.md b/packages/ui/src/components/back-top/demos/2.Icon.md new file mode 100644 index 00000000..5f4a141e --- /dev/null +++ b/packages/ui/src/components/back-top/demos/2.Icon.md @@ -0,0 +1,43 @@ +--- +title: + en-US: Custom icon + zh-Hant: 自定义图标 +--- + +# en-US + +Custom icon. + +# zh-Hant + +自定义图标。 + +```tsx +import { DBackTop } from '@react-devui/ui'; +import { CaretUpOutlined } from '@react-devui/ui/icons'; + +export default function Demo() { + return ( +
+
+
+
+ + + +
+ ); +} +``` + +```scss +#auto-place-container-2 { + height: 200px; + overflow: auto; + background-color: var(--d-background-color-primary); + + > .overflow { + height: 1000px; + } +} +``` diff --git a/packages/ui/src/components/back-top/demos/3.Scroll.md b/packages/ui/src/components/back-top/demos/3.Scroll.md new file mode 100644 index 00000000..2d1b8f4f --- /dev/null +++ b/packages/ui/src/components/back-top/demos/3.Scroll.md @@ -0,0 +1,21 @@ +--- +title: + en-US: Custom scroll + zh-Hant: 自定义滚动 +--- + +# en-US + +Customize scrolling behavior through `dScrollBehavior` and `dDistance`. + +# zh-Hant + +通过 `dScrollBehavior` 和 `dDistance` 自定义滚动行为。 + +```tsx +import { DBackTop } from '@react-devui/ui'; + +export default function Demo() { + return ; +} +``` diff --git a/packages/ui/src/components/back-top/index.ts b/packages/ui/src/components/back-top/index.ts new file mode 100644 index 00000000..6c91b4fc --- /dev/null +++ b/packages/ui/src/components/back-top/index.ts @@ -0,0 +1 @@ +export * from './BackTop'; diff --git a/packages/ui/src/components/drawer/Drawer.tsx b/packages/ui/src/components/drawer/Drawer.tsx index d267fec7..32ec9cbe 100644 --- a/packages/ui/src/components/drawer/Drawer.tsx +++ b/packages/ui/src/components/drawer/Drawer.tsx @@ -112,18 +112,19 @@ export function DDrawer(props: DDrawerProps): JSX.Element | null { })(); const containerEl = useElement( - isUndefined(dContainer) + isUndefined(dContainer) || dContainer === false ? () => { - let el = document.getElementById(`${dPrefix}drawer-root`); - if (!el) { - el = document.createElement('div'); - el.id = `${dPrefix}drawer-root`; - document.body.appendChild(el); + if (isUndefined(dContainer)) { + let el = document.getElementById(`${dPrefix}drawer-root`); + if (!el) { + el = document.createElement('div'); + el.id = `${dPrefix}drawer-root`; + document.body.appendChild(el); + } + return el; } - return el; + return null; } - : dContainer === false - ? null : dContainer ); diff --git a/packages/ui/src/components/form/FormItem.tsx b/packages/ui/src/components/form/FormItem.tsx index 10ff49e3..e272be4e 100644 --- a/packages/ui/src/components/form/FormItem.tsx +++ b/packages/ui/src/components/form/FormItem.tsx @@ -59,7 +59,7 @@ export function DFormItem(props: DFor //#region Context const dPrefix = usePrefixConfig(); - const { colNum } = useGridConfig(); + const { dColNum } = useGridConfig(); const { gBreakpointMatchs, gLabelWidth, gLabelColon, gRequiredType, gLayout, gInlineSpan, gFeedbackIcon } = useContextRequired(DFormContext); const formGroup = useContext(DFormGroupContext)!; @@ -95,7 +95,7 @@ export function DFormItem(props: DFor const { span, labelWidth } = (() => { const props = { - span: dSpan ?? (gLayout === 'inline' ? gInlineSpan : colNum), + span: dSpan ?? (gLayout === 'inline' ? gInlineSpan : dColNum), labelWidth: dLabelWidth ?? gLabelWidth, }; @@ -284,7 +284,7 @@ export function DFormItem(props: DFor ...restProps.style, flexGrow: span === true ? 1 : undefined, flexShrink: span === true ? undefined : 0, - width: span === true ? undefined : isNumber(span) ? `calc((100% / ${colNum}) * ${span})` : span, + width: span === true ? undefined : isNumber(span) ? `calc((100% / ${dColNum}) * ${span})` : span, }} >
diff --git a/packages/ui/src/components/grid/Col.tsx b/packages/ui/src/components/grid/Col.tsx index ebc0d804..296fe25c 100644 --- a/packages/ui/src/components/grid/Col.tsx +++ b/packages/ui/src/components/grid/Col.tsx @@ -27,7 +27,7 @@ export function DCol(props: DColProps): JSX.Element | null { //#region Context const dPrefix = usePrefixConfig(); - const { colNum } = useGridConfig(); + const { dColNum } = useGridConfig(); const { gMediaMatch, gSpace } = useContextRequired(DRowContext); //#endregion @@ -61,7 +61,7 @@ export function DCol(props: DColProps): JSX.Element | null { style={{ ...restProps.style, ...responsiveProps?.style, - width: isNumber(span) ? `calc(100% / ${colNum} * ${span})` : undefined, + width: isNumber(span) ? `calc(100% / ${dColNum} * ${span})` : undefined, flexGrow: span === true ? 1 : undefined, paddingLeft: gSpace, paddingRight: gSpace, diff --git a/packages/ui/src/components/grid/hooks.ts b/packages/ui/src/components/grid/hooks.ts index 64bf4863..4099a624 100644 --- a/packages/ui/src/components/grid/hooks.ts +++ b/packages/ui/src/components/grid/hooks.ts @@ -20,17 +20,17 @@ function getMediaMatch(mqlList?: Map) { } export function useMediaMatch() { - const { breakpoints } = useGridConfig(); + const { dBreakpoints } = useGridConfig(); const asyncCapture = useAsync(); const mqlList = useMemo | undefined>(() => { if (typeof window !== 'undefined') { return new Map( - MEDIA_QUERY_LIST.map((breakpoint) => [breakpoint, window.matchMedia(`(min-width: ${breakpoints.get(breakpoint)}px)`)]) + MEDIA_QUERY_LIST.map((breakpoint) => [breakpoint, window.matchMedia(`(min-width: ${dBreakpoints.get(breakpoint)}px)`)]) ); } - }, [breakpoints]); + }, [dBreakpoints]); const [mediaMatch, setMediaMatch] = useState(() => { if (typeof window !== 'undefined') { diff --git a/packages/ui/src/components/index.ts b/packages/ui/src/components/index.ts index 7314fa10..d86482d1 100644 --- a/packages/ui/src/components/index.ts +++ b/packages/ui/src/components/index.ts @@ -16,6 +16,9 @@ export { DAutoComplete } from './auto-complete'; export type { DAvatarProps } from './avatar'; export { DAvatar } from './avatar'; +export type { DBackTopProps } from './back-top'; +export { DBackTop } from './back-top'; + export type { DBadgeProps } from './badge'; export { DBadge } from './badge'; diff --git a/packages/ui/src/hooks/d-config/contex.ts b/packages/ui/src/hooks/d-config/contex.ts index 4464746a..0a8f9d82 100644 --- a/packages/ui/src/hooks/d-config/contex.ts +++ b/packages/ui/src/hooks/d-config/contex.ts @@ -5,6 +5,7 @@ import type { DAnchorProps, DAutoCompleteProps, DAvatarProps, + DBackTopProps, DBadgeProps, DBreadcrumbProps, DButtonProps, @@ -81,6 +82,7 @@ export type DComponentConfig = { DAnchor: DAnchorProps; DAutoComplete: DAutoCompleteProps; DAvatar: DAvatarProps; + DBackTop: DBackTopProps; DBadge: DBadgeProps; DBreadcrumb: DBreadcrumbProps; DButton: DButtonProps; @@ -151,9 +153,9 @@ export interface DConfigContextData { lang?: DLang; resources?: Resources; }; - updatePosition?: { - scroll?: DElementSelector[]; - resize?: DElementSelector[]; + layout?: { + scrollEl?: DElementSelector; + resizeEl?: DElementSelector; }; } export const DConfigContext = React.createContext(undefined); diff --git a/packages/ui/src/hooks/d-config/index.ts b/packages/ui/src/hooks/d-config/index.ts index 69bfece2..b09acff4 100644 --- a/packages/ui/src/hooks/d-config/index.ts +++ b/packages/ui/src/hooks/d-config/index.ts @@ -1,4 +1,4 @@ export { useComponentConfig } from './useComponentConfig'; export { useGridConfig } from './useGridConfig'; +export { useLayout } from './useLayout'; export { usePrefixConfig } from './usePrefixConfig'; -export { useUpdatePosition } from './useUpdatePosition'; diff --git a/packages/ui/src/hooks/d-config/useGridConfig.ts b/packages/ui/src/hooks/d-config/useGridConfig.ts index 3358bb47..d38cf71c 100644 --- a/packages/ui/src/hooks/d-config/useGridConfig.ts +++ b/packages/ui/src/hooks/d-config/useGridConfig.ts @@ -18,7 +18,7 @@ export function useGridConfig() { const colNum = grid?.colNum ?? 12; return { - breakpoints, - colNum, + dBreakpoints: breakpoints, + dColNum: colNum, }; } diff --git a/packages/ui/src/hooks/d-config/useLayout.ts b/packages/ui/src/hooks/d-config/useLayout.ts new file mode 100644 index 00000000..0af6b548 --- /dev/null +++ b/packages/ui/src/hooks/d-config/useLayout.ts @@ -0,0 +1,8 @@ +import { useContext } from 'react'; + +import { DConfigContext } from './contex'; + +export function useLayout() { + const { scrollEl = () => document.documentElement, resizeEl = () => null } = useContext(DConfigContext)?.layout ?? {}; + return { dScrollEl: scrollEl, dResizeEl: resizeEl }; +} diff --git a/packages/ui/src/hooks/d-config/useUpdatePosition.ts b/packages/ui/src/hooks/d-config/useUpdatePosition.ts deleted file mode 100644 index ae5e589f..00000000 --- a/packages/ui/src/hooks/d-config/useUpdatePosition.ts +++ /dev/null @@ -1,63 +0,0 @@ -import type { DElementSelector } from '../ui/useElement'; - -import { isNull, isString } from 'lodash'; -import { useContext, useEffect } from 'react'; - -import { useAsync } from '../side-effect'; -import { DConfigContext } from './contex'; - -function getEl(selector: DElementSelector) { - return selector instanceof HTMLElement || isNull(selector) - ? selector - : isString(selector) - ? (document.querySelector(selector) as HTMLElement | null) - : selector(); -} - -export function useUpdatePosition(fn: () => void, listen = true) { - const { scroll, resize } = useContext(DConfigContext)?.updatePosition ?? {}; - - const asyncCapture = useAsync(); - - useEffect(() => { - if (listen) { - const [asyncGroup, asyncId] = asyncCapture.createGroup(); - - for (const selector of scroll ?? []) { - const el = getEl(selector); - if (el) { - asyncGroup.fromEvent(el, 'scroll', { passive: true }).subscribe({ - next: () => { - fn(); - }, - }); - } - } - - return () => { - asyncCapture.deleteGroup(asyncId); - }; - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [asyncCapture, listen, scroll]); - - useEffect(() => { - if (listen) { - const [asyncGroup, asyncId] = asyncCapture.createGroup(); - - for (const selector of resize ?? []) { - const el = getEl(selector); - if (el) { - asyncGroup.onResize(el, () => { - fn(); - }); - } - } - - return () => { - asyncCapture.deleteGroup(asyncId); - }; - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [asyncCapture, listen, resize]); -} diff --git a/packages/ui/src/hooks/index.ts b/packages/ui/src/hooks/index.ts index 3b700ede..658742f2 100644 --- a/packages/ui/src/hooks/index.ts +++ b/packages/ui/src/hooks/index.ts @@ -1,6 +1,6 @@ export { useDValue, useEventCallback, useForceUpdate, useForkRef, useImmer, useMaxIndex, useWave } from './common'; export { useContextOptional, useContextRequired, useGeneralContext } from './context'; -export { usePrefixConfig, useGridConfig, useComponentConfig, useUpdatePosition } from './d-config'; +export { useComponentConfig, useGridConfig, useLayout, usePrefixConfig } from './d-config'; export { useTranslation } from './i18n'; export { useIsomorphicLayoutEffect, useMount, useUnmount } from './lifecycle'; export { useAsync, useEventNotify } from './side-effect'; diff --git a/packages/ui/src/hooks/ui/useElement.ts b/packages/ui/src/hooks/ui/useElement.ts index 06c41953..be29ce73 100644 --- a/packages/ui/src/hooks/ui/useElement.ts +++ b/packages/ui/src/hooks/ui/useElement.ts @@ -1,18 +1,12 @@ -import { isNull, isString } from 'lodash'; +import { isString } from 'lodash'; import { useEffect, useRef, useState } from 'react'; import { SSR_ENV } from '../../utils/global'; -export type DElementSelector = HTMLElement | null | string | (() => HTMLElement | null); +export type DElementSelector = string | (() => HTMLElement | null); export function useElement(selector: DElementSelector): HTMLElement | null { - const isElement = selector instanceof HTMLElement || isNull(selector); - const [el, setEl] = useState(() => { - if (isElement) { - return selector; - } - if (SSR_ENV) { return null; } else { @@ -23,21 +17,19 @@ export function useElement(selector: DElementSelector): HTMLElement | null { const prevEl = useRef(el); // eslint-disable-next-line react-hooks/exhaustive-deps useEffect(() => { - if (!isElement) { - let el = prevEl.current; + let el = prevEl.current; - if (isString(selector)) { - el = document.querySelector(selector) as HTMLElement | null; - } else { - el = selector(); - } + if (isString(selector)) { + el = document.querySelector(selector) as HTMLElement | null; + } else { + el = selector(); + } - if (el !== prevEl.current) { - prevEl.current = el; - setEl(el); - } + if (el !== prevEl.current) { + prevEl.current = el; + setEl(el); } }); - return isElement ? selector : el; + return el; } diff --git a/packages/ui/src/styles/_components.scss b/packages/ui/src/styles/_components.scss index b7b95c4e..ee29c13f 100644 --- a/packages/ui/src/styles/_components.scss +++ b/packages/ui/src/styles/_components.scss @@ -3,6 +3,7 @@ @import 'components/anchor'; @import 'components/auto-complete'; @import 'components/avatar'; +@import 'components/back-top'; @import 'components/badge'; @import 'components/breadcrumb'; @import 'components/button'; diff --git a/packages/ui/src/styles/_variables.scss b/packages/ui/src/styles/_variables.scss index 66ff30ec..c85541b4 100644 --- a/packages/ui/src/styles/_variables.scss +++ b/packages/ui/src/styles/_variables.scss @@ -90,9 +90,11 @@ $colors: ( } /** component **/ - --#{$variable-prefix}mask-background-color: rgb(0 0 0 / 20%); --#{$variable-prefix}avatar-background-color: #bdbdbd; --#{$variable-prefix}breadcrumb-text-color: #b1b1b1; + --#{$variable-prefix}back-top-background-color: rgb(0 0 0 / 45%); + --#{$variable-prefix}back-top-background-color-hover: rgb(0 0 0 / 60%); + --#{$variable-prefix}mask-background-color: rgb(0 0 0 / 20%); --#{$variable-prefix}skeleton-background-color-wave: rgb(255 255 255 / 50%); --#{$variable-prefix}switch-background-color: #d4d6d9; --#{$variable-prefix}tabs-background-color-slider: hsl(0deg 0% 97%); diff --git a/packages/ui/src/styles/components/back-top.scss b/packages/ui/src/styles/components/back-top.scss new file mode 100644 index 00000000..ddd69424 --- /dev/null +++ b/packages/ui/src/styles/components/back-top.scss @@ -0,0 +1,22 @@ +@include b(back-top) { + @include utils-button; + + position: absolute; + right: 80px; + bottom: 60px; + width: 40px; + height: 40px; + padding: 0; + margin: 0; + overflow: hidden; + color: map.get($colors, 'white'); + background-color: var(--#{$variable-prefix}back-top-background-color); + border: none; + border-radius: 50%; + box-shadow: 0 1px 4px 0 var(--#{$variable-prefix}shadow-color); + transition: background-color var(--#{$variable-prefix}animation-duration-base) linear; + + &:hover { + background-color: var(--#{$variable-prefix}back-top-background-color-hover); + } +} diff --git a/packages/ui/src/styles/theme-dark.scss b/packages/ui/src/styles/theme-dark.scss index 5bc128ab..d08c2d7a 100644 --- a/packages/ui/src/styles/theme-dark.scss +++ b/packages/ui/src/styles/theme-dark.scss @@ -55,9 +55,11 @@ body.dark { } /** component **/ - --#{$variable-prefix}mask-background-color: rgb(0 0 0 / 40%); --#{$variable-prefix}avatar-background-color: #666; --#{$variable-prefix}breadcrumb-text-color: #787878; + --#{$variable-prefix}back-top-background-color: rgb(255 255 255 / 30%); + --#{$variable-prefix}back-top-background-color-hover: rgb(255 255 255 / 50%); + --#{$variable-prefix}mask-background-color: rgb(0 0 0 / 40%); --#{$variable-prefix}skeleton-background-color-wave: rgb(255 255 255 / 4%); --#{$variable-prefix}switch-background-color: #5f6164; --#{$variable-prefix}tabs-background-color-slider: hsl(0deg 0% 26%); diff --git a/tools/executors/site/build-base/impl.js b/tools/executors/site/build-base/impl.js index 66c38b28..ee0d63c0 100644 --- a/tools/executors/site/build-base/impl.js +++ b/tools/executors/site/build-base/impl.js @@ -35,7 +35,7 @@ const rxjs_for_await_1 = require("rxjs-for-await"); const operators_1 = require("rxjs/operators"); const yamlFront = require('yaml-front-matter'); const COMPONENT_DIR = String.raw `packages/ui/src/components`; -const ROUTE_DIR = [String.raw `packages/site/src/app/routes/components`]; +const COMPONENT_ROUTE_DIR = [String.raw `packages/site/src/app/routes/components`]; const OUTPUT_DIR = String.raw `packages/site/src/app`; class GenerateSite { constructor() { @@ -309,7 +309,7 @@ class GenerateSite { this.generateComponentRoute({ name: component, path: componentPath, data: components }, path_1.default.join(OUTPUT_DIR, 'routes', 'components', component)); } } - for (const ROUTE of ROUTE_DIR) { + for (const ROUTE of COMPONENT_ROUTE_DIR) { const files = (0, fs_extra_1.readdirSync)(ROUTE); for (const file of files) { if (file.endsWith('.md') && ((_a = file.match(/\./g)) === null || _a === void 0 ? void 0 : _a.length) === 1) { @@ -421,7 +421,7 @@ function siteBuildExecutor(options, context) { } } } - for (const ROUTE of ROUTE_DIR) { + for (const ROUTE of COMPONENT_ROUTE_DIR) { const files = (0, fs_extra_1.readdirSync)(ROUTE); for (const file of files) { if (file.endsWith('.md') && ((_a = file.match(/\./g)) === null || _a === void 0 ? void 0 : _a.length) === 1) { diff --git a/tools/executors/site/build-base/impl.ts b/tools/executors/site/build-base/impl.ts index 9c6fd9c3..260c5801 100644 --- a/tools/executors/site/build-base/impl.ts +++ b/tools/executors/site/build-base/impl.ts @@ -12,7 +12,7 @@ import { debounceTime, tap, mapTo } from 'rxjs/operators'; const yamlFront = require('yaml-front-matter'); const COMPONENT_DIR = String.raw`packages/ui/src/components`; -const ROUTE_DIR = [String.raw`packages/site/src/app/routes/components`]; +const COMPONENT_ROUTE_DIR = [String.raw`packages/site/src/app/routes/components`]; const OUTPUT_DIR = String.raw`packages/site/src/app`; export interface SiteBuildExecutorOptions { @@ -335,7 +335,7 @@ class GenerateSite { } } - for (const ROUTE of ROUTE_DIR) { + for (const ROUTE of COMPONENT_ROUTE_DIR) { const files = readdirSync(ROUTE); for (const file of files) { if (file.endsWith('.md') && file.match(/\./g)?.length === 1) { @@ -453,7 +453,7 @@ export default async function* siteBuildExecutor(options: SiteBuildExecutorOptio } } } - for (const ROUTE of ROUTE_DIR) { + for (const ROUTE of COMPONENT_ROUTE_DIR) { const files = readdirSync(ROUTE); for (const file of files) { if (file.endsWith('.md') && file.match(/\./g)?.length === 1) { diff --git a/tools/executors/site/build-base/site/resources.json b/tools/executors/site/build-base/site/resources.json index e4760f48..71f79cda 100644 --- a/tools/executors/site/build-base/site/resources.json +++ b/tools/executors/site/build-base/site/resources.json @@ -10,11 +10,10 @@ "Copied!": "Copied!", "Open main navigation": "Open main navigation", "GitHub repository": "GitHub repository", - "Documentation": { - "Interface": "Interface" - }, "menu-group": {}, - "menu": {} + "menu": { + "Interface": "Interface" + } } }, "zh-Hant": { @@ -28,11 +27,10 @@ "Copied!": "复制成功!", "Open main navigation": "打开主导航栏", "GitHub repository": "GitHub 存储库", - "Documentation": { - "Interface": "接口" - }, "menu-group": {}, - "menu": {} + "menu": { + "Interface": "接口" + } } } }