refactor(menu)!: refactor menu-item to use md-item and not rely on md-list-item

BREAKING CHANGE: This change refactors menu-item to no longer subclass or import from list-item. It also refactors it to use md-item directly which means that the API of menu item has moved from properties to slots. `start-*` and `end-*` slots are now just `start` and `end`, many tokens are now gone in favor of slotting. `headline` property is now a `slot="headline"` slot. Typeahead search text can now be set via `typeaheadText` which defaults to the slotted headline `textContent`. `select-option` now has the `displayText` which is used to display text in the `md-select` when the option is selected; defaults to the slotted headline `textContent`.

PiperOrigin-RevId: 567719483
This commit is contained in:
Elliott Marquez 2023-09-22 14:35:10 -07:00 committed by Copybara-Service
parent 61b382a9df
commit 2a1d8776a7
15 changed files with 599 additions and 96 deletions

View File

@ -89,6 +89,9 @@ const sharedStyle = css`
[dir=rtl] md-icon {
transform: scaleX(-1);
}
[slot="headline"] {
white-space: nowrap;
}
`;
const standard: MaterialStoryInit<StoryKnobs> = {
@ -127,10 +130,10 @@ const standard: MaterialStoryInit<StoryKnobs> = {
@closed=${setButtonAriaExpandedFalse}>
${fruitNames.map((name, index) => html`
<md-menu-item
headline=${name}
id=${index}
.keepOpen=${knobs.keepOpen}
.disabled=${knobs.disabled}>
<div slot="headline">${name}</div>
</md-menu-item>`)}
</md-menu>
</div>
@ -150,13 +153,13 @@ const linkable: MaterialStoryInit<StoryKnobs> = {
const isLastItem = index === fruitNames.length - 1;
return html`
<md-menu-item
headline=${name}
id=${index}
.disabled=${knobs.disabled}
.target=${
knobs.target as '' | '_blank' | '_parent' | '_self' | '_top'}
.href=${knobs.href}>
<md-icon slot="end-icon">
<div slot="headline">${name}</div>
<md-icon slot="end">
${knobs['link icon']}
</md-icon>
</md-menu-item>
@ -214,10 +217,9 @@ const submenu: MaterialStoryInit<StoryKnobs> = {
return html`
<md-menu-item
headline=${name}
id=${currentIndex}
.keepOpen=${knobs.keepOpen}
.disabled=${knobs.disabled}>
.keepOpen=${knobs.keepOpen}
.disabled=${knobs.disabled}>
<div slot="headline">${name}</div>
</md-menu-item>`;
});
@ -228,16 +230,17 @@ const submenu: MaterialStoryInit<StoryKnobs> = {
return html`
<md-sub-menu
id=${currentIndex}
.anchorCorner=${knobs['submenu.anchorCorner']!}
.menuCorner=${knobs['submenu.menuCorner']!}
.hoverOpenDelay=${knobs.hoverOpenDelay}
.hoverCloseDelay=${knobs.hoverCloseDelay}>
<md-menu-item
slot="item"
headline=${name}
id=${currentIndex}
id=${++currentIndex}
.disabled=${knobs.disabled}>
<md-icon slot="end-icon">
<div slot="headline">${name}</div>
<md-icon slot="end">
${knobs['submenu item icon']}
</md-icon>
</md-menu-item>
@ -259,10 +262,10 @@ const submenu: MaterialStoryInit<StoryKnobs> = {
return html`
<md-menu-item
headline=${name}
id=${currentIndex}
.keepOpen=${knobs.keepOpen}
.disabled=${knobs.disabled}>
<div slot="headline">${name}</div>
</md-menu-item>`;
}),
];
@ -273,16 +276,17 @@ const submenu: MaterialStoryInit<StoryKnobs> = {
return html`
<md-sub-menu
id=${currentIndex}
.anchorCorner=${knobs['submenu.anchorCorner']!}
.menuCorner=${knobs['submenu.menuCorner']!}
.hoverOpenDelay=${knobs.hoverOpenDelay}
.hoverCloseDelay=${knobs.hoverCloseDelay}>
<md-menu-item
slot="item"
headline=${name}
id=${currentIndex}
id=${++currentIndex}
.disabled=${knobs.disabled}>
<md-icon slot="end-icon">
<div slot="headline">${name}</div>
<md-icon slot="end">
${knobs['submenu item icon']}
</md-icon>
</md-menu-item>
@ -382,10 +386,10 @@ const menuWithoutButton: MaterialStoryInit<StoryKnobs> = {
@close-menu=${displayCloseEvent}>
${fruitNames.map((name, index) => html`
<md-menu-item
headline=${name}
id=${index}
.keepOpen=${knobs.keepOpen}
.disabled=${knobs.disabled}>
<div slot="headline">${name}</div>
</md-menu-item>
`)}
</md-menu>
@ -438,9 +442,10 @@ function displayCloseEvent(event: CloseMenuEvent) {
const stringifyItem = (menuItem: MenuItem&HTMLElement) => {
const tagName = menuItem.tagName.toLowerCase();
const headline = menuItem.headline;
return `${tagName}${menuItem.id ? `[id="${menuItem.id}"]` : ''}[headline="${
headline}"]`;
const headline = menuItem.typeaheadText;
return `${tagName}${
menuItem.id ? `[id="${menuItem.id}"]` :
''} > [slot="headline"] > ${headline}`;
};
// display the event's details in the inner text of that output element

View File

@ -9,57 +9,162 @@
@use 'sass:string';
// go/keep-sorted end
// go/keep-sorted start
@use '../../../focus/focus-ring';
@use '../../../icon/icon';
@use '../../../list/list-item';
@use '../../../ripple/ripple';
@use '../../../tokens';
// go/keep-sorted end
@mixin theme($tokens) {
$list-item-supported-tokens: tokens.$md-comp-menu-list-item-supported-tokens;
$supported-tokens: tokens.$md-comp-menu-item-supported-tokens;
@each $token, $value in $tokens {
@if list.index($supported-tokens, $token) == null {
@if list.index($supported-tokens, $token) ==
null and
list.index($list-item-supported-tokens, $token) ==
null
{
@error 'Token `#{$token}` is not a supported token.';
}
@if $value {
@if $value and list.index($supported-tokens, $token) == null {
--md-menu-item-#{$token}: #{$value};
}
@if $value and list.index($list-item-supported-tokens, $token) == null {
--md-list-item-#{$token}: #{$value};
}
}
}
@mixin styles() {
$list-item-tokens: tokens.md-comp-menu-list-item-values();
$tokens: tokens.md-comp-menu-item-values();
:host {
@each $token, $value in $tokens {
--_#{$token}: var(--md-menu-item-#{$token}, #{$value});
}
border-radius: map.get($list-item-tokens, 'container-shape');
display: flex;
@include list-item.theme(
@include ripple.theme(
(
'container-color': var(--_container-color),
hover-color: map.get($list-item-tokens, 'hover-state-layer-color'),
hover-opacity: map.get($list-item-tokens, 'hover-state-layer-opacity'),
pressed-color: map.get($list-item-tokens, 'pressed-state-layer-color'),
pressed-opacity:
map.get($list-item-tokens, 'pressed-state-layer-opacity'),
)
);
}
:host([disabled]) {
opacity: map.get($list-item-tokens, 'disabled-opacity');
pointer-events: none;
}
md-focus-ring {
z-index: 1;
@include focus-ring.theme(
(
'shape': 8px,
)
);
}
a,
button,
li {
// Resets. These can be removed once we're no longer use these tags
background: none;
border: none;
padding: 0;
margin: 0;
text-align: unset;
text-decoration: none;
}
.list-item {
border-radius: inherit;
display: flex;
flex: 1;
outline: none;
// hide android tap color since we have ripple
-webkit-tap-highlight-color: transparent;
&:not(.disabled) {
cursor: pointer;
}
}
[slot='container'] {
pointer-events: none;
}
md-ripple {
border-radius: inherit;
}
md-item {
border-radius: inherit;
flex: 1;
color: map.get($list-item-tokens, 'label-text-color');
font-family: map.get($list-item-tokens, 'label-text-font');
font-size: map.get($list-item-tokens, 'label-text-size');
line-height: map.get($list-item-tokens, 'label-text-line-height');
font-weight: map.get($list-item-tokens, 'label-text-weight');
min-height: map.get($list-item-tokens, 'one-line-container-height');
padding-top: map.get($list-item-tokens, 'top-space');
padding-bottom: map.get($list-item-tokens, 'bottom-space');
padding-inline-start: map.get($list-item-tokens, 'leading-space');
padding-inline-end: map.get($list-item-tokens, 'trailing-space');
}
md-item[multiline] {
min-height: map.get($list-item-tokens, 'two-line-container-height');
}
[slot='supporting-text'] {
color: map.get($list-item-tokens, 'supporting-text-color');
font-family: map.get($list-item-tokens, 'supporting-text-font');
font-size: map.get($list-item-tokens, 'supporting-text-size');
line-height: map.get($list-item-tokens, 'supporting-text-line-height');
font-weight: map.get($list-item-tokens, 'supporting-text-weight');
}
[slot='trailing-supporting-text'] {
color: map.get($list-item-tokens, 'trailing-supporting-text-color');
font-family: map.get($list-item-tokens, 'trailing-supporting-text-font');
font-size: map.get($list-item-tokens, 'trailing-supporting-text-size');
line-height: map.get(
$list-item-tokens,
'trailing-supporting-text-line-height'
);
font-weight: map.get($list-item-tokens, 'trailing-supporting-text-weight');
}
:is([slot='start'], [slot='end'])::slotted(*) {
fill: currentColor;
}
[slot='start'] {
color: map.get($list-item-tokens, 'leading-icon-color');
}
[slot='end'] {
color: map.get($list-item-tokens, 'trailing-icon-color');
}
.list-item {
background-color: map.get($list-item-tokens, 'container-color');
}
.list-item.selected {
background-color: var(--_selected-container-color);
background-color: map.get($tokens, 'selected-container-color');
}
.selected:not(.disabled) .label-text {
color: var(--_selected-label-text-color);
}
// Set the ripple opacity to 0 if there is a submenu that is hovered.
.submenu-hover {
// Have to use ripple theme directly because :has selector in this case does
// not work in this case with the :has selector, thus we cannot override the
// custom props set in :host
@include ripple.theme(
(
hover-opacity: 0,
)
);
color: map.get($tokens, 'selected-label-text-color');
}
}

View File

@ -4,13 +4,17 @@
//
@media (forced-colors: active) {
:host([disabled]),
:host([disabled]) slot {
color: GrayText;
opacity: 1;
}
.list-item {
position: relative;
}
// Show double border only when selected, and the current list item does not
// have a focus ring on it.
.list-item.selected:not(.has-focus-ring)::before {
.list-item.selected::before {
content: '';
position: absolute;
inset: 0;

View File

@ -4,18 +4,59 @@
* SPDX-License-Identifier: Apache-2.0
*/
import {property, state} from 'lit/decorators.js';
import '../../../ripple/ripple.js';
import '../../../focus/md-focus-ring.js';
import '../../../item/item.js';
import type {MdFocusRing} from '../../../focus/md-focus-ring.js';
import {ListItemEl, ListItemRole} from '../../../list/internal/listitem/list-item.js';
import {html, LitElement, nothing, PropertyValues, TemplateResult} from 'lit';
import {property, query, queryAssignedElements} from 'lit/decorators.js';
import {ClassInfo, classMap} from 'lit/directives/class-map.js';
import {html as staticHtml, literal, StaticValue} from 'lit/static-html.js';
import {ARIAMixinStrict} from '../../../internal/aria/aria.js';
import {requestUpdateOnAriaChange} from '../../../internal/aria/delegate.js';
import {CLOSE_REASON, createDefaultCloseMenuEvent, isClosableKey, MenuItem} from '../shared.js';
export {ListItemRole} from '../../../list/internal/listitem/list-item.js';
/**
* Supported behaviors for a menu item.
*/
export type MenuItemType = 'menuitem'|'option'|'button'|'link';
/**
* @fires close-menu {CloseMenuEvent}
*/
export class MenuItemEl extends ListItemEl implements MenuItem {
export class MenuItemEl extends LitElement implements MenuItem {
static {
requestUpdateOnAriaChange(MenuItemEl);
}
/** @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 and role of the menu item, defaults to "menuitem".
*/
@property() type: MenuItemType = 'menuitem';
/**
* 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'|'' = '';
/**
* READONLY: self-identifies as a menu item and sets its identifying attribute
*/
@ -32,31 +73,168 @@ export class MenuItemEl extends ListItemEl implements MenuItem {
*/
@property({type: Boolean}) selected = false;
@state() protected hasFocusRing = false;
@query('.list-item') protected readonly listItemRoot!: HTMLElement|null;
override readonly type: ListItemRole = 'menuitem';
@queryAssignedElements({slot: 'headline'})
protected readonly headlineElements!: HTMLElement[];
protected override onClick() {
/**
* The text that is selectable via typeahead. If not set, defaults to the
* innerText of the item slotted into the `"headline"` slot.
*/
get typeaheadText() {
if (this.internalTypeaheadText !== null) {
return this.internalTypeaheadText;
}
const headlineElements = this.headlineElements;
let text = '';
headlineElements.forEach((headlineElement) => {
if (headlineElement.textContent && headlineElement.textContent.trim()) {
text += ` ${headlineElement.textContent.trim()}`;
}
});
return '';
}
set typeaheadText(text: string) {
this.internalTypeaheadText = text;
}
private internalTypeaheadText: string|null = null;
protected override willUpdate(changed: PropertyValues<MenuItemEl>) {
if (this.href) {
this.type = 'link';
}
super.willUpdate(changed);
}
protected override render() {
return this.renderListItem(html`
<md-item>
<div slot="container">
${this.renderRipple()}
${this.renderFocusRing()}
</div>
<slot name="start" slot="start"></slot>
<slot name="end" slot="end"></slot>
${this.renderBody()}
</md-item>
`);
}
/**
* 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;
let role: 'menuitem'|'option' = 'menuitem';
switch (this.type) {
case 'link':
tag = literal`a`;
break;
case 'button':
tag = literal`button`;
break;
default:
case 'menuitem':
tag = literal`li`;
break;
case 'option':
tag = literal`li`;
role = 'option';
break;
}
// TODO(b/265339866): announce "button"/"link" inside of a list item. Until
// then all are "menuitem" roles for correct announcement.
const target = isAnchor && !!this.target ? this.target : nothing;
return staticHtml`
<${tag}
id="item"
tabindex=${this.disabled && !isAnchor ? -1 : 0}
role=${role}
aria-label=${(this as ARIAMixinStrict).ariaLabel || nothing}
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}
@click=${this.onClick}
@keydown=${this.onKeydown}
>${content}</${tag}>
`;
}
/**
* Handles rendering of the ripple element.
*/
protected renderRipple(): TemplateResult|typeof nothing {
return html`
<md-ripple
part="ripple"
for="item"
?disabled=${this.disabled}></md-ripple>`;
}
/**
* Handles rendering of the focus ring.
*/
protected renderFocusRing(): TemplateResult|typeof nothing {
return html`
<md-focus-ring
part="focus-ring"
for="item"
inward></md-focus-ring>`;
}
/**
* 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`
<slot></slot>
<slot name="overline" slot="overline"></slot>
<slot name="headline" slot="headline"></slot>
<slot name="supporting-text" slot="supporting-text"></slot>
<slot name="trailing-supporting-text"
slot="trailing-supporting-text"></slot>
`;
}
override focus() {
// TODO(b/300334509): needed for some cases where delegatesFocus doesn't
// work programmatically like in FF and select-option
this.listItemRoot?.focus();
}
protected onClick() {
if (this.keepOpen) return;
this.dispatchEvent(createDefaultCloseMenuEvent(
this, {kind: CLOSE_REASON.CLICK_SELECTION}));
}
protected override getRenderClasses() {
return {
...super.getRenderClasses(),
'has-focus-ring': this.hasFocusRing,
selected: this.selected
};
}
protected override onFocusRingVisibilityChanged(e: Event) {
const focusRing = e.target as MdFocusRing;
this.hasFocusRing = focusRing.visible;
}
protected override onKeydown(event: KeyboardEvent) {
protected onKeydown(event: KeyboardEvent) {
if (this.keepOpen || event.defaultPrevented) return;
const keyCode = event.code;

View File

@ -13,14 +13,14 @@ interface MenuItemAdditions {
*/
disabled: boolean;
/**
* The text of the item that will be used for typeahead or Select's visible
* text when this item is selected.
* The text of the item that will be used for typeahead. If not set, defaults
* to the textContent of the element slotted into the headline.
*/
headline: string;
typeaheadText: string;
/**
* Whether it should keep the menu open after click.
*/
keepOpen?: boolean;
keepOpen: boolean;
/**
* Whether or not the item is in the selected visual state.
*/

View File

@ -7,8 +7,7 @@
import {html, isServer, LitElement} from 'lit';
import {property, queryAssignedElements} from 'lit/decorators.js';
import {List} from '../../../list/internal/list.js';
import {createDeactivateItemsEvent, createRequestActivationEvent} from '../../../list/internal/listitem/list-item.js';
import {createDeactivateItemsEvent, createRequestActivationEvent, deactivateActiveItem, getFirstActivatableItem} from '../list-navigation-helpers.js';
import {Corner, Menu} from '../menu.js';
import {CLOSE_REASON, CloseMenuEvent, createActivateTypeaheadEvent, createDeactivateTypeaheadEvent, KEYDOWN_CLOSE_KEYS, MenuItem, NAVIGABLE_KEY, SELECTION_KEY} from '../shared.js';
@ -110,6 +109,8 @@ export class SubMenu extends LitElement {
this.item.ariaExpanded = 'false';
this.dispatchEvent(createActivateTypeaheadEvent());
this.dispatchEvent(createDeactivateItemsEvent());
// aria-hidden required so ChromeVox doesn't announce the closed menu
menu.ariaHidden = 'true';
}, {once: true});
menu.quick = true;
// Submenus are in overflow when not fixed. Can remove once we have native
@ -119,6 +120,9 @@ export class SubMenu extends LitElement {
menu.menuCorner = this.menuCorner;
menu.anchorElement = this.item;
menu.defaultFocus = 'first-item';
// aria-hidden management required so ChromeVox doesn't announce the closed
// menu. Remove it here since we are about to show and focus it.
menu.removeAttribute('aria-hidden');
// This is required in the case where we have a leaf menu open and and the
// user hovers a parent menu's item which is not an md-sub-menu item.
// If this were set to true, then the menu would close and focus would be
@ -186,6 +190,13 @@ export class SubMenu extends LitElement {
this.item.setAttribute('aria-controls', this.menu.id);
}
this.item.keepOpen = true;
const menu = this.menu;
if (!menu) return;
menu.isSubmenu = true;
// Required for ChromeVox to not linearly navigate to the menu while closed
menu.ariaHidden = 'true';
}
/**
@ -271,7 +282,7 @@ export class SubMenu extends LitElement {
if (!submenu) return;
const submenuItems = submenu.items;
const firstActivatableItem = List.getFirstActivatableItem(submenuItems);
const firstActivatableItem = getFirstActivatableItem(submenuItems);
if (firstActivatableItem) {
await this.show();
@ -317,9 +328,9 @@ export class SubMenu extends LitElement {
await this.close();
List.deactivateActiveItem(this.menu.items);
deactivateActiveItem(this.menu.items);
this.item?.focus();
this.tabIndex = 0;
this.item.tabIndex = 0;
this.item.focus();
}

View File

@ -148,7 +148,7 @@ export class TypeaheadController {
// Generates the record array data structure which is the index, the element
// and a normalized header.
this.typeaheadRecords = this.items.map(
(el, index) => [index, el, el.headline.trim().toLowerCase()]);
(el, index) => [index, el, el.typeaheadText.trim().toLowerCase()]);
this.lastActiveRecord =
this.typeaheadRecords.find(
record => (record[TYPEAHEAD_RECORD.ITEM].tabIndex === 0)) ??

View File

@ -6,14 +6,10 @@
import {customElement} from 'lit/decorators.js';
import {styles as listItemForcedColorsStyles} from '../list/internal/listitem/forced-colors-styles.css.js';
import {styles as listItemStyles} from '../list/internal/listitem/list-item-styles.css.js';
import {styles as forcedColorsStyles} from './internal/menuitem/forced-colors-styles.css.js';
import {MenuItemEl} from './internal/menuitem/menu-item.js';
import {styles} from './internal/menuitem/menu-item-styles.css.js';
export {ListItem} from '../list/internal/listitem/list-item.js';
export {CloseMenuEvent, MenuItem} from './internal/shared.js';
declare global {
@ -39,6 +35,5 @@ declare global {
*/
@customElement('md-menu-item')
export class MdMenuItem extends MenuItemEl {
static override styles =
[listItemStyles, styles, listItemForcedColorsStyles, forcedColorsStyles];
static override styles = [styles, forcedColorsStyles];
}

View File

@ -82,15 +82,31 @@ function renderIcon(iconName: string, slot: 'leading-icon'|'trailing-icon') {
function renderItems() {
return html`
<md-select-option headline=""></md-select-option>
<md-select-option selected value="apple" headline="Apple"></md-select-option>
<md-select-option value="apricot" headline="Apricot"></md-select-option>
<md-select-option value="apricot" headline="Apricots"></md-select-option>
<md-select-option value="avocado" headline="Avocado"></md-select-option>
<md-select-option value="green_apple" headline="Green Apple"></md-select-option>
<md-select-option value="green_grapes" headline="Green Grapes"></md-select-option>
<md-select-option value="olive" headline="Olive"></md-select-option>
<md-select-option value="orange" headline="Orange"></md-select-option>`;
<md-select-option aria-label="blank" value=""></md-select-option>
<md-select-option selected value="apple">
<div slot="headline">Apple</div>
</md-select-option>
<md-select-option value="apricot" >
<div slot="headline">Apricot</div>
</md-select-option>
<md-select-option value="apricot" >
<div slot="headline">Apricots</div>
</md-select-option>
<md-select-option value="avocado" >
<div slot="headline">Avocado</div>
</md-select-option>
<md-select-option value="green_apple" >
<div slot="headline">Green Apple</div>
</md-select-option>
<md-select-option value="green_grapes" >
<div slot="headline">Green Grapes</div>
</md-select-option>
<md-select-option value="olive" >
<div slot="headline">Olive</div>
</md-select-option>
<md-select-option value="orange">
<div slot="headline">Orange</div>
</md-select-option>`;
}
/** Select stories. */

View File

@ -605,7 +605,7 @@ export abstract class Select extends LitElement {
this.lastSelectedOption !== firstSelectedOption;
this.lastSelectedOption = firstSelectedOption;
this[VALUE] = firstSelectedOption.value;
this.displayText = firstSelectedOption.headline;
this.displayText = firstSelectedOption.displayText;
} else {
hasSelectedOptionChanged = this.lastSelectedOption !== null;

View File

@ -7,7 +7,7 @@
import {PropertyValues} from 'lit';
import {property} from 'lit/decorators.js';
import {ListItemRole, MenuItemEl} from '../../../menu/internal/menuitem/menu-item.js';
import {MenuItemEl} from '../../../menu/internal/menuitem/menu-item.js';
import {createRequestDeselectionEvent, createRequestSelectionEvent, SelectOption} from '../shared.js';
/**
@ -23,7 +23,31 @@ export class SelectOptionEl extends MenuItemEl implements SelectOption {
*/
@property() value = '';
override readonly type: ListItemRole = 'option';
override readonly type = 'option';
private internalDisplayText: string|null = null;
/**
* 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() {
if (this.internalDisplayText !== null) {
return this.internalDisplayText;
}
const headlineElement = this.headlineElements[0];
if (headlineElement) {
return (headlineElement.textContent ?? '').trim();
}
return '';
}
set displayText(text: string) {
this.internalDisplayText = text;
}
override willUpdate(changed: PropertyValues<SelectOptionEl>) {
if (changed.has('selected')) {

View File

@ -19,6 +19,11 @@ interface SelectOptionSelf {
* 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;
}
/**

View File

@ -6,8 +6,6 @@
import {customElement} from 'lit/decorators.js';
import {styles as listItemForcedColorsStyles} from '../list/internal/listitem/forced-colors-styles.css.js';
import {styles as listItemStyles} from '../list/internal/listitem/list-item-styles.css.js';
import {styles as forcedColorsStyles} from '../menu/internal/menuitem/forced-colors-styles.css.js';
import {styles} from '../menu/internal/menuitem/menu-item-styles.css.js';
@ -52,6 +50,5 @@ declare global {
*/
@customElement('md-select-option')
export class MdSelectOption extends SelectOptionEl {
static override styles =
[listItemStyles, styles, listItemForcedColorsStyles, forcedColorsStyles];
static override styles = [styles, forcedColorsStyles];
}

View File

@ -34,6 +34,7 @@
@forward './md-comp-list-item' as md-comp-list-item-*;
@forward './md-comp-menu' as md-comp-menu-*;
@forward './md-comp-menu-item' as md-comp-menu-item-*;
@forward './md-comp-menu-list-item' as md-comp-menu-list-item-*;
@forward './md-comp-navigation-bar' as md-comp-navigation-bar-*;
@forward './md-comp-navigation-drawer' as md-comp-navigation-drawer-*;
@forward './md-comp-outlined-button' as md-comp-outlined-button-*;

View File

@ -0,0 +1,162 @@
//
// Copyright 2023 Google LLC
// SPDX-License-Identifier: Apache-2.0
//
// TODO: delete this file when we merge the list-item fixes
// go/keep-sorted start
@use 'sass:list';
@use 'sass:map';
@use 'sass:string';
// go/keep-sorted end
// go/keep-sorted start
@use './md-sys-color';
@use './md-sys-state';
@use './md-sys-typescale';
@use './v0_192/md-comp-list';
@use './values';
// go/keep-sorted end
$supported-tokens: (
// go/keep-sorted start
'bottom-space',
'disabled-opacity',
'focus-state-layer-color',
'focus-state-layer-opacity',
'hover-state-layer-color',
'hover-state-layer-opacity',
'label-text-color',
'label-text-font',
'label-text-line-height',
'label-text-size',
'label-text-weight',
'leading-icon-color',
'leading-space',
'one-line-container-height',
'pressed-state-layer-color',
'pressed-state-layer-opacity',
'supporting-text-color',
'supporting-text-font',
'supporting-text-line-height',
'supporting-text-size',
'supporting-text-weight',
'top-space',
'trailing-icon-color',
'trailing-space',
'trailing-supporting-text-color',
'trailing-supporting-text-font',
'trailing-supporting-text-line-height',
'trailing-supporting-text-size',
'trailing-supporting-text-weight',
'two-line-container-height',
// go/keep-sorted end
);
$unsupported-tokens: (
// go/keep-sorted start
'container-color',
'container-elevation',
'container-shape',
'disabled-label-text-color',
'disabled-label-text-opacity',
'disabled-leading-icon-color',
'disabled-leading-icon-opacity',
'disabled-state-layer-color',
'disabled-state-layer-opacity',
'disabled-trailing-icon-color',
'disabled-trailing-icon-opacity',
'divider-leading-space',
'divider-trailing-space',
'dragged-container-elevation',
'dragged-label-text-color',
'dragged-leading-icon-icon-color',
'dragged-state-layer-color',
'dragged-state-layer-opacity',
'dragged-trailing-icon-icon-color',
'focus-label-text-color',
'focus-leading-icon-icon-color',
'focus-trailing-icon-icon-color',
'hover-label-text-color',
'hover-leading-icon-icon-color',
'hover-trailing-icon-icon-color',
'label-text-tracking',
'label-text-type',
'large-leading-video-height',
'leading-avatar-color',
'leading-avatar-label-color',
'leading-avatar-label-font',
'leading-avatar-label-line-height',
'leading-avatar-label-size',
'leading-avatar-label-tracking',
'leading-avatar-label-type',
'leading-avatar-label-weight',
'leading-avatar-shape',
'leading-avatar-size',
'leading-icon-size',
'leading-image-height',
'leading-image-shape',
'leading-image-width',
'leading-video-shape',
'leading-video-width',
'overline-color',
'overline-font',
'overline-line-height',
'overline-size',
'overline-tracking',
'overline-type',
'overline-weight',
'pressed-label-text-color',
'pressed-leading-icon-icon-color',
'pressed-trailing-icon-icon-color',
'selected-trailing-icon-color',
'small-leading-video-height',
'supporting-text-tracking',
'supporting-text-type',
'three-line-container-height',
'trailing-icon-size',
'trailing-supporting-text-tracking',
'trailing-supporting-text-type',
'unselected-trailing-icon-color',
// go/keep-sorted end
);
$_default: (
'md-sys-color': md-sys-color.values-light(),
'md-sys-state': md-sys-state.values(),
'md-sys-typescale': md-sys-typescale.values(),
);
@function values($deps: $_default, $exclude-hardcoded-values: false) {
$original-tokens: md-comp-list.values($deps, $exclude-hardcoded-values);
$tokens: values.validate(
$original-tokens,
$supported-tokens: $supported-tokens,
$unsupported-tokens: $unsupported-tokens,
$new-tokens: (
'top-space': if($exclude-hardcoded-values, null, 12px),
'bottom-space': if($exclude-hardcoded-values, null, 12px),
'disabled-opacity':
map.get($original-tokens, 'list-item-disabled-label-text-opacity'),
),
$renamed-tokens: _get-renamed-tokens($original-tokens)
);
@return $tokens;
}
// remove list-item prefix from tokens
@function _get-renamed-tokens($tokens) {
$keys: map.keys($tokens);
$renamed-tokens: ();
@each $key in $keys {
@if string.index($key, 'list-item-') == 1 {
$renamed-key: string.slice($key, string.length('list-item-') + 1);
$renamed-tokens: map.set($renamed-tokens, $key, $renamed-key);
}
}
@return $renamed-tokens;
}