diff --git a/menu/demo/stories.ts b/menu/demo/stories.ts index 96d58a9b12..7c1166eb84 100644 --- a/menu/demo/stories.ts +++ b/menu/demo/stories.ts @@ -89,6 +89,9 @@ const sharedStyle = css` [dir=rtl] md-icon { transform: scaleX(-1); } + [slot="headline"] { + white-space: nowrap; + } `; const standard: MaterialStoryInit = { @@ -127,10 +130,10 @@ const standard: MaterialStoryInit = { @closed=${setButtonAriaExpandedFalse}> ${fruitNames.map((name, index) => html` +
${name}
`)} @@ -150,13 +153,13 @@ const linkable: MaterialStoryInit = { const isLastItem = index === fruitNames.length - 1; return html` - +
${name}
+ ${knobs['link icon']}
@@ -214,10 +217,9 @@ const submenu: MaterialStoryInit = { return html` + .keepOpen=${knobs.keepOpen} + .disabled=${knobs.disabled}> +
${name}
`; }); @@ -228,16 +230,17 @@ const submenu: MaterialStoryInit = { return html` - +
${name}
+ ${knobs['submenu item icon']}
@@ -259,10 +262,10 @@ const submenu: MaterialStoryInit = { return html` +
${name}
`; }), ]; @@ -273,16 +276,17 @@ const submenu: MaterialStoryInit = { return html` - +
${name}
+ ${knobs['submenu item icon']}
@@ -382,10 +386,10 @@ const menuWithoutButton: MaterialStoryInit = { @close-menu=${displayCloseEvent}> ${fruitNames.map((name, index) => html` +
${name}
`)} @@ -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 diff --git a/menu/internal/menuitem/_menu-item.scss b/menu/internal/menuitem/_menu-item.scss index 137205ffca..77ac5195db 100644 --- a/menu/internal/menuitem/_menu-item.scss +++ b/menu/internal/menuitem/_menu-item.scss @@ -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'), ) ); } - .list-item.selected { - background-color: var(--_selected-container-color); + :host([disabled]) { + opacity: map.get($list-item-tokens, 'disabled-opacity'); + pointer-events: none; } - .selected:not(.disabled) .label-text { - color: var(--_selected-label-text-color); - } + md-focus-ring { + z-index: 1; - // 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( + @include focus-ring.theme( ( - hover-opacity: 0, + '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: map.get($tokens, 'selected-container-color'); + } + + .selected:not(.disabled) .label-text { + color: map.get($tokens, 'selected-label-text-color'); + } } diff --git a/menu/internal/menuitem/forced-colors-styles.scss b/menu/internal/menuitem/forced-colors-styles.scss index 65784747de..edc92db6ee 100644 --- a/menu/internal/menuitem/forced-colors-styles.scss +++ b/menu/internal/menuitem/forced-colors-styles.scss @@ -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; diff --git a/menu/internal/menuitem/menu-item.ts b/menu/internal/menuitem/menu-item.ts index 5ec0dae6eb..ba430b5e12 100644 --- a/menu/internal/menuitem/menu-item.ts +++ b/menu/internal/menuitem/menu-item.ts @@ -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() { - if (this.keepOpen) return; + /** + * 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; + } - this.dispatchEvent(createDefaultCloseMenuEvent( - this, {kind: CLOSE_REASON.CLICK_SELECTION})); + 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; } - protected override getRenderClasses() { + private internalTypeaheadText: string|null = null; + + 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; + 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} + `; + } + + /** + * Handles rendering of the ripple element. + */ + protected renderRipple(): TemplateResult|typeof nothing { + return html` + `; + } + + /** + * Handles rendering of the focus ring. + */ + protected renderFocusRing(): TemplateResult|typeof nothing { + return html` + `; + } + + /** + * Classes applied to the list item root. + */ + protected getRenderClasses(): ClassInfo { return { - ...super.getRenderClasses(), - 'has-focus-ring': this.hasFocusRing, - selected: this.selected + 'disabled': this.disabled, + 'selected': this.selected, }; } - protected override onFocusRingVisibilityChanged(e: Event) { - const focusRing = e.target as MdFocusRing; - this.hasFocusRing = focusRing.visible; + /** + * 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(); + } + + protected onClick() { + if (this.keepOpen) return; + + this.dispatchEvent(createDefaultCloseMenuEvent( + this, {kind: CLOSE_REASON.CLICK_SELECTION})); } - protected override onKeydown(event: KeyboardEvent) { + protected onKeydown(event: KeyboardEvent) { if (this.keepOpen || event.defaultPrevented) return; const keyCode = event.code; diff --git a/menu/internal/shared.ts b/menu/internal/shared.ts index f91efa794c..1aa260a9ae 100644 --- a/menu/internal/shared.ts +++ b/menu/internal/shared.ts @@ -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. */ diff --git a/menu/internal/submenu/sub-menu.ts b/menu/internal/submenu/sub-menu.ts index 7d820b3a81..1a2e94aef6 100644 --- a/menu/internal/submenu/sub-menu.ts +++ b/menu/internal/submenu/sub-menu.ts @@ -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(); } diff --git a/menu/internal/typeaheadController.ts b/menu/internal/typeaheadController.ts index 79c405546d..0c3512d2bb 100644 --- a/menu/internal/typeaheadController.ts +++ b/menu/internal/typeaheadController.ts @@ -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)) ?? diff --git a/menu/menu-item.ts b/menu/menu-item.ts index 3b93ff9c4f..c450a7f62e 100644 --- a/menu/menu-item.ts +++ b/menu/menu-item.ts @@ -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]; } diff --git a/select/demo/stories.ts b/select/demo/stories.ts index 875174409b..d3c5506a3d 100644 --- a/select/demo/stories.ts +++ b/select/demo/stories.ts @@ -82,15 +82,31 @@ function renderIcon(iconName: string, slot: 'leading-icon'|'trailing-icon') { function renderItems() { return html` - - - - - - - - - `; + + +
Apple
+
+ +
Apricot
+
+ +
Apricots
+
+ +
Avocado
+
+ +
Green Apple
+
+ +
Green Grapes
+
+ +
Olive
+
+ +
Orange
+
`; } /** Select stories. */ diff --git a/select/internal/select.ts b/select/internal/select.ts index d64ca91ecf..17580822ca 100644 --- a/select/internal/select.ts +++ b/select/internal/select.ts @@ -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; diff --git a/select/internal/selectoption/select-option.ts b/select/internal/selectoption/select-option.ts index 0c0d6145b1..8a4b9ee35f 100644 --- a/select/internal/selectoption/select-option.ts +++ b/select/internal/selectoption/select-option.ts @@ -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) { if (changed.has('selected')) { diff --git a/select/internal/shared.ts b/select/internal/shared.ts index b515dc3e0f..2d9dc3a581 100644 --- a/select/internal/shared.ts +++ b/select/internal/shared.ts @@ -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; } /** diff --git a/select/select-option.ts b/select/select-option.ts index 7b3c252f31..d92a0ba83f 100644 --- a/select/select-option.ts +++ b/select/select-option.ts @@ -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]; } diff --git a/tokens/_index.scss b/tokens/_index.scss index 128070b492..29293e65bc 100644 --- a/tokens/_index.scss +++ b/tokens/_index.scss @@ -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-*; diff --git a/tokens/_md-comp-menu-list-item.scss b/tokens/_md-comp-menu-list-item.scss new file mode 100644 index 0000000000..05527a337e --- /dev/null +++ b/tokens/_md-comp-menu-list-item.scss @@ -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; +}