/** * @license * Copyright 2022 Google LLC * SPDX-License-Identifier: Apache-2.0 */ import '../../../focus/md-focus-ring.js'; import '../../../labs/item/item.js'; import '../../../ripple/ripple.js'; import {html, LitElement, nothing, PropertyValues, TemplateResult} from 'lit'; import {property, query} from 'lit/decorators.js'; import {ClassInfo, classMap} from 'lit/directives/class-map.js'; import {literal, html as staticHtml, StaticValue} from 'lit/static-html.js'; import {ARIAMixinStrict} from '../../../internal/aria/aria.js'; import {mixinDelegatesAria} from '../../../internal/aria/delegate.js'; import { createRequestActivationEvent, ListItem, } from '../list-navigation-helpers.js'; /** * Supported behaviors for a list item. */ export type ListItemType = 'text' | 'button' | 'link'; // Separate variable needed for closure. const listItemBaseClass = mixinDelegatesAria(LitElement); /** * @fires request-activation {Event} Requests the list to set `tabindex=0` on * the item and focus it. --bubbles --composed */ export class ListItemEl extends listItemBaseClass implements ListItem { /** @nocollapse */ static override shadowRootOptions = { ...LitElement.shadowRootOptions, delegatesFocus: true, }; /** * Disables the item and makes it non-selectable and non-interactive. */ @property({type: Boolean, reflect: true}) disabled = false; /** * Sets the behavior of the list item, defaults to "text". Change to "link" or * "button" for interactive items. */ @property({reflect: true}) type: ListItemType = 'text'; /** * READONLY. Sets the `md-list-item` attribute on the element. */ @property({type: Boolean, attribute: 'md-list-item', reflect: true}) isListItem = true; /** * Sets the underlying `HTMLAnchorElement`'s `href` resource attribute. */ @property() href = ''; /** * Sets the underlying `HTMLAnchorElement`'s `target` attribute when `href` is * set. */ @property() target: '_blank' | '_parent' | '_self' | '_top' | '' = ''; @query('.list-item') protected readonly listItemRoot!: HTMLElement | null; private get isDisabled() { return this.disabled && this.type !== 'link'; } protected override willUpdate(changed: PropertyValues) { if (this.href) { this.type = 'link'; } super.willUpdate(changed); } protected override render() { return this.renderListItem(html`
${this.renderRipple()} ${this.renderFocusRing()}
${this.renderBody()}
`); } /** * Renders the root list item. * * @param content the child content of the list item. */ protected renderListItem(content: unknown) { const isAnchor = this.type === 'link'; let tag: StaticValue; switch (this.type) { case 'link': tag = literal`a`; break; case 'button': tag = literal`button`; break; default: case 'text': tag = literal`li`; break; } const isInteractive = this.type !== 'text'; // TODO(b/265339866): announce "button"/"link" inside of a list item. Until // then all are "listitem" roles for correct announcement. const target = isAnchor && !!this.target ? this.target : nothing; return staticHtml` <${tag} id="item" tabindex="${this.isDisabled || !isInteractive ? -1 : 0}" ?disabled=${this.isDisabled} role="listitem" aria-selected=${(this as ARIAMixinStrict).ariaSelected || nothing} aria-checked=${(this as ARIAMixinStrict).ariaChecked || nothing} aria-expanded=${(this as ARIAMixinStrict).ariaExpanded || nothing} aria-haspopup=${(this as ARIAMixinStrict).ariaHasPopup || nothing} class="list-item ${classMap(this.getRenderClasses())}" href=${this.href || nothing} target=${target} @focus=${this.onFocus} >${content} `; } /** * Handles rendering of the ripple element. */ protected renderRipple(): TemplateResult | typeof nothing { if (this.type === 'text') { return nothing; } return html` `; } /** * Handles rendering of the focus ring. */ protected renderFocusRing(): TemplateResult | typeof nothing { if (this.type === 'text') { return nothing; } return html` `; } protected onFocusRingVisibilityChanged(e: Event) {} /** * Classes applied to the list item root. */ protected getRenderClasses(): ClassInfo { return {'disabled': this.isDisabled}; } /** * Handles rendering the headline and supporting text. */ protected renderBody() { return html` `; } protected onFocus() { if (this.tabIndex !== -1) { return; } // Handles the case where the user clicks on the element and then tabs. this.dispatchEvent(createRequestActivationEvent()); } override focus() { // TODO(b/300334509): needed for some cases where delegatesFocus doesn't // work programmatically like in FF and select-option this.listItemRoot?.focus(); } override click() { if (!this.listItemRoot) { // If the element has not finished rendering, call super to ensure click // events are dispatched. super.click(); return; } // Forward click to the element to ensure link .click() works correctly. this.listItemRoot.click(); } }