coral-spectrum/coral-component-shell/src/scripts/ShellWorkspaces.js
/**
* Copyright 2019 Adobe. All rights reserved.
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. You may obtain a copy
* of the License at http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
* OF ANY KIND, either express or implied. See the License for the specific language
* governing permissions and limitations under the License.
*/
import {BaseComponent} from '../../../coral-base-component';
import {SelectableCollection} from '../../../coral-collection';
import {Select} from '../../../coral-component-select';
const CLASSNAME = '_coral-Shell-workspaces';
/**
@class Coral.Shell.Workspaces
@classdesc A Shell Workspaces component
@htmltag coral-shell-workspaces
@extends {HTMLElement}
@extends {BaseComponent}
*/
class ShellWorkspaces extends BaseComponent(HTMLElement) {
/** @ignore */
constructor() {
super();
// Events
this._delegateEvents({
'key:down [is="coral-shell-workspace"]': '_focusNextItem',
'key:right [is="coral-shell-workspace"]': '_focusNextItem',
'key:left [is="coral-shell-workspace"]': '_focusPreviousItem',
'key:up [is="coral-shell-workspace"]': '_focusPreviousItem',
'key:pageup [is="coral-shell-workspace"]': '_focusPreviousItem',
'key:pagedown [is="coral-shell-workspace"]': '_focusNextItem',
'key:home [is="coral-shell-workspace"]': '_focusFirstItem',
'key:end [is="coral-shell-workspace"]': '_focusLastItem',
// private
'coral-shell-workspace:_selectedchanged': '_onItemSelectedChanged',
'change ._coral-Shell-workspaces-select': '_onSelectChanged'
});
// Template
this._elements = {
select: new Select().set({
variant: 'quiet'
})
};
this._elements.select.classList.add('_coral-Shell-workspaces-select');
// Used for eventing
this._oldSelection = null;
// Init the collection mutation observer
this.items._startHandlingItems(true);
}
/**
The item collection.
@type {SelectableCollection}
@readonly
*/
get items() {
// Construct the collection on first request:
if (!this._items) {
this._items = new SelectableCollection({
host: this,
itemTagName: 'coral-shell-workspace',
itemBaseTagName: 'a',
onItemAdded: this._validateSelection,
onItemRemoved: this._validateSelection
});
}
return this._items;
}
/**
Returns the selected workspace.
@type {HTMLElement}
@readonly
*/
get selectedItem() {
return this.items._getLastSelected();
}
/** @private */
_validateSelection(item) {
// gets the current selection
const selection = this.items._getAllSelected();
const selectionCount = selection.length;
// if no item is currently selected, we need to find a candidate
if (selectionCount === 0) {
// gets the first candidate for selection
const selectable = this.items._getFirstSelectable();
if (selectable) {
selectable.setAttribute('selected', '');
}
}
// more items are selected, so we find a single item and deselect everything else
else if (selectionCount > 1) {
// By default, the last one stays selected
item = item || selection[selection.length - 1];
for (let i = 0 ; i < selectionCount ; i++) {
if (selection[i] !== item) {
// Don't trigger change events
this._preventTriggeringEvents = true;
selection[i].removeAttribute('selected');
}
}
// We can trigger change events again
this._preventTriggeringEvents = false;
}
// Sync select items under the hood
this._renderSelectItems();
this._triggerChangeEvent();
}
_renderSelectItems() {
this._elements.select.items.clear();
this.items.getAll().forEach((item) => {
this._elements.select.items.add({
content: {
innerHTML: item.innerHTML
},
selected: item.hasAttribute('selected'),
_workspace: item
});
});
}
_onSelectChanged(event) {
event.stopImmediatePropagation();
this._elements.select.selectedItem._workspace.selected = true;
}
/** @private */
_triggerChangeEvent() {
const selectedItem = this.selectedItem;
const oldSelection = this._oldSelection;
if (!this._preventTriggeringEvents && selectedItem !== oldSelection) {
this.trigger('coral-shell-workspaces:change', {
oldSelection: oldSelection,
selection: selectedItem
});
this._oldSelection = selectedItem;
}
}
/** @private */
_onItemSelectedChanged(event) {
event.stopImmediatePropagation();
const item = event.target;
this._validateSelection(item);
}
/**
Returns true if the event is at the matched target.
@private
*/
_eventIsAtTarget(event) {
const target = event.target;
const listItem = event.matchedTarget;
const isAtTarget = target === listItem;
if (isAtTarget) {
// Don't let arrow keys etc scroll the page
event.preventDefault();
event.stopPropagation();
}
return isAtTarget;
}
/** @private */
_focusNextItem(event) {
if (!this._eventIsAtTarget(event)) {
return;
}
const target = event.matchedTarget;
if (target.nextElementSibling) {
target.nextElementSibling.focus();
} else {
this.items.first().focus();
}
}
/** @private */
_focusPreviousItem(event) {
if (!this._eventIsAtTarget(event)) {
return;
}
const target = event.matchedTarget;
if (target.previousElementSibling) {
target.previousElementSibling.focus();
} else {
this.items.last().focus();
}
}
/** @private */
_focusFirstItem(event) {
if (!this._eventIsAtTarget(event)) {
return;
}
this.items.first().focus();
}
/** @private */
_focusLastItem(event) {
if (!this._eventIsAtTarget(event)) {
return;
}
this.items.last().focus();
}
/** @ignore */
render() {
super.render();
this.classList.add(CLASSNAME);
// Don't trigger events once connected
this._preventTriggeringEvents = true;
this._validateSelection();
this._preventTriggeringEvents = false;
this._oldSelection = this.selectedItem;
// Support cloneNode
const template = this.querySelector('._coral-Shell-workspaces-select');
if (template) {
template.remove();
}
this.appendChild(this._elements.select);
}
}
export default ShellWorkspaces;