Skip to content

Commit bdb6f29

Browse files
committed
Focusable plugin
1 parent 44c9106 commit bdb6f29

File tree

15 files changed

+332
-125
lines changed

15 files changed

+332
-125
lines changed

README.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -80,8 +80,8 @@ You can find the documentation for each module within their respective directori
8080
- [DraggableEvent](src/Draggable/DraggableEvent)
8181
- [MirrorEvent](src/Draggable/MirrorEvent)
8282
- [Plugins](src/Draggable/Plugins)
83-
- [Accessibility](src/Draggable/Plugins/Accessibility)
8483
- [Announcement](src/Draggable/Plugins/Announcement)
84+
- [Focusable](src/Draggable/Plugins/Focusable)
8585
- [Mirror](src/Draggable/Plugins/Mirror)
8686
- [Scrollable](src/Draggable/Plugins/Scrollable)
8787
- [Sensors](src/Draggable/Sensors)

scripts/test/environment.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
const requestAnimationFrameTimeout = 15;
1+
import {REQUEST_ANIMATION_FRAME_TIMEOUT} from './helpers/environment';
22

33
window.requestAnimationFrame = (callback) => {
4-
return setTimeout(callback, requestAnimationFrameTimeout);
4+
return setTimeout(callback, REQUEST_ANIMATION_FRAME_TIMEOUT);
55
};
66

