UNPKG

@jupyter/web-components

Version:

A component library for building extensions in Jupyter frontends.

301 lines (300 loc) 10 kB
// Copyright (c) Jupyter Development Team. // Copyright (c) Microsoft Corporation. // Distributed under the terms of the Modified BSD License. import { __decorate } from "tslib"; import { Observable, attr, observable } from '@microsoft/fast-element'; import { ARIAGlobalStatesAndProperties, FoundationElement, StartEnd, applyMixins, getDirection, toolbarTemplate as template } from '@microsoft/fast-foundation'; import { ArrowKeys, Direction, Orientation, limit } from '@microsoft/fast-web-utilities'; import { isFocusable } from 'tabbable'; import { toolbarStyles as styles } from './toolbar.styles.js'; /** * A map for directionality derived from keyboard input strings, * visual orientation, and text direction. * * @internal */ const ToolbarArrowKeyMap = Object.freeze({ [ArrowKeys.ArrowUp]: { [Orientation.vertical]: -1 }, [ArrowKeys.ArrowDown]: { [Orientation.vertical]: 1 }, [ArrowKeys.ArrowLeft]: { [Orientation.horizontal]: { [Direction.ltr]: -1, [Direction.rtl]: 1 } }, [ArrowKeys.ArrowRight]: { [Orientation.horizontal]: { [Direction.ltr]: 1, [Direction.rtl]: -1 } } }); /** * A Toolbar Custom HTML Element. * Implements the {@link https://w3c.github.io/aria-practices/#Toolbar|ARIA Toolbar}. * * @slot start - Content which can be provided before the slotted items * @slot end - Content which can be provided after the slotted items * @slot - The default slot for slotted items * @slot label - The toolbar label * @csspart positioning-region - The element containing the items, start and end slots * * @public */ export class FoundationToolbar extends FoundationElement { constructor() { super(...arguments); /** * The internal index of the currently focused element. * * @internal */ this._activeIndex = 0; /** * The text direction of the toolbar. * * @internal */ this.direction = Direction.ltr; /** * The orientation of the toolbar. * * @public * @remarks * HTML Attribute: `orientation` */ this.orientation = Orientation.horizontal; } /** * The index of the currently focused element, clamped between 0 and the last element. * * @internal */ get activeIndex() { Observable.track(this, 'activeIndex'); return this._activeIndex; } set activeIndex(value) { if (this.$fastController.isConnected) { this._activeIndex = limit(0, this.focusableElements.length - 1, value); Observable.notify(this, 'activeIndex'); } } slottedItemsChanged() { if (this.$fastController.isConnected) { this.reduceFocusableElements(); } } /** * Set the activeIndex when a focusable element in the toolbar is clicked. * * @internal */ mouseDownHandler(e) { var _a; const activeIndex = (_a = this.focusableElements) === null || _a === void 0 ? void 0 : _a.findIndex(x => x.contains(e.target)); if (activeIndex > -1 && this.activeIndex !== activeIndex) { this.setFocusedElement(activeIndex); } return true; } childItemsChanged(prev, next) { if (this.$fastController.isConnected) { this.reduceFocusableElements(); } } /** * @internal */ connectedCallback() { super.connectedCallback(); this.direction = getDirection(this); } /** * When the toolbar receives focus, set the currently active element as focused. * * @internal */ focusinHandler(e) { const relatedTarget = e.relatedTarget; if (!relatedTarget || this.contains(relatedTarget)) { return; } this.setFocusedElement(); } /** * Determines a value that can be used to iterate a list with the arrow keys. * * @param this - An element with an orientation and direction * @param key - The event key value * @internal */ getDirectionalIncrementer(key) { var _a, _b, _c, _d, _e; return ( // @ts-expect-error ToolbarArrowKeyMap has not index (_e = (_c = (_b = (_a = ToolbarArrowKeyMap[key]) === null || _a === void 0 ? void 0 : _a[this.orientation]) === null || _b === void 0 ? void 0 : _b[this.direction]) !== null && _c !== void 0 ? _c : // @ts-expect-error ToolbarArrowKeyMap has not index (_d = ToolbarArrowKeyMap[key]) === null || _d === void 0 ? void 0 : _d[this.orientation]) !== null && _e !== void 0 ? _e : 0); } /** * Handle keyboard events for the toolbar. * * @internal */ keydownHandler(e) { const key = e.key; if (!(key in ArrowKeys) || e.defaultPrevented || e.shiftKey) { return true; } const incrementer = this.getDirectionalIncrementer(key); if (!incrementer) { return !e.target.closest('[role=radiogroup]'); } const nextIndex = this.activeIndex + incrementer; if (this.focusableElements[nextIndex]) { e.preventDefault(); } this.setFocusedElement(nextIndex); return true; } /** * get all the slotted elements * @internal */ get allSlottedItems() { return [ ...this.start.assignedElements(), ...this.slottedItems, ...this.end.assignedElements() ]; } /** * Prepare the slotted elements which can be focusable. * * @internal */ reduceFocusableElements() { var _a; const previousFocusedElement = (_a = this.focusableElements) === null || _a === void 0 ? void 0 : _a[this.activeIndex]; this.focusableElements = this.allSlottedItems.reduce(FoundationToolbar.reduceFocusableItems, []); // If the previously active item is still focusable, adjust the active index to the // index of that item. const adjustedActiveIndex = this.focusableElements.indexOf(previousFocusedElement); this.activeIndex = Math.max(0, adjustedActiveIndex); this.setFocusableElements(); } /** * Set the activeIndex and focus the corresponding control. * * @param activeIndex - The new index to set * @internal */ setFocusedElement(activeIndex = this.activeIndex) { this.activeIndex = activeIndex; this.setFocusableElements(); if (this.focusableElements[this.activeIndex] && // Don't focus the toolbar element if some event handlers moved // the focus on another element in the page. this.contains(document.activeElement)) { this.focusableElements[this.activeIndex].focus(); } } /** * Reduce a collection to only its focusable elements. * * @param elements - Collection of elements to reduce * @param element - The current element * * @internal */ static reduceFocusableItems(elements, element) { var _a, _b, _c, _d; const isRoleRadio = element.getAttribute('role') === 'radio'; const isFocusableFastElement = (_b = (_a = element.$fastController) === null || _a === void 0 ? void 0 : _a.definition.shadowOptions) === null || _b === void 0 ? void 0 : _b.delegatesFocus; const hasFocusableShadow = Array.from((_d = (_c = element.shadowRoot) === null || _c === void 0 ? void 0 : _c.querySelectorAll('*')) !== null && _d !== void 0 ? _d : []).some(x => isFocusable(x)); if (!element.hasAttribute('disabled') && !element.hasAttribute('hidden') && (isFocusable(element) || isRoleRadio || isFocusableFastElement || hasFocusableShadow)) { elements.push(element); return elements; } if (element.childElementCount) { return elements.concat(Array.from(element.children).reduce(FoundationToolbar.reduceFocusableItems, [])); } return elements; } /** * @internal */ setFocusableElements() { if (this.$fastController.isConnected && this.focusableElements.length > 0) { this.focusableElements.forEach((element, index) => { element.tabIndex = this.activeIndex === index ? 0 : -1; }); } } } __decorate([ observable ], FoundationToolbar.prototype, "direction", void 0); __decorate([ attr ], FoundationToolbar.prototype, "orientation", void 0); __decorate([ observable ], FoundationToolbar.prototype, "slottedItems", void 0); __decorate([ observable ], FoundationToolbar.prototype, "slottedLabel", void 0); __decorate([ observable ], FoundationToolbar.prototype, "childItems", void 0); /** * Includes ARIA states and properties relating to the ARIA toolbar role * * @public */ export class DelegatesARIAToolbar { } __decorate([ attr({ attribute: 'aria-labelledby' }) ], DelegatesARIAToolbar.prototype, "ariaLabelledby", void 0); __decorate([ attr({ attribute: 'aria-label' }) ], DelegatesARIAToolbar.prototype, "ariaLabel", void 0); applyMixins(DelegatesARIAToolbar, ARIAGlobalStatesAndProperties); applyMixins(FoundationToolbar, StartEnd, DelegatesARIAToolbar); /** * @public * @tagname jp-toolbar */ export class Toolbar extends FoundationToolbar { } /** * A function that returns a {@link @microsoft/fast-foundation#Toolbar} registration for configuring the component with a DesignSystem. * Implements {@link @microsoft/fast-foundation#toolbarTemplate} * * @public * @remarks * * Generates HTML Element: `<jp-toolbar>` * */ export const jpToolbar = Toolbar.compose({ baseName: 'toolbar', baseClass: FoundationToolbar, template, styles, shadowOptions: { delegatesFocus: true } }); export { styles as toolbarStyles };