-
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
Changes from 5 commits
61778c2
7254b05
b702f48
63f0c41
e02fc79
da31bf6
8166325
842dfa9
d81b7aa
011bf84
b72ef06
aec9d2f
c8e21ef
f44925f
8a0ebac
66580b3
e5df5e6
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line 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 | ||
|
||
When an item is moved from one list to a different list, it loses browser focus if it had it. This is because `React` creates a new node in this situation. It will not lose focus if transitioned within the same list. The dragging item will always have had browser focus if it is dragging with a keyboard. It is highly recommended that you give the item (which is now in a different list) focus again. You can see an example of how to do this in our stories. Here is an example of how you could do it: | ||
|
||
- `onDragEnd`: move the item into the new list and record the id of the item that has moved | ||
- When rendering the reordered list, pass down a prop which will tell the newly moved item to obtain focus | ||
- In the `componentDidMount` lifecycle call back check if the item needs to gain focus based on its props (such as an `autoFocus` prop) | ||
- If focus is required - call `.focus` on the node. You can obtain the node by using `ReactDOM.findDOMNode` or monkey patching the `provided.innerRef` callback. | ||
|
||
### `onDragStart` and `onDragEnd` pairing | ||
|
||
We try very hard to ensure that each `onDragStart` event is paired with a single `onDragEnd` event. However, there maybe a rogue situation where this is not the case. If that occurs - it is a bug. Currently there is no mechanism to tell the library to cancel a current drag externally. | ||
|
@@ -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 on movement | ||
|
||
When moving a `Draggable` from one list to another the default browser behaviour is for the *drag handle* element to loose focus. This 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 commentThe reason will be displayed to describe this comment to others. Learn more. s/loose/lose |
||
##### Extending `DraggableProps.style` | ||
|
||
If you are using inline styles you are welcome to extend the `DraggableProps.style` object. You are also welcome to apply the `DraggableProps.style` object using inline styles and use your own styling solution for the component itself - such as [styled-components](https://github.com/styled-components/styled-components). | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
// @flow | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. rather than repeating these everywhere i pulled them out |
||
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 commentThe 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 commentThe reason will be displayed to describe this comment to others. Learn more. i generally type every variable even if it can be inferred |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -6,6 +6,7 @@ import memoizeOne from 'memoize-one'; | |
import invariant from 'tiny-invariant'; | ||
import type { | ||
Position, | ||
DraggableId, | ||
DraggableDimension, | ||
InitialDragPositions, | ||
DroppableId, | ||
|
@@ -14,6 +15,7 @@ import type { | |
import DraggableDimensionPublisher from '../draggable-dimension-publisher/'; | ||
import Moveable from '../moveable/'; | ||
import DragHandle from '../drag-handle'; | ||
import focusOnDragHandle from './focus-on-drag-handle'; | ||
import getViewport from '../window/get-viewport'; | ||
// eslint-disable-next-line no-duplicate-imports | ||
import type { | ||
|
@@ -40,11 +42,19 @@ export const zIndexOptions: ZIndexOptions = { | |
dropAnimating: 4500, | ||
}; | ||
|
||
// When moving an item from one list to another | ||
// the component is: | ||
// - unmounted from the old list | ||
// - remounted in the new list | ||
// We help consumers by preserving focus when moving a | ||
// draggable from one list to another | ||
let lastFocused: ?DraggableId; | ||
|
||
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 commentThe reason will be displayed to describe this comment to others. Learn more. now handled by the drag handle |
||
isDragHandleFocused: boolean = false | ||
ref: ?HTMLElement = null | ||
|
||
// Need to declare contextTypes without flow | ||
|
@@ -58,8 +68,8 @@ export default class Draggable extends Component<Props> { | |
super(props, context); | ||
|
||
const callbacks: DragHandleCallbacks = { | ||
onFocus: this.onFocus, | ||
onBlur: this.onBlur, | ||
onFocus: this.onDragHandleFocus, | ||
onBlur: this.onDragHandleBlur, | ||
onLift: this.onLift, | ||
onMove: this.onMove, | ||
onDrop: this.onDrop, | ||
|
@@ -75,6 +85,24 @@ 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 commentThe 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 commentThe reason will be displayed to describe this comment to others. Learn more. Is innerRef a required prop? Under what circumstances is ref There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. it is a callback which they might not have called |
||
Draggable has not been provided with a ref. | ||
Please use the DraggableProvided > innerRef function | ||
`); | ||
return; | ||
} | ||
|
||
// This draggable was not previously focused | ||
if (this.props.draggableId !== lastFocused) { | ||
return; | ||
} | ||
|
||
// This drag handle was previously focused - give it focus! | ||
focusOnDragHandle(this.ref); | ||
} | ||
|
||
componentWillUnmount() { | ||
// releasing reference to ref for cleanup | ||
this.ref = null; | ||
|
@@ -117,12 +145,16 @@ export default class Draggable extends Component<Props> { | |
lift(draggableId, initial, getViewport(), autoScrollMode); | ||
} | ||
|
||
onFocus = () => { | ||
this.isFocused = true; | ||
onDragHandleFocus = () => { | ||
this.isDragHandleFocused = true; | ||
// Record that this was the last focused draggable | ||
lastFocused = this.props.draggableId; | ||
} | ||
|
||
onBlur = () => { | ||
this.isFocused = false; | ||
onDragHandleBlur = () => { | ||
this.isDragHandleFocused = false; | ||
// On blur we can clear our last focused | ||
lastFocused = null; | ||
} | ||
|
||
onMove = (client: Position) => { | ||
|
@@ -192,8 +224,8 @@ export default class Draggable extends Component<Props> { | |
// 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 commentThe 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 |
||
// When moving something into or out of a portal the element looses focus | ||
// https://github.com/facebook/react/issues/12454 | ||
if (this.ref && this.isFocused) { | ||
this.ref.focus(); | ||
if (this.ref && this.isDragHandleFocused) { | ||
focusOnDragHandle(this.ref); | ||
} | ||
}) | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
// @flow | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||
import { dragHandle } from '../data-attributes'; | ||
|
||
const selector: string = `[${dragHandle}]`; | ||
|
||
const getDragHandleRef = (draggableRef: HTMLElement): ?HTMLElement => { | ||
if (draggableRef.hasAttribute(dragHandle)) { | ||
return draggableRef; | ||
} | ||
|
||
// find the first nested drag handle | ||
// querySelector will return the first match on a breadth first search which is what we want | ||
// https://codepen.io/alexreardon/pen/erOqyZ | ||
const el: ?HTMLElement = draggableRef.querySelector(selector); | ||
|
||
return el || null; | ||
}; | ||
|
||
const focusOnDragHandle = (draggableRef: HTMLElement) => { | ||
const dragHandleRef: ?HTMLElement = getDragHandleRef(draggableRef); | ||
if (!dragHandleRef) { | ||
console.error('Draggable cannot focus on the drag handle as it cannot be found'); | ||
return; | ||
} | ||
dragHandleRef.focus(); | ||
}; | ||
|
||
export default focusOnDragHandle; |
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