Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 29 additions & 41 deletions semcore/drag-and-drop/src/DragAndDrop.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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<Omit<AttachDetails, 'index'> | 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<NSDragAndDrop.Component>,
typeof DragAndDropRoot.enhance,
{},
WithI18nEnhanceProps,
State,
DragAndDropDefaultProps
NSDragAndDrop.State,
NSDragAndDrop.DefaultProps
> {
static displayName = 'DragAndDrop';
static enhance = [i18nEnhance(localizedMessages), uniqueIDEnhance()] as const;
Expand All @@ -67,7 +43,7 @@ class DragAndDropRoot extends Component<

static style = style;

state: State = {
state: NSDragAndDrop.State = {
items: [],
dragging: null,
dragOver: null,
Expand Down Expand Up @@ -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) => {
Expand Down Expand Up @@ -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 };
Expand All @@ -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;
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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);
Expand All @@ -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],
);
Expand All @@ -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}
>
<Children />
Expand Down Expand Up @@ -698,7 +684,9 @@ const findNextRectangleIndex = <
return rectangles.indexOf(candidate!);
};

function DropZone(props: DropZoneProps) {
function DropZone(
props: Intergalactic.InternalTypings.InferComponentProps<NSDragAndDrop.DropZone.Component>,
) {
const SDropZone = Root;
const { styles } = props;

Expand All @@ -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,
Expand Down
183 changes: 115 additions & 68 deletions semcore/drag-and-drop/src/DragAndDrop.type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<HTMLElement | null>;
};
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<HTMLElement | null>;
};
type AriaProps = Intergalactic.RequireAtLeastOne<{
'aria-label'?: string;
'aria-labelledby'?: string;
'title'?: string;
}>;
type State = {
items: Array<Omit<NSDragAndDrop.AttachDetails, 'index'> | 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<typeof Box, Props>;
}

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<typeof Box, DropZoneProps>;
};
export type { NSDragAndDrop };
4 changes: 2 additions & 2 deletions semcore/drag-and-drop/src/index.ts
Original file line number Diff line number Diff line change
@@ -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 };
Loading
Loading