Skip to content

Commit

Permalink
feat(core): make Canvas focusable
Browse files Browse the repository at this point in the history
The `Canvas` is now a focusable component, that is recognized
accordingly by the browser, with all benefits for UX and interaction.

Components that pull focus from the `Canvas` during modeling must
ensure to restore the focus (if intended), via `Canvas#restoreFocus`.

Related to #661
  • Loading branch information
nikku authored and jarekdanielak committed Nov 4, 2024
1 parent c7cb8be commit 89a0a13
Show file tree
Hide file tree
Showing 6 changed files with 96 additions and 5 deletions.
56 changes: 53 additions & 3 deletions lib/core/Canvas.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ import {
} from 'min-dash';

import {
assignStyle
assignStyle,
attr as domAttr
} from 'min-dom';

import {
Expand Down Expand Up @@ -189,6 +190,11 @@ export default function Canvas(config, eventBus, graphicsFactory, elementRegistr
*/
this._rootElement = null;

/**
* @type {boolean}
*/
this._focused = false;

this._init(config || {});
}

Expand All @@ -215,14 +221,33 @@ Canvas.$inject = [
* @param {CanvasConfig} config
*/
Canvas.prototype._init = function(config) {

const eventBus = this._eventBus;

// html container
const container = this._container = createContainer(config);

const svg = this._svg = svgCreate('svg');
svgAttr(svg, { width: '100%', height: '100%' });

svgAttr(svg, {
width: '100%',
height: '100%'
});

domAttr(svg, 'tabindex', 0);

eventBus.on('element.hover', () => {
this.restoreFocus();
});

svg.addEventListener('focusin', () => {
this._focused = true;
eventBus.fire('canvas.focus.changed', { focused: true });
});

svg.addEventListener('focusout', () => {
this._focused = false;
eventBus.fire('canvas.focus.changed', { focused: false });
});

svgAppend(container, svg);

Expand Down Expand Up @@ -314,6 +339,31 @@ Canvas.prototype._clear = function() {
delete this._cachedViewbox;
};

/**
* Sets focus on the canvas SVG element.
*/
Canvas.prototype.focus = function() {
this._svg.focus({ preventScroll: true });
};

/**
* Sets focus on the canvas SVG element if `document.body` is currently focused.
*/
Canvas.prototype.restoreFocus = function() {
if (document.activeElement === document.body) {
this.focus();
}
};

/**
* Returns true if the canvas is focused.
*
* @return {boolean}
*/
Canvas.prototype.isFocused = function() {
return this._focused;
};

/**
* Returns the default layer on which
* all elements are drawn.
Expand Down
2 changes: 2 additions & 0 deletions lib/features/popup-menu/PopupMenu.js
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,8 @@ PopupMenu.prototype.close = function() {

this.reset();

this._canvas.restoreFocus();

this._current = null;
};

Expand Down
2 changes: 2 additions & 0 deletions lib/features/search-pad/SearchPad.js
Original file line number Diff line number Diff line change
Expand Up @@ -403,6 +403,8 @@ SearchPad.prototype.close = function(restoreCached = true) {
this._searchInput.blur();

this._eventBus.fire('searchPad.closed');

this._canvas.restoreFocus();
};


Expand Down
13 changes: 11 additions & 2 deletions test/spec/core/CanvasSpec.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,12 +47,18 @@ describe('Canvas', function() {

beforeEach(createDiagram());


it('should create <svg> element', inject(function() {

// then
// when
var svg = container.querySelector('svg');

expect(svg).not.to.be.null;
// then
// svg is created
expect(svg).to.exist;

// and user selectable
expect(svgAttr(svg, 'tabindex')).to.equal('0');
}));


Expand Down Expand Up @@ -152,6 +158,9 @@ describe('Canvas', function() {

// then
expect(document.activeElement).to.eql(inputEl);

// clean up
document.body.removeChild(inputEl);
}));

});
Expand Down
14 changes: 14 additions & 0 deletions test/spec/features/popup-menu/PopupMenuSpec.js
Original file line number Diff line number Diff line change
Expand Up @@ -706,6 +706,20 @@ describe('features/popup-menu', function() {
}).not.to.throw();
}));


it('should refocus canvas on close', inject(function(canvas, popupMenu) {

// given
sinon.spy(canvas, 'restoreFocus');

// when
popupMenu.close();

// then
expect(canvas.restoreFocus).to.have.been.calledOnce;

}));

});


Expand Down
14 changes: 14 additions & 0 deletions test/spec/features/search-pad/SearchPadSpec.js
Original file line number Diff line number Diff line change
Expand Up @@ -277,6 +277,20 @@ describe('features/searchPad', function() {
expect(capturedEvents).to.eql([ EVENTS.opened, EVENTS.closed ]);
}));


it('should refocus canvas on close', inject(function(canvas, searchPad) {

// given
sinon.spy(canvas, 'restoreFocus');
searchPad.open();

// when
triggerMouseEvent(canvas.getContainer(), 'click', 0, 0);

// then
expect(canvas.restoreFocus).to.have.been.calledOnce;
}));

});


Expand Down

0 comments on commit 89a0a13

Please sign in to comment.