77
window.cancelAnimationFrame = (id) => {

scripts/test/helpers/environment.js

+8
Original file line numberDiff line numberDiff line change
@@ -12,3 +12,11 @@ export function withElementFromPoint(elementFromPoint, callback) {
1212
callback();
1313
document.elementFromPoint = originalElementFromPoint;
1414
}
15+
16+
export const REQUEST_ANIMATION_FRAME_TIMEOUT = 15;
17+
18+
export function waitForRequestAnimationFrame(
19+
requestAnimationFrameTimeout = REQUEST_ANIMATION_FRAME_TIMEOUT,
20+
) {
21+
jest.runTimersToTime(requestAnimationFrameTimeout + 1);
22+
}

src/Draggable/Draggable.js

+21-6
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import {closest} from 'shared/utils';
2-
import {Accessibility, Mirror, Scrollable, Announcement} from './Plugins';
2+
3+
import {Announcement, Focusable, Mirror, Scrollable} from './Plugins';
4+
35
import Emitter from './Emitter';
46
import {MouseSensor, TouchSensor} from './Sensors';
57
import {DraggableInitializedEvent, DraggableDestroyEvent} from './DraggableEvent';
@@ -70,13 +72,13 @@ export default class Draggable {
7072
* Default plugins draggable uses
7173
* @static
7274
* @property {Object} Plugins
75+
* @property {Announcement} Plugins.Announcement
76+
* @property {Focusable} Plugins.Focusable
7377
* @property {Mirror} Plugins.Mirror
74-
* @property {Accessibility} Plugins.Accessibility
7578
* @property {Scrollable} Plugins.Scrollable
76-
* @property {Announcement} Plugins.Announcement
7779
* @type {Object}
7880
*/
79-
static Plugins = {Mirror, Accessibility, Scrollable, Announcement};
81+
static Plugins = {Announcement, Focusable, Mirror, Scrollable};
8082

8183
/**
8284
* Draggable constructor.
@@ -149,8 +151,11 @@ export default class Draggable {
149151
document.addEventListener('drag:stop', this[onDragStop], true);
150152
document.addEventListener('drag:pressure', this[onDragPressure], true);
151153

152-
this.addPlugin(...[Mirror, Accessibility, Scrollable, Announcement, ...this.options.plugins]);
153-
this.addSensor(...[MouseSensor, TouchSensor, ...this.options.sensors]);
154+
const defaultPlugins = Object.values(Draggable.Plugins).map((Plugin) => Plugin);
155+
const defaultSensors = [MouseSensor, TouchSensor];
156+
157+
this.addPlugin(...[...defaultPlugins, ...this.options.plugins]);
158+
this.addSensor(...[...defaultSensors, ...this.options.sensors]);
154159

155160
const draggableInitializedEvent = new DraggableInitializedEvent({
156161
draggable: this,
@@ -317,6 +322,16 @@ export default class Draggable {
317322
return Boolean(this.dragging);
318323
}
319324

325+
/**
326+
* Returns all draggable elements
327+
* @return {HTMLElement[]}
328+
*/
329+
getDraggableElements() {
330+
return this.containers.reduce((current, container) => {
331+
return [...current, ...this.getDraggableElementsForContainer(container)];
332+
}, []);
333+
}
334+
320335
/**
321336
* Returns draggable elements for a given container, excluding the mirror and
322337
* original source element if present

src/Draggable/Plugins/Accessibility/Accessibility.js

-89
This file was deleted.

src/Draggable/Plugins/Accessibility/README.md

-3
This file was deleted.

src/Draggable/Plugins/Accessibility/index.js

-3
This file was deleted.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
import AbstractPlugin from 'shared/AbstractPlugin';
2+
3+
const onInitialize = Symbol('onInitialize');
4+
const onDestroy = Symbol('onDestroy');
5+
6+
/**
7+
* Focusable default options
8+
* @property {Object} defaultOptions
9+
* @type {Object}
10+
*/
11+
const defaultOptions = {};
12+
13+
/**
14+
* Focusable plugin
15+
* @class Focusable
16+
* @module Focusable
17+
* @extends AbstractPlugin
18+
*/
19+
export default class Focusable extends AbstractPlugin {
20+
/**
21+
* Focusable constructor.
22+
* @constructs Focusable
23+
* @param {Draggable} draggable - Draggable instance
24+
*/
25+
constructor(draggable) {
26+
super(draggable);
27+
28+
/**
29+
* Focusable options
30+
* @property {Object} options
31+
* @type {Object}
32+
*/
33+
this.options = {
34+
...defaultOptions,
35+
...this.getOptions(),
36+
};
37+
38+
this[onInitialize] = this[onInitialize].bind(this);
39+
this[onDestroy] = this[onDestroy].bind(this);
40+
}
41+
42+
/**
43+
* Attaches listeners to draggable
44+
*/
45+
attach() {
46+
this.draggable.on('draggable:initialize', this[onInitialize]).on('draggable:destroy', this[onDestroy]);
47+
}
48+
49+
/**
50+
* Detaches listeners from draggable
51+
*/
52+
detach() {
53+
this.draggable.off('draggable:initialize', this[onInitialize]).off('draggable:destroy', this[onDestroy]);
54+
}
55+
56+
/**
57+
* Returns options passed through draggable
58+
* @return {Object}
59+
*/
60+
getOptions() {
61+
return this.draggable.options.focusable || {};
62+
}
63+
64+
/**
65+
* Returns draggable containers and elements
66+
* @return {HTMLElement[]}
67+
*/
68+
getElements() {
69+
return [...this.draggable.containers, ...this.draggable.getDraggableElements()];
70+
}
71+
72+
/**
73+
* Intialize handler
74+
* @private
75+
*/
76+
[onInitialize]() {
77+
// Can wait until the next best frame is available
78+
requestAnimationFrame(() => {
79+
this.getElements().forEach((element) => decorateElement(element));
80+
});
81+
}
82+
83+
/**
84+
* Destroy handler
85+
* @private
86+
*/
87+
[onDestroy]() {
88+
// Can wait until the next best frame is available
89+
requestAnimationFrame(() => {
90+
this.getElements().forEach((element) => stripElement(element));
91+
});
92+
}
93+
}
94+
95+
/**
96+
* Keeps track of all the elements that are missing tabindex attributes
97+
* so they can be reset when draggable gets destroyed
98+
* @const {HTMLElement[]} elementsWithMissingTabIndex
99+
*/
100+
const elementsWithMissingTabIndex = [];
101+
102+
/**
103+
* Decorates element with tabindex attributes
104+
* @param {HTMLElement} element
105+
* @return {Object}
106+
* @private
107+
*/
108+
function decorateElement(element) {
109+
const hasMissingTabIndex = Boolean(!element.getAttribute('tabindex') && element.tabIndex === -1);
110+
111+
if (hasMissingTabIndex) {
112+
elementsWithMissingTabIndex.push(element);
113+
element.tabIndex = 0;
114+
}
115+
}
116+
117+
/**
118+
* Removes elements tabindex attributes
119+
* @param {HTMLElement} element
120+
* @private
121+
*/
122+
function stripElement(element) {
123+
const tabIndexElementPosition = elementsWithMissingTabIndex.indexOf(element);
124+
125+
if (tabIndexElementPosition !== -1) {
126+
element.tabIndex = -1;
127+
elementsWithMissingTabIndex.splice(tabIndexElementPosition, 1);
128+
}
129+
}
+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
## Focusable
2+
3+
The Focusable plugin finds all draggable containers and elements on initialization
4+
and decorates them with `tabindex` attributes.
5+
The plugin will not override existing `tabindex` attributes on elements. This will
6+
make draggable elements and containers focusable.
7+
8+
### API
9+
10+
**`new Focusable(draggable: Draggable): Focusable`**
11+
To create an focusable plugin instance.
12+
13+
**`focusable.getElements(): HTMLElement[]`**
14+
Returns container and draggable elements.
15+
16+
### Options
17+
18+
_No options_
+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import Focusable from './Focusable';
2+
3+
export default Focusable;

0 commit comments

Comments
 (0)