-
Notifications
You must be signed in to change notification settings - Fork 82
feat(useDraggable): add public facing hook #7356
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
base: master
Are you sure you want to change the base?
Conversation
aaad243
to
d9688df
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull Request Overview
Introduces a new useDraggable
hook and refactors the existing drag-and-drop components to use Atlassian’s Pragmatic Drag & Drop under the hood, exposing an imperative API for manual re-ordering.
- Swap out
react-dnd
for@atlaskit/pragmatic-drag-and-drop
in both container and item implementations. - Expose a
DraggableHandle
interface viaforwardRef
onDraggableContainer
for programmaticreOrder
. - Update tests, stories, and documentation to cover real‐time and manual re-ordering scenarios.
Reviewed Changes
Copilot reviewed 44 out of 44 changed files in this pull request and generated 1 comment.
Show a summary per file
File | Description |
---|---|
src/components/draggable/index.ts | Export DraggableHandle type from the new useDraggable hook. |
src/components/draggable/draggable.test.tsx | Adjusted unit tests for polyfills, fake timers, and imperative re-order. |
src/components/draggable/draggable.stories.tsx | Added manual re-ordering story and updated IDs to strings. |
src/components/draggable/draggable.pw.tsx | Removed an outdated multi-container test; adjusted opacity assertion. |
src/components/draggable/draggable.mdx | Documented the new manual re-ordering API with DraggableHandle . |
src/components/draggable/draggable-test.stories.tsx | Updated storybook examples to use string IDs consistently. |
src/components/draggable/draggable-item/draggable-item.style.ts | Refactored styling to handle drag-state data attributes at the container level. |
src/components/draggable/draggable-item/draggable-item.component.tsx | Simplified the item component by removing react-dnd internals. |
src/components/draggable/draggable-container.component.tsx | Major refactor: remove react-dnd provider, integrate useDraggable , add forwardRef . |
src/components/draggable/internal/drop-target.component.tsx | Removed now-unnecessary internal DropTarget . |
src/spec_helper/internal/drag-event-polyfill.ts | Added pragmatic-drag-and-drop polyfills for unit tests. |
src/internal/utils/helpers/guid/index.ts | Added isGuid helper alongside existing GUID generator. |
src/internal/utils/helpers/guid/guid.test.ts | Comprehensive tests for the new isGuid function. |
src/internal/draggable/draggable-utils.tsx | Utility functions for internal drag-state and item data handling. |
src/internal/draggable/draggable-item.tsx | Core implementation of the new Pragmatic DnD-based DraggableItem . |
src/internal/draggable/draggable-item-context.ts | Introduced context for item indexing. |
src/internal/draggable/draggable-container-context.ts | Introduced context for container ID and drag mode. |
src/internal/draggable/components.test-pw.tsx | Playwright CT configs updated to reflect new internal implementations. |
playwright-ct.config.ts | Changed testDir to include the entire src/ directory. |
package.json | Added new Atlaskit dependencies under dependencies and optionalDependencies . |
Comments suppressed due to low confidence (2)
src/components/draggable/draggable.test.tsx:143
- The tests reference
user
but the imported helper isuserEvent
; replaceuser
withuserEvent
to avoid undefined variable errors.
await user.pointer({ keys: "[MouseLeft>]", target: apple });
src/components/draggable/draggable-container.component.tsx:48
- Relying on
displayName
for type checking is brittle and may break during minification; comparechild.type
directly to theDraggableItem
component reference for a more robust validation.
(child.type as React.FunctionComponent).displayName !== "DraggableItem"
{...paddingProps} | ||
id={id} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[nitpick] Passing id
directly to the styled component sets a DOM id
attribute, which can clash; consider using a data-item-id
attribute for internal tracking instead.
id={id} | |
data-item-id={id} |
Copilot uses AI. Check for mistakes.
@@ -8,7 +8,7 @@ const playwrightDir = resolve(__dirname, "./playwright"); | |||
* See https://playwright.dev/docs/test-configuration. | |||
*/ | |||
export default defineConfig({ | |||
testDir: resolve(__dirname, "./src/components"), | |||
testDir: resolve(__dirname, "./src/"), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
question(non-blocking): Why has this been changed?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So playwright can run on the .pw.tsx
file added to the useDraggable
hook, previously it would only run over components which would mean this test wouldn't be included.
if ( | ||
!Number.isNaN(indexOfTarget) && | ||
indexOfTarget >= 0 && | ||
destinationId !== undefined && | ||
destinationId !== null && | ||
(lastMoveRef.current.indexOfTarget !== indexOfTarget || | ||
lastMoveRef.current.destinationId !== destinationId) | ||
) { | ||
localMove(destinationId, indexOfTarget); | ||
lastMoveRef.current = { indexOfTarget, destinationId }; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
suggestion(non-blocking): Something like this here might make this a bit easier to read:
const isValidIndex = Number.isInteger(indexOfTarget) && indexOfTarget >= 0
const hasDestination = destinationId != null
const isNewMove =
lastMoveRef.current.indexOfTarget !== indexOfTarget ||
lastMoveRef.current.destinationId !== destinationId
if (isValidIndex && hasDestination && isNewMove) {
localMove(destinationId, indexOfTarget)
lastMoveRef.current = { indexOfTarget, destinationId };
}
Obviously feel free to choose better names for the const
declarations.
if ( | ||
!Number.isNaN(indexOfTarget) && | ||
indexOfTarget >= 0 && | ||
destinationId !== undefined && | ||
destinationId !== null && | ||
lastMoveRef.current.indexOfTarget !== indexOfTarget | ||
) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
suggestion(non-blocking): Same thing here. It might be best to make const
declarations where you can to make these easier to read.
}); | ||
|
||
it("handles ref prop when ref is null", () => { | ||
// This should not throw an error |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
question(non-blocking): You have the .not.toThrow
check here. Is the comment meant to be here or was this just left over from dev work?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just looks like a leftover comment - I'll remove as the assertion is pretty self-descriptive
}); | ||
|
||
it("handles ref prop when ref is undefined", () => { | ||
// This should not throw an error |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same here with regards to the comment being here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just looks like a leftover comment - I'll remove as the assertion is pretty self-descriptive
*/ | ||
useLayoutEffect(() => { | ||
// This runs after the component mounts and the children are rendered | ||
if (itemRef.current && itemRef.current.children.length > 0) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
suggestion(non-blocking): You should be able to do this here:
if (itemRef.current && itemRef.current.children.length > 0) { | |
if (itemRef?.current.children.length > 0) { |
return () => cleanup(); | ||
}, [uniqueId, draggableItemData, setDragState]); | ||
|
||
return React.createElement( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just an FYI here, that createElement
is classed as a legacy React API. React themselves state that they are not recommended for use in newly written code
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good spot mate, we can look at other alternatives which will let us create an element on the fly based on a prop - most likely jSX
@@ -151,6 +173,36 @@ test("items are reordered when their order is manually changed", () => { | |||
expect(allItems[1]).toHaveTextContent("Apple"); | |||
}); | |||
|
|||
test("items are reordered when their order is manually changed via imperative reo-order", () => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
test("items are reordered when their order is manually changed via imperative reo-order", () => { | |
test("items are reordered when their order is manually changed via imperative re-order", () => { |
|
||
return ( | ||
<DraggableContainerContext.Provider value={contextValue}> | ||
{React.createElement( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think I've mentioned it elsewhere but createElement
is part of React's legacy API's now and they state that they are not recommended for use in newly written code
.
0c024f4
to
3de4923
Compare
…Draggable hook Rebuilds the draggable component using the new useDraggable hook. During this work the type of the `id` prop on `DraggableItem` has been limited to string only to align with HTML types as the `id` prop is now passed to the DOM. BREAKING CHANGE: The `id` prop on `DraggableItem` now only accepts a string type
rebuilds flat-table-row-draggable, flat-table-body-draggable and changes logic within flat-table-row BREAKING CHANGE: consumers will now have to pass their own cells with draggable icons to indicate draggable behaviour. This is no longer done as standard
FYI: All commits will be squashed into one final commit, with any relevant issues also linked.
Proposed behaviour
This PR introduces a comprehensive drag-and-drop system built on top of Atlassian's Pragmatic Drag & Drop library.
For information about the Pragmatic Drag & Drop library, see here.
The implementation provides a flexible, reusable solution for creating draggable lists with automatic reordering capabilities.
Consumers can now use the
useDraggable
hook, which will be externally available to make any array of React Elements draggable.Key Components Added:
Core Architecture:
DraggableContainer
- Main container component that manages drag operations and list stateDraggableItem
- Individual draggable item wrapper with drop target capabilitiesuseDraggable
- Hook that simplifies usage by wrapping items automaticallyKey Features:
Dual Drag Modes:
continuous
- Items reorder in real-time as user dragsonDrop
- Items only reorder when dropped (better performance for large lists)Smart State Management:
Robust ID Handling:
Imperative API:
reOrder(itemId, toIndex)
method for programmatic movesEdge Detection:
Technical Implementation:
@atlaskit/pragmatic-drag-and-drop
for reliable cross-browser supportuseImperativeHandle
for external controlBenefits:
useDraggable
hook directly if needed. There has also been a major net improvement in bundle size within Carbon and Carbon components. Our current draggable libraryreact-dnd
has a minified size of33.9kb
whereaspragmatic-drag-and-drop
has a minified size of176b
which is a x200 reduction in size.Within our components, the biggest win is in
FlatTableBodyDraggable
with a with 52% reduction (~3KB saved). Overall, there has been a net saving across affected components of 3.7KB. The public facing hookuseDraggable
has a rendered size of815B
making it an attractive alternative for consumers.useImperativeHandle
can be used for manual re-ordering. Supporting graceful degradation.This implementation provides a production-ready drag-and-drop solution that can be easily integrated into existing components while maintaining full control over styling and behaviour.
Coming soon:
Once this work is completed and available in Carbon, a
DraggableProvider
will be developed which can be used to wrap multiple containers returned byuseDraggable
hook(s) which will facilitate the movement of items across containers.Once this is available and we can update uses of the
DndProvider
with this new provider in Carbon, thereact-dnd
depdendency will be removed.Current behaviour
Currently, consumers have to use third-party draggable libraries to build out draggable functionality. Internally, we use and re-use
react-dnd
which is now out of support and results in code repetition and complexity.Checklist
d.ts
file added or updated if requiredQA
Additional context
Testing instructions