Reference Source

coral-spectrum/coral-component-tabview/src/scripts/TabView.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 '../../../coral-component-panelstack';
import '../../../coral-component-tablist';
import {commons} from '../../../coral-utils';
import {Decorator} from '../../../coral-decorator';

/**
 Enumeration for {@link TabView} orientations.

 @typedef {Object} TabViewOrientationEnum

 @property {String} HORIZONTAL
 Tabs on top of the panels. This is the default.
 @property {String} VERTICAL
 Tabs are rendered on the side and match the height of the panels.
 */

const orientation = {
  HORIZONTAL: 'horizontal',
  VERTICAL: 'vertical'
};

// the tabview's base classname
const CLASSNAME = '_coral-TabView';

/**
 @class Coral.TabView
 @classdesc A TabView component is the wrapping container used to create the typical Tabbed pattern.
 This is intended to be used with a {@link TabList} and {@link PanelStack}.
 @htmltag coral-tabview
 @extends {HTMLElement}
 @extends {BaseComponent}
 */
const TabView = Decorator(class extends BaseComponent(HTMLElement) {
  /** @ignore */
  constructor() {
    super();

    // Prepare templates
    this._elements = {
      // Fetch or create the content zone elements
      tabList: this.querySelector('coral-tablist') || document.createElement('coral-tablist'),
      panelStack: this.querySelector('coral-panelstack') || document.createElement('coral-panelstack')
    };

    // Events
    this._delegateEvents({
      'coral-tablist:change > coral-tablist': '_onTabListChange',
      'coral-panelstack:change > coral-panelstack': '_onPanelStackChange',
      'coral-collection:add > coral-tablist': '_syncTabListAndPanelStack',
      'coral-collection:remove > coral-tablist': '_syncTabListAndPanelStack',
      'coral-collection:add > coral-panelstack': '_syncTabListAndPanelStack',
      'coral-collection:remove > coral-panelstack': '_syncTabListAndPanelStack'
    });
  }

  /**
   The TabView's orientation. See {@link TabViewOrientationEnum}.

   @type {String}
   @default TabViewOrientationEnum.HORIZONTAL
   @htmlattribute orientation
   @htmlattributereflected
   */
  get orientation() {
    return this._elements.tabList.getAttribute('orientation') || orientation.HORIZONTAL;
  }

  set orientation(value) {
    // We rely on the tablist orientation enum so don't need to double check enums
    this._elements.tabList.setAttribute('orientation', value);
    this._reflectAttribute('orientation', this.orientation);

    this.classList[this.orientation === orientation.VERTICAL ? 'add' : 'remove'](`${CLASSNAME}--vertical`);
  }

  /**
   The TabList which handles all the tabs.

   @type {TabList}
   @contentzone
   */
  get tabList() {
    return this._getContentZone(this._elements.tabList);
  }

  set tabList(value) {
    // Support nested coral-tablist
    if (value instanceof HTMLElement && !value.parentNode || value.parentNode === this) {
      this._setContentZone('tabList', value, {
        handle: 'tabList',
        tagName: 'coral-tablist',
        insert: function (tabs) {
          tabs.setAttribute('tracking', 'off');
          this.insertBefore(tabs, this._elements.panelStack || null);
        }
      });
    }
  }

  /**
   The PanelStack which contains all the panels.

   @type {PanelStack}
   @contentzone
   */
  get panelStack() {
    return this._getContentZone(this._elements.panelStack);
  }

  set panelStack(value) {
    // Support nested coral-panelstack
    if (value instanceof HTMLElement && !value.parentNode || value.parentNode === this) {
      this._setContentZone('panelStack', value, {
        handle: 'panelStack',
        tagName: 'coral-panelstack',
        insert: function (panels) {
          this.appendChild(panels);
          this._onNewPanelStack(panels);
        }
      });
    }
  }

  /**
   * This helps in syncing the tablist with new panelstack.
   * This helpful when panelstack is changed for tabview dynamically. 
   * @param {PanelStack} panels new/updated panelstack
   */
  _onNewPanelStack(panels) {
    const tabs = this._elements.tabList;

    // Bind the tablist and panel stack together, using the panel id
    panels.id = panels.id || commons.getUID();
    tabs.setAttribute('target', `#${panels.id}`);

    if(tabs.selectedItem) {
      tabs.selectedItem.selected = true;
    }
  }

  /**
   Detects a change in the TabList and triggers an event.

   @private
   */
  _onTabListChange(event) {
    this.trigger('coral-tabview:change', {
      selection: event.detail.selection,
      oldSelection: event.detail.oldSelection
    });
  }

  /** @private */
  _onPanelStackChange(event) {
    // everytime the panelstack changes, we verify that the tablist and panelstack are up to date
    if (event.detail.selection) {
      const tabSelector = event.detail.selection.getAttribute('aria-labelledby');
      const tab = document.getElementById(tabSelector);

      // we select the tab if this was not the case
      if (tab) {
        if (!tab.hasAttribute('selected')) {
          tab.setAttribute('selected', '');
        } else {
          this._trackEvent('display', 'coral-tab', event, event.detail.selection);
        }
      }
    }
  }

  /** @private */
  _syncTabListAndPanelStack() {
    this._elements.tabList.target = this._elements.tabList.target;
  }

  get _contentZones() {
    return {
      'coral-tablist': 'tabList',
      'coral-panelstack': 'panelStack'
    };
  }

  /**
   Returns {@link TabView} orientation options.

   @return {TabViewOrientationEnum}
   */
  static get orientation() {
    return orientation;
  }

  /** @ignore */
  static get observedAttributes() {
    return super.observedAttributes.concat(['orientation']);
  }

  /** @ignore */
  render() {
    super.render();

    this.classList.add(CLASSNAME);

    // Default reflected attributes
    if (!this._orientation) {
      this.orientation = this.orientation;
    }

    // Fetch or create the content zone elements
    const tabs = this._elements.tabList;
    const panels = this._elements.panelStack;

    // Bind the tablist and panel stack together, using the panel id
    panels.id = panels.id || commons.getUID();
    tabs.setAttribute('target', `#${panels.id}`);

    // Assign the content zones.
    this.panelStack = panels;
    this.tabList = tabs;
  }

  /**
   Triggered when the {@link TabView} selected tab panel item has changed.

   @typedef {CustomEvent} coral-tabview:change

   @property {Tab} event.detail.selection
   The new selected tab panel item.
   @param {Tab} event.detail.oldSelection
   The prior selected tab panel item.
   */
});

export default TabView;