refactor(menu,list): clean up list-menu tokens and remove list-item variant components

In this PR:

- Remove unnecessary list-item variants
  - `md-list-item-icon` -> md-icon[data-variant=icon]
  - `md-list-item-video` -> video[data-variant=video]
  - `md-list-item-avatar` -> :is(img,div)[data-variant=avatar]
  - `md-list-item-avatar` -> img[data-variant=avatar]
- also upgrade menu tokens to maximize token sharing in list-item and menu-item
- testing menu and list for unused & undefined tokens
- fixed some small things in list to align with spec more

PiperOrigin-RevId: 513932810
This commit is contained in:
Material Web Team 2023-03-03 15:04:37 -08:00 committed by Copybara-Service
parent f7649add43
commit 5092de07e9
33 changed files with 493 additions and 827 deletions

View File

@ -44,8 +44,8 @@ Elevation | ✅ | ❌ | ❌
Focus ring | ✅ | ❌ | ❌
Field | ✅ | ✅ | 🟡
Icon | ✅ | ✅ | ❌
List | ✅ | | 🟡
Menu | ✅ | | 🟡
List | ✅ | 🟡 | 🟡
Menu | ✅ | 🟡 | 🟡
Progress indicator (circular) | ❌ | ❌ | ❌
Progress indicator (linear) | ❌ | ❌ | ❌
Radio button | ✅ | ✅ | ❌

View File

@ -7,6 +7,7 @@
// Selector '.md3-*' should only be used in this project.
// go/keep-sorted start
@use 'sass:list';
@use 'sass:map';
@use 'sass:string';
// go/keep-sorted end
@ -18,9 +19,9 @@
// go/keep-sorted end
@mixin theme($tokens) {
$reference: resolve-tokens(tokens.md-comp-list-values());
$tokens: resolve-tokens($tokens);
$reference: tokens.md-comp-list-values();
$tokens: theme.validate-theme($reference, $tokens);
$tokens: resolve-tokens($tokens);
$tokens: theme.create-theme-vars($tokens, list);
@include theme.emit-theme-vars($tokens);
@ -65,16 +66,25 @@
container-color,
map.get($tokens, list-item-container-color)
);
$list-item-tokens: list-item.resolve-tokens($tokens);
$list-tokens: ();
@each $token, $value in $tokens {
$is-unique: not map.has-key($list-item-tokens, $token);
@if $is-unique {
$list-tokens: map.set($list-tokens, $token, $value);
}
}
$list-tokens: remove-unused-tokens($tokens);
@return $list-tokens;
}
// removes unused tokens
@function remove-unused-tokens($tokens) {
$unused-tokens: ();
@each $token in map-keys($tokens) {
$index: string.index($token, 'list-item');
@if $index {
$unused-tokens: list.append($unused-tokens, $token);
}
}
@each $token in $unused-tokens {
$tokens: map.remove($tokens, $token);
}
@return $tokens;
}

View File

@ -1,40 +0,0 @@
//
// Copyright 2022 Google LLC
// SPDX-License-Identifier: Apache-2.0
//
// stylelint-disable selector-class-pattern --
// Selector '.md3-*' should only be used in this project.
// go/keep-sorted start
@use '../../../sass/map-ext';
@use '../../../sass/theme';
@use '../../../tokens';
// go/keep-sorted end
@mixin styles() {
$tokens: map-ext.pick(
tokens.md-comp-list-values(),
(
list-item-leading-avatar-color,
list-item-leading-avatar-shape,
list-item-leading-avatar-size
)
);
$tokens: theme.create-theme-vars($tokens, list);
:host {
@each $token, $value in $tokens {
--_#{$token}: #{$value};
}
}
.md3-list-item__avatar {
border-radius: var(--_list-item-leading-avatar-shape);
display: inline-flex;
height: var(--_list-item-leading-avatar-size);
margin-inline-start: 16px;
width: var(--_list-item-leading-avatar-size);
}
}

View File

@ -1,10 +0,0 @@
//
// Copyright 2022 Google LLC
// SPDX-License-Identifier: Apache-2.0
//
// go/keep-sorted start
@use './list-item-avatar';
// go/keep-sorted end
@include list-item-avatar.styles;

View File

@ -1,36 +0,0 @@
/**
* @license
* Copyright 2022 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import {html, LitElement, nothing, TemplateResult} from 'lit';
import {property} from 'lit/decorators.js';
// tslint:disable-next-line:enforce-comments-on-exported-symbols
export class ListItemAvatar extends LitElement {
/**
* The image `src` for the avatar
*/
@property() avatar = '';
/**
* The image `alt`.
*/
@property() altText = '';
/**
* The image `loading` attribute.
*/
@property() loading: 'eager'|'lazy' = 'eager';
override render(): TemplateResult {
return html`
<img
src="${this.avatar}"
alt="${this.altText || nothing}"
loading="${this.loading}"
class="md3-list-item__avatar" />
`;
}
}

View File

@ -1,55 +0,0 @@
//
// Copyright 2022 Google LLC
// SPDX-License-Identifier: Apache-2.0
//
// stylelint-disable selector-class-pattern --
// Selector '.md3-*' should only be used in this project.
// go/keep-sorted start
@use 'sass:map';
// go/keep-sorted end
// go/keep-sorted start
@use '../../../icon/icon';
@use '../../../sass/map-ext';
@use '../../../sass/theme';
// go/keep-sorted end
$_custom-property-prefix: 'list-item-icon';
$_reference: (
list-item-icon-color: #000,
list-item-icon-size: 0,
list-item-icon-opacity: 1,
);
@mixin theme($tokens) {
$tokens: theme.validate-theme($_reference, $tokens);
$tokens: theme.create-theme-vars($tokens, $_custom-property-prefix);
@include theme.emit-theme-vars($tokens);
}
@mixin styles() {
$tokens: $_reference;
$tokens: theme.create-theme-vars($tokens, $_custom-property-prefix);
:host {
@each $token, $value in $tokens {
--_#{$token}: #{$value};
}
display: inline-flex;
}
.md3-list-item__icon {
@include icon.theme(
(
color: var(--_list-item-icon-color),
size: var(--_list-item-icon-size),
)
);
opacity: var(--_list-item-icon-opacity);
padding-inline-start: 16px;
}
}

