From e2f6d7837f78b92e30b0f055fa15472638562def Mon Sep 17 00:00:00 2001 From: Steven Spriggs Date: Tue, 8 Aug 2023 12:31:21 -0400 Subject: [PATCH 01/50] feat(tabs): implement tabs controller feat(tabs): implement tabs controller --- .../controllers/overflow-controller.ts | 37 ++- .../controllers/roving-tabindex-controller.ts | 10 +- elements/pf-tabs/BaseTab.ts | 96 ------- elements/pf-tabs/BaseTabPanel.ts | 35 --- elements/pf-tabs/BaseTabs.ts | 270 ------------------ elements/pf-tabs/TabsController.ts | 236 +++++++++++++++ elements/pf-tabs/pf-tab-panel.ts | 34 ++- elements/pf-tabs/pf-tab.css | 10 +- elements/pf-tabs/pf-tab.ts | 73 ++++- elements/pf-tabs/pf-tabs.ts | 94 +++++- 10 files changed, 460 insertions(+), 435 deletions(-) delete mode 100644 elements/pf-tabs/BaseTab.ts delete mode 100644 elements/pf-tabs/BaseTabPanel.ts delete mode 100644 elements/pf-tabs/BaseTabs.ts create mode 100644 elements/pf-tabs/TabsController.ts diff --git a/core/pfe-core/controllers/overflow-controller.ts b/core/pfe-core/controllers/overflow-controller.ts index 62899f0f35..2f3f7c345c 100644 --- a/core/pfe-core/controllers/overflow-controller.ts +++ b/core/pfe-core/controllers/overflow-controller.ts @@ -3,20 +3,38 @@ import type { ReactiveController, ReactiveControllerHost } from 'lit'; import { isElementInView } from '@patternfly/pfe-core/functions/isElementInView.js'; export interface Options { + /** + * Force hide the scroll buttons regardless of overflow + */ hideOverflowButtons?: boolean; + /** + * Delay in ms to wait before checking for overflow + */ + scrollTimeoutDelay?: number; } export class OverflowController implements ReactiveController { + static #instances = new Set(); + + static { + // on resize check for overflows to add or remove scroll buttons + window.addEventListener('resize', () => { + for (const instance of this.#instances) { + instance.onScroll(); + } + }, { capture: false }); + } + /** Overflow container */ #container?: HTMLElement; /** Children that can overflow */ #items: HTMLElement[] = []; - #scrollTimeoutDelay = 0; + #scrollTimeoutDelay: number; #scrollTimeout?: ReturnType; /** Default state */ - #hideOverflowButtons = false; + #hideOverflowButtons: boolean; showScrollButtons = false; overflowLeft = false; overflowRight = false; @@ -31,8 +49,10 @@ export class OverflowController implements ReactiveController { constructor(public host: ReactiveControllerHost & Element, private options?: Options) { this.host.addController(this); - if (options?.hideOverflowButtons) { - this.#hideOverflowButtons = options?.hideOverflowButtons; + this.#hideOverflowButtons = options?.hideOverflowButtons ?? false; + this.#scrollTimeoutDelay = options?.scrollTimeoutDelay ?? 0; + if (host.isConnected) { + OverflowController.#instances.add(this); } } @@ -40,6 +60,9 @@ export class OverflowController implements ReactiveController { if (!this.firstItem || !this.lastItem || !this.#container) { return; } + const prevLeft = this.overflowLeft; + const prevRight = this.overflowRight; + this.overflowLeft = !this.#hideOverflowButtons && !isElementInView(this.#container, this.firstItem); this.overflowRight = !this.#hideOverflowButtons && !isElementInView(this.#container, this.lastItem); let scrollButtonsWidth = 0; @@ -48,7 +71,11 @@ export class OverflowController implements ReactiveController { } this.showScrollButtons = !this.#hideOverflowButtons && this.#container.scrollWidth > (this.#container.clientWidth + scrollButtonsWidth); - this.host.requestUpdate(); + + // only request update if there has been a change + if ((prevLeft !== this.overflowLeft) || (prevRight !== this.overflowRight)) { + this.host.requestUpdate(); + } } init(container: HTMLElement, items: HTMLElement[]) { diff --git a/core/pfe-core/controllers/roving-tabindex-controller.ts b/core/pfe-core/controllers/roving-tabindex-controller.ts index e4a2ca53f1..16846432b2 100644 --- a/core/pfe-core/controllers/roving-tabindex-controller.ts +++ b/core/pfe-core/controllers/roving-tabindex-controller.ts @@ -198,13 +198,15 @@ export class RovingTabindexController< /** * from array of HTML items, and sets active items */ - initItems(items: ItemType[], itemsContainer: HTMLElement = this.host) { + initItems(items: ItemType[], itemsContainer: HTMLElement = this.host, setActiveItem = true) { this.#items = items ?? []; const focusableItems = this.#focusableItems; const [focusableItem] = focusableItems; - this.#activeItem = focusableItem; - for (const item of focusableItems) { - item.tabIndex = this.#activeItem === item ? 0 : -1; + if (setActiveItem) { + this.#activeItem = focusableItem; + for (const item of focusableItems) { + item.tabIndex = this.#activeItem === item ? 0 : -1; + } } /** * removes listener on previous contained and applies it to new container diff --git a/elements/pf-tabs/BaseTab.ts b/elements/pf-tabs/BaseTab.ts deleted file mode 100644 index 0065c82f55..0000000000 --- a/elements/pf-tabs/BaseTab.ts +++ /dev/null @@ -1,96 +0,0 @@ -import type { PropertyValues } from 'lit'; - -import { LitElement, html } from 'lit'; -import { queryAssignedElements } from 'lit/decorators/query-assigned-elements.js'; -import { query } from 'lit/decorators/query.js'; - -import { getRandomId } from '@patternfly/pfe-core/functions/random.js'; -import { ComposedEvent } from '@patternfly/pfe-core'; - -import style from './BaseTab.css'; - -export class TabExpandEvent extends ComposedEvent { - constructor( - public active: boolean, - public tab: BaseTab, - ) { - super('expand'); - } -} - -export abstract class BaseTab extends LitElement { - static readonly styles = [style]; - - static shadowRootOptions = { ...LitElement.shadowRootOptions, delegatesFocus: true }; - - @queryAssignedElements({ slot: 'icon', flatten: true }) - private icons!: Array; - - @query('button') private button!: HTMLButtonElement; - - /** `active` should be observed, and true when the tab is selected */ - abstract active: boolean; - - /** `active` should be observed, and true when the tab is disabled */ - abstract disabled: boolean; - - #internals = this.attachInternals(); - - connectedCallback() { - super.connectedCallback(); - this.id ||= getRandomId(this.localName); - this.addEventListener('click', this.#clickHandler); - this.#internals.role = 'tab'; - } - - render() { - return html` - - `; - } - - updated(changed: PropertyValues) { - this.#internals.ariaSelected = String(this.ariaSelected); - if (changed.has('active')) { - this.#activeChanged(); - } - if (changed.has('disabled')) { - this.#disabledChanged(); - } - } - - focus() { - this.button.focus(); - } - - #clickHandler() { - if (!this.disabled && this.#internals.ariaDisabled !== 'true' && this.ariaDisabled !== 'true') { - this.active = true; - this.focus(); // safari fix - } - } - - #activeChanged() { - if (this.active && !this.disabled) { - this.#internals.ariaSelected = 'true'; - } else { - this.#internals.ariaSelected = 'false'; - } - this.dispatchEvent(new TabExpandEvent(this.active, this)); - } - - /** - * if a tab is disabled, then it is also aria-disabled - * if a tab is removed from disabled its not necessarily - * not still aria-disabled so we don't remove the aria-disabled - */ - #disabledChanged() { - this.#internals.ariaDisabled = String(!!this.disabled); - } -} diff --git a/elements/pf-tabs/BaseTabPanel.ts b/elements/pf-tabs/BaseTabPanel.ts deleted file mode 100644 index ce6e3441e5..0000000000 --- a/elements/pf-tabs/BaseTabPanel.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { LitElement, html } from 'lit'; - -import style from './BaseTabPanel.css'; - -import { getRandomId } from '@patternfly/pfe-core/functions/random.js'; - -export abstract class BaseTabPanel extends LitElement { - static readonly styles = [style]; - - #internals = this.attachInternals(); - - render() { - return html` - - `; - } - - connectedCallback() { - super.connectedCallback(); - this.id ||= getRandomId('pf-tab-panel'); - this.hidden ??= true; - this.#internals.role = 'tabpanel'; - - /* - To make it easy for screen reader users to navigate from a tab - to the beginning of content in the active tabpanel, the tabpanel - element has tabindex="0" to include the panel in the page Tab sequence. - It is recommended that all tabpanel elements in a tab set are focusable - if there are any panels in the set that contain content where the first - element in the panel is not focusable. - https://www.w3.org/WAI/ARIA/apg/example-index/tabs/tabs-automatic - */ - this.tabIndex = 0; - } -} diff --git a/elements/pf-tabs/BaseTabs.ts b/elements/pf-tabs/BaseTabs.ts deleted file mode 100644 index 88267bd0f6..0000000000 --- a/elements/pf-tabs/BaseTabs.ts +++ /dev/null @@ -1,270 +0,0 @@ -import { LitElement, html } from 'lit'; - -import { property } from 'lit/decorators/property.js'; -import { query } from 'lit/decorators/query.js'; -import { queryAssignedElements } from 'lit/decorators/query-assigned-elements.js'; - -import { classMap } from 'lit/directives/class-map.js'; - -import { RovingTabindexController } from '@patternfly/pfe-core/controllers/roving-tabindex-controller.js'; -import { OverflowController } from '@patternfly/pfe-core/controllers/overflow-controller.js'; -import { Logger } from '@patternfly/pfe-core/controllers/logger.js'; - -import { getRandomId } from '@patternfly/pfe-core/functions/random.js'; - -import { BaseTab, TabExpandEvent } from './BaseTab.js'; -import { BaseTabPanel } from './BaseTabPanel.js'; - -import styles from './BaseTabs.css'; - -/** - * BaseTabs - * - * @attr [label-scroll-left="Scroll left"] - accessible label for the tab panel's scroll left button. - * @attr [label-scroll-right="Scroll right"] - accessible label for the tab panel's scroll right button. - * - */ -export abstract class BaseTabs extends LitElement { - static readonly styles = [styles]; - - static isTab(element: BaseTab): element is BaseTab { - return element instanceof BaseTab; - } - - static isPanel(element: BaseTabPanel): element is BaseTabPanel { - return element instanceof BaseTabPanel; - } - - /** Time in milliseconds to debounce between scroll events and updating scroll button state */ - protected static readonly scrollTimeoutDelay: number = 0; - /** Icon name to use for the scroll left button */ - protected static readonly scrollIconLeft: string = 'angle-left'; - /** Icon name to use for the scroll right button */ - protected static readonly scrollIconRight: string = 'angle-right'; - /** Icon set to use for the scroll buttons */ - protected static readonly scrollIconSet: string = 'fas'; - - static #instances = new Set(); - - static { - // on resize check for overflows to add or remove scroll buttons - window.addEventListener('resize', () => { - for (const instance of this.#instances) { - instance.#overflow.onScroll(); - } - }, { capture: false }); - } - - @queryAssignedElements({ slot: 'tab' }) private tabs!: BaseTab[]; - - @queryAssignedElements() private panels!: BaseTabPanel[]; - - @query('[part="tabs"]') private tabList!: HTMLElement; - - #tabindex = new RovingTabindexController(this); - - #overflow = new OverflowController(this); - - #logger = new Logger(this); - - #_allTabs: BaseTab[] = []; - - #_allPanels: BaseTabPanel[] = []; - - #activeIndex = 0; - - /** - * Tab activation - * Tabs can be either [automatic](https://w3c.github.io/aria-practices/examples/tabs/tabs-automatic.html) activated - * or [manual](https://w3c.github.io/aria-practices/examples/tabs/tabs-manual.html) - */ - @property({ reflect: true, type: Boolean }) manual = false; - - @property({ attribute: false }) - get activeIndex() { - return this.#activeIndex; - } - - set activeIndex(index: number) { - const oldIndex = this.activeIndex; - const tab = this.#allTabs[index]; - if (tab) { - if (tab.disabled) { - this.#logger.warn(`Disabled tabs can not be active, setting first focusable tab to active`); - this.#tabindex.updateActiveItem(this.#firstFocusable); - index = this.#activeItemIndex; - } else if (!tab.active) { - // if the activeIndex was set through the CLI e.g.`$0.activeIndex = 2` - tab.active = true; - return; - } - } - - if (index === -1) { - this.#logger.warn(`No active tab found, setting first focusable tab to active`); - const first = this.#tabindex.firstItem; - this.#tabindex.updateActiveItem(first); - index = this.#activeItemIndex; - } - this.#activeIndex = index; - this.requestUpdate('activeIndex', oldIndex); - - this.#allPanels[this.#activeIndex].hidden = false; - // close all tabs that are not the activeIndex - this.#deactivateExcept(this.#activeIndex); - } - - get #activeTab() { - const [tab] = this.#_allTabs.filter(tab => tab.active); - return tab; - } - - get #allTabs() { - return this.#_allTabs; - } - - set #allTabs(tabs: BaseTab[]) { - this.#_allTabs = tabs.filter(tab => (this.constructor as typeof BaseTabs).isTab(tab)); - } - - get #allPanels() { - return this.#_allPanels; - } - - set #allPanels(panels: BaseTabPanel[]) { - this.#_allPanels = panels.filter(panel => (this.constructor as typeof BaseTabs).isPanel(panel)); - } - - override connectedCallback() { - super.connectedCallback(); - this.id ||= getRandomId(this.localName); - this.addEventListener('expand', this.#onTabExpand); - BaseTabs.#instances.add(this); - } - - override disconnectedCallback(): void { - super.disconnectedCallback(); - BaseTabs.#instances.delete(this); - } - - override willUpdate(): void { - const { activeItem } = this.#tabindex; - // If RTI has an activeItem, update the roving tabindex controller - if (!this.manual && - activeItem && - activeItem !== this.#activeTab && - activeItem.ariaDisabled !== 'true') { - activeItem.active = true; - } - } - - async firstUpdated() { - this.tabList.addEventListener('scroll', this.#overflow.onScroll.bind(this)); - } - - override render() { - const { scrollIconSet, scrollIconLeft, scrollIconRight } = this.constructor as typeof BaseTabs; - return html` -
-
${!this.#overflow.showScrollButtons ? '' : html` - `} - ${!this.#overflow.showScrollButtons ? '' : html` - `} -
- -
- `; - } - - #onSlotchange(event: { target: { name: string } }) { - if (event.target.name === 'tab') { - this.#allTabs = this.tabs; - } else { - this.#allPanels = this.panels; - } - - if ((this.#allTabs.length === this.#allPanels.length) && - (this.#allTabs.length !== 0 || this.#allPanels.length !== 0)) { - this.#updateAccessibility(); - this.#firstLastClasses(); - this.#tabindex.initItems(this.#allTabs); - this.activeIndex = this.#allTabs.findIndex(tab => tab.active); - this.#tabindex.updateActiveItem(this.#activeTab); - this.#overflow.init(this.tabList, this.#allTabs); - } - } - - #updateAccessibility(): void { - this.#allTabs.forEach((tab, index) => { - const panel = this.#allPanels[index]; - if (!panel.hasAttribute('aria-labelledby')) { - panel.setAttribute('aria-labelledby', tab.id); - } - tab.setAttribute('aria-controls', panel.id); - }); - } - - #onTabExpand = (event: Event): void => { - if (!(event instanceof TabExpandEvent) || - !this.#allTabs.length || - !this.#allPanels.length) { - return; - } - - if (event.active) { - if (event.tab !== this.#tabindex.activeItem) { - this.#tabindex.updateActiveItem(event.tab); - } - this.activeIndex = this.#allTabs.findIndex(tab => tab === event.tab); - } - }; - - #deactivateExcept(index: number) { - this.#allTabs.forEach((tab, i) => tab.active = i === index); - this.#allPanels.forEach((panel, i) => panel.hidden = i !== index); - } - - get #firstFocusable(): BaseTab | undefined { - return this.#tabindex.firstItem; - } - - get #firstTab(): BaseTab | undefined { - const [tab] = this.#allTabs; - return tab; - } - - get #lastTab(): BaseTab | undefined { - return this.#allTabs.at(-1); - } - - get #activeItemIndex() { - const { activeItem } = this.#tabindex; - return this.#allTabs.findIndex(t => t === activeItem); - } - - #firstLastClasses() { - this.#firstTab?.classList.add('first'); - this.#lastTab?.classList.add('last'); - } - - #scrollLeft() { - this.#overflow.scrollLeft(); - } - - #scrollRight() { - this.#overflow.scrollRight(); - } -} diff --git a/elements/pf-tabs/TabsController.ts b/elements/pf-tabs/TabsController.ts new file mode 100644 index 0000000000..773a3610e8 --- /dev/null +++ b/elements/pf-tabs/TabsController.ts @@ -0,0 +1,236 @@ +import type { ReactiveController, ReactiveElement } from 'lit'; + +import { Logger } from '@patternfly/pfe-core/controllers/logger.js'; +import { RovingTabindexController } from '@patternfly/pfe-core/controllers/roving-tabindex-controller.js'; + +import { ComposedEvent } from '@patternfly/pfe-core'; + +export class TabExpandEvent extends ComposedEvent { + constructor( + public tab: HTMLElement, + ) { + super('expand'); + } +} + +export interface Tab extends HTMLElement { + active?: boolean; + disabled?: boolean; +} + +export type Panel = HTMLElement + +export interface Options { + isTab?: (node?: Node) => node is Tab; + isPanel?: (node?: Node) => node is Panel; +} + +export class TabsController implements ReactiveController { + static #instances = new Set(); + + static #tabsClasses = new WeakSet(); + static #tabClasses = new WeakSet(); + static #panelClasses = new WeakSet(); + + static isTabs(node?: Node) { + return !!node && TabsController.#tabsClasses.has(node.constructor); + } + + static isTab(node?: Node): node is Tab { + return !!node && TabsController.#tabClasses.has(node.constructor); + } + + static isPanel(node?: Node): node is Panel { + return !!node && TabsController.#panelClasses.has(node.constructor); + } + + static { + window.addEventListener('expand', event => { + for (const instance of this.#instances) { + instance.#onTabExpand(event as TabExpandEvent); + } + }); + } + + #logger: Logger; + + #host: ReactiveElement; + + #tabs = new Map(); + + #isTab: Required['isTab']; + + #isPanel: Required['isPanel']; + + #slottedTabs: Tab[] = []; + + #slottedPanels: Panel[] = []; + + #tabindex: RovingTabindexController; + + #observer: MutationObserver; + + #rebuilding: Promise | null = null; + + #init = 0; + + // Setting active tab from js/console (ie: $0.activeIndex = 2) + set activeIndex(index: number) { + const firstFocusableTab = this.#tabindex.firstItem; + if (!firstFocusableTab) { + this.#logger.error(`No focusable tabs found.`); + return; + } + + let error = false; + if (this._tabs[index] === undefined) { + error = true; + this.#logger.warn(`The index provided is out of bounds: 0 - ${this._tabs.length - 1}. Setting to first focusable tab.`); + } + if (this._tabs[index].disabled) { + error = true; + this.#logger.warn(`The tab at index ${index} is disabled. Setting to first focusable tab.`); + } + if (error) { + index = this._tabs.indexOf(firstFocusableTab); + } + this._tabs[index].active = true; + } + + protected get _tabs() { + return [...this.#tabs.keys()] as Tab[]; + } + + get #activeTab() { + return this.#tabindex.activeItem; + } + + constructor(host: ReactiveElement, options?: Options) { + this.#tabindex = new RovingTabindexController(host); + this.#logger = new Logger(host); + this.#isTab = options?.isTab ?? TabsController.isTab; + this.#isPanel = options?.isPanel ?? TabsController.isPanel; + TabsController.#tabsClasses.add(host.constructor); + if (host.isConnected) { + TabsController.#instances.add(this); + } + this.#observer = new MutationObserver(this.#mutationsCallback); + this.#observer.observe(host, { attributes: true, childList: true, subtree: true }); + host.addEventListener('slotchange', this.#onSlotchange); + (this.#host = host).addController(this); + } + + hostConnected() { + TabsController.#instances.add(this); + this.#onSlotchange(); + } + + hostDisconnected() { + TabsController.#instances.delete(this); + } + + async hostUpdate() { + if (this.#init <= 1) { + this.#rebuilding ??= await this.#rebuild(); + this.#host.requestUpdate(); + } + this.#init++; + } + + #mutationsCallback = async (mutations: MutationRecord[]): Promise => { + for (const mutation of mutations) { + if (mutation.type === 'childList' && this.#isTab(mutation.target)) { + /* this will run when tabs added and deleted */ + this.#rebuild(); + } + } + }; + + async #rebuild() { + const tabSlot = this.#host.shadowRoot?.querySelector('slot[name=tab]'); + const panelSlot = this.#host.shadowRoot?.querySelector('slot:not([name])'); + this.#slottedPanels = panelSlot?.assignedElements().filter(this.#isPanel) ?? []; + this.#slottedTabs = tabSlot?.assignedElements().filter(this.#isTab) ?? []; + + this.#tabs.clear(); + this.#registerSlottedTabs(); + + if (this._tabs.length > 0) { + this.#updateAccessibility(); + this.#tabindex.initItems(this._tabs, this.#host); + this.#setActiveTab(); + } + + return null; + } + + async #onSlotchange() { + this.#host.requestUpdate(); + } + + #onTabExpand(event: TabExpandEvent) { + if (event instanceof TabExpandEvent && this.#tabs.has(event.tab)) { + this.#deactivateExcept(this._tabs.indexOf(event.tab)); + } + } + + #registerSlottedTabs() { + for (const [index, slotted] of this.#slottedTabs.entries()) { + this.#addPairForTab(index, slotted); + } + } + + #addPairForTab(index: number, tab: Tab) { + const panel = this.#slottedPanels[index]; + if (this.#isPanel(panel)) { + this.#tabs.set(tab, panel); + } else { + this.#logger.warn(`Tab and panel do not match`, tab, panel); + } + } + + #deactivateExcept(indexToKeep: number) { + [...this.#tabs].forEach(([tab, panel], currentIndex) => { + tab.active = currentIndex === indexToKeep; + panel.hidden = currentIndex !== indexToKeep; + }); + } + + #setActiveTab() { + // check for an active tab, if not set one + if (!this.#activeTab) { + this.#logger.warn('No active tab found. Setting to first focusable tab.'); + this.#setFirstFocusableTabActive(); + return; + } + if (this.#activeTab.disabled) { + this.#logger.warn('Active tab is disabled. Setting to first focusable tab.'); + this.#setFirstFocusableTabActive(); + return; + } + // update RTI with active tab and deactivate others + this.#tabindex.updateActiveItem(this.#activeTab); + this.#deactivateExcept(this._tabs.indexOf(this.#activeTab)); + } + + #setFirstFocusableTabActive() { + const first = this.#tabindex.firstItem; + if (!first) { + this.#logger.warn('No focusable tab found.'); + return; + } + const firstFocusable = this._tabs.find(tab => tab === first); + if (firstFocusable) { + firstFocusable.active = true; + } + } + + #updateAccessibility(): void { + for (const [tab, panel] of this.#tabs) { + if (!panel.hasAttribute('aria-labelledby')) { + panel.setAttribute('aria-labelledby', tab.id); + } + tab.setAttribute('aria-controls', panel.id); + } + } +} diff --git a/elements/pf-tabs/pf-tab-panel.ts b/elements/pf-tabs/pf-tab-panel.ts index b272e571aa..41f2fefc78 100644 --- a/elements/pf-tabs/pf-tab-panel.ts +++ b/elements/pf-tabs/pf-tab-panel.ts @@ -1,8 +1,10 @@ +import { LitElement, html } from 'lit'; import { customElement } from 'lit/decorators/custom-element.js'; +import BaseStyles from './BaseTabPanel.css'; import styles from './pf-tab-panel.css'; -import { BaseTabPanel } from './BaseTabPanel.js'; +import { getRandomId } from '@patternfly/pfe-core/functions/random.js'; /** * @slot - Tab panel content @@ -12,8 +14,34 @@ import { BaseTabPanel } from './BaseTabPanel.js'; * @csspart container - container for the panel content */ @customElement('pf-tab-panel') -export class PfTabPanel extends BaseTabPanel { - static readonly styles = [...BaseTabPanel.styles, styles]; +export class PfTabPanel extends LitElement { + static readonly styles = [BaseStyles, styles]; + + #internals = this.attachInternals(); + + render() { + return html` + + `; + } + + connectedCallback() { + super.connectedCallback(); + this.id ||= getRandomId('pf-tab-panel'); + this.hidden ??= true; + this.#internals.role = 'tabpanel'; + + /* + To make it easy for screen reader users to navigate from a tab + to the beginning of content in the active tabpanel, the tabpanel + element has tabindex="0" to include the panel in the page Tab sequence. + It is recommended that all tabpanel elements in a tab set are focusable + if there are any panels in the set that contain content where the first + element in the panel is not focusable. + https://www.w3.org/WAI/ARIA/apg/example-index/tabs/tabs-automatic + */ + this.tabIndex = 0; + } } declare global { diff --git a/elements/pf-tabs/pf-tab.css b/elements/pf-tabs/pf-tab.css index d36f3990f6..865ff73aad 100644 --- a/elements/pf-tabs/pf-tab.css +++ b/elements/pf-tabs/pf-tab.css @@ -13,9 +13,9 @@ --pf-c-tabs__link--before--BorderBottomColor: var(--pf-c-tabs__link--BackgroundColor, transparent); } -:host(.first[box][active]) #current::before { +/* :host(.first[box][active]) #current::before { left: calc(var(--pf-c-tabs__link--before--border-width--base, var(--pf-global--BorderWidth--sm, 1px)) * -1); -} +} */ button { align-items: center; @@ -93,7 +93,7 @@ button:active { top: calc(var(--pf-c-tabs__link--before--border-width--base, var(--pf-global--BorderWidth--sm, 1px)) * -1); } -:host(.first[box][vertical]) button::after, +:host([box][vertical]:first-of-type) button::after, :host([box][vertical][active]) button::after { top: 0; } @@ -104,12 +104,12 @@ button:active { --pf-c-tabs__link--before--BorderBottomColor: var(--pf-c-tabs__link--before--border-color--base, var(--pf-global--BorderColor--100, #d2d2d2)); } -:host(.first[box][active]) button::before { +:host([box][active]:first-of-type) button::before { border-block-start-width: var(--pf-c-tabs--m-box__item--m-current--first-child__link--before--BorderTopWidth, var(--pf-c-tabs__link--before--border-width--base, var(--pf-global--BorderWidth--sm, 1px))); border-inline-start-width: var(--pf-c-tabs--m-box__item--m-current--first-child__link--before--BorderLeftWidth, var(--pf-c-tabs__link--before--border-width--base, var(--pf-global--BorderWidth--sm, 1px))); } -:host(.last[box][active]) button::before { +:host([box][active]:last-of-type) button::before { border-inline-end-width: var(--pf-c-tabs--m-box__item--m-current--last-child__link--before--BorderRightWidth, var(--pf-c-tabs--before--border-width--base, var(--pf-global--BorderWidth--sm, 1px))); } diff --git a/elements/pf-tabs/pf-tab.ts b/elements/pf-tabs/pf-tab.ts index d87b53317c..958c7acba0 100644 --- a/elements/pf-tabs/pf-tab.ts +++ b/elements/pf-tabs/pf-tab.ts @@ -1,10 +1,14 @@ +import { LitElement, html } from 'lit'; import { customElement } from 'lit/decorators/custom-element.js'; import { property } from 'lit/decorators/property.js'; +import { queryAssignedElements } from 'lit/decorators/query-assigned-elements.js'; +import { query } from 'lit/decorators/query.js'; -import { observed } from '@patternfly/pfe-core/decorators.js'; +import { getRandomId } from '@patternfly/pfe-core/functions/random.js'; -import { BaseTab } from './BaseTab.js'; +import { TabExpandEvent } from './TabsController.js'; +import BaseStyles from './BaseTab.css'; import styles from './pf-tab.css'; /** @@ -68,14 +72,71 @@ import styles from './pf-tab.css'; * @fires { TabExpandEvent } tab-expand - when a tab expands */ @customElement('pf-tab') -export class PfTab extends BaseTab { - static readonly styles = [...BaseTab.styles, styles]; +export class PfTab extends LitElement { + static readonly styles = [BaseStyles, styles]; + + static override shadowRootOptions: ShadowRootInit = { ...LitElement.shadowRootOptions, delegatesFocus: true }; + + @query('button') private button!: HTMLButtonElement; + + @queryAssignedElements({ slot: 'icon', flatten: true }) + private icons!: Array; + + @property({ type: Boolean }) manual = false; - @observed @property({ reflect: true, type: Boolean }) active = false; - @observed @property({ reflect: true, type: Boolean }) disabled = false; + + #internals = this.attachInternals(); + + connectedCallback() { + super.connectedCallback(); + this.id ||= getRandomId(this.localName); + this.#internals.role = 'tab'; + this.#internals.ariaDisabled = this.#setInternalsAriaDisabled(); + this.addEventListener('click', this.#onClick); + this.addEventListener('focus', this.#onFocus); + } + + render() { + return html` + + `; + } + + #onClick() { + if (this.disabled || this.#internals.ariaDisabled === 'true') { + return; + } + this.#activate(); + } + + #onFocus() { + if (this.manual || this.#internals.ariaDisabled === 'true') { + return; + } + this.#activate(); + } + + #activate() { + this.active = true; + this.button.focus(); + this.dispatchEvent(new TabExpandEvent(this)); + } + + #setInternalsAriaDisabled() { + if (this.disabled) { + return 'true'; + } + return this.ariaDisabled ?? 'false'; + } } declare global { diff --git a/elements/pf-tabs/pf-tabs.ts b/elements/pf-tabs/pf-tabs.ts index fb356ed858..cf7a1f6865 100644 --- a/elements/pf-tabs/pf-tabs.ts +++ b/elements/pf-tabs/pf-tabs.ts @@ -1,12 +1,19 @@ +import { html, LitElement } from 'lit'; import { customElement } from 'lit/decorators/custom-element.js'; import { property } from 'lit/decorators/property.js'; +import { query } from 'lit/decorators/query.js'; +import { queryAssignedElements } from 'lit/decorators/query-assigned-elements.js'; +import { classMap } from 'lit/directives/class-map.js'; +import { getRandomId } from '@patternfly/pfe-core/functions/random.js'; import { cascades } from '@patternfly/pfe-core/decorators.js'; +import { OverflowController } from '@patternfly/pfe-core/controllers/overflow-controller.js'; -import { BaseTabs } from './BaseTabs.js'; +import { TabsController } from './TabsController.js'; import { PfTab } from './pf-tab.js'; import { PfTabPanel } from './pf-tab-panel.js'; +import BaseStyles from './BaseTabs.css'; import styles from './pf-tabs.css'; /** @@ -60,19 +67,11 @@ import styles from './pf-tabs.css'; * @cssprop {} --pf-c-tabs__scroll-button--disabled--Color {@default `#d2d2d2`} */ @customElement('pf-tabs') -export class PfTabs extends BaseTabs { - static readonly styles = [...BaseTabs.styles, styles]; +export class PfTabs extends LitElement { + static readonly styles = [BaseStyles, styles]; protected static readonly scrollTimeoutDelay = 150; - static isTab(element: HTMLElement): element is PfTab { - return element instanceof PfTab; - } - - static isPanel(element: HTMLElement): element is PfTabPanel { - return element instanceof PfTabPanel; - } - @cascades('pf-tab', 'pf-tab-panel') @property({ reflect: true }) box: 'light' | 'dark' | null = null; @@ -85,9 +84,82 @@ export class PfTabs extends BaseTabs { @cascades('pf-tab') @property({ attribute: 'border-bottom' }) borderBottom: 'true' | 'false' = 'true'; + @cascades('pf-tab') + @property({ reflect: true, type: Boolean }) manual = false; + + @property({ reflect: false, attribute: 'label-scroll-left' }) labelScrollLeft = 'Scroll left'; + + @property({ reflect: false, attribute: 'label-scroll-right' }) labelScrollRight = 'Scroll left'; + + @query('#tabs') private _tabsContainer!: HTMLElement; + + @queryAssignedElements({ slot: 'tab' }) private _tabs?: PfTab[]; + protected get canShowScrollButtons(): boolean { return !this.vertical; } + + #overflow = new OverflowController(this, { scrollTimeoutDelay: 200 }); + + #tabs = new TabsController(this, { + isTab: (x?: Node): x is PfTab => x instanceof PfTab, + isPanel: (x?: Node): x is PfTabPanel => x instanceof PfTabPanel, + }); + + connectedCallback() { + super.connectedCallback(); + this.id ||= getRandomId(this.localName); + } + + willUpdate(): void { + this.#overflow.update(); + } + + render() { + return html` +
+
${!this.#overflow.showScrollButtons ? '' : html` + `} + ${!this.#overflow.showScrollButtons ? '' : html` + `} +
+ +
+ `; + } + + set activeIndex(index: number) { + this.#tabs.activeIndex = index; + } + + #scrollLeft() { + this.#overflow.scrollLeft(); + } + + #scrollRight() { + this.#overflow.scrollRight(); + } + + #onSlotChange() { + if (this._tabs) { + this.#overflow.init(this._tabsContainer, this._tabs); + } + } } declare global { From e668dc9fc2d5ca3ff300b4f38cbb351051f6e511 Mon Sep 17 00:00:00 2001 From: Steven Spriggs Date: Fri, 11 Aug 2023 11:03:18 -0400 Subject: [PATCH 02/50] fix(tabs): remove BaseTab from test --- elements/pf-tabs/test/pf-tabs.spec.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/elements/pf-tabs/test/pf-tabs.spec.ts b/elements/pf-tabs/test/pf-tabs.spec.ts index a60ba9d728..59f1c7da3e 100644 --- a/elements/pf-tabs/test/pf-tabs.spec.ts +++ b/elements/pf-tabs/test/pf-tabs.spec.ts @@ -3,7 +3,6 @@ import { createFixture } from '@patternfly/pfe-tools/test/create-fixture.js'; import { a11ySnapshot } from '@patternfly/pfe-tools/test/a11y-snapshot.js'; import { setViewport, sendKeys } from '@web/test-runner-commands'; -import { BaseTab } from '../BaseTab.js'; import { PfTabs } from '../pf-tabs.js'; import { PfTab } from '../pf-tab.js'; import { PfTabPanel } from '../pf-tab-panel.js'; @@ -147,7 +146,7 @@ describe('', function() { it('should aria-disable the tab if disabled', async function() { const el = await createFixture(TEMPLATE); - const disabledTab = el.querySelector('pf-tab:nth-of-type(2)')! as BaseTab; + const disabledTab = el.querySelector('pf-tab:nth-of-type(2)')! as PfTab; disabledTab.disabled = true; await nextFrame(); const tab = (await a11ySnapshot()).children.find(x => x.role === 'tab' && x.name === 'Containers'); From e3479ac2014a018449ad523f2fb8cc06bbdfd987 Mon Sep 17 00:00:00 2001 From: Steven Spriggs Date: Fri, 11 Aug 2023 11:53:40 -0400 Subject: [PATCH 03/50] fix(tabs): correct disabled test to check child button --- elements/pf-tabs/test/pf-tabs.spec.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/elements/pf-tabs/test/pf-tabs.spec.ts b/elements/pf-tabs/test/pf-tabs.spec.ts index 59f1c7da3e..9c0f135dc4 100644 --- a/elements/pf-tabs/test/pf-tabs.spec.ts +++ b/elements/pf-tabs/test/pf-tabs.spec.ts @@ -144,13 +144,13 @@ describe('', function() { await setViewport({ width: 320, height: 640 }); }); - it('should aria-disable the tab if disabled', async function() { + it('should disable the tab button if disabled attr is present', async function() { const el = await createFixture(TEMPLATE); const disabledTab = el.querySelector('pf-tab:nth-of-type(2)')! as PfTab; disabledTab.disabled = true; await nextFrame(); const tab = (await a11ySnapshot()).children.find(x => x.role === 'tab' && x.name === 'Containers'); - expect(tab?.disabled).to.be.true; + expect(tab?.children.find(x => x.role === 'button')?.disabled).to.equal(true); }); it('should have disabled css styles if disabled', async function() { From e0397ea7c26456652660ed5516b07af88593251d Mon Sep 17 00:00:00 2001 From: Steven Spriggs Date: Fri, 11 Aug 2023 11:54:46 -0400 Subject: [PATCH 04/50] fix(tabs): dispatch event when active property changes on tab --- elements/pf-tabs/pf-tab.ts | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/elements/pf-tabs/pf-tab.ts b/elements/pf-tabs/pf-tab.ts index 958c7acba0..1f2a8bdb42 100644 --- a/elements/pf-tabs/pf-tab.ts +++ b/elements/pf-tabs/pf-tab.ts @@ -1,9 +1,12 @@ +import type { PropertyValues } from 'lit'; + import { LitElement, html } from 'lit'; import { customElement } from 'lit/decorators/custom-element.js'; import { property } from 'lit/decorators/property.js'; import { queryAssignedElements } from 'lit/decorators/query-assigned-elements.js'; import { query } from 'lit/decorators/query.js'; +import { observed } from '@patternfly/pfe-core/decorators.js'; import { getRandomId } from '@patternfly/pfe-core/functions/random.js'; import { TabExpandEvent } from './TabsController.js'; @@ -84,13 +87,14 @@ export class PfTab extends LitElement { @property({ type: Boolean }) manual = false; + @observed @property({ reflect: true, type: Boolean }) active = false; @property({ reflect: true, type: Boolean }) disabled = false; #internals = this.attachInternals(); - connectedCallback() { + override connectedCallback() { super.connectedCallback(); this.id ||= getRandomId(this.localName); this.#internals.role = 'tab'; @@ -116,6 +120,7 @@ export class PfTab extends LitElement { return; } this.#activate(); + this.button.focus(); } #onFocus() { @@ -127,8 +132,12 @@ export class PfTab extends LitElement { #activate() { this.active = true; - this.button.focus(); - this.dispatchEvent(new TabExpandEvent(this)); + } + + private _activeChanged() { + if (this.active) { + this.dispatchEvent(new TabExpandEvent(this)); + } } #setInternalsAriaDisabled() { From 1833746fdaf2593b376bb60307f51a3bb53a0cfc Mon Sep 17 00:00:00 2001 From: Steven Spriggs Date: Fri, 11 Aug 2023 12:21:33 -0400 Subject: [PATCH 05/50] fix(tabs): dispatch event only when changing from false to true --- elements/pf-tabs/pf-tab.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/elements/pf-tabs/pf-tab.ts b/elements/pf-tabs/pf-tab.ts index 1f2a8bdb42..7fb56c8790 100644 --- a/elements/pf-tabs/pf-tab.ts +++ b/elements/pf-tabs/pf-tab.ts @@ -134,8 +134,8 @@ export class PfTab extends LitElement { this.active = true; } - private _activeChanged() { - if (this.active) { + private _activeChanged(oldVal: boolean, newVal: boolean) { + if (oldVal !== newVal && newVal === true) { this.dispatchEvent(new TabExpandEvent(this)); } } From 285a6d600b815351a02f89a35ccf6bb74d7d7e25 Mon Sep 17 00:00:00 2001 From: Steven Spriggs Date: Fri, 11 Aug 2023 14:09:08 -0400 Subject: [PATCH 06/50] fix(tabs): add dynamic tab demo --- elements/pf-tabs/TabsController.ts | 16 +++++--- elements/pf-tabs/demo/dynamic-tabs.html | 49 +++++++++++++++++++++++++ elements/pf-tabs/demo/dynamic.js | 23 ++++++++++++ 3 files changed, 82 insertions(+), 6 deletions(-) create mode 100644 elements/pf-tabs/demo/dynamic-tabs.html create mode 100644 elements/pf-tabs/demo/dynamic.js diff --git a/elements/pf-tabs/TabsController.ts b/elements/pf-tabs/TabsController.ts index 773a3610e8..d90b5c1ad0 100644 --- a/elements/pf-tabs/TabsController.ts +++ b/elements/pf-tabs/TabsController.ts @@ -102,7 +102,7 @@ export class TabsController implements ReactiveController { } get #activeTab() { - return this.#tabindex.activeItem; + return this._tabs.find(tab => tab.active); } constructor(host: ReactiveElement, options?: Options) { @@ -115,9 +115,9 @@ export class TabsController implements ReactiveController { TabsController.#instances.add(this); } this.#observer = new MutationObserver(this.#mutationsCallback); + (this.#host = host).addController(this); this.#observer.observe(host, { attributes: true, childList: true, subtree: true }); host.addEventListener('slotchange', this.#onSlotchange); - (this.#host = host).addController(this); } hostConnected() { @@ -139,9 +139,13 @@ export class TabsController implements ReactiveController { #mutationsCallback = async (mutations: MutationRecord[]): Promise => { for (const mutation of mutations) { - if (mutation.type === 'childList' && this.#isTab(mutation.target)) { - /* this will run when tabs added and deleted */ - this.#rebuild(); + if (mutation.type === 'childList') { + if (this.#isTab(mutation.addedNodes[0])) { + this.#rebuild(); + } + if (this.#isTab(mutation.removedNodes[0])) { + this.#rebuild(); + } } } }; @@ -197,7 +201,6 @@ export class TabsController implements ReactiveController { } #setActiveTab() { - // check for an active tab, if not set one if (!this.#activeTab) { this.#logger.warn('No active tab found. Setting to first focusable tab.'); this.#setFirstFocusableTabActive(); @@ -208,6 +211,7 @@ export class TabsController implements ReactiveController { this.#setFirstFocusableTabActive(); return; } + // update RTI with active tab and deactivate others this.#tabindex.updateActiveItem(this.#activeTab); this.#deactivateExcept(this._tabs.indexOf(this.#activeTab)); diff --git a/elements/pf-tabs/demo/dynamic-tabs.html b/elements/pf-tabs/demo/dynamic-tabs.html new file mode 100644 index 0000000000..f1af585450 --- /dev/null +++ b/elements/pf-tabs/demo/dynamic-tabs.html @@ -0,0 +1,49 @@ + + + + + +
+

Default

+ + + Add Tab + + + + + + Delete Tab + + + + Users + Users + Containers + Containers Focusable element + Database + Database + Disabled + Disabled + Aria Disabled + Aria Disabled + + + +
+ diff --git a/elements/pf-tabs/demo/dynamic.js b/elements/pf-tabs/demo/dynamic.js new file mode 100644 index 0000000000..8228394d5b --- /dev/null +++ b/elements/pf-tabs/demo/dynamic.js @@ -0,0 +1,23 @@ +const addButton = document.querySelector('#add'); +const removeButton = document.querySelector('#delete'); +const tabs = document.querySelector('pf-tabs'); + +addButton.addEventListener('click', () => { + const tab = document.createElement('pf-tab'); + tab.textContent = 'Tab'; + tab.setAttribute('slot', 'tab'); + const panel = document.createElement('pf-tab-panel'); + panel.textContent = 'Panel'; + + tabs.appendChild(tab); + tabs.appendChild(panel); +}); + +removeButton.addEventListener('click', () => { + const lastTab = tabs.querySelector('pf-tab:last-of-type'); + const lastPanel = tabs.querySelector('pf-tab-panel:last-of-type'); + + tabs.removeChild(lastTab); + tabs.removeChild(lastPanel); +}); + From 2b50ad892fc4895d5e224ce71a6b12d84d3c4d81 Mon Sep 17 00:00:00 2001 From: Steven Spriggs Date: Fri, 11 Aug 2023 15:13:26 -0400 Subject: [PATCH 07/50] fix(tabs): update RTI if tab is disabled --- elements/pf-tabs/TabsController.ts | 32 +++++++++++++++++++++++++----- elements/pf-tabs/pf-tab.ts | 9 ++++++--- 2 files changed, 33 insertions(+), 8 deletions(-) diff --git a/elements/pf-tabs/TabsController.ts b/elements/pf-tabs/TabsController.ts index d90b5c1ad0..68f2ae9c16 100644 --- a/elements/pf-tabs/TabsController.ts +++ b/elements/pf-tabs/TabsController.ts @@ -7,12 +7,20 @@ import { ComposedEvent } from '@patternfly/pfe-core'; export class TabExpandEvent extends ComposedEvent { constructor( - public tab: HTMLElement, + public tab: Tab, ) { super('expand'); } } +export class TabDisabledEvent extends ComposedEvent { + constructor( + public tab: Tab, + ) { + super('disabled'); + } +} + export interface Tab extends HTMLElement { active?: boolean; disabled?: boolean; @@ -50,6 +58,12 @@ export class TabsController implements ReactiveController { instance.#onTabExpand(event as TabExpandEvent); } }); + + window.addEventListener('disabled', event => { + for (const instance of this.#instances) { + instance.#onTabDisabled(event as TabDisabledEvent); + } + }); } #logger: Logger; @@ -87,7 +101,7 @@ export class TabsController implements ReactiveController { error = true; this.#logger.warn(`The index provided is out of bounds: 0 - ${this._tabs.length - 1}. Setting to first focusable tab.`); } - if (this._tabs[index].disabled) { + if (this._tabs[index].disabled || this._tabs[index].hasAttribute('aria-disabled')) { error = true; this.#logger.warn(`The tab at index ${index} is disabled. Setting to first focusable tab.`); } @@ -157,11 +171,11 @@ export class TabsController implements ReactiveController { this.#slottedTabs = tabSlot?.assignedElements().filter(this.#isTab) ?? []; this.#tabs.clear(); - this.#registerSlottedTabs(); + await this.#registerSlottedTabs(); if (this._tabs.length > 0) { this.#updateAccessibility(); - this.#tabindex.initItems(this._tabs, this.#host); + await this.#tabindex.initItems(this._tabs, this.#host); this.#setActiveTab(); } @@ -178,10 +192,17 @@ export class TabsController implements ReactiveController { } } - #registerSlottedTabs() { + async #onTabDisabled(event: TabDisabledEvent) { + if (event instanceof TabDisabledEvent && this.#tabs.has(event.tab)) { + await this.#rebuild(); + } + } + + async #registerSlottedTabs(): Promise { for (const [index, slotted] of this.#slottedTabs.entries()) { this.#addPairForTab(index, slotted); } + return null; } #addPairForTab(index: number, tab: Tab) { @@ -201,6 +222,7 @@ export class TabsController implements ReactiveController { } #setActiveTab() { + // check for an active tab, if not set one if (!this.#activeTab) { this.#logger.warn('No active tab found. Setting to first focusable tab.'); this.#setFirstFocusableTabActive(); diff --git a/elements/pf-tabs/pf-tab.ts b/elements/pf-tabs/pf-tab.ts index 7fb56c8790..9eed1e3368 100644 --- a/elements/pf-tabs/pf-tab.ts +++ b/elements/pf-tabs/pf-tab.ts @@ -1,5 +1,3 @@ -import type { PropertyValues } from 'lit'; - import { LitElement, html } from 'lit'; import { customElement } from 'lit/decorators/custom-element.js'; import { property } from 'lit/decorators/property.js'; @@ -9,7 +7,7 @@ import { query } from 'lit/decorators/query.js'; import { observed } from '@patternfly/pfe-core/decorators.js'; import { getRandomId } from '@patternfly/pfe-core/functions/random.js'; -import { TabExpandEvent } from './TabsController.js'; +import { TabExpandEvent, TabDisabledEvent } from './TabsController.js'; import BaseStyles from './BaseTab.css'; import styles from './pf-tab.css'; @@ -90,6 +88,7 @@ export class PfTab extends LitElement { @observed @property({ reflect: true, type: Boolean }) active = false; + @observed @property({ reflect: true, type: Boolean }) disabled = false; #internals = this.attachInternals(); @@ -140,6 +139,10 @@ export class PfTab extends LitElement { } } + private _disabledChanged() { + this.dispatchEvent(new TabDisabledEvent(this)); + } + #setInternalsAriaDisabled() { if (this.disabled) { return 'true'; From aff35d45300881df655fe4235788c11418a89db3 Mon Sep 17 00:00:00 2001 From: Steven Spriggs Date: Mon, 14 Aug 2023 13:24:41 -0400 Subject: [PATCH 08/50] refactor(tabs): mutation observer --- elements/pf-tabs/TabsController.ts | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/elements/pf-tabs/TabsController.ts b/elements/pf-tabs/TabsController.ts index 68f2ae9c16..6b6f768f07 100644 --- a/elements/pf-tabs/TabsController.ts +++ b/elements/pf-tabs/TabsController.ts @@ -82,7 +82,7 @@ export class TabsController implements ReactiveController { #tabindex: RovingTabindexController; - #observer: MutationObserver; + #mo = new MutationObserver(this.#mutationsCallback.bind(this)); #rebuilding: Promise | null = null; @@ -128,9 +128,8 @@ export class TabsController implements ReactiveController { if (host.isConnected) { TabsController.#instances.add(this); } - this.#observer = new MutationObserver(this.#mutationsCallback); (this.#host = host).addController(this); - this.#observer.observe(host, { attributes: true, childList: true, subtree: true }); + this.#mo.observe(host, { attributes: true, childList: true, subtree: true }); host.addEventListener('slotchange', this.#onSlotchange); } @@ -151,7 +150,7 @@ export class TabsController implements ReactiveController { this.#init++; } - #mutationsCallback = async (mutations: MutationRecord[]): Promise => { + async #mutationsCallback(mutations: MutationRecord[]): Promise { for (const mutation of mutations) { if (mutation.type === 'childList') { if (this.#isTab(mutation.addedNodes[0])) { @@ -162,7 +161,7 @@ export class TabsController implements ReactiveController { } } } - }; + } async #rebuild() { const tabSlot = this.#host.shadowRoot?.querySelector('slot[name=tab]'); From 29177032a146048b3b33a8fd4115e2c7bdd90bd9 Mon Sep 17 00:00:00 2001 From: Steven Spriggs Date: Mon, 14 Aug 2023 13:29:34 -0400 Subject: [PATCH 09/50] fix(tabs): demo js link --- elements/pf-tabs/demo/dynamic-tabs.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/elements/pf-tabs/demo/dynamic-tabs.html b/elements/pf-tabs/demo/dynamic-tabs.html index f1af585450..1aa0520660 100644 --- a/elements/pf-tabs/demo/dynamic-tabs.html +++ b/elements/pf-tabs/demo/dynamic-tabs.html @@ -2,7 +2,7 @@ + + + diff --git a/elements/pf-tabs/demo/box.html b/elements/pf-tabs/demo/box.html new file mode 100644 index 0000000000..6d32bd2da0 --- /dev/null +++ b/elements/pf-tabs/demo/box.html @@ -0,0 +1,43 @@ + + Users + Users + Containers + Containers + Database + Database + Disabled + Disabled + Aria Disabled + Aria Disabled + + +
+ + +
+ + + + diff --git a/elements/pf-tabs/demo/demo.css b/elements/pf-tabs/demo/demo.css deleted file mode 100644 index 4debbdf06a..0000000000 --- a/elements/pf-tabs/demo/demo.css +++ /dev/null @@ -1,62 +0,0 @@ -main { - display: block; - grid: unset; -} - -.example { - margin: 0 auto; - padding: 0 16px 32px; - width: 100%; - max-width: 1008px; -} - -.example:last-of-type { - padding-block-end: 64px; -} - -.input { - margin-block-start: 16px; -} - -@media screen and (min-width: 768px) { - .example { - padding: 0 32px 32px; - } -} - -/* - Inset Examples set class on host::part(tabs-container) -*/ -.inset-sm::part(tabs-container) { - --pf-c-tabs--inset: var(--pf-global--spacer--sm, 0.5rem); - --pf-c-tabs--m-vertical--inset: var(--pf-global--spacer--sm, 0.5rem); - --pf-c-tabs--m-vertical--m-box--inset: var(--pf-global--spacer--sm, 0.5rem); -} - -.inset-md::part(tabs-container) { - --pf-c-tabs--inset: var(--pf-global--spacer--md, 1rem); - --pf-c-tabs--m-vertical--inset: var(--pf-global--spacer--md, 1rem); - --pf-c-tabs--m-vertical--m-box--inset: var(--pf-global--spacer--md, 1rem); -} - -.inset-lg::part(tabs-container) { - --pf-c-tabs--inset: var(--pf-global--spacer--lg, 1.5rem); - --pf-c-tabs--m-vertical--inset: var(--pf-global--spacer--lg, 1.5rem); - --pf-c-tabs--m-vertical--m-box--inset: var(--pf-global--spacer--lg, 1.5rem); -} - -.inset-xl::part(tabs-container) { - --pf-c-tabs--inset: var(--pf-global--spacer--xl, 2rem); - --pf-c-tabs--m-vertical--inset: var(--pf-global--spacer--xl, 2rem); - --pf-c-tabs--m-vertical--m-box--inset: var(--pf-global--spacer--xl, 2rem); -} - -.inset-2xl::part(tabs-container) { - --pf-c-tabs--inset: var(--pf-global--spacer--2xl, 3rem); - --pf-c-tabs--m-vertical--inset: var(--pf-global--spacer--2xl, 3rem); - --pf-c-tabs--m-vertical--m-box--inset: var(--pf-global--spacer--2xl, 3rem); -} - -.inset-page::part(tabs-container) { - --pf-c-tabs--inset: var(--pf-c-tabs--m-page-insets--inset, var(--pf-global--spacer--md, 1rem)); -} diff --git a/elements/pf-tabs/demo/dynamic-tabs.html b/elements/pf-tabs/demo/dynamic-tabs.html index 1aa0520660..7ee1944f99 100644 --- a/elements/pf-tabs/demo/dynamic-tabs.html +++ b/elements/pf-tabs/demo/dynamic-tabs.html @@ -1,49 +1,69 @@ - +
+ + Add Tab + + + + + Delete Tab + +
+ + + Users + Users + Containers + Containers Focusable element + Database + Database + Disabled + Disabled + Aria Disabled + Aria Disabled + + + -
-

Default

- - - Add Tab - - - - - - Delete Tab - - - - Users - Users - Containers - Containers Focusable element - Database - Database - Disabled - Disabled - Aria Disabled - Aria Disabled - - - -
- diff --git a/elements/pf-tabs/demo/dynamic.js b/elements/pf-tabs/demo/dynamic.js deleted file mode 100644 index 8228394d5b..0000000000 --- a/elements/pf-tabs/demo/dynamic.js +++ /dev/null @@ -1,23 +0,0 @@ -const addButton = document.querySelector('#add'); -const removeButton = document.querySelector('#delete'); -const tabs = document.querySelector('pf-tabs'); - -addButton.addEventListener('click', () => { - const tab = document.createElement('pf-tab'); - tab.textContent = 'Tab'; - tab.setAttribute('slot', 'tab'); - const panel = document.createElement('pf-tab-panel'); - panel.textContent = 'Panel'; - - tabs.appendChild(tab); - tabs.appendChild(panel); -}); - -removeButton.addEventListener('click', () => { - const lastTab = tabs.querySelector('pf-tab:last-of-type'); - const lastPanel = tabs.querySelector('pf-tab-panel:last-of-type'); - - tabs.removeChild(lastTab); - tabs.removeChild(lastPanel); -}); - diff --git a/elements/pf-tabs/demo/filled-with-icons.html b/elements/pf-tabs/demo/filled-with-icons.html new file mode 100644 index 0000000000..498285bb49 --- /dev/null +++ b/elements/pf-tabs/demo/filled-with-icons.html @@ -0,0 +1,30 @@ + + Users + Containers + Database + Users + Containers + Database + + + + + + + + diff --git a/elements/pf-tabs/demo/filled.html b/elements/pf-tabs/demo/filled.html new file mode 100644 index 0000000000..709f9b389c --- /dev/null +++ b/elements/pf-tabs/demo/filled.html @@ -0,0 +1,30 @@ + + Users + Users + Containers + Containers + Database + Database + + + + + + + + diff --git a/elements/pf-tabs/demo/icons-and-text.html b/elements/pf-tabs/demo/icons-and-text.html new file mode 100644 index 0000000000..a779efcbd2 --- /dev/null +++ b/elements/pf-tabs/demo/icons-and-text.html @@ -0,0 +1,36 @@ + + Users + Containers + Database + Server + System + Network + Users + Containers + Database + Server + System + Network + + + + + + + diff --git a/elements/pf-tabs/demo/inset.html b/elements/pf-tabs/demo/inset.html new file mode 100644 index 0000000000..039a6c3889 --- /dev/null +++ b/elements/pf-tabs/demo/inset.html @@ -0,0 +1,82 @@ + + Users + Users + Containers + Containers + Database + Database + Disabled + Disabled + Aria Disabled + Aria Disabled + + +
+

Inset size:

+ + + + + + +
+ + + + + diff --git a/elements/pf-tabs/demo/manual-activation.html b/elements/pf-tabs/demo/manual-activation.html new file mode 100644 index 0000000000..f76936cda0 --- /dev/null +++ b/elements/pf-tabs/demo/manual-activation.html @@ -0,0 +1,34 @@ + + Users + Containers + Database + Disabled + Aria Disabled + Users + Containers + Database + Disabled + Aria Disabled + + + + + + + + diff --git a/elements/pf-tabs/demo/nested.html b/elements/pf-tabs/demo/nested.html index c20c36a8e3..1b9e83554b 100644 --- a/elements/pf-tabs/demo/nested.html +++ b/elements/pf-tabs/demo/nested.html @@ -1,33 +1,45 @@ - + + Users + Users + Containers + Containers Focusable element + Database + + + Users 2 + Users 2 + Containers 2 + Containers 2Focusable element + Database 2 + Database 2 + Disabled 2 + Disabled 2 + Aria Disabled 2 + Aria Disabled 2 + + + Disabled + Disabled + Aria Disabled + Aria Disabled + + -
-

Default

- - Users - Users - Containers - Containers Focusable element - Database - - - Users 2 - Users 2 - Containers 2 - Containers 2Focusable element - Database 2 - Database 2 - Disabled 2 - Disabled 2 - Aria Disabled 2 - Aria Disabled 2 - - - Disabled - Disabled - Aria Disabled - Aria Disabled - -
+ + diff --git a/elements/pf-tabs/demo/overflow.html b/elements/pf-tabs/demo/overflow.html new file mode 100644 index 0000000000..ec75811334 --- /dev/null +++ b/elements/pf-tabs/demo/overflow.html @@ -0,0 +1,49 @@ + + Users + Users + Containers + Containers + Database + Database + Server + Server + System + System + Network + Network + + +
+ + +
+ + + + + diff --git a/elements/pf-tabs/demo/pf-tabs.html b/elements/pf-tabs/demo/pf-tabs.html index e257a1ab36..f666039a68 100644 --- a/elements/pf-tabs/demo/pf-tabs.html +++ b/elements/pf-tabs/demo/pf-tabs.html @@ -1,206 +1,34 @@ - - + + Users + Users + Containers + Containers Focusable element + Database + Database + Disabled + Disabled + Aria Disabled + Aria Disabled + + + + + -
-

Default

- - Users - Users - Containers - Containers Focusable element - Database - Database - Disabled - Disabled - Aria Disabled - Aria Disabled - -
-
-

Box

- - Users - Users - Containers - Containers - Database - Database - Disabled - Disabled - Aria Disabled - Aria Disabled - -
- Box Type: - - - - -
- -
-

Default overflow

- - Users - Users - Containers - Containers - Database - Database - Server - Server - System - System - Network - Network - - -
- Box variant: - - - - -
- -
-

Vertical

- - Users - Users - Containers - Containers - Database - Database - Disabled - Disabled - Aria Disabled - Aria Disabled - - -
- Box variant: - - - - -
- -
-

Inset

- - Users - Users - Containers - Containers - Database - Database - Disabled - Disabled - Aria Disabled - Aria Disabled - - -
-

Inset size:

- - - - - - -
-
- -
-

Icons and text

- - Users - Containers - Database - Server - System - Network - Users - Containers - Database - Server - System - Network - -
- -
-

Filled

- - Users - Users - Containers - Containers - Database - Database - -
- -
-

Filled with icons

- - Users - Containers - Database - Users - Containers - Database - -
- -
-

Active Tab is Disabled

- - Users - Users - Containers - Containers - Database - Database - Disabled - Disabled - Aria Disabled - Aria Disabled - -
- -
-

Tabs first in markup

- - Users - Containers - Database - Disabled - Aria Disabled - Users - Containers - Database - Disabled - Aria Disabled - -
- - -
-

Manual Activation

- - Users - Containers - Database - Disabled - Aria Disabled - Users - Containers - Database - Disabled - Aria Disabled - -
diff --git a/elements/pf-tabs/demo/pf-tabs.js b/elements/pf-tabs/demo/pf-tabs.js deleted file mode 100644 index 55ae934a57..0000000000 --- a/elements/pf-tabs/demo/pf-tabs.js +++ /dev/null @@ -1,43 +0,0 @@ -import '@patternfly/elements/pf-icon/pf-icon.js'; -import '@patternfly/elements/pf-switch/pf-switch.js'; -import '@patternfly/elements/pf-tabs/pf-tabs.js'; - -const toggleVariant = document.getElementById('toggle-variant'); -const resize = document.getElementById('overflow'); -const verticalInput = document.getElementById('toggle-vertical'); -const resizeInput = document.getElementById('toggle-resize'); -const verticalVariant = document.querySelector('pf-tabs[vertical]'); -const boxVariant = document.querySelector('pf-tabs[box]'); -const inset = document.querySelector('#inset'); - -function variantToggle() { - boxVariant.setAttribute('box', toggleVariant.checked ? 'dark' : 'light'); -} - -function verticalToggle() { - if (verticalInput.checked) { - verticalVariant.setAttribute('box', 'dark'); - } else { - verticalVariant.removeAttribute('box'); - } -} - -function resizeToggle() { - if (resizeInput.checked) { - resize.setAttribute('box', 'dark'); - } else { - resize.removeAttribute('box'); - } -} - -function insetToggle(event) { - inset.classList = event.target.value; -} - -for (const input of document.querySelectorAll('input[name="toggle-inset"]')) { - input.addEventListener('change', insetToggle); -} - -toggleVariant.addEventListener('change', variantToggle); -resizeInput.addEventListener('change', resizeToggle); -verticalInput.addEventListener('change', verticalToggle); diff --git a/elements/pf-tabs/demo/tabs-first-in-markup.html b/elements/pf-tabs/demo/tabs-first-in-markup.html new file mode 100644 index 0000000000..9290d43d1e --- /dev/null +++ b/elements/pf-tabs/demo/tabs-first-in-markup.html @@ -0,0 +1,34 @@ + + Users + Containers + Database + Disabled + Aria Disabled + Users + Containers + Database + Disabled + Aria Disabled + + + + + + + + diff --git a/elements/pf-tabs/demo/vertical.html b/elements/pf-tabs/demo/vertical.html new file mode 100644 index 0000000000..ee5490dfc7 --- /dev/null +++ b/elements/pf-tabs/demo/vertical.html @@ -0,0 +1,48 @@ + + Users + Users + Containers + Containers + Database + Database + Disabled + Disabled + Aria Disabled + Aria Disabled + + +
+ + +
+ + + + + + From eae3f522d8789a3733bec683a18b1f6f4859d596 Mon Sep 17 00:00:00 2001 From: Benny Powers Date: Mon, 4 Dec 2023 12:00:09 +0200 Subject: [PATCH 48/50] fix(tabs): loosen type of host for tabscontroller --- core/pfe-core/controllers/tabs-controller.ts | 118 +++++++++++-------- 1 file changed, 68 insertions(+), 50 deletions(-) diff --git a/core/pfe-core/controllers/tabs-controller.ts b/core/pfe-core/controllers/tabs-controller.ts index df18afc727..4fa228e238 100644 --- a/core/pfe-core/controllers/tabs-controller.ts +++ b/core/pfe-core/controllers/tabs-controller.ts @@ -1,38 +1,41 @@ -import type { ReactiveController, ReactiveElement } from 'lit'; +import type { ReactiveController, ReactiveControllerHost } from 'lit'; import { Logger } from '@patternfly/pfe-core/controllers/logger.js'; import { RovingTabindexController } from '@patternfly/pfe-core/controllers/roving-tabindex-controller.js'; -import { ComposedEvent } from '@patternfly/pfe-core'; +export interface Tab extends HTMLElement { + active: boolean; + disabled: boolean; +} -export class TabExpandEvent extends ComposedEvent { +export type Panel = HTMLElement + +export interface TabsControllerOptions { + isTab: (node: unknown) => node is Tab; + isPanel: (node: unknown) => node is Panel; + getHTMLElement?: () => HTMLElement; +} + +function isReactiveControllerHost(element: HTMLElement): element is HTMLElement & ReactiveControllerHost { + return 'addController' in element; +} + +export class TabExpandEvent extends Event { constructor( public tab: Tab, ) { - super('expand'); + super('expand', { bubbles: true, cancelable: true }); } } -export class TabDisabledEvent extends ComposedEvent { +export class TabDisabledEvent extends Event { constructor( public tab: Tab, ) { - super('disabled'); + super('disabled', { bubbles: true, cancelable: true }); } } -export interface Tab extends HTMLElement { - active: boolean; - disabled: boolean; -} - -export type Panel = HTMLElement - -export interface Validations { - isTab: (node: Node) => node is Tab; - isPanel: (node: Node) => node is Panel; -} - export class TabsController implements ReactiveController { static #instances = new Set(); @@ -40,23 +43,21 @@ export class TabsController implements ReactiveController { static { window.addEventListener('expand', event => { - if (!(event instanceof TabExpandEvent)) { - return; - } - for (const instance of this.#instances) { - if (instance.#isTab(event.tab) && instance.#tabs.has(event.tab)) { - instance.#onTabExpand(event.tab); + if (event instanceof TabExpandEvent) { + for (const instance of this.#instances) { + if (instance.#isTab(event.tab) && instance.#tabPanelMap.has(event.tab)) { + instance.#onTabExpand(event.tab); + } } } }); window.addEventListener('disabled', event => { - if (!(event instanceof TabDisabledEvent)) { - return; - } - for (const instance of this.#instances) { - if (instance.#isTab(event.tab) && instance.#tabs.has(event.tab)) { - instance.#onTabDisabled(); + if (event instanceof TabDisabledEvent) { + for (const instance of this.#instances) { + if (instance.#isTab(event.tab) && instance.#tabPanelMap.has(event.tab)) { + instance.#onTabDisabled(); + } } } }); @@ -64,13 +65,15 @@ export class TabsController implements ReactiveController { #logger: Logger; - #host: ReactiveElement; + #host: ReactiveControllerHost; + + #element: HTMLElement; - #tabs = new Map(); + #tabPanelMap = new Map(); - #isTab: Required['isTab']; + #isTab: TabsControllerOptions['isTab']; - #isPanel: Required['isPanel']; + #isPanel: TabsControllerOptions['isPanel']; #slottedTabs: Tab[] = []; @@ -121,7 +124,7 @@ export class TabsController implements ReactiveController { } set activeTab(tab: Tab | undefined) { - if (tab === undefined || !this.#tabs.has(tab)) { + if (tab === undefined || !this.#tabPanelMap.has(tab)) { this.#logger.warn(`The tab provided is not a valid tab.`); return; } @@ -131,7 +134,7 @@ export class TabsController implements ReactiveController { } protected get _tabs() { - return [...this.#tabs.keys()] as Tab[]; + return [...this.#tabPanelMap.keys()] as Tab[]; } get #activeTab(): Tab | undefined { @@ -146,18 +149,32 @@ export class TabsController implements ReactiveController { * isPanel: (x: Node): x is PfTabPanel => x instanceof PfTabPanel * }); */ - constructor(host: ReactiveElement, validations: Validations) { - this.#tabindex = new RovingTabindexController(host); + constructor(host: ReactiveControllerHost, options: TabsControllerOptions) { this.#logger = new Logger(host); - this.#isTab = validations.isTab; - this.#isPanel = validations.isPanel; + if (host instanceof HTMLElement) { + this.#element = host; + this.#tabindex = new RovingTabindexController(host); + } else { + const element = options.getHTMLElement?.(); + if (!element) { + throw new Error('TabsController must be instantiated with an HTMLElement of a `getHTMLElement` function'); + } + // TODO(bennypowers): remove after #2570, by forwarding the `getHTMLElement` options + if (!isReactiveControllerHost(element)) { + throw new Error('TabsController\'s host HTMLElement must be a controller host as well'); + } + this.#element = element; + this.#tabindex = new RovingTabindexController(element); + } + this.#isTab = options.isTab; + this.#isPanel = options.isPanel; TabsController.#tabsClasses.add(host.constructor); - if (host.isConnected) { + if (this.#element.isConnected) { TabsController.#instances.add(this); } (this.#host = host).addController(this); - this.#mo.observe(host, { attributes: false, childList: true, subtree: false }); - host.addEventListener('slotchange', this.#onSlotchange); + this.#mo.observe(this.#element, { attributes: false, childList: true, subtree: false }); + this.#element.addEventListener('slotchange', this.#onSlotchange); } hostConnected() { @@ -192,17 +209,18 @@ export class TabsController implements ReactiveController { } async #rebuild() { - const tabSlot = this.#host.shadowRoot?.querySelector('slot[name=tab]'); - const panelSlot = this.#host.shadowRoot?.querySelector('slot:not([name])'); + const tabSlot = this.#element.shadowRoot?.querySelector('slot[name=tab]'); + const panelSlot = this.#element.shadowRoot?.querySelector('slot:not([name])'); this.#slottedPanels = panelSlot?.assignedElements().filter(this.#isPanel) ?? []; this.#slottedTabs = tabSlot?.assignedElements().filter(this.#isTab) ?? []; - this.#tabs.clear(); + this.#tabPanelMap.clear(); await this.#registerSlottedTabs(); if (this._tabs.length > 0) { this.#updateAccessibility(); - await this.#tabindex.initItems(this._tabs, this.#host); + // TODO(bennypowers): adjust to fit, in or after #2570 + this.#tabindex.initItems(this._tabs, this.#element); this.#setActiveTab(); } @@ -232,14 +250,14 @@ export class TabsController implements ReactiveController { #addPairForTab(index: number, tab: Tab) { const panel = this.#slottedPanels[index]; if (this.#isPanel(panel)) { - this.#tabs.set(tab, panel); + this.#tabPanelMap.set(tab, panel); } else { this.#logger.warn(`Tab and panel do not match`, tab, panel); } } #deactivateExcept(indexToKeep: number) { - [...this.#tabs].forEach(([tab, panel], currentIndex) => { + [...this.#tabPanelMap].forEach(([tab, panel], currentIndex) => { tab.active = currentIndex === indexToKeep; panel.hidden = currentIndex !== indexToKeep; }); @@ -276,7 +294,7 @@ export class TabsController implements ReactiveController { } #updateAccessibility(): void { - for (const [tab, panel] of this.#tabs) { + for (const [tab, panel] of this.#tabPanelMap) { if (!panel.hasAttribute('aria-labelledby')) { panel.setAttribute('aria-labelledby', tab.id); } From ab9cc17a66d0c576d27a61735832e53eab254264 Mon Sep 17 00:00:00 2001 From: Benny Powers Date: Mon, 4 Dec 2023 12:11:23 +0200 Subject: [PATCH 49/50] refactor(tabs): docs and ordering --- core/pfe-core/controllers/tabs-controller.ts | 16 +++++++----- elements/pf-tabs/pf-tabs.ts | 26 +++++++++----------- 2 files changed, 22 insertions(+), 20 deletions(-) diff --git a/core/pfe-core/controllers/tabs-controller.ts b/core/pfe-core/controllers/tabs-controller.ts index 4fa228e238..efca501ffd 100644 --- a/core/pfe-core/controllers/tabs-controller.ts +++ b/core/pfe-core/controllers/tabs-controller.ts @@ -11,8 +11,11 @@ export interface Tab extends HTMLElement { export type Panel = HTMLElement export interface TabsControllerOptions { + /** Add an `isTab` predicate to ensure this tabs instance' state does not leak into parent tabs' state */ isTab: (node: unknown) => node is Tab; + /** Add an `isPanel` predicate to ensure this tabs instance' state does not leak into parent tabs' state */ isPanel: (node: unknown) => node is Panel; + /** If the controller host is not an element, pass a getter to supply the tabs container element */ getHTMLElement?: () => HTMLElement; } @@ -142,12 +145,13 @@ export class TabsController implements ReactiveController { } /** - * @param host - The host element of the tabs. - * @param validations - A set of methods (isTab, isPanel) to validate tabs and panels. - * @example new TabsController(this, { - * isTab: (x: Node): x is PfTab => x instanceof PfTab, - * isPanel: (x: Node): x is PfTabPanel => x instanceof PfTabPanel - * }); + * @example Usage in PfTab + * ```ts + * new TabsController(this, { + * isTab: (x): x is PfTab => x instanceof PfTab, + * isPanel: (x): x is PfTabPanel => x instanceof PfTabPanel + * }); + * ``` */ constructor(host: ReactiveControllerHost, options: TabsControllerOptions) { this.#logger = new Logger(host); diff --git a/elements/pf-tabs/pf-tabs.ts b/elements/pf-tabs/pf-tabs.ts index 33c370db5a..0a06b0be01 100644 --- a/elements/pf-tabs/pf-tabs.ts +++ b/elements/pf-tabs/pf-tabs.ts @@ -14,6 +14,8 @@ import { cascades } from '@patternfly/pfe-core/decorators.js'; import { PfTab } from './pf-tab.js'; import { PfTabPanel } from './pf-tab-panel.js'; +import '@patternfly/elements/pf-icon/pf-icon.js'; + import styles from './pf-tabs.css'; /** @@ -70,6 +72,10 @@ export class PfTabs extends LitElement { protected static readonly scrollTimeoutDelay = 150; + static isExpandEvent(event: Event): event is TabExpandEvent { + return event instanceof TabExpandEvent; + } + /** * Box styling on tabs. Defaults to null */ @@ -132,25 +138,17 @@ export class PfTabs extends LitElement { this.#tabs.activeTab = tab; } - @query('#tabs') private _tabsContainer!: HTMLElement; + @query('#tabs') private tabsContainer!: HTMLElement; - @queryAssignedElements({ slot: 'tab' }) private _tabs?: PfTab[]; - - protected get canShowScrollButtons(): boolean { - return !this.vertical; - } + @queryAssignedElements({ slot: 'tab' }) private tabs?: PfTab[]; #overflow = new OverflowController(this, { scrollTimeoutDelay: 200 }); #tabs = new TabsController(this, { - isTab: (x: Node): x is PfTab => x instanceof PfTab, - isPanel: (x: Node): x is PfTabPanel => x instanceof PfTabPanel, + isTab: (x): x is PfTab => x instanceof PfTab, + isPanel: (x): x is PfTabPanel => x instanceof PfTabPanel, }); - static isExpandEvent(event: Event): event is TabExpandEvent { - return event instanceof TabExpandEvent; - } - override connectedCallback() { super.connectedCallback(); this.id ||= getRandomId(this.localName); @@ -197,8 +195,8 @@ export class PfTabs extends LitElement { } #onSlotChange() { - if (this._tabs) { - this.#overflow.init(this._tabsContainer, this._tabs); + if (this.tabs) { + this.#overflow.init(this.tabsContainer, this.tabs); } } From 8f54b253c476a751575bdf2819f68c13dff47475 Mon Sep 17 00:00:00 2001 From: Steven Spriggs Date: Mon, 4 Dec 2023 10:55:36 -0500 Subject: [PATCH 50/50] chore(tabs): remove commented out code --- elements/pf-tabs/pf-tab.css | 4 ---- 1 file changed, 4 deletions(-) diff --git a/elements/pf-tabs/pf-tab.css b/elements/pf-tabs/pf-tab.css index 78bdf39c7a..e9c19d2995 100644 --- a/elements/pf-tabs/pf-tab.css +++ b/elements/pf-tabs/pf-tab.css @@ -15,10 +15,6 @@ --pf-c-tabs__link--before--BorderBottomColor: var(--pf-c-tabs__link--BackgroundColor, transparent); } -/* :host(.first[box][active]) #current::before { - left: calc(var(--pf-c-tabs__link--before--border-width--base, var(--pf-global--BorderWidth--sm, 1px)) * -1); -} */ - :host([vertical]) [part="text"] { max-width: 100%; overflow-wrap: break-word;