/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ import { html, staticHtml, literal } from "../vendor/lit.all.mjs"; import { MozLitElement } from "../lit-utils.mjs"; export const GROUP_TYPES = { list: "list", reorderable: "reorderable-list", }; /** * An element used to group combinations of moz-box-item, moz-box-link, and * moz-box-button elements and provide the expected styles. * * @tagname moz-box-group * @property {string} type * The type of the group, either "list", "reorderable-list", or undefined. * Note that "reorderable-list" only works with moz-box-item elements for now. * @slot default - Slot for rendering various moz-box-* elements. * @slot static - Slot for rendering non-reorderable moz-box-item elements. * @slot - Slots used to assign moz-box-* elements to
  • elements when * the group is type="list". * @slot * Slots used to render moz-box-item elements that are not intended to be reorderable * when the group is type="reorderable-list". * @fires reorder * Fired when items are reordered via drag-and-drop or keyboard shortcuts. * The detail object contains draggedElement, targetElement, position, draggedIndex, and targetIndex. */ export default class MozBoxGroup extends MozLitElement { #tabbable = true; static properties = { type: { type: String }, listItems: { type: Array, state: true }, staticItems: { type: Array, state: true }, }; static queries = { reorderableList: "moz-reorderable-list", headerSlot: "slot[name='header']", footerSlot: "slot[name='footer']", }; constructor() { super(); /** @type {Element[]} */ this.listItems = []; /** @type {Element[]} */ this.staticItems = []; this.listMutationObserver = new MutationObserver( this.updateItems.bind(this) ); } firstUpdated(changedProperties) { super.firstUpdated(changedProperties); this.listMutationObserver.observe(this, { attributeFilter: ["hidden"], subtree: true, childList: true, }); this.updateItems(); } contentTemplate() { if (this.type == GROUP_TYPES.reorderable) { return html` ${this.slotTemplate()} `; } return this.slotTemplate(); } slotTemplate() { let isReorderable = this.type == GROUP_TYPES.reorderable; if (this.type == GROUP_TYPES.list || isReorderable) { let listTag = isReorderable ? literal`ol` : literal`ul`; return staticHtml`<${listTag} tabindex="-1" class="list scroll-container" aria-orientation="vertical" @keydown=${this.handleKeydown} @focusin=${this.handleFocus} @focusout=${this.handleBlur} > ${this.listItems.map((_, i) => { return html`
  • `; })} ${this.staticItems?.map((_, i) => { return html`
  • `; })} ${isReorderable ? html`` : ""}`; } return html`
    `; } /** * Handles reordering of items in the list. * * @param {object} event - Event object or wrapper containing detail from moz-reorderable-list. * @param {object} event.detail - Detail object from moz-reorderable-list.evaluateKeyDownEvent or drag-and-drop event. * @param {Element} event.detail.draggedElement - The element being reordered. * @param {Element} event.detail.targetElement - The target element to reorder relative to. * @param {number} event.detail.position - Position relative to target (-1 for before, 0 for after). * @param {number} event.detail.draggedIndex - The index of the element being reordered. * @param {number} event.detail.targetIndex - The new index of the draggedElement. */ handleReorder(event) { let { targetIndex } = event.detail; this.dispatchEvent( new CustomEvent("reorder", { bubbles: true, detail: event.detail, }) ); /** * Without requesting an animation frame, we will lose focus within * the box group when using Ctrl + Shift + ArrowDown. The focus will * move to the browser chrome which is unexpected. * */ requestAnimationFrame(() => { this.listItems[targetIndex]?.focus(); }); } handleKeydown(event) { if ( this.type == GROUP_TYPES.reorderable && event.originalTarget == event.target.handleEl ) { let detail = this.reorderableList.evaluateKeyDownEvent(event); if (detail) { event.stopPropagation(); this.handleReorder({ detail }); return; } } let positionElement = event.target.closest("[position]"); if (!positionElement) { // If the user has clicked on the MozBoxGroup it may get keydown events // even if there is no focused element within it. Then the event target // will be the