View File

@ -1,10 +0,0 @@
//
// Copyright 2022 Google LLC
// SPDX-License-Identifier: Apache-2.0
//
// go/keep-sorted start
@use './list-item-icon';
// go/keep-sorted end
@include list-item-icon.styles;

View File

@ -1,18 +0,0 @@
/**
* @license
* Copyright 2021 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import '../../../icon/icon.js';
import {html, LitElement, TemplateResult} from 'lit';
// tslint:disable-next-line:enforce-comments-on-exported-symbols
export class ListItemIcon extends LitElement {
override render(): TemplateResult {
return html`
<md-icon class="md3-list-item__icon"><slot></slot></md-icon>
`;
}
}

View File

@ -1,63 +0,0 @@
//
// Copyright 2022 Google LLC
// SPDX-License-Identifier: Apache-2.0
//
// stylelint-disable selector-class-pattern --
// Selector '.md3-*' should only be used in this project.
// go/keep-sorted start
@use 'sass:map';
// go/keep-sorted end
// go/keep-sorted start
@use '../../../sass/map-ext';
@use '../../../sass/theme';
@use '../../../tokens';
// go/keep-sorted end
$_custom-property-prefix: 'list-item-image';
@mixin theme($tokens) {
$reference: map-ext.pick(
tokens.md-comp-list-values(),
(
list-item-leading-image-height,
list-item-leading-image-width,
list-item-leading-image-shape
)
);
$tokens: theme.validate-theme($reference, $tokens);
$tokens: theme.create-theme-vars($tokens, $_custom-property-prefix);
@include theme.emit-theme-vars($tokens);
}
@mixin styles() {
$tokens: map-ext.pick(
tokens.md-comp-list-values(),
(
list-item-leading-image-height,
list-item-leading-image-width,
list-item-leading-image-shape
)
);
$tokens: theme.create-theme-vars($tokens, $_custom-property-prefix);
:host {
@each $token, $value in $tokens {
--_#{$token}: #{$value};
}
}
.md3-list-item__image {
display: inline-flex;
margin-block-end: 8px;
margin-block-start: 8px;
margin-inline-start: 16px;
height: var(--_list-item-leading-image-height);
width: var(--_list-item-leading-image-width);
border-radius: var(--_list-item-leading-image-shape);
}
}

View File

@ -1,10 +0,0 @@
//
// Copyright 2022 Google LLC
// SPDX-License-Identifier: Apache-2.0
//
// go/keep-sorted start
@use './list-item-image';
// go/keep-sorted end
@include list-item-image.styles;

View File

@ -1,36 +0,0 @@
/**
* @license
* Copyright 2022 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import {html, LitElement, nothing, TemplateResult} from 'lit';
import {property} from 'lit/decorators.js';
// tslint:disable-next-line:enforce-comments-on-exported-symbols
export class ListItemImage extends LitElement {
/**
* The image `src`.
*/
@property() image = '';
/**
* The image `alt`.
*/
@property() altText = '';
/**
* The image `loading` attribute.
*/
@property() loading: 'eager'|'lazy' = 'eager';
override render(): TemplateResult {
return html`
<img
src="${this.image}"
alt="${this.altText || nothing}"
loading=${this.loading}
class="md3-list-item__image" />
`;
}
}

View File

