diff --git a/apps/docs/react/guides/multiple-sortable-lists.mdx b/apps/docs/react/guides/multiple-sortable-lists.mdx index f3bed326..6c286e54 100644 --- a/apps/docs/react/guides/multiple-sortable-lists.mdx +++ b/apps/docs/react/guides/multiple-sortable-lists.mdx @@ -6,6 +6,7 @@ mode: "wide" --- import {Story} from '/snippets/story.mdx'; +import {CodeSandbox} from '/snippets/code.mdx'; @@ -21,150 +22,38 @@ We'll be setting up three columns, and each column will have its own list of ite First, let's set up the initial setup for the columns and items. We'll be creating three files, `App.js`, `Column.js`, and `Item.js`, and applying some basic styles in the `Styles.css` file. - -```jsx App.js -import {Column} from './Column'; -import {Item} from './Item'; -import "./Styles.css"; - -export function App() { - const [items] = useState({ - A: ['A0', 'A1', 'A2'], - B: ['B0', 'B1'], - C: [], - }); - - return ( -
- {Object.entries(items).map(([column, items]) => ( - - {items.map((id, index) => ( - - ))} - - ))} -
- ); -} -``` -```jsx Column.js -import React from 'react'; - -export function Column({children, id}) { - return ( -
- {children} -
- ); -} -``` - -```jsx Item.js -import React from 'react'; - -export function Item({id, index}) { - return ( - - ); -} - -const styles = { - appearance: "none", backgroundColor: "#FFF", color: "#666"; - padding: "12px 20px"; borderRadius: "5px" -}; -``` - -```css Styles.css -.Root { - display: inline-flex; - flex-direction: row; - gap: 20; -} - -.Column { - display: flex; - flex-direction: column; - gap: 10px; - padding: 20px; - min-width: 200px; - background-color: rgba(0, 0, 0, 0.1); - border-radius: 10px; -} - -.Item { - appearance: none; - background: #FFF; - color: #666; - padding: 12px 20px; - border: none; - border-radius: 5px; - cursor: grab; -}; -``` -
- - + ## Adding drag and drop functionality Now, let's add drag and drop functionality to the items. We'll be using the [useSortable](/react/hooks/use-sortable) hook to make the items sortable. Let's modify the `Item` component to make it sortable: -```jsx Item.js -import React from 'react'; -import {useSortable} from '@dnd-kit/react/sortable'; -export function Item({id, index, column}) { - const {ref} = useSortable({ - id, - index, - type: 'item', - accept: 'item', - group: column - }); - - return ( - - ); -} -``` - - + As you can see, we've added the `useSortable` hook to the `Item` component. We've also passed the `id`, `index`, `type`, `accept`, and `group` props to the hook. -This creates an uncontrolled list of sortable items that can be sorted within each column. In order to be able to move items between columns, we need to add some additional logic. +This creates an uncontrolled list of sortable items that can be sorted within each column, and across columns thanks to the `group` property. However, in order to be able to move items to an empty column, we need to add some additional logic. ## Moving items between columns -To move items between columns, we need to add a drop target to each column. +To move items to empty columns, we need to add make each column droppable. We'll be using the [useDroppable](/react/hooks/use-droppable) hook to create a drop target for each column. Let's modify the `Column` component to make it droppable: -```jsx Column.js -import React from 'react'; -import {useDroppable} from '@dnd-kit/react'; -import {CollisionPriority} from '@dnd-kit/abstract'; - -export function Column({children, id}) { - const {ref} = useDroppable({ - id, - type: 'column', - accept: 'item', - collisionPriority: CollisionPriority.Low, - }); - - return ( -
- {children} -
- ); -} -``` + We're setting the `collisionPriority` to `CollisionPriority.Low` to prioritize collisions of items over collisions of columns. Learn more about [detecting collisions](/concepts/droppable#detecting-collisions). @@ -172,81 +61,32 @@ This will allow us to drop items into each column. However, we still need to han We'll be using the [DragDropProvider](/react/components/drag-drop-provider) component to listen and respond to the drag and drop events. Let's modify the `App` component to add the `DragDropProvider`. We'll be using the `move` helper function from `@dnd-kit/helpers` to help us mutate the array of items between columns: -```jsx App.js -import React, {useState} from 'react'; -import {DragDropProvider} from '@dnd-kit/react'; -import {move} from '@dnd-kit/helpers'; -import "./Styles.css"; - -import {Column} from './Column'; -import {Item} from './Item'; - -export function App() { - const [items, setItems] = useState({ - A: ['A0', 'A1', 'A2'], - B: ['B0', 'B1'], - C: [], - }); - - return ( - { - setItems((items) => move(items, eventt)); - }} - > -
- {Object.entries(items).map(([column, items]) => ( - - {items.map((id, index) => ( - - ))} - - ))} -
-
- ); -} -``` + As you can see, we've added the `DragDropProvider` component to the `App` component. We've also added an `onDragOver` event handler to listen for drag and drop events. When an item is dragged over a column, the `onDragOver` event handler will be called. We'll use the `move` helper function to move the item between columns. -The result is a sortable list of items that can be moved between columns: - - +The result is a sortable list of items that can be moved between columns. ### Making the columns sortable If you want to make the columns themselves sortable, you can use the `useSortable` hook in the `Column` component. Here's how you can modify the `Column` component to make it sortable: -```jsx Column.js -import React from 'react'; -import {CollisionPriority} from '@dnd-kit/abstract'; -import {useSortable} from '@dnd-kit/react/sortable'; -export function Column({children, id, index}) { - const {ref} = useSortable({ - id, - index, - type: 'column', - collisionPriority: CollisionPriority.Low, - accept: ['item', 'column'], - }); - - return ( -
- {children} -
- ); -} -``` + You'll also need to pass the column `index` prop to the `Column` component in the `App` component. - - If we want to control the state of the columns in React, we can update the `App` component to handle the column order in the `onDragEnd` callback: ```jsx App.js @@ -361,3 +201,226 @@ export function App({style = styles}) { ``` Optimistic updates performed by `@dnd-kit` are automatically reverted when a drag operation is canceled. + +export const App = `import React, {useState} from 'react'; +import {Column} from './Column'; +import {Item} from './Item'; +import './Styles.css'; + +export default function App() { + const [items] = useState({ + A: ['A0', 'A1', 'A2'], + B: ['B0', 'B1'], + C: [], + }); + + return ( +
+ {Object.entries(items).map(([column, items]) => ( + + {items.map((id, index) => ( + + ))} + + ))} +
+ ); +}`; + +export const AppDroppableColumns = `import React, {useState} from 'react'; +import {DragDropProvider} from '@dnd-kit/react'; +import {move} from '@dnd-kit/helpers'; +import {Column} from './Column'; +import {Item} from './Item'; +import './Styles.css'; + +export default function App() { + const [items, setItems] = useState({ + A: ['A0', 'A1', 'A2'], + B: ['B0', 'B1'], + C: [], + }); + + return ( + { + setItems((items) => move(items, event)); + }} + > +
+ {Object.entries(items).map(([column, items]) => ( + + {items.map((id, index) => ( + + ))} + + ))} +
+
+ ); +}`; + +export const AppSortableColumns = `import React, {useState} from 'react'; +import {DragDropProvider} from '@dnd-kit/react'; +import {move} from '@dnd-kit/helpers'; +import {Column} from './Column'; +import {Item} from './Item'; +import './Styles.css'; + +export default function App() { + const [items, setItems] = useState({ + A: ['A0', 'A1', 'A2'], + B: ['B0', 'B1'], + C: [], + }); + + return ( + { + const {source} = event.operation; + + if (source.type === 'column') return; + + setItems((items) => move(items, event)); + }} + > +
+ {Object.entries(items).map(([column, items], index) => ( + + {items.map((id, index) => ( + + ))} + + ))} +
+
+ ); +}`; + +export const Column = `import React from 'react'; + +export function Column({children, id}) { + return ( +
+ {children} +
+ ); +}`; + +export const DroppableColumn = `import React from 'react'; +import {useDroppable} from '@dnd-kit/react'; +import {CollisionPriority} from '@dnd-kit/abstract'; + +export function Column({children, id}) { + const {isDropTarget, ref} = useDroppable({ + id, + type: 'column', + accept: 'item', + collisionPriority: CollisionPriority.Low, + }); + const style = isDropTarget ? {background: '#00000030'} : undefined; + + return ( +
+ {children} +
+ ); +}`; + +export const SortableColumn = `import React from 'react'; +import {CollisionPriority} from '@dnd-kit/abstract'; +import {useSortable} from '@dnd-kit/react/sortable'; + +export function Column({children, id, index}) { + const {ref} = useSortable({ + id, + index, + type: 'column', + collisionPriority: CollisionPriority.Low, + accept: ['item', 'column'], + }); + + return ( +
+ {children} +
+ ); +}`; + +export const Item = `import React from 'react'; + +export function Item({id, index}) { + return ( + + ); +}`; + +export const SortableItem = `import React from 'react'; +import {useSortable} from '@dnd-kit/react/sortable'; + +export function Item({id, index, column}) { + const {ref} = useSortable({ + id, + index, + type: 'item', + accept: 'item', + group: column + }); + + return ( + + ); +}`; + +export const Styles = (() => ` +html { + background-color: ${typeof document != 'undefined' && document.documentElement.classList.contains('dark') ? '#11131b' : '#fafafd'}; +} + +.Root { + display: flex; + flex-direction: row; + gap: 20px; + flex-wrap: wrap; +} + +.Column { + display: flex; + flex-direction: column; + gap: 10px; + padding: 20px; + min-width: 175px; + min-height: 200px; + background-color: rgba(0, 0, 0, 0.1); + border: 1px solid rgba(0, 0, 0, 0.1); + border-radius: 10px; +} + +.Item { + appearance: none; + background: #FFF; + color: #666; + padding: 12px 20px; + border: none; + border-radius: 5px; + cursor: grab; + transition: transform 0.3s ease-in, box-shadow 0.3s ease-in; + transform: scale(1); + box-shadow: inset 0px 0px 1px rgba(0,0,0,0.4), 0 0 0 calc(1px / var(--scale-x, 1)) rgba(63, 63, 68, 0.05), 0px 1px calc(2px / var(--scale-x, 1)) 0 rgba(34, 33, 81, 0.05); +} + +.Item[data-dnd-dragging="true"] { + transform: scale(1.02); + box-shadow: inset 0px 0px 1px rgba(0,0,0,0.5), -1px 0 15px 0 rgba(34, 33, 81, 0.01), 0px 15px 15px 0 rgba(34, 33, 81, 0.25) +}`)(); + +export const files = { + "/Item.js": {code: SortableItem, hidden: true}, + "/App.js": {code: App, hidden: true}, + "/Column.js": {code: Column, hidden: true}, + "/Styles.css": {code: Styles, hidden: true} +}; diff --git a/apps/docs/sandpack.js b/apps/docs/sandpack.js new file mode 100644 index 00000000..b269ca52 --- /dev/null +++ b/apps/docs/sandpack.js @@ -0,0 +1,97 @@ +const importMap = { + imports: { + react: 'https://esm.sh/react@18.2.0', + 'react-dom': 'https://esm.sh/react-dom@18.2.0', + 'react-dom/': 'https://esm.sh/react-dom@18.2.0/', + '@codesandbox/sandpack-react': + 'https://esm.sh/@codesandbox/sandpack-react@2.8.0', + }, +}; + +const importMapScript = document.createElement('script'); +importMapScript.type = 'importmap'; +importMapScript.textContent = JSON.stringify(importMap); + +document.head.appendChild(importMapScript); + +const script = document.createElement('script'); +const code = ` +import React from "react"; +import {createRoot} from "react-dom/client"; +import {Sandpack} from "@codesandbox/sandpack-react"; + +const theme = { + colors: { + surface1: '#0a0a0c', + surface2: 'transparent', + surface3: '#f7f7f710', + clickable: '#969696', + base: '#808080', + disabled: '#4D4D4D', + hover: '#596dff', + accent: '#596dff', + error: '#ffcdca', + errorSurface: '#811e18', + }, + syntax: { + plain: '#d6deeb', + comment: { + color: '#999999', + }, + keyword: { + color: '#c792ea', + }, + tag: '#569cd6', + punctuation: '#d4d4d4', + definition: '#dcdcaa', + property: { + color: '#9cdcfe', + }, + static: '#f78c6c', + string: '#ce9178', + }, + font: { + body: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"', + mono: '"Fira Mono", "DejaVu Sans Mono", Menlo, Consolas, "Liberation Mono", Monaco, "Lucida Console", monospace', + size: '14px', + lineHeight: '20px', + }, +}; + +class SandpackElement extends HTMLElement { + connectedCallback() { + const root = createRoot(this); + let files; + const height = parseInt(this.getAttribute("height")); + + try { + files = JSON.parse(this.getAttribute("files")); + } catch {} + + const sandpackComponent = React.createElement(Sandpack, { + files, + template: "react", + theme: theme, + options: { + showTabs: true, + resizablePanels: false, + editorHeight: height || undefined, + }, + customSetup: { + dependencies: { + "@dnd-kit/react": "beta", + "@dnd-kit/helpers": "beta", + } + } + }, null); + root.render(sandpackComponent); + } +} + +customElements.define("code-sandbox", SandpackElement); +`.replace(/\n/g, ' '); + +script.type = 'module'; +script.innerText = code; + +document.head.appendChild(script); diff --git a/apps/docs/snippets/code.mdx b/apps/docs/snippets/code.mdx new file mode 100644 index 00000000..f307340a --- /dev/null +++ b/apps/docs/snippets/code.mdx @@ -0,0 +1,12 @@ +export const CodeSandbox = ({ files, height, previewHeight }) => { + const Element = 'code-sandbox'; + + return ( + + ); +}; diff --git a/apps/docs/style.css b/apps/docs/style.css index ba0ea218..b580dff7 100644 --- a/apps/docs/style.css +++ b/apps/docs/style.css @@ -108,3 +108,42 @@ kbd { #content-container.max-w-6xl { max-width: 80rem; } + +.sp-layout { + border-radius: var(--rounded-xl,.75rem); + --tw-ring-color: rgba(0,0,0,.05); + --tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color); + --tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color); + box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000); +} + +.sp-tabs-scrollable-container { + padding-left: 0; + padding-right: 0; +} + +.sp-tab-button { + padding: 0.5rem 1rem; + border-bottom: 1px solid transparent; +} + +.sp-tab-button[data-active="true"] { + border-color: rgb(var(--primary-light)); +} + +.dark .sp-layout { + --tw-ring-color: rgb(var(--gray-800) / 0.5); +} + +.sp-editor { + flex-basis: 100% !important; + order: 1; +} + +.sp-preview, .sp-preview-container { + background-color: transparent; +} + +code-sandbox[style*="--preview-height"] .sp-preview { + height: var(--preview-height) !important; +} diff --git a/apps/stories/stories/react/Sortable/MultipleLists/MultipleLists.stories.tsx b/apps/stories/stories/react/Sortable/MultipleLists/MultipleLists.stories.tsx index 39bd60c6..ff40ed65 100644 --- a/apps/stories/stories/react/Sortable/MultipleLists/MultipleLists.stories.tsx +++ b/apps/stories/stories/react/Sortable/MultipleLists/MultipleLists.stories.tsx @@ -1,7 +1,6 @@ import type {Meta, StoryObj} from '@storybook/react'; import {MultipleLists} from './MultipleLists'; -import {Guide} from './docs/examples/Guide'; import docs from './docs/MultipleLists.mdx'; const meta: Meta = { @@ -44,26 +43,6 @@ export const Hero: Story = { }, }; -export const Guide1: Story = { - tags: ['hidden'], - render: () => , -}; - -export const Guide2: Story = { - tags: ['hidden'], - render: () => , -}; - -export const Guide3: Story = { - tags: ['hidden'], - render: () => , -}; - -export const Guide4: Story = { - tags: ['hidden'], - render: () => , -}; - export const Scrollable: Story = { name: 'Scrollable containers', args: { diff --git a/apps/stories/stories/react/Sortable/MultipleLists/docs/examples/Guide.jsx b/apps/stories/stories/react/Sortable/MultipleLists/docs/examples/Guide.jsx deleted file mode 100644 index 47be5869..00000000 --- a/apps/stories/stories/react/Sortable/MultipleLists/docs/examples/Guide.jsx +++ /dev/null @@ -1,88 +0,0 @@ -import React, {useState} from 'react'; -import {CollisionPriority} from '@dnd-kit/abstract'; -import {DragDropProvider, useDroppable} from '@dnd-kit/react'; -import {useSortable} from '@dnd-kit/react/sortable'; -import {move} from '@dnd-kit/helpers'; - -import {Item} from './Item'; - -const styles = {display: 'inline-flex', flexDirection: 'row', gap: 20}; - -export function Guide({ - style = styles, - disabled = false, - uncontrolled = false, - sortableColumns = false, -}) { - const [items, setItems] = useState({ - A: ['A0', 'A1', 'A2'], - B: ['B0', 'B1'], - C: [], - }); - - return ( - { - if (uncontrolled) { - return; - } - - setItems((items) => move(items, event)); - }} - > -
- {Object.entries(items).map(([column, items], columnIndex) => ( - - {items.map((id, index) => - disabled ? ( - - ) : ( - - ) - )} - - ))} -
-
- ); -} - -export function Column({children, id, index, sortable}) { - const useHook = sortable ? useSortable : useDroppable; - const {ref} = useHook({ - id, - index, - type: 'column', - accept: ['item', 'column'], - collisionPriority: CollisionPriority.Low, - }); - - return ( -
- {children} -
- ); -}