Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add typesafety to data attributes #1324

Draft
wants to merge 1 commit into
base: master
Choose a base branch
from
Draft
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
10 changes: 7 additions & 3 deletions packages/accessibility/src/components/LiveRegion/LiveRegion.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,14 @@ import React from 'react';
export interface Props {
id: string;
announcement: string;
ariaLiveType?: "polite" | "assertive" | "off";
ariaLiveType?: 'polite' | 'assertive' | 'off';
}

export function LiveRegion({id, announcement, ariaLiveType = "assertive"}: Props) {
export function LiveRegion({
id,
announcement,
ariaLiveType = 'assertive',
}: Props) {
// Hide element visually but keep it readable by screen readers
const visuallyHidden: React.CSSProperties = {
position: 'fixed',
Expand All @@ -20,7 +24,7 @@ export function LiveRegion({id, announcement, ariaLiveType = "assertive"}: Props
clipPath: 'inset(100%)',
whiteSpace: 'nowrap',
};

return (
<div
id={id}
Expand Down
15 changes: 9 additions & 6 deletions packages/core/src/components/Accessibility/Accessibility.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,19 +11,22 @@ import {
defaultScreenReaderInstructions,
} from './defaults';

interface Props {
announcements?: Announcements;
interface Props<DraggableData, DroppableData> {
announcements?: Announcements<DraggableData, DroppableData>;
container?: Element;
screenReaderInstructions?: ScreenReaderInstructions;
hiddenTextDescribedById: string;
}

export function Accessibility({
announcements = defaultAnnouncements,
export function Accessibility<DraggableData, DroppableData>({
announcements = defaultAnnouncements as Announcements<
DraggableData,
DroppableData
>,
container,
hiddenTextDescribedById,
screenReaderInstructions = defaultScreenReaderInstructions,
}: Props) {
}: Props<DraggableData, DroppableData>) {
const {announce, announcement} = useAnnouncement();
const liveRegionId = useUniqueId(`DndLiveRegion`);
const [mounted, setMounted] = useState(false);
Expand All @@ -33,7 +36,7 @@ export function Accessibility({
}, []);

useDndMonitor(
useMemo<DndMonitorListener>(
useMemo<DndMonitorListener<DraggableData, DroppableData>>(
() => ({
onDragStart({active}) {
announce(announcements.onDragStart({active}));
Expand Down
3 changes: 2 additions & 1 deletion packages/core/src/components/Accessibility/defaults.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type {AnyData} from '../../store/types';
import type {Announcements, ScreenReaderInstructions} from './types';

export const defaultScreenReaderInstructions: ScreenReaderInstructions = {
Expand All @@ -8,7 +9,7 @@ export const defaultScreenReaderInstructions: ScreenReaderInstructions = {
`,
};

export const defaultAnnouncements: Announcements = {
export const defaultAnnouncements: Announcements<AnyData, AnyData> = {
onDragStart({active}) {
return `Picked up draggable item ${active.id}.`;
},
Expand Down
37 changes: 27 additions & 10 deletions packages/core/src/components/Accessibility/types.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,33 @@
import type {Active, Over} from '../../store';
import type {Active, AnyData, Over} from '../../store';

export interface Arguments {
active: Active;
over: Over | null;
export interface Arguments<DraggableData, DroppableData> {
active: Active<DraggableData>;
over: Over<DroppableData> | null;
}

export interface Announcements {
onDragStart({active}: Pick<Arguments, 'active'>): string | undefined;
onDragMove?({active, over}: Arguments): string | undefined;
onDragOver({active, over}: Arguments): string | undefined;
onDragEnd({active, over}: Arguments): string | undefined;
onDragCancel({active, over}: Arguments): string | undefined;
export interface Announcements<
DraggableData = AnyData,
DroppableData = AnyData
> {
onDragStart({
active,
}: Pick<Arguments<DraggableData, never>, 'active'>): string | undefined;
onDragMove?({
active,
over,
}: Arguments<DraggableData, DroppableData>): string | undefined;
onDragOver({
active,
over,
}: Arguments<DraggableData, DroppableData>): string | undefined;
onDragEnd({
active,
over,
}: Arguments<DraggableData, DroppableData>): string | undefined;
onDragCancel({
active,
over,
}: Arguments<DraggableData, DroppableData>): string | undefined;
}

export interface ScreenReaderInstructions {
Expand Down
85 changes: 49 additions & 36 deletions packages/core/src/components/DndContext/DndContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import React, {
useReducer,
useRef,
useState,
Context,
} from 'react';
import {unstable_batchedUpdates} from 'react-dom';
import {
Expand Down Expand Up @@ -62,7 +63,7 @@ import {
rectIntersection,
} from '../../utilities';
import {applyModifiers, Modifiers} from '../../modifiers';
import type {Active, Over} from '../../store/types';
import type {Active, AnyData, DataRef, Over} from '../../store/types';
import type {
DragStartEvent,
DragCancelEvent,
Expand All @@ -85,37 +86,38 @@ import {
} from './hooks';
import type {MeasuringConfiguration} from './types';

export interface Props {
export interface Props<DraggableData, DroppableData> {
id?: string;
accessibility?: {
announcements?: Announcements;
announcements?: Announcements<DraggableData, DroppableData>;
container?: Element;
restoreFocus?: boolean;
screenReaderInstructions?: ScreenReaderInstructions;
};
autoScroll?: boolean | AutoScrollOptions;
cancelDrop?: CancelDrop;
cancelDrop?: CancelDrop<DraggableData, DroppableData>;
children?: React.ReactNode;
collisionDetection?: CollisionDetection;
measuring?: MeasuringConfiguration;
modifiers?: Modifiers;
sensors?: SensorDescriptor<any>[];
onDragStart?(event: DragStartEvent): void;
onDragMove?(event: DragMoveEvent): void;
onDragOver?(event: DragOverEvent): void;
onDragEnd?(event: DragEndEvent): void;
onDragCancel?(event: DragCancelEvent): void;
sensors?: SensorDescriptor<any, DraggableData, DroppableData>[];
onDragStart?(event: DragStartEvent<DraggableData>): void;
onDragMove?(event: DragMoveEvent<DraggableData, DroppableData>): void;
onDragOver?(event: DragOverEvent<DraggableData, DroppableData>): void;
onDragEnd?(event: DragEndEvent<DraggableData, DroppableData>): void;
onDragCancel?(event: DragCancelEvent<DraggableData, DroppableData>): void;
}

export interface CancelDropArguments extends DragEndEvent {}
export interface CancelDropArguments<DraggableData, DroppableData>
extends DragEndEvent<DraggableData, DroppableData> {}

export type CancelDrop = (
args: CancelDropArguments
export type CancelDrop<DraggableData = AnyData, DroppableData = AnyData> = (
args: CancelDropArguments<DraggableData, DroppableData>
) => boolean | Promise<boolean>;

interface DndEvent extends Event {
interface DndEvent<DraggableData, DroppableData> extends Event {
dndKit?: {
capturedBy: Sensor<any>;
capturedBy: Sensor<any, DraggableData, DroppableData>;
};
}

Expand All @@ -131,7 +133,10 @@ enum Status {
Initialized,
}

export const DndContext = memo(function DndContext({
export const DndContext = memo(function DndContext<
DraggableData extends AnyData,
DroppableData extends AnyData
>({
id,
accessibility,
autoScroll = true,
Expand All @@ -141,8 +146,11 @@ export const DndContext = memo(function DndContext({
measuring,
modifiers,
...props
}: Props) {
const store = useReducer(reducer, undefined, getInitialState);
}: Props<DraggableData, DroppableData>) {
const typedReducer = reducer<DraggableData, DroppableData>
const typedGetInitialState = getInitialState<DraggableData, DroppableData>
const store = useReducer(typedReducer, undefined, typedGetInitialState);

const [state, dispatch] = store;
const [dispatchMonitorEvent, registerMonitorListener] =
useDndMonitorProvider();
Expand All @@ -153,17 +161,18 @@ export const DndContext = memo(function DndContext({
droppable: {containers: droppableContainers},
} = state;
const node = activeId ? draggableNodes.get(activeId) : null;
const activeRects = useRef<Active['rect']['current']>({
const activeRects = useRef<Active<DraggableData>['rect']['current']>({
initial: null,
translated: null,
});
const active = useMemo<Active | null>(

const active = useMemo<Active<DraggableData> | null>(
() =>
activeId != null
? {
id: activeId,
// It's possible for the active node to unmount while dragging
data: node?.data ?? defaultData,
data: node?.data ?? defaultData as DataRef<DraggableData>,
rect: activeRects,
}
: null,
Expand Down Expand Up @@ -211,7 +220,7 @@ export const DndContext = memo(function DndContext({
const containerNodeRect = useRect(
activeNode ? activeNode.parentElement : null
);
const sensorContext = useRef<SensorContext>({
const sensorContext = useRef<SensorContext<DraggableData, DroppableData>>({
activatorEvent: null,
active: null,
activeNode,
Expand Down Expand Up @@ -305,7 +314,7 @@ export const DndContext = memo(function DndContext({
})
: null;
const overId = getFirstCollision(collisions, 'id');
const [over, setOver] = useState<Over | null>(null);
const [over, setOver] = useState<Over<DroppableData> | null>(null);

// When there is no drag overlay used, we need to account for the
// window scroll delta
Expand All @@ -322,7 +331,7 @@ export const DndContext = memo(function DndContext({
const instantiateSensor = useCallback(
(
event: React.SyntheticEvent,
{sensor: Sensor, options}: SensorDescriptor<any>
{sensor: Sensor, options}: SensorDescriptor<any, DraggableData, DroppableData>
) => {
if (activeRef.current == null) {
return;
Expand Down Expand Up @@ -358,7 +367,7 @@ export const DndContext = memo(function DndContext({
}

const {onDragStart} = latestProps.current;
const event: DragStartEvent = {
const event: DragStartEvent<DraggableData> = {
active: {id, data: draggableNode.data, rect: activeRects},
};

Expand Down Expand Up @@ -392,7 +401,7 @@ export const DndContext = memo(function DndContext({
return async function handler() {
const {active, collisions, over, scrollAdjustedTranslate} =
sensorContext.current;
let event: DragEndEvent | null = null;
let event: DragEndEvent<DraggableData, DroppableData> | null = null;

if (active && scrollAdjustedTranslate) {
const {cancelDrop} = latestProps.current;
Expand Down Expand Up @@ -442,11 +451,11 @@ export const DndContext = memo(function DndContext({

const bindActivatorToSensorInstantiator = useCallback(
(
handler: SensorActivatorFunction<any>,
sensor: SensorDescriptor<any>
handler: SensorActivatorFunction<any, DraggableData>,
sensor: SensorDescriptor<any, DraggableData, DroppableData>
): SyntheticListener['handler'] => {
return (event, active) => {
const nativeEvent = event.nativeEvent as DndEvent;
const nativeEvent = event.nativeEvent as DndEvent<DraggableData, DroppableData>;
const activeDraggableNode = draggableNodes.get(active);

if (
Expand Down Expand Up @@ -483,7 +492,7 @@ export const DndContext = memo(function DndContext({
[draggableNodes, instantiateSensor]
);

const activators = useCombineActivators(
const activators = useCombineActivators<DraggableData, DroppableData>(
sensors,
bindActivatorToSensorInstantiator
);
Expand All @@ -505,7 +514,7 @@ export const DndContext = memo(function DndContext({
return;
}

const event: DragMoveEvent = {
const event: DragMoveEvent<DraggableData, DroppableData> = {
active,
activatorEvent,
collisions,
Expand Down Expand Up @@ -555,7 +564,7 @@ export const DndContext = memo(function DndContext({
disabled: overContainer.disabled,
}
: null;
const event: DragOverEvent = {
const event: DragOverEvent<DraggableData, DroppableData> = {
active,
activatorEvent,
collisions,
Expand Down Expand Up @@ -622,7 +631,7 @@ export const DndContext = memo(function DndContext({
});

const publicContext = useMemo(() => {
const context: PublicContextDescriptor = {
const context: PublicContextDescriptor<DraggableData, DroppableData> = {
active,
activeNode,
activeNodeRect,
Expand Down Expand Up @@ -664,7 +673,7 @@ export const DndContext = memo(function DndContext({
]);

const internalContext = useMemo(() => {
const context: InternalContextDescriptor = {
const context: InternalContextDescriptor<DraggableData, DroppableData> = {
activatorEvent,
activators,
active,
Expand All @@ -691,16 +700,20 @@ export const DndContext = memo(function DndContext({
measureDroppableContainers,
]);

const TypedInternalContext = InternalContext as Context<
InternalContextDescriptor<DraggableData, DroppableData>
>

return (
<DndMonitorContext.Provider value={registerMonitorListener}>
<InternalContext.Provider value={internalContext}>
<TypedInternalContext.Provider value={internalContext}>
<PublicContext.Provider value={publicContext}>
<ActiveDraggableContext.Provider value={transform}>
{children}
</ActiveDraggableContext.Provider>
</PublicContext.Provider>
<RestoreFocus disabled={accessibility?.restoreFocus === false} />
</InternalContext.Provider>
</TypedInternalContext.Provider>
<Accessibility
{...accessibility}
hiddenTextDescribedById={draggableDescribedById}
Expand Down
Loading