@ -9,14 +9,12 @@
// go/keep-sorted end
// go/keep-sorted start
@use '../../../focus/focus-ring';
@use '../../../icon/icon';
@use '../../../ripple/ripple';
@use '../../../sass/map-ext';
@use '../../../sass/theme';
@use '../../../sass/typography';
@use '../../../tokens';
@use '../icon/list-item-icon';
@use '../image/list-item-image';
@use '../video/list-item-video';
// go/keep-sorted end
@mixin theme($tokens) {
@ -30,26 +28,26 @@
}
@mixin styles() {
$tokens: resolve-tokens(tokens.md-comp-list-values());
$tokens: theme.create-theme-vars($tokens, 'list');
:host {
@each $token, $value in $tokens {
--_#{$token}: #{$value};
}
}
@include _list-item;
@include _image;
@include _icon;
@include _avatar;
@include _video;
}
@mixin _list-item() {
:host {
color: unset;
@include list-item-image.theme(
(
list-item-leading-image-height: var(--_list-item-leading-image-height),
list-item-leading-image-width: var(--_list-item-leading-image-width),
list-item-leading-image-shape: var(--_list-item-leading-image-shape),
)
);
@include list-item-video.theme(
(
list-item-small-leading-video-height:
var(--_list-item-small-leading-video-height),
list-item-large-leading-video-height:
var(--_list-item-large-leading-video-height),
list-item-leading-video-width: var(--_list-item-leading-video-width),
list-item-leading-video-shape: var(--_list-item-leading-video-shape),
)
);
@include focus-ring.theme(
(
offset-vertical: -2px,
@ -80,7 +78,7 @@
background-color: var(--_list-item-container-color);
border-radius: var(--_list-item-container-shape);
&.enabled {
&:not(.disabled) {
cursor: pointer;
}
@ -89,6 +87,11 @@
}
}
.content-wrapper {
display: flex;
width: 100%;
}
.with-one-line {
min-height: var(--_list-item-one-line-container-height);
}
@ -102,41 +105,15 @@
}
.start {
display: inline-flex;
flex-direction: column;
justify-content: center;
align-items: center;
flex: 0 0 auto;
z-index: 1;
@include list-item-icon.theme(
(
list-item-icon-color: var(--_list-item-leading-icon-color),
list-item-icon-size: var(--_list-item-leading-icon-size),
)
);
:hover & {
@include list-item-icon.theme(
(
list-item-icon-color: var(--_list-item-hover-leading-icon-icon-color),
)
);
}
:active & {
@include list-item-icon.theme(
(
list-item-icon-color:
var(--_list-item-pressed-leading-icon-icon-color),
)
);
}
.disabled & {
@include list-item-icon.theme(
(
list-item-icon-color: var(--_list-item-disabled-leading-icon-color),
list-item-icon-opacity:
var(--_list-item-disabled-leading-icon-opacity),
)
);
.with-three-line & {
justify-content: start;
}
.with-leading-thumbnail &,
@ -150,50 +127,25 @@
}
.body {
display: inline-flex;
justify-content: center;
flex-direction: column;
box-sizing: border-box;
flex: 1 0 0;
padding-inline-start: 16px;
width: 100%;
z-index: 1;
}
.end {
display: inline-flex;
flex-direction: column;
justify-content: center;
flex: 0 0 auto;
padding-inline-end: 24px;
z-index: 1;
@include list-item-icon.theme(
(
list-item-icon-color: var(--_list-item-trailing-icon-color),
list-item-icon-size: var(--_list-item-trailing-icon-size),
)
);
:hover & {
@include list-item-icon.theme(
(
list-item-icon-color: var(--_list-item-hover-trailing-icon-icon-color),
)
);
}
:active & {
@include list-item-icon.theme(
(
list-item-icon-color:
var(--_list-item-pressed-trailing-icon-icon-color),
)
);
}
.disabled & {
@include list-item-icon.theme(
(
list-item-icon-color: var(--_list-item-disabled-trailing-icon-color),
list-item-icon-opacity:
var(--_list-item-disabled-trailing-icon-opacity),
)
);
.with-three-line & {
justify-content: start;
}
}
@ -202,6 +154,18 @@
color: var(--_list-item-label-text-color);
font: var(--_list-item-label-text-type);
:hover & {
color: var(--_list-item-hover-label-text-color);
}
:focus & {
color: var(--_list-item-focus-label-text-color);
}
:active & {
color: var(--_list-item-pressed-label-text-color);
}
.disabled & {
color: var(--_list-item-disabled-label-text-color);
opacity: var(--_list-item-disabled-label-text-opacity);
@ -210,7 +174,6 @@
.supporting-text {
display: block;
padding-block-start: 4px;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
@ -244,13 +207,19 @@
color: var(--_list-item-disabled-label-text-color);
opacity: var(--_list-item-disabled-label-text-opacity);
}
}
.ripple {
display: inline-flex;
inset: 0;
position: absolute;
z-index: 0;
.with-three-line & {
/*
* In three line, trailing-supporting-text must align with the mid-line of
* the headline text.
*/
padding-block-start: calc(
(
var(--_list-item-label-text-line-height) -
var(--_list-item-trailing-supporting-text-line-height)
) / 2
);
}
}
.focus-ring {
@ -258,6 +227,199 @@
}
}
@mixin _image() {
::slotted([data-variant='image']) {
display: inline-flex;
margin-inline-start: 16px;
height: var(--_list-item-leading-image-height);
width: var(--_list-item-leading-image-width);
border-radius: var(--_list-item-leading-image-shape);
/* Min height is two-line height */
padding-block: calc(
(
var(--_list-item-two-line-container-height) -
var(--_list-item-leading-image-height)
) / 2
);
.with-three-line & {
padding-block: 0;
}
}
}
@mixin _icon() {
slot[name='start']::slotted([data-variant='icon']) {
@include icon.theme(
(
color: var(--_list-item-leading-icon-color),
size: var(--_list-item-leading-icon-size),
)
);
.with-three-line & {
/* In three line, icon must align with the mid-line of headline text */
padding-block-start: calc(
(
var(--_list-item-label-text-line-height) -
var(--_list-item-leading-icon-size)
) / 2
);
}
}
slot[name='end']::slotted([data-variant='icon']) {
@include icon.theme(
(
color: var(--_list-item-trailing-icon-color),
size: var(--_list-item-trailing-icon-size),
)
);
.with-three-line & {
/* In three line, icon must align with the mid-line of headline text */
padding-block-start: calc(
(
var(--_list-item-label-text-line-height) -
var(--_list-item-trailing-icon-size)
) / 2
);
}
}
::slotted([data-variant='icon']) {
padding-inline-start: 16px;
}
:hover {
slot[name='start']::slotted([data-variant='icon']) {
@include icon.theme(
(
color: var(--_list-item-hover-leading-icon-icon-color),
)
);
}
slot[name='end']::slotted([data-variant='icon']) {
@include icon.theme(
(
color: var(--_list-item-hover-trailing-icon-icon-color),
)
);
}
}
:focus {
slot[name='start']::slotted([data-variant='icon']) {
@include icon.theme(
(
color: var(--_list-item-focus-leading-icon-icon-color),
)
);
}
slot[name='end']::slotted([data-variant='icon']) {
@include icon.theme(
(
color: var(--_list-item-focus-trailing-icon-icon-color),
)
);
}
}
:active {
slot[name='start']::slotted([data-variant='icon']) {
@include icon.theme(
(
color: var(--_list-item-pressed-leading-icon-icon-color),
)
);
}
slot[name='end']::slotted([data-variant='icon']) {
@include icon.theme(
(
color: var(--_list-item-pressed-trailing-icon-icon-color),
)
);
}
}
.disabled {
slot[name='start']::slotted([data-variant='icon']) {
opacity: var(--_list-item-disabled-leading-icon-opacity);
@include icon.theme(
(
color: var(--_list-item-disabled-leading-icon-color),
)
);
}
slot[name='end']::slotted([data-variant='icon']) {
opacity: var(--_list-item-disabled-trailing-icon-opacity);
@include icon.theme(
(
color: var(--_list-item-disabled-trailing-icon-color),
)
);
}
}
}
@mixin _avatar() {
::slotted([data-variant='avatar']) {
display: inline-flex;
justify-content: center;
align-items: center;
margin-inline-start: 16px;
background-color: var(--_list-item-leading-avatar-color);
height: var(--_list-item-leading-avatar-size);
width: var(--_list-item-leading-avatar-size);
border-radius: var(--_list-item-leading-avatar-shape);
color: var(--_list-item-leading-avatar-label-color);
font: var(--_list-item-leading-avatar-label-type);
}
}
@mixin _video() {
::slotted([data-variant='video']),
::slotted([data-variant='video-large']) {
display: inline-flex;
object-fit: cover;
height: var(--_list-item-small-leading-video-height);
width: var(--_list-item-leading-video-width);
border-radius: var(--_list-item-leading-video-shape);
/* Min height is three-line height */
padding-block: calc(
(
var(--_list-item-three-line-container-height) -
var(--_list-item-small-leading-video-height)
) / 2
);
/*
* Let it auto-layout so that we don't mess with the icons and supporting
* text that is supposed to be top-aligned in three-line.
*/
.with-three-line & {
padding-block: 0;
}
}
::slotted([data-variant='video-large']) {
/* Min height is three-line height */
padding-block: calc(
(
var(--_list-item-three-line-container-height) -
var(--_list-item-large-leading-video-height)
) / 2
);
height: var(--_list-item-large-leading-video-height);
}
}
/// Resolves the tokens that are specific to list-item.
///
/// The tokenset for list include list plus all of list item. We do not want to
@ -275,22 +437,51 @@
}
}
// Do not include list-item-trailing-supporting-text or list-item-label-text
// because we actually need the line-height tokens separately from the
// *-type tokens.
$list-item-tokens: typography.resolve-tokens(
$list-item-tokens,
'list-item-label-text',
'list-item-supporting-text',
'list-item-trailing-supporting-text'
'list-item-leading-avatar-label',
'list-item-overline'
);
$list-item-tokens: _remove-unused-tokens($list-item-tokens);
@return $list-item-tokens;
}
@mixin private-props() {
$tokens: resolve-tokens(tokens.md-comp-list-values());
$tokens: theme.create-theme-vars($tokens, 'list');
// removes unused tokens
@function _remove-unused-tokens($tokens) {
$unused-tokens: (
'list-item-container-elevation',
'list-item-disabled-state-layer-color',
'list-item-disabled-state-layer-opacity',
'list-item-dragged-container-elevation',
'list-item-dragged-label-text-color',
'list-item-dragged-leading-icon-icon-color',
'list-item-dragged-state-layer-color',
'list-item-dragged-state-layer-opacity',
'list-item-dragged-trailing-icon-icon-color',
'list-item-overline-color',
'list-item-overline-type',
'list-item-selected-trailing-icon',
'list-item-selected-trailing-icon-color',
'list-item-unselected-trailing-icon-color',
'list-item-label-text-font',
'list-item-label-text-size',
'list-item-label-text-tracking',
'list-item-label-text-weight',
'list-item-trailing-supporting-text-font',
'list-item-trailing-supporting-text-size',
'list-item-trailing-supporting-text-tracking',
'list-item-trailing-supporting-text-weight'
);
:host {
@each $token, $value in $tokens {
--_#{$token}: #{$value};
}
@each $token in $unused-tokens {
$tokens: map.remove($tokens, $token);
}
@return $tokens;
}

View File

@ -1,10 +0,0 @@
//
// Copyright 2023 Google LLC
// SPDX-License-Identifier: Apache-2.0
//
// go/keep-sorted start
@use './list-item';
// go/keep-sorted end
@include list-item.private-props;

View File

@ -51,15 +51,16 @@ export class ListItemEl extends LitElement implements ListItem {
@property() headline = '';
/**
* The one-line supporting text below the headline.
* The one-line supporting text below the headline. Set
* `multiLineSupportingText` to `true` to support multiple lines in the
* supporting text.
*/
@property() supportingText = '';
/**
* The multi-line supporting text below the headline. __NOTE:__ if set to a
* truthy value, overrides the visibility and behavior of `supportingText`.
* Modifies `supportingText` to support multiple lines.
*/
@property() multiLineSupportingText = '';
@property({type: Boolean}) multiLineSupportingText = false;
/**
* The supporting text placed at the end of the item. Overriden by elements
@ -85,7 +86,7 @@ export class ListItemEl extends LitElement implements ListItem {
* tabindex is set to 0, and in some list item variants (like md-list-item),
* focuses the underlying item.
*/
@property({type: Boolean, reflect: true}) active = false;
@property({type: Boolean, reflect: true}) active = false;
/**
* READONLY. Sets the `md-list-item` attribute on the element.
@ -132,13 +133,11 @@ export class ListItemEl extends LitElement implements ListItem {
override render(): TemplateResult {
return this.renderListItem(html`
${this.renderStart()}
${this.renderBody()}
${this.renderEnd()}
<div class="ripple">
<div class="content-wrapper">
${this.renderStart()}
${this.renderBody()}
${this.renderEnd()}
${this.renderRipple()}
</div>
<div class="focus-ring">
${this.renderFocusRing()}
</div>`);
}
@ -179,7 +178,7 @@ export class ListItemEl extends LitElement implements ListItem {
* Handles rendering of the focus ring.
*/
protected renderFocusRing(): TemplateResult {
return html`<md-focus-ring .visible="${
return html`<md-focus-ring class="focus-ring" .visible="${
this.showFocusRing}"></md-focus-ring>`;
}
@ -188,13 +187,12 @@ export class ListItemEl extends LitElement implements ListItem {
*/
protected getRenderClasses(): ClassInfo {
return {
'with-one-line':
this.supportingText === '' && this.multiLineSupportingText === '',
'with-one-line': this.supportingText === '',
'with-two-line':
this.supportingText !== '' && this.multiLineSupportingText === '',
'with-three-line': this.multiLineSupportingText !== '',
'disabled': this.disabled,
'enabled': !this.disabled,
this.supportingText !== '' && !this.multiLineSupportingText,
'with-three-line':
this.supportingText !== '' && this.multiLineSupportingText,
'disabled': this.disabled
};
}
@ -209,10 +207,8 @@ export class ListItemEl extends LitElement implements ListItem {
* Handles rendering the headline and supporting text.
*/
protected renderBody(): TemplateResult {
const supportingText = this.multiLineSupportingText !== '' ?
this.renderMultiLineSupportingText() :
this.supportingText !== '' ? this.renderSupportingText() :
'';
const supportingText =
this.supportingText !== '' ? this.renderSupportingText() : '';
return html`<div class="body"
><span class="label-text">${this.headline}</span>${supportingText}</div>`;
@ -222,15 +218,16 @@ export class ListItemEl extends LitElement implements ListItem {
* Renders the one-line supporting text.
*/
protected renderSupportingText(): TemplateResult {
return html`<span class="supporting-text">${this.supportingText}</span>`;
return html`<span
class="supporting-text ${classMap(this.getSupportingTextClasses())}"
>${this.supportingText}</span>`;
}
/**
* Renders the multi-line supporting text
* Gets the classes for the supporting text node
*/
protected renderMultiLineSupportingText(): TemplateResult {
return html`<span class="supporting-text supporting-text--multi-line"
>${this.multiLineSupportingText}</span>`;
protected getSupportingTextClasses(): ClassInfo {
return {'supporting-text--multi-line': this.multiLineSupportingText};
}
/**

View File

@ -1,71 +0,0 @@
//
// Copyright 2022 Google LLC
// SPDX-License-Identifier: Apache-2.0
//
// stylelint-disable selector-class-pattern --
// Selector '.md3-*' should only be used in this project.
// go/keep-sorted start
@use 'sass:map';
// go/keep-sorted end
// go/keep-sorted start
@use '../../../sass/map-ext';
@use '../../../sass/theme';
@use '../../../tokens';
// go/keep-sorted end
$_custom-property-prefix: 'list-item-video';
@mixin theme($tokens) {
$reference: map-ext.pick(
tokens.md-comp-list-values(),
(
list-item-small-leading-video-height,
list-item-large-leading-video-height,
list-item-leading-video-width,
list-item-leading-video-shape
)
);
$tokens: theme.validate-theme($reference, $tokens);
$tokens: theme.create-theme-vars($tokens, $_custom-property-prefix);
@include theme.emit-theme-vars($tokens);
}
@mixin styles() {
$tokens: map-ext.pick(
tokens.md-comp-list-values(),
(
list-item-small-leading-video-height,
list-item-large-leading-video-height,
list-item-leading-video-width,
list-item-leading-video-shape
)
);
$tokens: theme.create-theme-vars($tokens, $_custom-property-prefix);
:host {
@each $token, $value in $tokens {
--_#{$token}: #{$value};
}
display: inline-flex;
}
.md3-list-item__video {
display: inline-flex;
margin-block-end: 12px;
margin-block-start: 12px;
object-fit: cover;
height: var(--_list-item-small-leading-video-height);
width: var(--_list-item-leading-video-width);
border-radius: var(--_list-item-leading-video-shape);
&.large {
height: var(--_list-item-large-leading-video-height);
}
}
}

View File

@ -1,10 +0,0 @@
//
// Copyright 2022 Google LLC
// SPDX-License-Identifier: Apache-2.0
//
// go/keep-sorted start
@use './list-item-video';
// go/keep-sorted end
@include list-item-video.styles;

View File

@ -1,82 +0,0 @@
/**
* @license
* Copyright 2022 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import {html, LitElement, nothing, TemplateResult} from 'lit';
import {property} from 'lit/decorators.js';
/**
* @fires loadeddata {Event} Dispatched whenever the native HTMLVideoElement
* fires the loadeddate event.
*/
export class ListItemVideo extends LitElement {
/**
* Displays the video in a taller format
*/
@property({type: Boolean}) large = false;
/**
* The underlying `<video>`'s `autoplay` property.
*/
@property({type: Boolean}) autoplay = false;
/**
* The underlying `<video>`'s `muted` property.
*/
@property({type: Boolean}) muted = false;
/**
* The underlying `<video>`'s `loop` property.
*/
@property({type: Boolean}) loop = false;
/**
* The underlying `<video>`'s `controls` property.
*/
@property({type: Boolean}) controls = false;
/**
* The underlying `<video>`'s `playsinline` property.
*/
@property({type: Boolean}) playsinline = false;
/**
* The underlying `<video>`'s `preload` property.
*/
@property({type: String}) preload: ''|'auto'|'metadata'|'none' = '';
/**
* The underlying `<video>`'s `poster` property.
*/
@property({type: String}) poster = '';
/**
* The `src` of the video.
*/
@property({type: String}) video = '';
/**
* The `alt` attribute if the video.
*/
@property({type: String}) altText = '';
override render(): TemplateResult {
return html`
<video
@loadeddata=${() => this.dispatchEvent(new Event('loadeddata'))}
.src="${this.video || nothing}"
.poster="${this.poster || nothing}"
alt="${this.altText || nothing}"
.autoplay=${this.autoplay}
.muted=${this.muted}
.loop=${this.loop}
.playsinline=${this.playsinline}
.controls=${this.controls}
class="md3-list-item__video ${this.large ? 'large' : ''}">
<slot></slot>
</video>
`;
}
}

View File

@ -1,27 +0,0 @@
/**
* @license
* Copyright 2022 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import {customElement} from 'lit/decorators.js';
import {ListItemAvatar} from './lib/avatar/list-item-avatar.js';
import {styles} from './lib/avatar/list-item-avatar-styles.css.js';
declare global {
interface HTMLElementTagNameMap {
'md-list-item-avatar': MdListItemAvatar;
}
}
/**
* @summary An image avatar that is expected to be slotted into a list item.
*
* @final
* @suppress {visibility}
*/
@customElement('md-list-item-avatar')
export class MdListItemAvatar extends ListItemAvatar {
static override styles = [styles];
}

View File

@ -1,27 +0,0 @@
/**
* @license
* Copyright 2021 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import {customElement} from 'lit/decorators.js';
import {ListItemIcon} from './lib/icon/list-item-icon.js';
import {styles} from './lib/icon/list-item-icon-styles.css.js';
declare global {
interface HTMLElementTagNameMap {
'md-list-item-icon': MdListItemIcon;
}
}
/**
* @summary A material icon that is expected to be slotted into a list item.
*
* @final
* @suppress {visibility}
*/
@customElement('md-list-item-icon')
export class MdListItemIcon extends ListItemIcon {
static override styles = [styles];
}

View File

@ -1,27 +0,0 @@
/**
* @license
* Copyright 2022 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import {customElement} from 'lit/decorators.js';
import {ListItemImage} from './lib/image/list-item-image.js';
import {styles} from './lib/image/list-item-image-styles.css.js';
declare global {
interface HTMLElementTagNameMap {
'md-list-item-image': MdListItemImage;
}
}
/**
* @summary An image that is expected to be slotted into a list item.
*
* @final
* @suppress {visibility}
*/
@customElement('md-list-item-image')
export class MdListItemImage extends ListItemImage {
static override styles = [styles];
}

View File

@ -6,7 +6,6 @@
import {customElement} from 'lit/decorators.js';
import {styles as privateProps} from './lib/listitem/list-item-private-styles.css.js';
import {styles} from './lib/listitem/list-item-styles.css.js';
import {ListItemLink} from './lib/listitemlink/list-item-link.js';
@ -34,10 +33,28 @@ declare global {
* item in a collection and act on it.
* - Lists should present icons, text, and actions in a consistent format.
*
* Example slottable child variants are:
*
* - `video[data-variant=video]`
* - `img,span[data-variant=avatar]`
* - `img[data-variant=image]`
* - `md-icon[data-variant=icon]`
*
* @example
* ```html
* <md-list-item-link
* headline="User Name"
* supportingText="user@name.com"
* href="/accounts">
* <md-icon data-variant="icon" slot="start">account_circle</md-icon>
* <md-icon data-variant="icon" slot="end">open_in_new</md-icon>
* </md-list-item-link>
* ```
*
* @final
* @suppress {visibility}
*/
@customElement('md-list-item-link')
export class MdListItemLink extends ListItemLink {
static override styles = [privateProps, styles];
static override styles = [styles];
}

View File

@ -1,27 +0,0 @@
/**
* @license
* Copyright 2022 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import {customElement} from 'lit/decorators.js';
import {ListItemVideo} from './lib/video/list-item-video.js';
import {styles} from './lib/video/list-item-video-styles.css.js';
declare global {
interface HTMLElementTagNameMap {
'md-list-item-video': MdListItemVideo;
}
}
/**
* @summary A video that is expected to be slotted into a list item.
*
* @final
* @suppress {visibility}
*/
@customElement('md-list-item-video')
export class MdListItemVideo extends ListItemVideo {
static override styles = [styles];
}

View File

@ -7,7 +7,6 @@
import {customElement} from 'lit/decorators.js';
import {ListItemEl as ListItem} from './lib/listitem/list-item.js';
import {styles as privateProps} from './lib/listitem/list-item-private-styles.css.js';
import {styles} from './lib/listitem/list-item-styles.css.js';
declare global {
@ -34,10 +33,29 @@ declare global {
* item in a collection and act on it.
* - Lists should present icons, text, and actions in a consistent format.
*
* Acceptable slottable child variants are:
*
* - `video[data-variant=video]`
* - `img,span[data-variant=avatar]`
* - `img[data-variant=image]`
* - `md-icon[data-variant=icon]`
*
* @example
* ```html
* <md-list-item
* headline="User Name"
* supportingText="user@name.com">
* <md-icon data-variant="icon" slot="start">account_circle</md-icon>
* <md-icon data-variant="icon" slot="end">check</md-icon>
* </md-list-item>
* ```
*
* @example
*
* @final
* @suppress {visibility}
*/
@customElement('md-list-item')
export class MdListItem extends ListItem {
static override styles = [privateProps, styles];
static override styles = [styles];
}

View File

@ -4,10 +4,21 @@
* SPDX-License-Identifier: Apache-2.0
*/
import './list.js';
import './list-item.js';
// import 'jasmine'; (google3-only)
describe('list tests', () => {
// TODO(b/265211574): test list
it('TODO', () => {});
import {createTokenTests} from '../testing/tokens.js';
import {MdList} from './list.js';
import {MdListItem} from './list-item.js';
describe('<md-list>', () => {
describe('.styles', () => {
createTokenTests(MdList.styles);
});
});
describe('<md-list-item>', () => {
describe('.styles', () => {
createTokenTests(MdListItem.styles);
});
});

View File

@ -4,16 +4,18 @@
//
// go/keep-sorted start
@use 'sass:list';
@use 'sass:map';
@use 'sass:string';
// go/keep-sorted end
// go/keep-sorted start
@use '../../elevation/lib/elevation';
@use '../../focus/focus-ring';
@use '../../list/list';
@use '../../list/list' as md-list;
@use '../../list/list-item';
@use '../../sass/resolvers';
@use '../../sass/theme';
@use '../../tokens/v0_160' as tokens; // TODO(b/270568087): update to latest
@use './menuitem/menu-item';
@use '../../tokens';
// go/keep-sorted end
$_custom-property-prefix: 'menu';
@ -36,9 +38,9 @@ $_custom-property-prefix: 'menu';
--_#{$token}: #{$value};
}
@include list.theme(
@include md-list.theme(
(
container-color: var(--_container-color),
list-item-container-color: var(--_container-color),
)
);
@ -106,5 +108,24 @@ $_custom-property-prefix: 'menu';
}
@function _resolve-tokens($tokens) {
@return elevation.resolve-tokens($tokens, 'elevation-key');
$menu-tokens: _remove-unused-tokens($tokens);
@return elevation.resolve-tokens($menu-tokens, 'elevation-key');
}
// removes unused tokens
@function _remove-unused-tokens($tokens) {
$unused-tokens: ();
@each $token in map-keys($tokens) {
$index: string.index($token, 'list-item');
@if $index {
$unused-tokens: list.append($unused-tokens, $token);
}
}
@each $token in $unused-tokens {
$tokens: map.remove($tokens, $token);
}
@return $tokens;
}

View File

@ -120,8 +120,7 @@ export abstract class Menu extends LitElement {
/**
* The tabindex of the underlying list element.
*/
@property({type: Number, attribute: 'list-tab-index'})
listTabIndex = 0;
@property({type: Number, attribute: 'list-tab-index'}) listTabIndex = 0;
/**
* The max time between the keystrokes of the typeahead menu behavior before
* it clears the typeahead buffer.
@ -242,13 +241,14 @@ export abstract class Menu extends LitElement {
protected renderList() {
return html`
<md-list
.ariaLabel=${this.ariaLabel}
role="menu"
listTabIndex=${this.listTabIndex}
@focus=${this.onListFocus}
@blur=${this.onListBlur}
@click=${this.onListClick}
@keydown=${this.typeaheadController.onKeydown}>
role="menu"
class="list"
.ariaLabel=${this.ariaLabel}
listTabIndex=${this.listTabIndex}
@focus=${this.onListFocus}
@blur=${this.onListBlur}
@click=${this.onListClick}
@keydown=${this.typeaheadController.onKeydown}>
${this.renderMenuItems()}
</md-list>`;
}

View File

@ -4,47 +4,54 @@
//
// go/keep-sorted start
@use 'sass:list';
@use 'sass:map';
@use 'sass:string';
// go/keep-sorted end
// go/keep-sorted start
@use '../../../elevation/lib/elevation';
@use '../../../focus/focus-ring';
@use '../../../list/list';
@use '../../../list/list-item';
@use '../../../ripple/ripple';
@use '../../../sass/map-ext';
@use '../../../sass/string-ext';
@use '../../../sass/theme';
@use '../../../sass/typography';
@use '../../../tokens/v0_160' as tokens; // TODO(b/270568087): update to latest
@use '../../../tokens';
// go/keep-sorted end
$_custom-property-prefix: 'menu';
@mixin theme($theme) {
$reference: resolve-tokens(tokens.md-comp-menu-values());
$theme: theme.validate-theme($reference, $theme);
$theme: resolve-tokens($theme);
$theme: theme.validate-theme(tokens.md-comp-menu-values(), $theme);
$theme: _resolve-tokens($theme);
$theme: theme.create-theme-vars($theme, $_custom-property-prefix);
@include theme.emit-theme-vars($theme);
}
@mixin styles() {
:host([active]) .list-item {
background-color: var(--_list-item-selected-container-color);
$tokens: tokens.md-comp-menu-values();
$tokens: _resolve-tokens($tokens);
$tokens: theme.create-theme-vars($tokens, $_custom-property-prefix);
:host {
@each $token, $value in $tokens {
--_#{$token}: #{$value};
}
}
.list-item {
:host([active]) &,
:host(:active) &,
&:focus {
background-color: var(--_list-item-selected-container-color);
}
}
/*
Set the ripple opacity to 0 if there is a submenu that is hovered.
*/
Set the ripple opacity to 0 if there is a submenu that is hovered.
*/
.list-item:has(.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
*/
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,
@ -53,71 +60,24 @@ $_custom-property-prefix: 'menu';
}
}
@function resolve-tokens($tokens) {
$list-item-tokens: ();
$tokens: map-ext.duplicate-key(
$tokens,
list-item-container-height,
list-item-one-line-container-height
);
@function _resolve-tokens($tokens) {
@return _remove-unused-tokens($tokens);
}
@each $token, $value in $tokens {
$index: string.index($token, list-item);
$with-leading-index: string.index($token, list-item-with-leading-icon-);
$with-trailing-index: string.index($token, list-item-with-trailing-icon-);
// removes unused tokens
@function _remove-unused-tokens($tokens) {
$unused-tokens: ();
@each $token in map-keys($tokens) {
$index: string.index($token, 'list-item');
@if $index {
// Replace list-item-with-leading-icon-focus-icon-color
// with list-item-focus-leading-icon-icon-color
// and replace list-item-with-leading-icon-leading-icon-size
// with list-item-leading-icon-size
@if $with-leading-index {
$token: string-ext.replace(
$token,
list-item-with-leading-icon-,
list-item-
);
$leading-index: string.index($token, -leading-);
@if not $leading-index {
$token: string-ext.replace($token, -icon-, -leading-icon-icon-);
}
}
// Replace list-item-with-trailing-icon-focus-icon-color
// with list-item-focus-trailing-icon-icon-color
// and replace list-item-with-trailing-icon-trailing-icon-size
// with list-item-trailing-icon-size
@if $with-trailing-index {
$token: string-ext.replace(
$token,
list-item-with-trailing-icon-,
list-item-
);
$trailing-index: string.index($token, -trailing-);
@if not $trailing-index {
$token: string-ext.replace($token, -icon-, -trailing-icon-icon-);
}
}
$list-item-tokens: map.set($list-item-tokens, $token, $value);
@if not $index {
$unused-tokens: list.append($unused-tokens, $token);
}
}
$list-item-tokens: typography.resolve-tokens(
$list-item-tokens,
'list-item-label-text'
);
@return $list-item-tokens;
}
@mixin private-props() {
$tokens: resolve-tokens(tokens.md-comp-menu-values());
$tokens: theme.create-theme-vars($tokens, menu);
:host {
@each $token, $value in $tokens {
--_#{$token}: #{$value};
}
@each $token in $unused-tokens {
$tokens: map.remove($tokens, $token);
}
@return $tokens;
}

View File

@ -1,10 +0,0 @@
//
// Copyright 2023 Google LLC
// SPDX-License-Identifier: Apache-2.0
//
// go/keep-sorted start
@use './menu-item';
// go/keep-sorted end
@include menu-item.private-props;

View File

@ -46,7 +46,6 @@ export class SubMenuItem extends MenuItemEl {
protected override keepOpenOnClick = true;
protected previousOpenTimeout = 0;
protected previousCloseTimeout = 0;
protected submenuOpen = false;
protected get submenuEl(): Menu|undefined {
return this.menus[0];

View File

@ -8,7 +8,6 @@ import {customElement} from 'lit/decorators.js';
import {styles as listItemStyles} from '../list/lib/listitem/list-item-styles.css.js';
import {styles as privateProps} from './lib/menuitem/menu-item-private-styles.css.js';
import {styles} from './lib/menuitem/menu-item-styles.css.js';
import {MenuItemLink} from './lib/menuitemlink/menu-item-link.js';
@ -41,5 +40,5 @@ declare global {
*/
@customElement('md-menu-item-link')
export class MdMenuItemLink extends MenuItemLink {
static override styles = [privateProps, listItemStyles, styles];
static override styles = [listItemStyles, styles];
}

View File

@ -8,9 +8,8 @@ import {customElement} from 'lit/decorators.js';
import {styles as listItemStyles} from '../list/lib/listitem/list-item-styles.css.js';
import {MenuItemEl} from './lib/menuitem/menu-item.js';
import {styles as privateProps} from './lib/menuitem/menu-item-private-styles.css.js';
import {styles} from './lib/menuitem/menu-item-styles.css.js';
import {MenuItemEl} from './lib/menuitem/menu-item.js';
export {ListItem} from '../list/lib/listitem/list-item.js';
export {CloseMenuEvent, DeactivateItemsEvent, MenuItem} from './lib/shared.js';
@ -38,5 +37,5 @@ declare global {
*/
@customElement('md-menu-item')
export class MdMenuItem extends MenuItemEl {
static override styles = [privateProps, listItemStyles, styles];
static override styles = [listItemStyles, styles];
}

View File

@ -4,7 +4,21 @@
* SPDX-License-Identifier: Apache-2.0
*/
describe('menu button tests', () => {
// TODO(b/265220649): unit test menu
it('TODO', () => {});
// import 'jasmine'; (google3-only)
import {createTokenTests} from '../testing/tokens.js';
import {MdMenu} from './menu.js';
import {MdMenuItem} from './menu-item.js';
describe('<md-menu>', () => {
describe('.styles', () => {
createTokenTests(MdMenu.styles);
});
});
describe('<md-menu-item>', () => {
describe('.styles', () => {
createTokenTests(MdMenuItem.styles);
});
});

View File

@ -8,7 +8,6 @@ import {customElement} from 'lit/decorators.js';
import {styles as listItemStyles} from '../list/lib/listitem/list-item-styles.css.js';
import {styles as privateProps} from './lib/menuitem/menu-item-private-styles.css.js';
import {styles} from './lib/menuitem/menu-item-styles.css.js';
import {SubMenuItem} from './lib/submenuitem/sub-menu-item.js';
@ -67,5 +66,5 @@ declare global {
*/
@customElement('md-sub-menu-item')
export class MdSubMenuItem extends SubMenuItem {
static override styles = [privateProps, listItemStyles, styles];
static override styles = [listItemStyles, styles];
}