Skip to content

Commit

Permalink
Merge pull request #15119 from strapi/feature/relations-reordering
Browse files Browse the repository at this point in the history
  • Loading branch information
joshuaellis committed Jan 17, 2023
2 parents 0c99882 + fb358ef commit ea3f7fa
Show file tree
Hide file tree
Showing 97 changed files with 5,862 additions and 1,637 deletions.
44 changes: 44 additions & 0 deletions docs/docs/core/content-manager/hooks/use-callback-ref.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
---
title: useCallbackRef
description: API reference for the useCallbackRef hook in Strapi's Content Manager
tags:
- content-manager
- hooks
- refs
- callbacks
- effects
---

A custom hook that converts a callback to a ref to avoid triggering re-renders when passed as a
prop or avoid re-executing effects when passed as a dependency. Helpful for working with `modifiedData`
or `initialData` in the content-manager.

Stolen from [`@radix-ui/react-use-callback-ref`](https://www.npmjs.com/package/@radix-ui/react-use-callback-ref).

## Usage

```jsx
import { useCallbackRef } from 'path/to/hooks';

const MyComponent = ({ callbackFromSomewhere }) => {
const mySafeCallback = useCallbackRef(callbackFromSomewhere);

useEffect(() => {
const handleKeyDown = (event) => {
mySafeCallback(event);
};

document.addEventListener('keydown', handleKeyDown);

return () => document.removeEventListener('keydown', handleKeyDown);
}, [mySafeCallback]);

return <div>{children}</div>;
};
```

## Typescript

```ts
function useCallbackRef<T extends (...args: any[]) => any>(callback: T | undefined): T;
```
220 changes: 220 additions & 0 deletions docs/docs/core/content-manager/hooks/use-drag-and-drop.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,220 @@
---
title: useDragAndDrop
slug: /content-manager/hooks/use-drag-and-drop
description: API reference for the useDragAndDrop hook in Strapi's Content Manager
tags:
- content-manager
- hooks
- drag-and-drop
---

An abstraction around `react-dnd`'s `useDrag` and `useDrop` hooks. It provides a simple API to handle drag and drop
events maintaining the same behaviour across the application e.g. when we consider the item to be above a new drop zone.

This hook also wraps an internal hook `useKeyboardDragAndDrop` which implements keyboard accessibile drag and drop by
returning an onKeyDown handler to be passed to the component's drag icon button.

## Usage

:::note
The following examples assume that you have already set up the `DndProvider` with `HTML5Backend` in your application and
that you are somewhat familiar with `@strapi/design-system` components.
:::

### Basic usage

Below is a basic example usage where we're not interested in rendering custom previews in the DragLayer. However, we do replace
the current item with a placeholder.

```jsx
import { Box, Flex, IconButton } from '@strapi/design-system';
import { Drag } from '@strapi/icons';

import { useDragAndDrop } from 'path/to/hooks';
import { composeRefs } from 'path/to/utils';

import { Placeholder } from './Placeholder';

const MyComponent = ({ onMoveItem }) => {
const [{ handlerId, isDragging, handleKeyDown }, myRef, dropRef, dragRef] = useDragAndDrop(true, {
type: 'my-type',
index,
onMoveItem,
});

const composedRefs = composeRefs(myRef, dragRef);

return (
<Box ref={dropRef} cursor={'all-scroll'}>
{isDragging ? (
<Placeholder />
) : (
<Flex ref={composedRefs} data-handler-id={handlerId}>
<IconButton
forwardedAs="div"
role="button"
tabIndex={0}
aria-label="Drag"
noBorder
onKeyDown={handleKeyDown}
>
<Drag />
</IconButton>
{'My item'}
</Flex>
)}
</Box>
);
};
```

### Using custom previews

The only really difference between the previous example and this one is
that we're using the `getEmptyImage` function from `react-dnd-html5-backend`.

```jsx
import { getEmptyImage } from 'react-dnd-html5-backend';
import { Box, Flex, IconButton } from '@strapi/design-system';
import { Drag } from '@strapi/icons';

import { useDragAndDrop } from 'path/to/hooks';
import { composeRefs } from 'path/to/utils';

import { Placeholder } from './Placeholder';

const MyComponent = ({ onMoveItem }) => {
const [{ handlerId, isDragging, handleKeyDown }, myRef, dropRef, dragRef, dragPreviewRef] =
useDragAndDrop(true, {
type: 'my-type',
index,
onMoveItem,
});

// highlight-start
useEffect(() => {
dragPreviewRef(getEmptyImage());
}, [dragPreviewRef]);
// highlight-end

const composedRefs = composeRefs(myRef, dragRef);

return (
<Box ref={dropRef} cursor={'all-scroll'}>
{isDragging ? (
<Placeholder />
) : (
<Flex ref={composedRefs} data-handler-id={handlerId}>
<IconButton
forwardedAs="div"
role="button"
tabIndex={0}
aria-label="Drag"
noBorder
onKeyDown={handleKeyDown}
>
<Drag />
</IconButton>
{'My item'}
</Flex>
)}
</Box>
);
};
```

## Typescript

```ts
import { Identifier } from 'dnd-core';
import { ConnectDropTarget, ConnectDragSource, ConnectDragPreview } from 'react-dnd';

interface UseDragAndDropOptions {
index: number;
onMoveItem: (newIndex: number, currentIndex: number) => void;
/**
* @default "regular"
* Defines whether the change in index should be immediately over another
* dropzone or half way over it (regular).
*/
dropSensitivity?: 'immediate' | 'regular';
item?: object;
/**
* @default 'STRAPI_DND'
*/
type?: string;
onCancel?: (index: number) => void;
onDropItem?: (index: number) => void;
onEnd?: () => void;
onGrabItem?: (index: number) => void;
onStart?: () => void;
}

type UseDragAndDropReturn = [
props: {
handlerId: Identifier;
isDragging: boolean;
handleKeyDown: (event: KeyboardEvent<HTMLButtonElement>) => void;
},
objectRef: React.RefObject<HTMLElement>,
dropRef: ConnectDropTarget,
dragRef: ConnectDragSource,
dragPreviewRef: ConnectDragPreview
];

type UseDragAndDrop = (active: boolean, options: UseDragAndDropOptions) => UseDragAndDropReturn;
```

## Accessibility

Its advised to implement a [live text region](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/ARIA_Live_Regions) in the
parent component holding your individual dnd children. This should be done to inform the user of the current state of the drag and drop.
To implement this, you need to pass the `onDropItem`, `onGrabItem` and `onCancel` callbacks to the `useDragAndDrop` hook which are fired
only with the purpose of updating the live region, hence why they're optional. You would also update the live region as part of your
`onMoveItem` callback. There are generic messages that can be used in the `intl` provider, an example of using this may look like:

```js
setLiveText(
formatMessage(
{
id: getTrad('dnd.drop-item'),
defaultMessage: `{item}, dropped. Final position in list: {position}.`,
},
{
item: 'my item',
position: 1,
}
)
);
```

## Further Reading

- [react-dnd docs](https://react-dnd.github.io/react-dnd/docs/overview)
- [useDrag API](https://react-dnd.github.io/react-dnd/docs/api/use-drag)
- [useDrop API](https://react-dnd.github.io/react-dnd/docs/api/use-drop)
- [useDragLayer API](https://react-dnd.github.io/react-dnd/docs/api/use-drag-layer)

## Troubleshooting

### Firefox quirks

You might notice in the [basic usage](#basic-usage) section this piece of code:

```jsx
<IconButton
forwardedAs="div"
role="button"
tabIndex={0}
aria-label="Drag"
noBorder
onKeyDown={handleKeyDown}
>
<Drag />
</IconButton>
```

In `firefox` the drag handler will not work if you click and drag when the element is a `button`, this is known [bug in the browser](bugzilla.mozilla.org/show_bug.cgi?id=568313).
Therefore the workaround is to use the `forwardedAs` prop to render a `div` instead of a `button`
and add the `role` and `tabIndex` props to make this accessible. The actual `IconButton` component
adds an accessible lable from the `aria-label` prop. So we don't have to concern ourselves with that.
16 changes: 16 additions & 0 deletions docs/sidebars.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,22 @@ const sidebars = {
id: 'core/content-manager/intro',
},
items: [
{
type: 'category',
label: 'Hooks',
items: [
{
type: 'doc',
label: 'useCallbackRef',
id: 'core/content-manager/hooks/use-callback-ref',
},
{
type: 'doc',
label: 'useDragAndDrop',
id: 'core/content-manager/hooks/use-drag-and-drop',
},
],
},
{
type: 'doc',
label: 'Relations',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,19 +57,33 @@
},
"enumeration": {
"type": "enumeration",
"enum": ["A", "B", "C", "D", "E"]
"enum": [
"A",
"B",
"C",
"D",
"E"
]
},
"single_media": {
"type": "media",
"multiple": false,
"required": false,
"allowedTypes": ["images", "files", "videos"]
"allowedTypes": [
"images",
"files",
"videos"
]
},
"multiple_media": {
"type": "media",
"multiple": true,
"required": false,
"allowedTypes": ["images", "files", "videos"]
"allowedTypes": [
"images",
"files",
"videos"
]
},
"json": {
"type": "json"
Expand All @@ -86,7 +100,10 @@
},
"dynamiczone": {
"type": "dynamiczone",
"components": ["basic.simple"]
"components": [
"basic.simple",
"blog.test-como"
]
},
"one_way_tag": {
"type": "relation",
Expand Down Expand Up @@ -139,6 +156,13 @@
"type": "customField",
"regex": "^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$",
"customField": "plugin::color-picker.color"
},
"cats": {
"type": "dynamiczone",
"components": [
"basic.relation",
"basic.simple"
]
}
}
}
14 changes: 14 additions & 0 deletions examples/getstarted/src/components/basic/relation.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"collectionName": "components_basic_relations",
"info": {
"displayName": "Relation"
},
"options": {},
"attributes": {
"categories": {
"type": "relation",
"relation": "oneToMany",
"target": "api::category.category"
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -57,13 +57,7 @@ const ComponentInitializer = ({ error, isReadOnly, onClick }) => {
</Box>
{error?.id && (
<Typography textColor="danger600" variant="pi">
{formatMessage(
{
id: error.id,
defaultMessage: error.id,
},
{ ...error.values }
)}
{formatMessage(error, { ...error.values })}
</Typography>
)}
</>
Expand Down

0 comments on commit ea3f7fa

Please sign in to comment.