diff --git a/semcore/drag-and-drop/src/DragAndDrop.tsx b/semcore/drag-and-drop/src/DragAndDrop.tsx index 20c0da4f52..6343617e61 100644 --- a/semcore/drag-and-drop/src/DragAndDrop.tsx +++ b/semcore/drag-and-drop/src/DragAndDrop.tsx @@ -1,4 +1,5 @@ import { Box, ScreenReaderOnly } from '@semcore/base-components'; +import type { Intergalactic } from '@semcore/core'; import { createComponent, sstyled, Component, Root } from '@semcore/core'; import canUseDOM from '@semcore/core/lib/utils/canUseDOM'; import type { WithI18nEnhanceProps } from '@semcore/core/lib/utils/enhances/i18nEnhance'; @@ -7,56 +8,31 @@ import uniqueIDEnhance from '@semcore/core/lib/utils/uniqueID'; import useEnhancedEffect from '@semcore/core/lib/utils/use/useEnhancedEffect'; import React from 'react'; -import type { DragAndDropComponent, DragAndDropProps, DropZoneProps, DragAndDropDefaultProps } from './DragAndDrop.type'; +import type { NSDragAndDrop } from './DragAndDrop.type'; import style from './style/drag-and-drop.shadow.css'; import type { LocalizedMessages } from './translations/__intergalactic-dynamic-locales'; import { localizedMessages } from './translations/__intergalactic-dynamic-locales'; -type AttachDetails = { - index: number; - children: React.ReactNode; - node: HTMLElement; - id: string; - draggingAllowed: boolean; - isDropZone?: boolean; - zoneName?: string; -}; - const noop: (...args: any[]) => any = () => { /* do nothing */ }; const DragAndDropContext = React.createContext<{ - attach: (details: AttachDetails) => void; + attach: (details: NSDragAndDrop.AttachDetails) => void; detach: (index: number) => void; }>({ attach: noop, detach: noop, }); -type State = { - items: Array | undefined>; - dragging: null | { - index: number; - initialItemsRects: Array<{ x: number; y: number; width: number; height: number } | undefined>; - placeholder: HTMLElement | null; - }; - dragOver: number | null; - hideHoverEffect: boolean; - a11yHint: string | null; - keyboardDraggingIndex: number | null; - animatedScaling: number | null; - reversedScaling: boolean; -}; - type A11yHintKeys = keyof LocalizedMessages['en']; class DragAndDropRoot extends Component< - DragAndDropProps, + Intergalactic.InternalTypings.InferComponentProps, typeof DragAndDropRoot.enhance, {}, WithI18nEnhanceProps, - State, - DragAndDropDefaultProps + NSDragAndDrop.State, + NSDragAndDrop.DefaultProps > { static displayName = 'DragAndDrop'; static enhance = [i18nEnhance(localizedMessages), uniqueIDEnhance()] as const; @@ -67,7 +43,7 @@ class DragAndDropRoot extends Component< static style = style; - state: State = { + state: NSDragAndDrop.State = { items: [], dragging: null, dragOver: null, @@ -126,7 +102,7 @@ class DragAndDropRoot extends Component< const yOffset = this.asProps.scrollableContainerRef?.current?.scrollTop ?? 0; const xOffset = this.asProps.scrollableContainerRef?.current?.scrollLeft ?? 0; - this.setState((prevState: State) => ({ + this.setState((prevState: NSDragAndDrop.State) => ({ dragging: { index, initialItemsRects: prevState.items.map((item) => { @@ -501,8 +477,8 @@ class DragAndDropRoot extends Component< } }; - attach = ({ index, children, node, id, draggingAllowed, zoneName, isDropZone }: AttachDetails) => { - this.setState((prevState: State) => { + attach = ({ index, children, node, id, draggingAllowed, zoneName, isDropZone }: NSDragAndDrop.AttachDetails) => { + this.setState((prevState: NSDragAndDrop.State) => { if (prevState.items[index]?.children === children && prevState.items[index]?.node === node) return prevState; const { items } = prevState; items[index] = { children, node, id, draggingAllowed, zoneName, isDropZone }; @@ -511,7 +487,7 @@ class DragAndDropRoot extends Component< }; detach = (index: number) => { - this.setState((prevState: State) => { + this.setState((prevState: NSDragAndDrop.State) => { if (!prevState.items[index]) return prevState; const { items } = prevState; items[index] = undefined; @@ -554,7 +530,7 @@ class DragAndDropRoot extends Component< document.removeEventListener('keydown', this.handlePageKeyDown, { capture: true }); } - componentDidUpdate(prevProps: DragAndDropProps) { + componentDidUpdate(prevProps: typeof this.asProps) { if (prevProps.customFocus !== this.asProps.customFocus) { const itemIndex = this.getCustomFocusItemIndex(this.asProps.customFocus); if (this.state.items[itemIndex!]) this.handleItemFocus(); @@ -585,7 +561,16 @@ class DragAndDropRoot extends Component< } } -function Draggable(props: any) { +function Draggable( + props: Intergalactic.InternalTypings.InferChildComponentProps< + NSDragAndDrop.Draggable.Component, + typeof DragAndDropRoot, + 'Draggable' + > & { + // Passed from DropZone. + isDropZone?: boolean; + }, +) { const SDraggable = Root; const ref = React.useRef(); const { attach, detach } = React.useContext(DragAndDropContext); @@ -601,9 +586,10 @@ function Draggable(props: any) { isDropZone = false, uid, isCustomFocus = false, - keyboardFocused, } = props; + const resolvedChildren = React.useMemo( + // @ts-expect-error () => (typeof children === 'function' ? children(props) : children), [children, props], ); @@ -629,7 +615,7 @@ function Draggable(props: any) { placement={placement} role='group' aria-describedby={`describe-draggable-${uid}`} - use:keyboardFocused={isCustomFocus ? false : keyboardFocused} + use:keyboardFocused={isCustomFocus || undefined} tabIndex={0} > @@ -698,7 +684,9 @@ const findNextRectangleIndex = < return rectangles.indexOf(candidate!); }; -function DropZone(props: DropZoneProps) { +function DropZone( + props: Intergalactic.InternalTypings.InferComponentProps, +) { const SDropZone = Root; const { styles } = props; @@ -713,7 +701,7 @@ function DropZone(props: DropZoneProps) { * {@link https://developer.semrush.com/intergalactic/components/drag-and-drop/drag-and-drop-api/|API} | {@link https://developer.semrush.com/intergalactic/components/drag-and-drop/drag-and-drop-code/|Examples} */ const DragAndDrop = createComponent< - DragAndDropComponent, + NSDragAndDrop.Component, typeof DragAndDropRoot >(DragAndDropRoot, { Draggable, diff --git a/semcore/drag-and-drop/src/DragAndDrop.type.ts b/semcore/drag-and-drop/src/DragAndDrop.type.ts index 148c82d10b..94f9762095 100644 --- a/semcore/drag-and-drop/src/DragAndDrop.type.ts +++ b/semcore/drag-and-drop/src/DragAndDrop.type.ts @@ -3,79 +3,126 @@ import type { PropGetterFn, Intergalactic } from '@semcore/core'; import type { LocalizedMessages } from './translations/__intergalactic-dynamic-locales'; -/** - * DragAndDrop and Draggable containers must have an accessible names (aria-group-name). - */ -type DNDAriaProps = Intergalactic.RequireAtLeastOne<{ - 'aria-label'?: string; - 'aria-labelledby'?: string; - 'title'?: string; -}>; - -export type DragAndDropProps = BoxProps & { - /** - * Controlled drag and drop handler - */ - onDnD: (dndData: { fromIndex: number; fromId: string; toIndex: number; toId: string }) => void; - /** - * Index of id that indicates item that is currently under the user focus in case of real browser focus cannot be used. - * When provided, drag and drop listens to whole page keyboard events. Doesn't provide `onCustomFocusChange` callback. - */ - customFocus?: number | string; - /** Specifies the locale for i18n support */ - locale?: string; +declare namespace NSDragAndDrop { + type Props = BoxProps & + NSDragAndDrop.AriaProps & { + /** + * Controlled drag and drop handler + */ + onDnD: (dndData: { fromIndex: number; fromId: NSDragAndDrop.AttachDetails['id']; toIndex: number; toId: NSDragAndDrop.AttachDetails['id'] }) => void; + /** + * Index of id that indicates item that is currently under the user focus in case of real browser focus cannot be used. + * When provided, drag and drop listens to whole page keyboard events. Doesn't provide `onCustomFocusChange` callback. + */ + customFocus?: number | string; + /** Specifies the locale for i18n support */ + locale?: string; + /** + * Ref to a scrollable container, if exists + */ + scrollableContainerRef?: React.MutableRefObject; + }; + type DefaultProps = { + i18n: LocalizedMessages; + locale: 'en'; + }; /** - * Ref to a scrollable container, if exists + * DragAndDrop and Draggable containers must have an accessible names (aria-group-name). */ - scrollableContainerRef?: React.MutableRefObject; -}; + type AriaProps = Intergalactic.RequireAtLeastOne<{ + 'aria-label'?: string; + 'aria-labelledby'?: string; + 'title'?: string; + }>; + type State = { + items: Array | undefined>; + dragging: null | { + index: number; + initialItemsRects: Array<{ x: number; y: number; width: number; height: number } | undefined>; + placeholder: HTMLElement | null; + }; + dragOver: number | null; + hideHoverEffect: boolean; + a11yHint: string | null; + keyboardDraggingIndex: number | null; + animatedScaling: number | null; + reversedScaling: boolean; + }; + type Ctx = { + getDraggableProps: PropGetterFn; + getDroppableProps: PropGetterFn; + }; + type AttachDetails = { + index: number; + children: React.ReactNode; + node: HTMLElement; + id?: string; + draggingAllowed: boolean; + isDropZone?: boolean; + zoneName?: string; + }; -export type DragAndDropDefaultProps = { - i18n: LocalizedMessages; - locale: 'en'; -}; + namespace Draggable { + type Props = BoxProps & + NSDragAndDrop.AriaProps & { + /** Placement of visual drag-and-drop marker + * @default right + * */ + placement?: 'top' | 'right' | 'bottom' | 'left' | false; + /** Disabled DropZone abilities of component + * @default false + * */ + noDrop?: boolean; + /** + * Used as `fromId` or `toId` in `onDnD` handler. + */ + id?: string; + /** + * Used for add zoneName in a11y hints + */ + zoneName?: string; + /** + * Flag for disable keyboardFocused style form DnD.Draggable element + */ + isCustomFocus?: boolean; + /** + * Disable drag ability + */ + noDrag?: boolean; + }; -export type DraggableProps = BoxProps & { - /** Placement of visual drag-and-drop marker - * @default right - * */ - placement?: 'top' | 'right' | 'bottom' | 'left' | false; - /** Disabled DropZone abilities of component - * @default false - * */ - noDrop?: boolean; - /** - * Used as `fromId` or `toId` in `onDnD` handler. - */ - id?: string; - /** - * Used for add zoneName in a11y hints - */ - zoneName?: string; - /** - * Flag for disable keyboardFocused style form DnD.Draggable element - */ - isCustomFocus?: boolean; -}; + type Component = Intergalactic.Component<'div', Props>; + } -export type DragAndDropContext = { - getDraggableProps: PropGetterFn; - getDroppableProps: PropGetterFn; -}; + namespace DropZone { + type Props = BoxProps & + NSDragAndDrop.AriaProps & { + /** + * Used for add zoneName in a11y hints + */ + zoneName?: string; + }; -export type DropZoneProps = BoxProps & - DNDAriaProps & { - /** - * Used for add zoneName in a11y hints - */ - zoneName?: string; + type Component = Intergalactic.Component; + } + + type Component = Intergalactic.Component<'div', Props, Ctx> & { + Draggable: Draggable.Component; + DropZone: DropZone.Component; }; +} + +/** @deprecated It will be removed in v18. */ +export type DragAndDropProps = NSDragAndDrop.Props; +/** @deprecated It will be removed in v18. */ +export type DragAndDropDefaultProps = NSDragAndDrop.DefaultProps; +/** @deprecated It will be removed in v18. */ +export type DraggableProps = NSDragAndDrop.Draggable.Props; +/** @deprecated It will be removed in v18. */ +export type DragAndDropContext = NSDragAndDrop.Ctx; +/** @deprecated It will be removed in v18. */ +export type DropZoneProps = NSDragAndDrop.DropZone.Props; +/** @deprecated It will be removed in v18. */ +export type DragAndDropComponent = NSDragAndDrop.Component; -export type DragAndDropComponent = Intergalactic.Component< - 'div', - DragAndDropProps & DNDAriaProps, - DragAndDropContext -> & { - Draggable: Intergalactic.Component<'div', DraggableProps & DNDAriaProps>; - DropZone: Intergalactic.Component; -}; +export type { NSDragAndDrop }; diff --git a/semcore/drag-and-drop/src/index.ts b/semcore/drag-and-drop/src/index.ts index 4eead7089f..cac94abadd 100644 --- a/semcore/drag-and-drop/src/index.ts +++ b/semcore/drag-and-drop/src/index.ts @@ -1,5 +1,5 @@ -import type { DragAndDropProps, DropZoneProps } from './DragAndDrop.type'; +import type { DragAndDropProps, DropZoneProps, NSDragAndDrop } from './DragAndDrop.type'; export { default } from './DragAndDrop'; -export type { DragAndDropProps, DropZoneProps }; +export type { DragAndDropProps, DropZoneProps, NSDragAndDrop }; diff --git a/website/docs/components/drag-and-drop/drag-and-drop-api.md b/website/docs/components/drag-and-drop/drag-and-drop-api.md index bbcce3a382..a3e4069e8f 100644 --- a/website/docs/components/drag-and-drop/drag-and-drop-api.md +++ b/website/docs/components/drag-and-drop/drag-and-drop-api.md @@ -13,7 +13,7 @@ import DnD from '@semcore/ui/drag-and-drop'; ; ``` - + ## DnD.Draggable @@ -26,7 +26,7 @@ import DnD from '@semcore/ui/drag-and-drop'; ``` - + ## DnD.DropZone @@ -39,6 +39,6 @@ import DnD from '@semcore/ui/drag-and-drop'; ``` - +