coral-spectrum/coral-component-colorinput/src/scripts/ColorInputSwatches.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 BaseColorInputAbstractSubview from './BaseColorInputAbstractSubview';
import ColorInputSwatch from './ColorInputSwatch';
import Color from './Color';
import {SelectableCollection} from '../../../coral-collection';
import swatchesHeader from '../templates/swatchesHeader';
import {commons, i18n} from '../../../coral-utils';
import {Decorator} from '../../../coral-decorator';
const CLASSNAME = '_coral-ColorInput-swatches';
/**
@class Coral.ColorInput.Swatches
@classdesc A ColorInput Swatches component
@htmltag coral-colorinput-swatches
@extends {HTMLElement}
@extends {BaseComponent}
@extends {BaseColorInputAbstractSubview}
*/
const ColorInputSwatches = Decorator(class extends BaseColorInputAbstractSubview(BaseComponent(HTMLElement)) {
/** @ignore */
constructor() {
super();
// Events
this._delegateEvents(commons.extend(this._events, {
'click coral-colorinput-swatch': '_onSwatchClicked',
'keydown ._coral-ColorInput-swatch': '_onKeyDown',
'capture:focus coral-colorinput-swatch': '_onFocus',
// private
'coral-colorinput-swatch:_selectedchanged': '_onItemSelectedChanged'
}));
// Templates
this._elements = {};
swatchesHeader.call(this._elements, {commons, i18n});
// Used for eventing
this._oldSelection = null;
// Init the collection mutation observer
this.items._startHandlingItems(true);
}
/**
The Collection Interface that allows interacting with the items that the component contains.
@type {SelectableCollection}
@readonly
*/
get items() {
// just init on demand
if (!this._items) {
this._items = new SelectableCollection({
host: this,
itemTagName: 'coral-colorinput-swatch',
onItemAdded: this._validateSelection
});
}
return this._items;
}
/**
The selected item.
@type {HTMLElement}
@readonly
*/
get selectedItem() {
return this.items._getLastSelected();
}
/** @private */
_onItemSelectedChanged(event) {
event.stopImmediatePropagation();
this._validateSelection(event.target);
}
/** @private */
_validateSelection(item) {
const selectedItems = this.items._getAllSelected();
// Last selected item wins
item = item || selectedItems[selectedItems.length - 1];
if (item && item.hasAttribute('selected') && selectedItems.length > 1) {
selectedItems.forEach((selectedItem) => {
if (selectedItem !== item) {
// Don't trigger change events
this._preventTriggeringEvents = true;
selectedItem.removeAttribute('selected');
}
});
// We can trigger change events again
this._preventTriggeringEvents = false;
}
this._triggerChangeEvent();
}
/** @private */
_triggerChangeEvent() {
const selectedItem = this.selectedItem;
const oldSelection = this._oldSelection;
if (!this._preventTriggeringEvents && selectedItem !== oldSelection) {
// update hidden fields
if (selectedItem) {
this.value = selectedItem.value;
}
this.trigger('coral-colorinput-swatches:change', {
oldSelection: oldSelection,
selection: selectedItem
});
this._oldSelection = selectedItem;
}
}
/** @ignore */
_beforeOverlayOpen() {
// relayout swatches if items have been added/removed/moved...
const colorElements = this._colorinput.items.getAll();
let colorsElementsChanged = false;
if (!this._cachedColorElements) {
colorsElementsChanged = true;
} else if (this._cachedColorElements.length !== colorElements.length) {
colorsElementsChanged = true;
} else if (this._cachedColorElements.length === colorElements.length) {
for (let i = 0 ; i < colorElements.length ; i++) {
if (this._cachedColorElements[i] !== colorElements[i]) {
colorsElementsChanged = true;
break;
}
}
}
this._cachedColorElements = colorElements;
if (colorsElementsChanged) {
this._layoutColorSwatch();
}
this._ensureKeyboardAccess();
}
/** @ignore */
_onColorInputChange() {
this._ensureKeyboardAccess();
}
/**
If no swatch is selected, make sure that the first swatch is tabbable
@ignore
*/
_ensureKeyboardAccess() {
if (!this.querySelector('coral-colorinput-swatch[selected]')) {
const firstSwatch = this.querySelector('coral-colorinput-swatch');
if (firstSwatch) {
firstSwatch.tabIndex = 0;
}
}
}
/** @ignore */
_layoutColorSwatch() {
// Clear container before adding elements to avoid multiple addition
this._elements.swatchesContainer.innerHtml = '';
const colors = this._colorinput.items.getAll();
const colorsLength = colors.length;
let swatchSelected = false;
for (let colorCount = 0 ; colorCount < colorsLength ; colorCount++) {
const color = colors[colorCount];
const swatch = new ColorInputSwatch();
this._elements.swatchesContainer.appendChild(swatch);
swatch.targetColor = color;
if (color.selected) {
swatch[color.selected ? 'setAttribute' : 'removeAttribute']('selected', color.selected);
swatchSelected = true;
}
// Update color button tabindex depending on selected state
swatch.tabIndex = swatch.selected ? 0 : -1;
}
// If no swatch is selected, make sure that the first swatch is focusable
if (!swatchSelected) {
this._ensureKeyboardAccess();
}
}
/** @ignore */
_onSwatchClicked(event) {
event.stopPropagation();
const colorButton = event.target;
const swatch = colorButton.closest('coral-colorinput-swatch');
if (!swatch.selected) {
const color = new Color();
color.value = swatch.targetColor ? swatch.targetColor.value : '';
this._colorinput._setActiveColor(color);
swatch.selected = true;
}
swatch.firstChild.focus();
}
/** @ignore */
_onKeyDown(event) {
const overlay = this._colorinput._elements.overlay;
// only if overlay is open
if (!overlay.open) {
return;
}
const allItems = this.items.getAll();
const currentIndex = allItems.indexOf(event.matchedTarget);
let preventDefault = true;
let newIndex = currentIndex;
switch (event.which) {
// return
case 13:
// Wait a frame before closing so that focus is restored correctly
window.requestAnimationFrame(() => {
overlay.open = false;
});
break;
// left arrow
case 37:
newIndex -= 1;
break;
// up arrow
case 38:
newIndex -= 4;
break;
// right arrow
case 39:
newIndex += 1;
break;
// down arrow
case 40:
newIndex += 4;
break;
default:
preventDefault = false;
break;
}
// If any action has been taken prevent event propagation
if (preventDefault) {
event.preventDefault();
if (newIndex < 0 || newIndex >= allItems.length) {
return;
}
// show right page in carousel and focus right swatch
const swatch = allItems[newIndex];
const color = new Color();
color.value = swatch.targetColor ? swatch.targetColor.value : '';
this._colorinput._setActiveColor(color);
swatch.selected = true;
swatch.firstChild.focus();
}
}
/**
Ensure that only one swatch can receive tab focus at a time
@ignore
*/
_onFocus(event) {
const allItems = this.items.getAll();
for (let i = 0 ; i < allItems.length ; i++) {
const swatch = allItems[i];
if (!swatch.contains(event.matchedTarget)) {
swatch.tabIndex = -1;
}
}
event.matchedTarget.tabIndex = 0;
if (document.activeElement !== event.matchedTarget.firstChild) {
event.matchedTarget.firstChild.focus();
}
}
/** @ignore */
render() {
super.render();
this.classList.add(CLASSNAME);
// adds the role to support accessibility
this.setAttribute('role', 'listbox');
// Support cloneNode
const swatchesSubview = this.querySelector('._coral-ColorInput-swatchesSubview');
if (swatchesSubview) {
swatchesSubview.remove();
}
// add header
this.appendChild(this._elements.swatchesSubview);
// add accessibility label
this.setAttribute('aria-labelledby', this._elements.swatchesHeaderTitle.id);
// Don't trigger events once connected
this._preventTriggeringEvents = true;
this._validateSelection();
this._preventTriggeringEvents = false;
this._oldSelection = this.selectedItem;
}
});
export default ColorInputSwatches;