/** * @license * Copyright 2023 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} from 'lit'; import { property, query, queryAssignedElements, queryAssignedNodes, } from 'lit/decorators.js'; import {ClassInfo, classMap} from 'lit/directives/class-map.js'; import {ARIAMixinStrict} from '../../../internal/aria/aria.js'; import {mixinDelegatesAria} from '../../../internal/aria/delegate.js'; import {MenuItem} from '../../../menu/internal/controllers/menuItemController.js'; import {SelectOptionController} from './selectOptionController.js'; /** * The interface specific to a Select Option */ interface SelectOptionSelf { /** * The form value associated with the Select Option. (Note: the visual portion * of the SelectOption is the headline defined in ListItem) */ value: string; /** * Whether or not the SelectOption is selected. */ selected: boolean; /** * The text to display in the select when selected. Defaults to the * textContent of the Element slotted into the headline. */ displayText: string; } /** * The interface to implement for a select option. Additionally, the element * must have `md-list-item` and `md-menu-item` attributes on the host. */ export type SelectOption = SelectOptionSelf & MenuItem; // Separate variable needed for closure. const selectOptionBaseClass = mixinDelegatesAria(LitElement); /** * @fires close-menu {CustomEvent<{initiator: SelectOption, reason: Reason, itemPath: SelectOption[]}>} * Closes the encapsulating menu on closable interaction. --bubbles --composed * @fires request-selection {Event} Requests the parent md-select to select this * element (and deselect others if single-selection) when `selected` changed to * `true`. --bubbles --composed * @fires request-deselection {Event} Requests the parent md-select to deselect * this element when `selected` changed to `false`. --bubbles --composed */ export class SelectOptionEl extends selectOptionBaseClass implements SelectOption { /** @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; /** * READONLY: self-identifies as a menu item and sets its identifying attribute */ @property({type: Boolean, attribute: 'md-menu-item', reflect: true}) isMenuItem = true; /** * Sets the item in the selected visual state when a submenu is opened. */ @property({type: Boolean}) selected = false; /** * Form value of the option. */ @property() value = ''; @query('.list-item') protected readonly listItemRoot!: HTMLElement | null; @queryAssignedElements({slot: 'headline'}) protected readonly headlineElements!: HTMLElement[]; @queryAssignedElements({slot: 'supporting-text'}) protected readonly supportingTextElements!: HTMLElement[]; @queryAssignedNodes({slot: ''}) protected readonly defaultElements!: Element[]; type = 'option' as const; /** * The text that is selectable via typeahead. If not set, defaults to the * innerText of the item slotted into the `"headline"` slot. */ get typeaheadText() { return this.selectOptionController.typeaheadText; } @property({attribute: 'typeahead-text'}) set typeaheadText(text: string) { this.selectOptionController.setTypeaheadText(text); } /** * The text that is displayed in the select field when selected. If not set, * defaults to the textContent of the item slotted into the `"headline"` slot. */ get displayText() { return this.selectOptionController.displayText; } @property({attribute: 'display-text'}) set displayText(text: string) { this.selectOptionController.setDisplayText(text); } private readonly selectOptionController = new SelectOptionController(this, { getHeadlineElements: () => { return this.headlineElements; }, getSupportingTextElements: () => { return this.supportingTextElements; }, getDefaultElements: () => { return this.defaultElements; }, getInteractiveElement: () => this.listItemRoot, }); 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) { return html`
  • ${content}
  • `; } /** * Handles rendering of the ripple element. */ protected renderRipple() { return html` `; } /** * Handles rendering of the focus ring. */ protected renderFocusRing() { return html` `; } /** * Classes applied to the list item root. */ protected getRenderClasses(): ClassInfo { return { 'disabled': this.disabled, 'selected': this.selected, }; } /** * Handles rendering the headline and supporting text. */ protected renderBody() { return html` `; } override focus() { // TODO(b/300334509): needed for some cases where delegatesFocus doesn't // work programmatically like in FF and select-option this.listItemRoot?.focus(); } }