{"version":3,"file":"table.Cmjai_q5.js","sources":["../../../../../packages/web-components/src/lib/components/table/table.ts"],"sourcesContent":["import { PropertyValues, html, isServer } from 'lit';\nimport { property, query, state } from 'lit/decorators.js';\nimport { ResizeObserver as ResizeObserverPolyfill } from '@juggle/resize-observer';\nimport { pdsCustomElement as customElement } from '../../decorators/pds-custom-element';\nimport { PdsElement } from '../PdsElement';\nimport styles from './table.scss?inline';\nimport shadowStyles from './table-shadow-styles.scss?inline';\n\n/**\n * @summary A table wrapper that accepts table as its children\n *\n * @example\n * \n *
...
\n * \n *\n * @slot default Required: Populate with the html table element\n *\n * @event pds-table-collapse-all Can be fired on the .pds-c-table__wrapper element if you need to manually collapse all rows\n * @event pds-table-expand-all Can be fired on the .pds-c-table__wrapper element if you need to manually expand all rows\n * @event change Can be fired on the .pds-c-table__wrapper element if you need to manually state that the HTML has changed\n * @event pds-table-changed Fired after the change event has been triggered\n *\n * @warn\n * pds-c-table css can affect other components\n */\n@customElement('pds-table', {\n category: 'component',\n type: 'component',\n styles: shadowStyles,\n})\nexport class PdsTable extends PdsElement {\n /**\n * Boolean to determine if the table should have \"zebra\" striping\n */\n @property()\n striped: 'odd' | 'even' | 'default' = 'default';\n\n /**\n * Boolean to expand all rows on a collapsible table on initial page load\n */\n @property({ type: Boolean })\n expandAllOnLoad: boolean = false;\n\n /**\n * Boolean to remove the borders and rounded corners of the table. Default is false.\n */\n @property({ type: Boolean })\n removeBorder: boolean = false;\n\n /**\n * Boolean to add hoverable rows functionality to the table. Default is false.\n */\n @property({ type: Boolean })\n hoverableRows: boolean = false;\n\n /**\n * Boolean to set the header row to sticky, default is false.\n *\n * Sticky row header will stick to the top of the page when scrolled away unless the table is fixed height, in which case it will stick to the top of the scrollable container.\n */\n @property({ type: Boolean })\n stickyHeader: boolean = false;\n\n /**\n * Boolean to set the first column to sticky, default is false.\n */\n @property({ type: Boolean })\n stickyColumn: boolean = false;\n\n /**\n * String to set a fixed height for the table. Example values: 300px, .25vh, 25%\n */\n @property()\n fixedHeight: string;\n\n /** @internal */\n get classNames() {\n return {\n 'striped-even': this.striped === 'even',\n 'striped-odd': this.striped === 'odd',\n 'hoverable-rows': this.hoverableRows,\n };\n }\n\n /** @internal */\n @query('.pds-c-table__wrapper')\n wrapper: HTMLElement;\n\n table: HTMLTableElement;\n\n /** @internal */\n @state()\n ResizeObserver: any;\n\n /** @internal */\n @state()\n childNodeObserver: any;\n\n /** @internal */\n @state()\n resizeObserver: any;\n\n /** @internal */\n @state()\n // jest doesn't have an IntersectionObserver implementation, so including an any here\n intersectionObserver: IntersectionObserver | any;\n\n /**\n * @internal\n */\n disableTransition(elementArray: Array, transitionOff: boolean) {\n elementArray.forEach((element: HTMLElement) => {\n element.setAttribute('style', transitionOff ? 'transition: none' : '');\n });\n }\n\n /**\n * Initialize functions\n */\n constructor() {\n super();\n this.updateScroll = this.updateScroll.bind(this);\n }\n\n connectedCallback() {\n super.connectedCallback();\n this.initLocalization();\n if (!isServer && typeof window !== 'undefined') {\n window.addEventListener('scroll', this.updateScroll);\n }\n }\n\n disconnectedCallback(): void {\n super.disconnectedCallback();\n this.childNodeObserver.disconnect();\n window.removeEventListener('scroll', this.updateScroll);\n }\n\n childNodeObserverCallback(currentTable: PdsElement): void {\n // When child nodes change, we need to reapply the classes for them to be styled appropriately\n this.applyScrollClasses();\n // @ts-expect-error prepareExpandableRows does exist, because we're calling it on this component\n currentTable.prepareExpandableRows({ initialLoad: false });\n }\n\n protected override async firstUpdated(): Promise {\n super.firstUpdated();\n this.ResizeObserver = window.ResizeObserver || ResizeObserverPolyfill;\n this.resizeObserver = new this.ResizeObserver(\n (entries: ResizeObserverEntry[]) => {\n entries.forEach(() => this.resizedCallback());\n },\n );\n this.childNodeObserver = new MutationObserver(() =>\n this.childNodeObserverCallback(this),\n );\n\n this.table = this.querySelector('table')!;\n /**\n * Search for table and its element\n * and add the pds class to its respective element\n *\n * For Example\n * => \n */\n this.applyScrollClasses();\n this.classList.add(this.classMod('can-be-scrolled-left'));\n\n const lightDomExists = document.head.querySelector('#pds-table-styles');\n if (!lightDomExists) {\n const lightDomStyle = document.createElement('style');\n\n lightDomStyle.id = 'pds-table-styles';\n lightDomStyle.innerHTML = styles.toString();\n document.head.appendChild(lightDomStyle);\n }\n // We need to wait for the update to complete to get accurate height measurements\n // on our expandable rows\n // See https://lit.dev/docs/components/lifecycle/#updatecomplete for more information\n // on why this is necessary\n await this.updateComplete;\n\n // Do not worry about hydration for Jest tests\n if (\n typeof window !== 'undefined' &&\n window.navigator &&\n window.navigator.userAgent &&\n window.navigator.userAgent.includes('jsdom')\n ) {\n this.prepareExpandableRows({ initialLoad: true });\n } else {\n // This fixes the hydration error with the table component\n // where these hooks are fired during hydration (though they should only occur after render)\n // this was the smallest value that consistently worked\n // TODO: we should re-write table so that the trigger button for expandable\n // rows is an element (maybe even a PDS component) provided by the user\n // so we don't do this janky, inject-the-td-into-the-dom nonsense\n /* istanbul ignore next */\n setTimeout(() => {\n this.prepareExpandableRows({ initialLoad: true });\n }, 750);\n }\n\n // Watch border-box for changes in our resize observer\n const observerOptions = {\n box: 'border-box',\n };\n\n if (this.table && this.resizeObserver) {\n this.resizeObserver.observe(this.table, observerOptions);\n }\n\n this.wrapper.addEventListener('scroll', () => {\n this.applyScrollClasses();\n });\n\n // Options for the observer (which mutations to observe)\n const config = { childList: true, subtree: true };\n\n // Start observing the target node for configured mutations\n if (this.table && this.childNodeObserver) {\n this.childNodeObserver.observe(this.table, config);\n }\n\n let options: IntersectionObserverInit = {\n rootMargin: '-150px',\n threshold: [1],\n };\n if (this.stickyHeader && this.fixedHeight) {\n options = { ...options, root: this.wrapper, rootMargin: '-110px' };\n }\n this.intersectionObserver = new IntersectionObserver(\n // Jest won't allow us to get into this function\n /* istanbul ignore next */\n ([e]) => {\n /* istanbul ignore next */\n e.target\n .closest(`.pds-c-table`)\n ?.querySelector('thead')\n ?.classList.toggle(\n `${this.classMod('is-pinned')}`,\n !e.isIntersecting,\n );\n },\n options,\n );\n }\n\n updated(changedProperties: PropertyValues) {\n if (this.stickyColumn) {\n this.classList.add(this.classMod('sticky-column'));\n } else {\n this.classList.remove(this.classMod('sticky-column'));\n }\n\n if (!this.removeBorder) {\n this.classList.add(this.classMod('with-border'));\n } else {\n this.classList.remove(this.classMod('with-border'));\n }\n // Remove all striped classes and re-add the correct one\n // if striped has changed\n if (changedProperties.has('striped')) {\n this.classList.remove(this.classMod('striped-even'));\n this.classList.remove(this.classMod('striped-odd'));\n this.classList.remove(this.classMod('striped-default'));\n this.classList.add(this.classMod(`striped-${this.striped}`));\n }\n\n if (changedProperties.has('hoverableRows')) {\n if (this.hoverableRows) {\n this.classList.add(this.classMod('hoverable-rows'));\n } else {\n this.classList.remove(this.classMod('hoverable-rows'));\n }\n }\n\n // listen for sticky headers\n if (this.stickyHeader) {\n this.classList.add(`${this.classMod('sticky-header')}`);\n const firstTableRow = this.querySelector(`.pds-c-table > tbody > tr`);\n\n if (firstTableRow && this.intersectionObserver) {\n this.intersectionObserver.observe(firstTableRow);\n }\n } else if (this.intersectionObserver) {\n this.intersectionObserver.disconnect();\n this.classList.remove(`${this.classMod('sticky-header')}`);\n this.querySelectorAll(`.${this.classMod('is-pinned')}`).forEach((el) => {\n el.classList.remove(`${this.classMod('is-pinned')}`);\n });\n }\n }\n\n handleChange() {\n this.prepareExpandableRows({ initialLoad: false });\n const event = new Event('pds-table-changed', {\n bubbles: true,\n composed: true,\n });\n\n this.dispatchEvent(event);\n }\n\n handleCollapseAll(animate = true) {\n const expandableRegions = Array.from(\n this.querySelectorAll('.pds-c-table__expandable-row'),\n ) as HTMLElement[];\n const expandableRowWrappers = Array.from(\n this.querySelectorAll('.pds-c-table__expandable-row-wrapper'),\n ) as HTMLElement[];\n const expandableRowToggles = Array.from(\n this.querySelectorAll('.pds-c-table__toggle'),\n ) as HTMLElement[];\n\n // check if we need to disable transition\n if (!animate) {\n this.disableTransition(expandableRowWrappers, true);\n this.disableTransition(expandableRowToggles, true);\n }\n\n expandableRegions.forEach((region: HTMLElement) => {\n const trigger = region.querySelector(\n '.pds-c-table__toggle',\n ) as HTMLElement;\n const wrapper = region.querySelector(\n '.pds-c-table__expandable-row-wrapper',\n ) as HTMLElement;\n wrapper.classList.add('pds-c-table__expandable-row--is-collapsed');\n this.resetRegionHeight(region, trigger, wrapper);\n this.adjustKeyboardFocus(wrapper);\n });\n\n // reset transition\n if (!animate) {\n this.disableTransition(expandableRowWrappers, false);\n this.disableTransition(expandableRowToggles, false);\n }\n }\n\n handleExpandAll(animate = true) {\n const expandableRegions = Array.from(\n this.querySelectorAll('.pds-c-table__expandable-row'),\n ) as HTMLElement[];\n const expandableRowWrappers = Array.from(\n this.querySelectorAll('.pds-c-table__expandable-row-wrapper'),\n ) as HTMLElement[];\n const expandableRowToggles = Array.from(\n this.querySelectorAll('.pds-c-table__toggle'),\n ) as HTMLElement[];\n\n // check if we need to disable transition\n if (!animate) {\n this.disableTransition(expandableRowWrappers, true);\n this.disableTransition(expandableRowToggles, true);\n }\n\n expandableRegions.forEach((region: HTMLElement) => {\n const trigger = region.querySelector(\n '.pds-c-table__toggle',\n ) as HTMLElement;\n const wrapper = region.querySelector(\n '.pds-c-table__expandable-row-wrapper',\n ) as HTMLElement;\n wrapper.classList.remove('pds-c-table__expandable-row--is-collapsed');\n this.resetRegionHeight(region, trigger, wrapper);\n this.adjustKeyboardFocus(wrapper);\n });\n\n // reset transition\n if (!animate) {\n this.disableTransition(expandableRowWrappers, false);\n this.disableTransition(expandableRowToggles, false);\n }\n }\n\n /**\n * Set the child elements to be focusable or remove them from the tab order, based on if they're shown/hidden\n */\n adjustKeyboardFocus(wrapper: HTMLElement) {\n const tableBody = wrapper.querySelector('tbody');\n const focusableElements = [\n 'a',\n 'button',\n 'input',\n 'textarea',\n 'select',\n 'details',\n ];\n const isRowExpanded = !wrapper.classList.contains(\n 'pds-c-table__expandable-row--is-collapsed',\n );\n\n if (tableBody) {\n if (isRowExpanded) {\n // If expanded, set aria-hidden on the tbody to true\n tableBody.setAttribute('aria-hidden', 'true');\n } else {\n // If collapsed, set aria-hidden on the tbody to false\n tableBody.setAttribute('aria-hidden', 'false');\n }\n }\n\n // Get all rows to loop through, but we'll skip the expandable ones because they're always visible\n const wrapperRows = Array.from(\n wrapper.querySelectorAll('tr'),\n ) as HTMLElement[];\n wrapperRows.forEach((wrapperRow: HTMLElement, index: number) => {\n if (index !== 0) {\n // Not on the first row, so now we can get all the child elements\n const allRowChildren = Array.from(\n wrapperRow.querySelectorAll('*'),\n ) as HTMLElement[];\n allRowChildren.forEach((rowChildElement: HTMLElement) => {\n // If the child is a keyboard focusable element, add or remove tabindex\n if (\n focusableElements.includes(rowChildElement.tagName.toLowerCase())\n ) {\n if (isRowExpanded) {\n // If the row is expanded, add the element back into the natural tab order\n rowChildElement.removeAttribute('tabindex');\n } else {\n // If the row is closed, remove this element from the tab order by setting tabindex = -1\n rowChildElement.setAttribute('tabindex', '-1');\n }\n }\n // The above will work for all non-web components, but for web components we need to check their shadow dom\n if (rowChildElement.shadowRoot) {\n const shadowFocusableElements = Array.from(\n rowChildElement.shadowRoot.querySelectorAll(\n 'a[href], button, input, textarea, select, details',\n ),\n ) as HTMLElement[];\n\n // Loop through all the focuable elements in shadowRoot and add/remove tabindex\n shadowFocusableElements.forEach((element: HTMLElement) => {\n if (isRowExpanded) {\n // If the row is expanded, add the elements back into the natural tab order\n element.removeAttribute('tabindex');\n } else {\n // If the row is collapsed, remove these elements from the tab order by setting tabindex = -1\n element.setAttribute('tabindex', '-1');\n }\n });\n }\n });\n }\n });\n }\n\n setCssCustomProps() {\n // set css custom properties\n const verticalAdjust =\n this.wrapper.offsetHeight - this.wrapper.clientHeight - 1;\n const horizontalAdjust =\n this.wrapper.offsetWidth - this.wrapper.clientWidth - 1;\n const pinnedAdjust = this.querySelector('thead')?.offsetHeight;\n this.style.setProperty(\n '--pds-table-horizontal-scroller-offset',\n `${horizontalAdjust}px`,\n );\n this.style.setProperty(\n '--pds-table-vertical-scroller-offset',\n `${verticalAdjust}px`,\n );\n this.style.setProperty('--pds-table-pinned-offset', `${pinnedAdjust}px`);\n if (this.fixedHeight) {\n this.style.setProperty('--pds-table-fixed-height', `${this.fixedHeight}`);\n }\n }\n\n updateScroll(): void {\n if (this.stickyHeader && !this.fixedHeight) {\n const rect = this.table.getBoundingClientRect();\n const header = this.table.querySelector('thead') as HTMLElement;\n const scrollTop = window.scrollY;\n const distFromTop = header.offsetHeight || 0;\n const top = rect?.top;\n const tableOffSetTop = top + scrollTop;\n const tableOuterHeight = this.table.offsetHeight;\n if (\n scrollTop > tableOffSetTop &&\n scrollTop < tableOffSetTop + tableOuterHeight - distFromTop\n ) {\n header.style.setProperty('top', `${scrollTop - tableOffSetTop}px`);\n } else {\n header.style.removeProperty('top');\n }\n }\n }\n\n applyScrollClasses(): void {\n // if we can scroll any direction, we need a tabindex=0 on the wrapper for a11y\n if (\n this.wrapper.scrollLeft > 0 ||\n this.wrapper.scrollTop > 0 ||\n ((!this.stickyHeader || this.fixedHeight) &&\n this.wrapper.scrollLeft <\n this.wrapper.scrollWidth - this.wrapper.clientWidth) ||\n this.wrapper.scrollTop <\n this.wrapper.scrollHeight - this.wrapper.clientHeight\n ) {\n this.wrapper.setAttribute('tabindex', '0');\n } else {\n this.wrapper.removeAttribute('tabindex');\n }\n\n if (this.wrapper.scrollLeft > 0) {\n this.classList.add(this.classMod('can-be-scrolled-left'));\n } else {\n this.classList.remove(this.classMod('can-be-scrolled-left'));\n }\n\n if (this.wrapper.scrollTop > 0) {\n this.classList.add(this.classMod('can-be-scrolled-up'));\n } else {\n this.classList.remove(this.classMod('can-be-scrolled-up'));\n }\n\n if (\n (!this.stickyHeader || this.fixedHeight) &&\n Math.ceil(this.wrapper.scrollLeft) <\n this.wrapper.scrollWidth - this.wrapper.clientWidth\n ) {\n this.wrapper.classList.add(this.classMod('can-be-scrolled-right'));\n } else {\n this.wrapper.classList.remove(this.classMod('can-be-scrolled-right'));\n }\n\n if (\n this.wrapper.scrollTop <\n this.wrapper.scrollHeight - this.wrapper.clientHeight\n ) {\n this.wrapper.classList.add(this.classMod('can-be-scrolled-down'));\n } else {\n this.wrapper.classList.remove(this.classMod('can-be-scrolled-down'));\n }\n }\n\n resizedCallback() {\n // If the wrapper isn't as wide as the table itself, it must be scrollable\n if (\n ((this.wrapper && this.wrapper.clientWidth) || 0) < this.table.clientWidth\n ) {\n // So we'll add the scroll classes\n this.applyScrollClasses();\n } else {\n // Remove the scroll classes if it's not scrollable anymore\n this.classList.remove(this.classMod('can-be-scrolled-left'));\n this.wrapper.classList.remove(this.classMod('can-be-scrolled-left'));\n this.classList.remove(this.classMod('can-be-scrolled-right'));\n this.wrapper.classList.remove(this.classMod('can-be-scrolled-right'));\n }\n\n this.setCssCustomProps();\n }\n\n /**\n * Add classes, attributes and trigger buttons to the expandable rows\n */\n prepareExpandableRows(options: { initialLoad: boolean }): void {\n const expandableRegions = Array.from(\n this.querySelectorAll('.pds-c-table__expandable-row'),\n ) as HTMLElement[];\n\n if (expandableRegions.length > 0) {\n this.classList.add('pds-c-table__has-collapsible-rows');\n }\n const headers = this.querySelectorAll(\n '.pds-c-table > thead > tr > th',\n ).length;\n this.querySelectorAll('.pds-c-table__expandable-row > td').forEach((td) => {\n td.setAttribute('colspan', headers.toString());\n });\n this.style.setProperty(\n '--pds-table-column-percentage',\n `${100 / headers}%`,\n );\n\n expandableRegions.forEach((region: HTMLElement) => {\n const wrapper = region.querySelector(\n '.pds-c-table__expandable-row-wrapper',\n ) as HTMLElement;\n wrapper.classList.add('pds-c-table__expandable-row--is-expandable');\n wrapper.classList.add('pds-c-table--rendered');\n if (options.initialLoad) {\n // If first update and expandAllOnLoad is true, don't add the collapsed class\n // but if it's the first update and expandAllOnLoad is false, we want to add it in\n if (!this.expandAllOnLoad) {\n wrapper.classList.add('pds-c-table__expandable-row--is-collapsed');\n }\n }\n // If it's not first update, don't mess with the class because we're just repainting\n\n const id = `pds-table__expandable-row--row${this.getRandomId()}`;\n region.setAttribute('id', id);\n const triggerButton = this.createTriggerButton(region);\n // Target the first td and add the toggle button in\n const firstTd = region.querySelector(\n '.pds-c-table__expandable-row-wrapper td',\n );\n\n // If firstTd exists and has a child, and that child is our cell wrapper,\n // we've already done this and we don't need to run it again\n // If firstTd exists but doesn't have a child, or that child does not have our cell wrapper,\n // we need to add it\n if (\n firstTd &&\n ((firstTd.firstElementChild &&\n !firstTd.firstElementChild.classList.contains(\n 'pds-c-table__expandable-row__cell-wrapper',\n )) ||\n !firstTd.firstElementChild)\n ) {\n // Create a wrapper div and move everything in the td inside it\n const wrapperDiv = document.createElement('div');\n wrapperDiv.classList.add('pds-c-table__expandable-row__cell-wrapper');\n while (firstTd.firstChild) {\n wrapperDiv.appendChild(firstTd.firstChild);\n }\n firstTd.appendChild(wrapperDiv);\n\n wrapperDiv.prepend(triggerButton);\n }\n\n if (\n wrapper.classList.contains('pds-c-table__expandable-row--is-collapsed')\n ) {\n // If collapsed, height is just the expandable TR height\n const regionsTR = region.querySelector('tr') as HTMLElement;\n const initialHeight = regionsTR.scrollHeight;\n wrapper.style.setProperty('height', `${initialHeight}px`);\n } else {\n // If open, height is the expandable TR height + TR height of every contained row\n const allRowsInExpandable = wrapper.querySelectorAll(\n '.pds-c-table__expandable-row-table > tbody > tr',\n );\n let totalHeight = 0;\n allRowsInExpandable.forEach((row) => {\n totalHeight += row.scrollHeight;\n });\n\n wrapper.style.setProperty('height', `${totalHeight}px`);\n }\n // Determine if elements should be within the natural tab flow or not, based on whether they are shown/hidden\n this.adjustKeyboardFocus(wrapper);\n });\n }\n\n createTriggerButton(region: HTMLElement): HTMLButtonElement {\n const triggerButton = document.createElement('button');\n triggerButton.setAttribute('type', 'button');\n triggerButton.setAttribute('aria-expanded', 'false');\n triggerButton.setAttribute('aria-controls', region.id);\n triggerButton.setAttribute(\n 'aria-label',\n this.translateText('expand-collapse-row'),\n );\n triggerButton.classList.add('pds-c-table__toggle');\n triggerButton.innerHTML = this.getToggleChevron();\n triggerButton.addEventListener('click', () => {\n this.toggleRegionCollapse(region, triggerButton);\n });\n return triggerButton;\n }\n\n getToggleChevron(): string {\n return ``;\n }\n\n toggleRegionCollapse(region: HTMLElement, triggerButton: HTMLElement) {\n const wrapper = region.querySelector(\n '.pds-c-table__expandable-row-wrapper',\n ) as HTMLElement;\n wrapper.classList.toggle('pds-c-table__expandable-row--is-collapsed');\n this.resetRegionHeight(region, triggerButton, wrapper);\n this.adjustKeyboardFocus(wrapper);\n }\n\n // Readjust the expandable region's height after an expand/collapse event\n // This forces the open/close animation to occur because we have a height transition animation in css\n resetRegionHeight(\n region: HTMLElement,\n triggerButton: HTMLElement,\n wrapper: HTMLElement,\n ) {\n if (\n wrapper.classList.contains('pds-c-table__expandable-row--is-collapsed') &&\n triggerButton\n ) {\n const regionTR = region.querySelector('tr') as HTMLElement;\n const initialHeight = regionTR.scrollHeight;\n wrapper.style.setProperty('height', `${initialHeight}px`);\n triggerButton.setAttribute('aria-expanded', 'false');\n } else if (triggerButton) {\n const initialHeight = wrapper.scrollHeight;\n wrapper.style.setProperty('height', `${initialHeight}px`);\n triggerButton.setAttribute('aria-expanded', 'true');\n }\n }\n\n render() {\n return html`