-
Notifications
You must be signed in to change notification settings - Fork 2.6k
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
Maintain focus when moving between lists #449
Conversation
This will result in a minor version change |
@@ -692,15 +692,6 @@ Here are a few poor user experiences that can occur if you change things *during | |||
- If you remove the node that the user is dragging, then the drag will instantly end | |||
- If you change the dimension of the dragging node, then other things will not move out of the way at the correct time. | |||
|
|||
#### Force focus after a transition between lists |
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 now do this out of the box
@@ -0,0 +1,28 @@ | |||
// @flow |
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 new file to help us to get the draggable ref which is currently not provided by consumers
@@ -1367,6 +1374,161 @@ describe('Draggable - unconnected', () => { | |||
}); | |||
}); | |||
|
|||
describe('Cross list movement focus retention', () => { |
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.
tests for everyone!
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.
these exercise the feature fairly hard
@@ -40,7 +40,6 @@ type Props = {| | |||
type State = {| | |||
columns: QuoteMap, | |||
ordered: string[], | |||
autoFocusQuoteId: ?string, |
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 got rid of all the lame custom auto focus code
@@ -1027,6 +1018,10 @@ It is a contract of this library that it owns the positioning logic of the dragg | |||
|
|||
To get around this you can use [`React.Portal`](https://reactjs.org/docs/portals.html). We do not enable this functionality by default as it has performance problems. We have a [using a portal guide](/guides/using-a-portal.md) explaining the performance problem in more detail and how you can set up your own `React.Portal` if you want to. | |||
|
|||
##### Focus retention when moving between lists |
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 explains the rationale for the change
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.
Not convinced that this information is useful for the user. I would prefer to see this as a code comment in the context of the logic.
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.
Given that we are controlling the focus I would like to call it out. I have found that calling behaviour out leads to the least confusion (and issues raised). For example: how we use dom events
Maintaining focus across list movements is a feature of the library that it is not native to react or the dom
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 call out other pieces of information like this through the docs
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.
To me it seems like this change makes the focus behave more intuitively. What do you think about moving this section into the "how we use dom events" page? If you think this is important enough to have in the README I am happy to leave it as is.
@@ -0,0 +1,5 @@ | |||
// @flow |
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.
rather than repeating these everywhere i pulled them out
@@ -75,6 +78,18 @@ export default class Draggable extends Component<Props> { | |||
this.styleContext = context[styleContextKey]; | |||
} | |||
|
|||
componentDidMount() { | |||
if (!this.ref) { | |||
console.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.
yummy new error message
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.
Is innerRef a required prop? Under what circumstances is ref undefined
?
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.
it is a callback which they might not have called
src/view/draggable/focus-retainer.js
Outdated
// our shared state | ||
let retainingFocusFor: ?DraggableId = null; | ||
|
||
// If we focus on |
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.
TODO: add comment
@@ -1367,6 +1374,161 @@ describe('Draggable - unconnected', () => { | |||
}); | |||
}); | |||
|
|||
describe('Cross list movement focus retention', () => { |
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.
these exercise the feature fairly hard
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.
Let's move the information about retaining focus from the readme into a code comment.
@@ -75,6 +78,18 @@ export default class Draggable extends Component<Props> { | |||
this.styleContext = context[styleContextKey]; | |||
} | |||
|
|||
componentDidMount() { | |||
if (!this.ref) { | |||
console.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.
Is innerRef a required prop? Under what circumstances is ref undefined
?
@@ -1027,6 +1018,10 @@ It is a contract of this library that it owns the positioning logic of the dragg | |||
|
|||
To get around this you can use [`React.Portal`](https://reactjs.org/docs/portals.html). We do not enable this functionality by default as it has performance problems. We have a [using a portal guide](/guides/using-a-portal.md) explaining the performance problem in more detail and how you can set up your own `React.Portal` if you want to. | |||
|
|||
##### Focus retention when moving between lists |
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.
Not convinced that this information is useful for the user. I would prefer to see this as a code comment in the context of the logic.
src/view/draggable/draggable.jsx
Outdated
componentWillUnmount() { | ||
// releasing reference to ref for cleanup | ||
this.ref = null; | ||
|
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.
Simplification (thanks @petegleeson)
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.
Now only doing this focus management stuff if they where dragging or dropping when the unmount occurs
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 also simplified the focus retainer
I have pushed up a simplification so that we only do the focus retention on a drag end which is way less work on the browser. I'll update the tests tomorrow |
@@ -188,13 +186,6 @@ export default class Draggable extends Component<Props> { | |||
// At this point the ref has been changed or initially populated | |||
|
|||
this.ref = ref; | |||
|
|||
// After a ref change we might need to manually force focus onto the ref. |
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.
focus retention logic is now controlled by the component that has focus: the drag handle
@@ -44,7 +44,6 @@ export default class Draggable extends Component<Props> { | |||
/* eslint-disable react/sort-comp */ | |||
callbacks: DragHandleCallbacks | |||
styleContext: string | |||
isFocused: boolean = false |
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.
now handled by the drag handle
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, just found some typos.
##### Focus retention when moving between lists | ||
|
||
When moving a `Draggable` from one list to another the default browser behaviour is for the *drag handle* element to loose focus. This is because the old element is being destroyed and a new one is being created. The loss of focus is not good when dragging with a keyboard as the user is then unable to continue to interact with the element. To improve this user experience we give a *drag handle* focus as it mounts if it had browser focus when it unmounted and nothing else has obtained browser focus. | ||
|
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.
s/loose/lose
export const prefix: string = 'data-react-beautiful-dnd'; | ||
export const dragHandle: string = `${prefix}-drag-handle`; | ||
export const draggable: string = `${prefix}-draggable`; | ||
export const droppable: string = `${prefix}-droppable`; |
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.
Flow question, do these need to be typed as string or can the type be inferred from the assignment since they are constants?
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 generally type every variable even if it can be inferred
@@ -54,6 +52,8 @@ export type Props = {| | |||
isEnabled: boolean, | |||
// whether the application thinks a drag is occurring | |||
isDragging: boolean, | |||
// whether the application thinks a drop is occurring | |||
isDropAnimating: boolean, |
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.
Nit: Would it be better to name this isDropping
to be consistent with the isDragging
prop?
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.
Perhaps! We currently use this naming in multiple places so i i'll leave it for now
return; | ||
} | ||
|
||
// We are about to force force onto a drag handle |
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.
s/force force/force focus/?
this.lastDraggableRef = ref; | ||
|
||
// After a ref change we might need to manually force focus onto the ref. | ||
// When moving something into or out of a portal the element looses focus |
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.
s/looses/loses
} | ||
|
||
// drag handle ref will not be available when not enabled | ||
if (!this.props.isEnabled) { |
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.
Unrelated to this PR but any reason why this prop isn't called isDisabled
instead? It wasn't immediately obvious to me what this prop meant exactly. Changing this to isDisabled
also aligns with the draggable's disabled prop.
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 generally favour positive props for internal usage. As you mentioned, not part of this PR - but perhaps work thinking more about
@@ -266,6 +272,32 @@ const customViewport: Viewport = { | |||
subject: getArea({ top: 200, left: 100, right: 300, bottom: 300 }), | |||
}; | |||
|
|||
const looseFocus = (wrapper: ReactWrapper) => { |
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.
s/loose/lose
Previously there was a lot of ad hoc code to maintain focus of a draggable when it moved from one list to another. It is so important for the user experience that I have baked it into the library