Skip to content

Commit

Permalink
feat(keyboard): make keyboard binding implicit
Browse files Browse the repository at this point in the history
Keyboard is now implicitly bound to the canvas element (svg).

Legacy configuration via `keyboard.bindTo` config or by passing an element
to `Keyboard#bind()` results in a descriptive error to be raised.

BREAKING CHANGES:

* Keyboard is now implicitly bound to the (focusable) canvas parent.
  Prior usages result in human readable errors to be raised.
  • Loading branch information
philippfromme authored and jarekdanielak committed Nov 4, 2024
1 parent c1ce7bc commit 29247c8
Show file tree
Hide file tree
Showing 12 changed files with 5,920 additions and 397 deletions.
8 changes: 7 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,13 @@ All notable changes to [diagram-js](https://github.com/bpmn-io/diagram-js) are d

## Unreleased

_**Note:** Yet to be released changes appear here._
* `FEAT`: only copy if selected elements ([#660](https://github.com/bpmn-io/diagram-js/pull/660))
* `FEAT`: make canvas browser selectable ([#659](https://github.com/bpmn-io/diagram-js/pull/659))
* `FEAT`: trigger keyboard bindings on browser selection only ([#661](https://github.com/bpmn-io/diagram-js/issues/661))

### Breaking Changes

* 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))

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

Expand Down
20 changes: 12 additions & 8 deletions assets/diagram-js.css
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,18 @@
--tooltip-error-color: var(--color-red-360-100-45);
}

/**
* SVG styles
*/

.djs-container svg.drop-not-ok {
background: var(--shape-drop-not-allowed-fill-color) !important;
}

.djs-container svg.new-parent {
background: var(--shape-drop-allowed-fill-color) !important;
}

/**
* outline styles
*/
Expand Down Expand Up @@ -147,14 +159,6 @@
fill: var(--shape-drop-allowed-fill-color) !important;
}

svg.drop-not-ok {
background: var(--shape-drop-not-allowed-fill-color) !important;
}

svg.new-parent {
background: var(--shape-drop-allowed-fill-color) !important;
}


/* Override move cursor during drop and connect */
.drop-not-ok,
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;

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
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';


/**
* 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
Loading

0 comments on commit 29247c8

Please sign in to comment.