import { html, LitElement } from 'lit'; import { customElement, property, queryAssignedNodes } from 'lit/decorators.js'; import { classMap } from 'lit-html/directives/class-map.js'; import { ifDefined } from 'lit/directives/if-defined.js'; import '@kyndryl-design-system/shidoka-foundation/components/icon'; import arrowUpIcon from '@carbon/icons/es/arrow--up/16'; import styles from './table-header.scss'; import { SORT_DIRECTION, TABLE_CELL_ALIGN } from './defs'; /** * `kyn-th` Web Component. * * Represents a custom table header cell (``) for Shidoka's design system tables. * Provides sorting functionality when enabled and allows alignment customization. * * @fires on-sort-changed - Dispatched when the sort direction is changed. * @slot unnamed - The content slot for adding header text or content. */ @customElement('kyn-th') export class TableHeader extends LitElement { static override styles = [styles]; @property({ type: Boolean }) dense = false; /** * Specifies the alignment of the content within the table header. * Options: 'left', 'center', 'right' */ @property({ type: String, reflect: true }) align: TABLE_CELL_ALIGN = TABLE_CELL_ALIGN.LEFT; /** * Specifies if the column is sortable. * If set to true, an arrow icon will be displayed unpon hover, * allowing the user to toggle sort directions. */ @property({ type: Boolean, reflect: true }) sortable = false; /** Specifies the direction of sorting applied to the column. */ @property({ type: String, reflect: true }) sortDirection: SORT_DIRECTION = SORT_DIRECTION.DEFAULT; /** * The textual content associated with this component. * Represents the primary content or label that will be displayed. */ @property({ type: String }) headerLabel = ''; /** * The unique identifier representing this column header. * Used to distinguish between different sortable columns and * to ensure that only one column is sorted at a time. */ @property({ type: String }) sortKey = ''; /** * Determines whether the content should be hidden from visual view but remain accessible * to screen readers for accessibility purposes. When set to `true`, the content * will not be visibly shown, but its content can still be read by screen readers. * This is especially useful for providing additional context or information to * assistive technologies without cluttering the visual UI. */ @property({ type: Boolean }) visiblyHidden = false; /** * @ignore */ @queryAssignedNodes({ flatten: true }) listItems!: Array; /** * Resets the sorting direction of the component to its default state. * Useful for initializing or clearing any applied sorting on the element. */ resetSort() { this.sortDirection = SORT_DIRECTION.DEFAULT; } /** * Toggles the sort direction between ascending, descending, and default states. * It also dispatches an event to notify parent components of the sorting change. */ private toggleSortDirection() { if (!this.sortKey) { console.error('sortKey is missing for a sortable column.'); return; } switch (this.sortDirection) { case SORT_DIRECTION.DEFAULT: case SORT_DIRECTION.DESC: this.sortDirection = SORT_DIRECTION.ASC; break; case SORT_DIRECTION.ASC: this.sortDirection = SORT_DIRECTION.DESC; break; } // Dispatch event to notify parent components of the sorting change this.dispatchEvent( new CustomEvent('on-sort-changed', { bubbles: true, composed: true, detail: { sortDirection: this.sortDirection, sortKey: this.sortKey }, }) ); } override updated() { this.getTextContent(); } getTextContent() { const nonWhitespaceNodes = this.listItems.filter((node) => { return ( node?.nodeType !== Node.TEXT_NODE || node?.textContent?.trim() !== '' ); }); this.headerLabel = nonWhitespaceNodes[0]?.textContent || ''; } override render() { const iconClasses = { 'sort-icon': true, 'sort-icon--sorting': this.sortDirection !== SORT_DIRECTION.DEFAULT, 'sort-icon--sorting-asc': this.sortDirection === SORT_DIRECTION.ASC, 'sort-icon--sorting-desc': this.sortDirection === SORT_DIRECTION.DESC, }; const slotClasses = { 'slot-wrapper': true, 'sr-only': this.visiblyHidden, }; /** * Accessibility Enhancements: * - role: Sets the appropriate role for interactive headers (e.g., when sortable). * - ariaSort: Indicates the sorting direction to assistive technologies. * - ariaLabel: Provides a descriptive label to assistive technologies for sortable headers. * - tabIndex: Enables keyboard interaction for sortable headers. * - onKeyDown: Handles keyboard events for sortable headers to allow sorting via the keyboard. */ const role = this.sortable ? 'button' : undefined; const arialSort = this.sortable ? this.sortDirection : undefined; const ariaLabel = this.sortable && this.headerLabel ? `Sort by ${this.headerLabel}` : undefined; const tabIndex = this.sortable ? 0 : undefined; const onKeyDown = this.sortable ? (e: KeyboardEvent) => { if (e.key === 'Enter' || e.key === ' ') { this.toggleSortDirection(); } } : undefined; return html`
this.toggleSortDirection() : undefined} role=${ifDefined(role)} arial-label=${ifDefined(ariaLabel)} arial-sort=${ifDefined(arialSort)} tabindex=${ifDefined(tabIndex)} @keydown=${onKeyDown} >
${this.sortable ? html`` : null}
`; } } // Define the custom element in the global namespace declare global { interface HTMLElementTagNameMap { 'kyn-th': TableHeader; } }