Skip to content
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

Merged
merged 17 commits into from
Apr 19, 2018
Merged

Maintain focus when moving between lists #449

merged 17 commits into from
Apr 19, 2018

Conversation

alexreardon
Copy link
Collaborator

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

@alexreardon
Copy link
Collaborator Author

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
Copy link
Collaborator Author

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
Copy link
Collaborator Author

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', () => {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tests for everyone!

Copy link
Collaborator Author

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,
Copy link
Collaborator Author

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

@alexreardon alexreardon changed the title Maintain focus between mounts Maintain focus when moving between lists Apr 18, 2018
@@ -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
Copy link
Collaborator Author

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

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.

Copy link
Collaborator Author

@alexreardon alexreardon Apr 18, 2018

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

Copy link
Collaborator Author

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

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
Copy link
Collaborator Author

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(`
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yummy new error message

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?

Copy link
Collaborator Author

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

// our shared state
let retainingFocusFor: ?DraggableId = null;

// If we focus on
Copy link
Collaborator Author

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', () => {
Copy link
Collaborator Author

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

Copy link

@petegleeson petegleeson left a 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(`

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

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.

componentWillUnmount() {
// releasing reference to ref for cleanup
this.ref = null;

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Simplification (thanks @petegleeson)

Copy link
Collaborator Author

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

Copy link
Collaborator Author

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

@alexreardon
Copy link
Collaborator Author

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.
Copy link
Collaborator Author

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
Copy link
Collaborator Author

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

@alexreardon alexreardon merged commit f966b6f into master Apr 19, 2018
@alexreardon alexreardon deleted the auto-focus branch April 19, 2018 00:50
Copy link
Member

@Blasz Blasz left a 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.

Copy link
Member

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`;
Copy link
Member

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?

Copy link
Collaborator Author

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,
Copy link
Member

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?

Copy link
Collaborator Author

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
Copy link
Member

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
Copy link
Member

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) {
Copy link
Member

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.

Copy link
Collaborator Author

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) => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

s/loose/lose

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants