-
Notifications
You must be signed in to change notification settings - Fork 0
ERA-11053: Support EFB schemas in ER > Collections > Drag-and-drop #1225
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
Conversation
…nterface to reorder collection items in the new forms
@@ -157,21 +157,28 @@ | |||
"closed": "Open the {{collectionLabel}} list", | |||
"open": "Close the {{collectionLabel}} list" | |||
}, | |||
"item": { |
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.
The old item
translationd didn't really change, they were just nested under sortableList
to keep the same organization than the file structure.
The new translations added are the screen reader instructions and the announcements to stay a11y compliant. The new keys are: dragCancelAnnouncement
, dragEndAnnouncement
, draggableScreenReaderInstructions
, dragOverAnnouncement
and dragStartAnnouncement
.
}); | ||
|
||
const hasError = !!errors; | ||
|
||
return <ul | ||
className={`${styles.formPreview} ${hasError ? styles.error : ''}`} | ||
className={`${styles.formPreview} ${isDragOverlay ? styles.dragOverlay : ''} ${hasError ? styles.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.
@@ -87,6 +83,21 @@ const Item = ({ | |||
setIsFormModalOpen(false); | |||
}; | |||
|
|||
const onActionButtonKeyDown = (event) => { |
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.
A few key changes to the Item
component:
- The action buttons now stop the propagation when the user clicks them while navigating the widget with the keyboard.
Enter
andSpace
keys are also used by the sortable to initiate the dragging mode, so if the focus is in the buttons we stop the propagation and just trigger the click. - A
useEffect
that listens to theisDragging
prop to set the cursor asgrabbing
in the entire document until the user drops the item. - The new drag handle with its hover state.
- The action button listeners get disables if the
Item
is just a drag overlay. - Other styles for the drag overlay and if the item is being dragged.
@@ -53,3 +53,8 @@ export const getHumanizedValue = (field, value, defaultHumanizedValue, language, | |||
return value; | |||
}; | |||
}; | |||
|
|||
export const getItemTitle = (formData, identifier, defaultTitle, identifierField, language, t) => |
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.
This method isn't new, but it was moved to the utils
file so it can be reused by the new SortableList
when rendering the a11y announcement texts.
|
||
const BOOTSTRAP_COLLAPSE_TRANSITION_TIME = '300ms'; | ||
|
||
const SortableItem = ({ id, isFormModalOpen = false, ...otherProps }) => { |
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.
This is just a wrapper for our Item
component that registers it to the @dnd-kit
framework as a new sortable node (through useSortable
) and injects the properties it needs. Of course we could have just placed this code directly in the Item
, but we need the Item
presentational component for our drag overlay, not as a sortable. This ref forwarding
pattern is suggested by the @dnd-kit
docs in the Drag Overlay
section: https://docs.dndkit.com/api-documentation/draggable/drag-overlay#ref-forwarding
const BOOTSTRAP_MODAL_ZINDEX = 1055; | ||
const ITEM_HEADER_HEIGHT = 40; | ||
|
||
const customKeyboardCoordinateGetter = (event, args) => { |
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.
A custom coordinate getter algorithm. It's very simple and straightforward. It would have been better to follow the approach of the sortableKeyboardCoordinates
that comes included in the sortable preset of dnd-kit:
https://docs.dndkit.com/presets/sortable#sensors . However, given that we can open our collection item previews, the sizes of the items is unknown and the included algorithm doesn't work properly. With this custom implementation we move the item with every arrow up or down the user presses the exact height of a closed item. If all the items are closed it will work perfectly, if not, you will have to press several times to do an actual swap, but still works.
setIsItemFormPreviewOpen, | ||
renderField, | ||
}) => { | ||
const sensors = useSensors( |
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.
We configure three sensors for maximal accessibility: mouse, touch and keyboard. We configure them with proper activator constraints so the buttons inside the items (delete, edit, open / close) are still clickable and draggable.
...(overId === null ? [] : [items.findIndex((item) => item.id === overId) + 1]), | ||
]; | ||
}; | ||
const dndAnnouncements = { |
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.
}; | ||
|
||
return <ul> | ||
<DndContext |
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.
The implementation is very readable, and I followed the patterns described in the docs. Nothing weird here:
<DndContext {...configurationProps}>
<SortableContext
items={temporalIdsForOurCollectionItems}
strategy={verticalListSortingStrategy}
>
// SortableItem calls useSortable with the id and injects the properties
{items.map((item) => <SortableItem />)}
</SortableContext>
</DndContext>
// Items is an internal state variable to assign temporal ids to each collection item (used as the key prop, sortable | ||
// id and numeric identifier) and to track their state. It's stored as an array and the index of each item in the | ||
// value prop will always be matched in here. | ||
const [items, setItems] = useState(value.map((_, index) => ({ |
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.
The new state variable. We lifted the items isFormModalOpen
and isFormPreviewOpen
states up to the parent (collection) so we can easily control them from here and close / open the modal and the preview with other actions (like opening the modal immediately after the user creates a new item). It also holds the temporal id and since the structure is an array, it keeps the same ordering than the value
array that contains the form data of each item. So the most important thing to track an item is to know its index which will be the same in value
, in this items
array and visually in the UI.
}; | ||
|
||
const onItemMove = (originalItemIndex, newItemIndex) => { |
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.
The new callback to trigger drag and drop reordering of the collection items. We get the original index and the new index and update the errors, the parent form data and the new items
array using the dnd-kit
helper arrayMove
.
The drag and drop looks good 😎 ! The only thing that may be missing is if we're going to address the cancel flow from the comments in the jira ticket. If not, approved! |
@JoshuaVulcan created a separate ticket to work on those changes 😄 |
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.
Hi @luixlive, this looks great. Approved!
I can definitely circle back to this is changes are made due to feedback but, this looks great!
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.
LGTM 🥳
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.
As always, code looks good and the test coverage is A+. Look and feel of the app is great. I added a couple of minor questions/comments but it's nothing blocking at all. Well done
}, [isFormModalOpen]); | ||
|
||
return <li | ||
className={`${styles.item} ${isFormPreviewOpen ? styles.open : ''} ${isDragging ? styles.isDragging : ''} ${isDragOverlay ? styles.dragOverlay : ''} ${hasError ? styles.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.
At this point, the class string is so long that it may be more legible/reasonable to compute it above the return statement.
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'll do this and then merge 😄
</li>; | ||
}; | ||
|
||
export default forwardRef(Item); |
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.
Why are there some redundant anonymous event handler functions throughout? Shouldn't those be named and consolidated?
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.
Well I don't think we have a hard rule on this topic. My mind flow is usually as follows:
- If a method isn't complex, don't memoize it. Memoization costs are usually more expensive than doing trivial operations like math, concatenation, calling a parent callback, sometimes even iterating a small array...
- If a method isn't memoized and it's a one-liner, keep it anonymous in the prop definition.
- If the method is not a one liner or needs to be memoized, define it in the component body with a nice self explanatory name.
I'm always glad to set our team agreements on code style. Let me know what you think.
What does this PR do?
Add
dnd-kit
to the project and implement a drag and drop interface to the collection lists in the newSchemaForm
to reorder the items.How does it look
Relevant link(s)
Where / how to start reviewing (optional)
The easier way is to go from top to bottom.
The
.../SchemaForm/fields/Collection/index.js
component is the entrypoint for the rendering of the collections. There you'll see that we pulled the item state to the parent so we handle a newitems
state variable that holds a temporal id, and the state of the modal and form preview of each item, making it more flexible to change those states from the parent. Also, from here we render the newSortableList
.Then, we have
.../SchemaForm/fields/Collection/SortableList/index.js
from where we render the<DndContext .../>
and the<SortableContext .../>
providers, needed to use theuseSortable
hook in the draggable items. This component configures and handles the sensors, accessibility (announcements, screen reader instructions, keyboard navigation), dragging events and everything that is related to the implementation ofdnd-kit
from the list perspective. It also renders the items of the collection through the newSortableItem
..../SchemaForm/fields/Collection/SortableList/SortableItem/index.js
is wrapper over our existingItem
component that registers the item as a sortable through theuseSortable
hook and injects the properties (attributes, listeners, refs...) to the list items so they work as draggable elements.In the rest of the files you will see other implementation details related to the drag and drop, like new hovering styles, a drag handle, etc.