carbon-components
Version:
The Carbon Design System is IBM’s open-source design system for products and experiences.
200 lines (181 loc) • 6.29 kB
JavaScript
import NavigationMenuPanel from './navigation-menu-panel';
import on from '../../globals/js/misc/on';
import settings from '../../globals/js/settings';
import onFocusOutByKeyboard from '../../globals/js/misc/on-focus-by-keyboard';
let seq = 0;
class ProductSwitcher extends NavigationMenuPanel {
/**
* A navigation menu.
* @extends NavigationMenuPanel
* @param {HTMLElement} element The element working as a selector.
* @param {object} [options] The component options.
* @param {string} [options.selectorInit] The CSS class to find product
* switchers
* @param {string} [options.attribInitTarget] The attribute name in the
* launcher buttons to find target product switcher
* @param {string} [options.classProductSwitcherExpanded] The CSS class
* for an expanded product switcher
*/
constructor(element, options) {
super(element, options);
this.manage(on(element, 'keydown', this._handleKeyDown));
this.manage(onFocusOutByKeyboard(element, 'blur', this._handleFocusOut));
}
/**
* id of currently active trigger button
* @type {string}
*/
current = '';
/**
* A magic string indicting that no product switcher should be selected.
* @type {string}
*/
static SELECT_NONE /* #__PURE_CLASS_PROPERTY__ */ =
'__carbon-product-switcher-launcher-NONE';
/**
* The list of the IDs of the trigger buttons that have been used.
* @type {Set}
*/
triggerButtonIds = new Set();
/**
* When losing focus of the element via keyboard, place focus back on
* trigger button
* @param {Event} event The event triggering this method
*/
_handleFocusOut = (event) => {
if (this.element.contains(event.relatedTarget)) {
return;
}
const currentTriggerButton = this.element.ownerDocument.getElementById(
this.current
);
if (
currentTriggerButton &&
event.relatedTarget &&
!event.relatedTarget.matches(this.options.selectorFloatingMenus)
) {
currentTriggerButton.focus();
}
};
/**
* @param {Event} event The event triggering this method
*/
_handleKeyDown = (event) => {
const isExpanded = !this.element.hasAttribute('hidden');
if (event.which === 27 && isExpanded) {
const triggerButton = this.current;
this.changeState(this.constructor.SELECT_NONE);
this.element.ownerDocument.getElementById(triggerButton).focus();
}
};
createdByLauncher = (event) => {
const isExpanded = this.element.classList.contains(
this.options.classProductSwitcherExpanded
);
const launcher = event.delegateTarget;
if (!launcher.id) {
launcher.id = `__carbon-product-switcher-launcher-${seq++}`;
}
const current = launcher.id;
this.changeState(
isExpanded && this.current === current
? this.constructor.SELECT_NONE
: current
);
};
/**
*
* @param {string} current
* @returns {boolean} true if given state is different from current state
*/
shouldStateBeChanged = (current) => this.current !== current;
/**
* Changes the expanded/collapsed state.
* @private
* @param {string} state The new state.
* @param {Function} callback Callback called when change in state completes.
*/
_changeState = (state, callback) => {
this.element.classList.toggle(
this.options.classProductSwitcherExpanded,
state !== this.constructor.SELECT_NONE
);
this.current = state;
if (this.current !== this.constructor.SELECT_NONE) {
this.triggerButtonIds.add(this.current);
}
// deactivate all other trigger buttons
this.triggerButtonIds.forEach((id) => {
const button = this.element.ownerDocument.getElementById(id);
const label = button.getAttribute(this.options.attribLabelExpand);
button.classList.remove(
this.options.classNavigationMenuPanelHeaderActionActive
);
button.setAttribute('aria-label', label);
button.setAttribute('title', label);
});
// set active trigger button attributes
const currentTriggerButton = this.element.ownerDocument.getElementById(
this.current
);
if (currentTriggerButton) {
const label = currentTriggerButton.getAttribute(
this.options.attribLabelCollapse
);
currentTriggerButton.classList.toggle(
this.options.classNavigationMenuPanelHeaderActionActive
);
currentTriggerButton.setAttribute('aria-label', label);
currentTriggerButton.setAttribute('title', label);
}
if (state !== this.constructor.SELECT_NONE) {
this.element.setAttribute('tabindex', '0');
this.element.focus();
} else {
this.element.setAttribute('tabindex', '-1');
}
callback();
};
release() {
this.triggerButtonIds.clear();
return super.release();
}
/**
* The map associating DOM element and ProductSwitcher instance.
* @member ProductSwitcher.components
* @type {WeakMap}
*/
static components /* #__PURE_CLASS_PROPERTY__ */ = new WeakMap();
/**
* The component options.
* If `options` is specified in the constructor,
* {@linkcode ProductSwitcher.create .create()}, or
* {@linkcode ProductSwitcher.init .init()},
* properties in this object are overridden for the instance being create and
* how {@linkcode ProductSwitcher.init .init()} works.
* @member ProductSwitcher.options
* @type {object}
* @property {string} selectorInit The CSS class to find popup navs.
* @property {string} attribInitTarget The attribute name in the
* launcher buttons to find target popup nav.
* @property {string[]} initEventNames The events that the component
* will handles
*/
static get options() {
const { prefix } = settings;
return Object.assign(Object.create(NavigationMenuPanel.options), {
selectorInit: '[data-product-switcher]',
selectorFloatingMenus: `
.${prefix}--overflow-menu-options,
.${prefix}--overflow-menu-options *,
.${prefix}--tooltip,
.${prefix}--tooltip *,
.flatpicker-calendar,
.flatpicker-calendar *
`,
attribInitTarget: 'data-product-switcher-target',
classProductSwitcherExpanded: `${prefix}--panel--expanded`,
});
}
}
export default ProductSwitcher;