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

Make canvas selectable / keyboard binding implicit #662

Merged
merged 3 commits into from
Nov 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,17 @@ All notable changes to [diagram-js](https://github.com/bpmn-io/diagram-js) are d

_**Note:** Yet to be released changes appear here._

* `FEAT`: make multi-selection outline an outline concern ([#944](https://github.com/bpmn-io/diagram-js/issues/944)
## 15.0.0

* `FEAT`: make canvas browser selectable ([#659](https://github.com/bpmn-io/diagram-js/pull/659))
* `FEAT`: make keyboard binding implicit ([#661](https://github.com/bpmn-io/diagram-js/issues/661))
* `FEAT`: make multi-selection outline an outline concern ([#944](https://github.com/bpmn-io/diagram-js/issues/944))

### Breaking Changes

* The `selection` feature does not provide visual outline by default anymore. Use the `outline` feature to re-enable it.
* `Keyboard` binding target can no longer be chosen. Configure keyboard binding via the `keyboard.bind` configuration and rely on keybindings to work if the canvas has browser focus. ([#661](https://github.com/bpmn-io/diagram-js/issues/661))
* 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`. ([#661](https://github.com/bpmn-io/diagram-js/issues/661))
* The `selection` feature does not provide visual outline by default anymore. Use the `outline` feature to re-enable it. ([#944](https://github.com/bpmn-io/diagram-js/issues/944))

## 14.11.3

Expand Down
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;
nikku marked this conversation as resolved.
Show resolved Hide resolved

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() {
jarekdanielak marked this conversation as resolved.
Show resolved Hide resolved
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
84 changes: 26 additions & 58 deletions lib/features/keyboard/Keyboard.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,7 @@ import {
} from 'min-dash';

import {
closest as domClosest,
event as domEvent,
matches as domMatches
event as domEvent
} from 'min-dom';

import {
Expand All @@ -24,10 +22,11 @@ import {
var KEYDOWN_EVENT = 'keyboard.keydown',
KEYUP_EVENT = 'keyboard.keyup';

var HANDLE_MODIFIER_ATTRIBUTE = 'input-handle-modified-keys';

var DEFAULT_PRIORITY = 1000;

var compatMessage = 'Keyboard binding is now implicit; explicit binding to an element got removed. For more information, see https://github.com/bpmn-io/diagram-js/issues/661';
nikku marked this conversation as resolved.
Show resolved Hide resolved


/**
* A keyboard abstraction that may be activated and
* deactivated by users at will, consuming global key events
Expand All @@ -46,8 +45,8 @@ var DEFAULT_PRIORITY = 1000;
*
* All events contain one field which is node.
*
* A default binding for the keyboard may be specified via the
* `keyboard.bindTo` configuration option.
* Specify the initial keyboard binding state via the
* `keyboard.bind=true|false` configuration option.
*
* @param {Object} config
* @param {EventTarget} [config.bindTo]
Expand All @@ -56,7 +55,8 @@ var DEFAULT_PRIORITY = 1000;
export default function Keyboard(config, eventBus) {
var self = this;

this._config = config || {};
this._config = config = config || {};

this._eventBus = eventBus;

this._keydownHandler = this._keydownHandler.bind(this);
Expand All @@ -69,19 +69,22 @@ export default function Keyboard(config, eventBus) {
self.unbind();
});

eventBus.on('diagram.init', function() {
self._fire('init');
});
if (config.bindTo) {
console.error('unsupported configuration <keyboard.bindTo>', new Error(compatMessage));
}

var bind = config && config.bind !== false;

eventBus.on('canvas.init', function(event) {
self._target = event.svg;

eventBus.on('attach', function() {
if (config && config.bindTo) {
self.bind(config.bindTo);
if (bind) {
self.bind();
}
});

eventBus.on('detach', function() {
self.unbind();
self._fire('init');
});

}

Keyboard.$inject = [
Expand Down Expand Up @@ -116,34 +119,7 @@ Keyboard.prototype._keyHandler = function(event, type) {
};

Keyboard.prototype._isEventIgnored = function(event) {
if (event.defaultPrevented) {
return true;
}

return (
isInput(event.target) || (
isButton(event.target) && isKey([ ' ', 'Enter' ], event)
)
) && this._isModifiedKeyIgnored(event);
};

Keyboard.prototype._isModifiedKeyIgnored = function(event) {
if (!isCmd(event)) {
return true;
}

var allowedModifiers = this._getAllowedModifiers(event.target);
return allowedModifiers.indexOf(event.key) === -1;
};

Keyboard.prototype._getAllowedModifiers = function(element) {
var modifierContainer = domClosest(element, '[' + HANDLE_MODIFIER_ATTRIBUTE + ']', true);

if (!modifierContainer || (this._node && !this._node.contains(modifierContainer))) {
return [];
}

return modifierContainer.getAttribute(HANDLE_MODIFIER_ATTRIBUTE).split(',');
return false;
};

/**
Expand All @@ -153,10 +129,14 @@ Keyboard.prototype._getAllowedModifiers = function(element) {
*/
Keyboard.prototype.bind = function(node) {

if (node) {
console.error('unsupported argument <node>', new Error(compatMessage));
}

// make sure that the keyboard is only bound once to the DOM
this.unbind();

this._node = node;
node = this._node = this._target;

// bind key events
domEvent.bind(node, 'keydown', this._keydownHandler);
Expand Down Expand Up @@ -226,15 +206,3 @@ Keyboard.prototype.hasModifier = hasModifier;
Keyboard.prototype.isCmd = isCmd;
Keyboard.prototype.isShift = isShift;
Keyboard.prototype.isKey = isKey;



// helpers ///////

function isInput(target) {
return target && (domMatches(target, 'input, textarea') || target.contentEditable === 'true');
}

function isButton(target) {
return target && domMatches(target, 'button, input[type=submit], input[type=button], a[href], [aria-role=button]');
}
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
Loading
Loading