mirror of
https://github.com/material-components/material-web.git
synced 2024-10-26 21:56:56 +03:00
feat(item): add <md-item>
layout component
PiperOrigin-RevId: 567095805
This commit is contained in:
parent
54fbb2ed5e
commit
ffe4f79b5d
25
item/demo/demo.ts
Normal file
25
item/demo/demo.ts
Normal file
@ -0,0 +1,25 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2023 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import './index.js';
|
||||
import './material-collection.js';
|
||||
|
||||
import {KnobTypesToKnobs, MaterialCollection, materialInitsToStoryInits, setUpDemo} from './material-collection.js';
|
||||
import {boolInput, Knob, textInput} from './index.js';
|
||||
|
||||
import {stories, StoryKnobs} from './stories.js';
|
||||
|
||||
const collection =
|
||||
new MaterialCollection<KnobTypesToKnobs<StoryKnobs>>('Item', [
|
||||
new Knob('overline', {ui: textInput()}),
|
||||
new Knob('trailingSupportingText', {ui: textInput()}),
|
||||
new Knob('leadingIcon', {ui: boolInput()}),
|
||||
new Knob('trailingIcon', {ui: boolInput()}),
|
||||
]);
|
||||
|
||||
collection.addStories(...materialInitsToStoryInits(stories));
|
||||
|
||||
setUpDemo(collection, {fonts: 'roboto', icons: 'material-symbols'});
|
9
item/demo/project.json
Normal file
9
item/demo/project.json
Normal file
@ -0,0 +1,9 @@
|
||||
{
|
||||
"extends": "/assets/stories/base.json",
|
||||
"files": {
|
||||
"demo.ts": {
|
||||
"hidden": true
|
||||
},
|
||||
"stories.ts": {}
|
||||
}
|
||||
}
|
151
item/demo/stories.ts
Normal file
151
item/demo/stories.ts
Normal file
@ -0,0 +1,151 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2023 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import '@material/web/icon/icon.js';
|
||||
import '@material/web/item/item.js';
|
||||
|
||||
import {MaterialStoryInit} from './material-collection.js';
|
||||
import {css, html, nothing} from 'lit';
|
||||
import {classMap} from 'lit/directives/class-map.js';
|
||||
|
||||
/** Knob types for item stories. */
|
||||
export interface StoryKnobs {
|
||||
overline: string;
|
||||
trailingSupportingText: string;
|
||||
leadingIcon: boolean;
|
||||
trailingIcon: boolean;
|
||||
}
|
||||
|
||||
const styles = css`
|
||||
/* Use this CSS to prevent lines from wrapping */
|
||||
.nowrap {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* Use this CSS on items to limit the number of wrapping lines */
|
||||
.clamp-lines {
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 2;
|
||||
}
|
||||
|
||||
/* Use this on start/end content when the item requires it,
|
||||
typically 3+ line height items) */
|
||||
.align-start {
|
||||
align-self: flex-start;
|
||||
/* Optional, some items line icons and text should visually appear 16px from
|
||||
the top. Others, like interactive controls, should hug the top at 12px */
|
||||
padding-top: 4px;
|
||||
}
|
||||
|
||||
.container {
|
||||
align-items: flex-start;
|
||||
display: flex;
|
||||
gap: 32px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
md-item {
|
||||
border-radius: 16px;
|
||||
outline: 1px solid var(--md-sys-color-outline);
|
||||
width: 300px;
|
||||
}
|
||||
`;
|
||||
|
||||
const LOREM_IPSUM =
|
||||
'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus condimentum rhoncus est volutpat venenatis.';
|
||||
|
||||
const items: MaterialStoryInit<StoryKnobs> = {
|
||||
name: 'Items',
|
||||
styles,
|
||||
render(knobs) {
|
||||
return html`
|
||||
<div class="container">
|
||||
<md-item>
|
||||
Single line item
|
||||
${getKnobContent(knobs)}
|
||||
</md-item>
|
||||
|
||||
<md-item>
|
||||
Two line item
|
||||
<div slot="supporting-text">Supporting text</div>
|
||||
${getKnobContent(knobs)}
|
||||
</md-item>
|
||||
|
||||
<md-item>
|
||||
Three line item
|
||||
<div slot="supporting-text">
|
||||
<div>Second line text</div>
|
||||
<div>Third line text</div>
|
||||
</div>
|
||||
${getKnobContent(knobs, /* threeLines */ true)}
|
||||
</md-item>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
};
|
||||
|
||||
const longText: MaterialStoryInit<StoryKnobs> = {
|
||||
name: 'Items with long text',
|
||||
styles,
|
||||
render(knobs) {
|
||||
return html`
|
||||
<div class="container">
|
||||
<md-item class="nowrap">
|
||||
Item with a truncated headline and supporting text.
|
||||
<div slot="supporting-text">
|
||||
Supporting text. ${LOREM_IPSUM}
|
||||
</div>
|
||||
${getKnobContent(knobs)}
|
||||
</md-item>
|
||||
|
||||
<md-item>
|
||||
Item with clamped lines
|
||||
<div slot="supporting-text" class="clamp-lines">
|
||||
Supporting text that wraps up to two lines. ${LOREM_IPSUM}
|
||||
</div>
|
||||
${getKnobContent(knobs, /* threeLines */ true)}
|
||||
</md-item>
|
||||
|
||||
<md-item>
|
||||
Item that always shows long wrapping text.
|
||||
<div slot="supporting-text">
|
||||
Supporting text. ${LOREM_IPSUM}
|
||||
</div>
|
||||
${getKnobContent(knobs, /* threeLines */ true)}
|
||||
</md-item>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
};
|
||||
|
||||
function getKnobContent(knobs: StoryKnobs, threeLines = false) {
|
||||
const overline = knobs.overline ?
|
||||
html`<div slot="overline">${knobs.overline}</div>` :
|
||||
nothing;
|
||||
|
||||
const classes = {
|
||||
'align-start': threeLines,
|
||||
};
|
||||
|
||||
const trailingText = knobs.trailingSupportingText ?
|
||||
html`<div class=${classMap(classes)} slot="trailing-supporting-text">${
|
||||
knobs.trailingSupportingText}</div>` :
|
||||
nothing;
|
||||
|
||||
const leadingIcon = knobs.leadingIcon ?
|
||||
html`<md-icon class=${classMap(classes)} slot="start">event</md-icon>` :
|
||||
nothing;
|
||||
|
||||
const trailingIcon = knobs.trailingIcon ?
|
||||
html`<md-icon class=${classMap(classes)} slot="end">star</md-icon>` :
|
||||
nothing;
|
||||
|
||||
return html`${overline}${trailingText}${leadingIcon}${trailingIcon}`;
|
||||
}
|
||||
|
||||
/** Item stories. */
|
||||
export const stories = [items, longText];
|
99
item/internal/_item.scss
Normal file
99
item/internal/_item.scss
Normal file
@ -0,0 +1,99 @@
|
||||
//
|
||||
// Copyright 2023 Google LLC
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
//
|
||||
|
||||
// go/keep-sorted start
|
||||
@use 'sass:map';
|
||||
// go/keep-sorted end
|
||||
// go/keep-sorted start
|
||||
@use '../../tokens';
|
||||
// go/keep-sorted end
|
||||
|
||||
/// `<md-item>` does not provide `--md-item-*` custom properties. Instead, use
|
||||
/// CSS on slotted elements to change their styles.
|
||||
///
|
||||
/// @example css
|
||||
/// md-item {
|
||||
/// color: var(--headline-color);
|
||||
/// font: var(--headline-font);
|
||||
/// }
|
||||
/// md-item [slot='supporting-text'] {
|
||||
/// color: var(--supporting-text-color);
|
||||
/// font: var(--supporting-text-font);
|
||||
/// }
|
||||
/// // ...
|
||||
///
|
||||
@mixin styles() {
|
||||
$tokens: tokens.md-comp-item-values();
|
||||
|
||||
:host {
|
||||
color: map.get($tokens, 'label-text-color');
|
||||
font-family: map.get($tokens, 'label-text-font');
|
||||
font-size: map.get($tokens, 'label-text-size');
|
||||
font-weight: map.get($tokens, 'label-text-weight');
|
||||
line-height: map.get($tokens, 'label-text-line-height');
|
||||
align-items: center;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
min-height: 56px;
|
||||
overflow: hidden;
|
||||
padding: 12px 16px;
|
||||
position: relative;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
:host([multiline]) {
|
||||
min-height: 72px;
|
||||
}
|
||||
|
||||
[name='overline'] {
|
||||
color: map.get($tokens, 'overline-color');
|
||||
font-family: map.get($tokens, 'overline-font');
|
||||
font-size: map.get($tokens, 'overline-size');
|
||||
font-weight: map.get($tokens, 'overline-weight');
|
||||
line-height: map.get($tokens, 'overline-line-height');
|
||||
}
|
||||
|
||||
[name='supporting-text'] {
|
||||
color: map.get($tokens, 'supporting-text-color');
|
||||
font-family: map.get($tokens, 'supporting-text-font');
|
||||
font-size: map.get($tokens, 'supporting-text-size');
|
||||
font-weight: map.get($tokens, 'supporting-text-weight');
|
||||
line-height: map.get($tokens, 'supporting-text-line-height');
|
||||
}
|
||||
|
||||
[name='trailing-supporting-text'] {
|
||||
color: map.get($tokens, 'trailing-supporting-text-color');
|
||||
font-family: map.get($tokens, 'trailing-supporting-text-font');
|
||||
font-size: map.get($tokens, 'trailing-supporting-text-size');
|
||||
font-weight: map.get($tokens, 'trailing-supporting-text-weight');
|
||||
line-height: map.get($tokens, 'trailing-supporting-text-line-height');
|
||||
}
|
||||
|
||||
// A slot for background container elements, such as ripples and focus rings.
|
||||
[name='container']::slotted(*) {
|
||||
inset: 0;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.default-slot {
|
||||
// Needed since the default slot can have just text content, and ellipsis
|
||||
// need an inline display.
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.default-slot,
|
||||
::slotted(*) {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.text {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
10
item/internal/item-styles.scss
Normal file
10
item/internal/item-styles.scss
Normal file
@ -0,0 +1,10 @@
|
||||
//
|
||||
// Copyright 2023 Google LLC
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
//
|
||||
|
||||
// go/keep-sorted start
|
||||
@use './item';
|
||||
// go/keep-sorted end
|
||||
|
||||
@include item.styles;
|
78
item/internal/item.ts
Normal file
78
item/internal/item.ts
Normal file
@ -0,0 +1,78 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2023 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import {html, LitElement} from 'lit';
|
||||
import {property, queryAll} from 'lit/decorators.js';
|
||||
|
||||
/**
|
||||
* An item layout component.
|
||||
*/
|
||||
export class Item extends LitElement {
|
||||
/**
|
||||
* Only needed for SSR.
|
||||
*
|
||||
* Add this attribute when an item has two lines to avoid a Flash Of Unstyled
|
||||
* Content. This attribute is not needed for single line items or items with
|
||||
* three or more lines.
|
||||
*/
|
||||
@property({type: Boolean, reflect: true}) multiline = false;
|
||||
|
||||
@queryAll('.text slot') private readonly textSlots!: HTMLSlotElement[];
|
||||
|
||||
override render() {
|
||||
return html`
|
||||
<slot name="container"></slot>
|
||||
<slot class="non-text" name="start"></slot>
|
||||
<div class="text">
|
||||
<slot name="overline"
|
||||
@slotchange=${this.handleTextSlotChange}></slot>
|
||||
<slot class="default-slot"
|
||||
@slotchange=${this.handleTextSlotChange}></slot>
|
||||
<slot name="headline"
|
||||
@slotchange=${this.handleTextSlotChange}></slot>
|
||||
<slot name="supporting-text"
|
||||
@slotchange=${this.handleTextSlotChange}></slot>
|
||||
</div>
|
||||
<slot class="non-text" name="trailing-supporting-text"></slot>
|
||||
<slot class="non-text" name="end"></slot>
|
||||
`;
|
||||
}
|
||||
|
||||
private handleTextSlotChange() {
|
||||
// Check if there's more than one text slot with content. If so, the item is
|
||||
// multiline, which has a different min-height than single line items.
|
||||
let isMultiline = false;
|
||||
let slotsWithContent = 0;
|
||||
for (const slot of this.textSlots) {
|
||||
if (slotHasContent(slot)) {
|
||||
slotsWithContent += 1;
|
||||
}
|
||||
|
||||
if (slotsWithContent > 1) {
|
||||
isMultiline = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
this.multiline = isMultiline;
|
||||
}
|
||||
}
|
||||
|
||||
function slotHasContent(slot: HTMLSlotElement) {
|
||||
for (const node of slot.assignedNodes({flatten: true})) {
|
||||
// Assume there's content if there's an element slotted in
|
||||
const isElement = node.nodeType === Node.ELEMENT_NODE;
|
||||
// If there's only text nodes for the default slot, check if there's
|
||||
// non-whitespace.
|
||||
const isTextWithContent =
|
||||
node.nodeType === Node.TEXT_NODE && node.textContent?.match(/\S/);
|
||||
if (isElement || isTextWithContent) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
77
item/item.ts
Normal file
77
item/item.ts
Normal file
@ -0,0 +1,77 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2023 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import {customElement} from 'lit/decorators.js';
|
||||
|
||||
import {Item} from './internal/item.js';
|
||||
import {styles} from './internal/item-styles.css.js';
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'md-item': MdItem;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* An item layout component that can be used inside list items to give them
|
||||
* their customizable structure.
|
||||
*
|
||||
* `<md-item>` does not have any functionality, which must be added by the
|
||||
* component using it.
|
||||
*
|
||||
* All text will wrap unless `white-space: nowrap` is set on the item or any of
|
||||
* its children.
|
||||
*
|
||||
* Slots available:
|
||||
* - `<default>`: The headline, or custom content.
|
||||
* - `headline`: The first line.
|
||||
* - `supporting-text`: Supporting text lines underneath the headline.
|
||||
* - `trailing-supporting-text`: A small text snippet at the end of the item.
|
||||
* - `start`: Any leading content, such as icons, avatars, or checkboxes.
|
||||
* - `end`: Any trailing content, such as icons and buttons.
|
||||
* - `container`: Background container content, intended for adding additional
|
||||
* styles, such as ripples or focus rings.
|
||||
*
|
||||
* @example
|
||||
* ```html
|
||||
* <md-item>Single line</md-item>
|
||||
*
|
||||
* <md-item>
|
||||
* <div class="custom-content">...</div>
|
||||
* </md-item>
|
||||
*
|
||||
* <!-- Classic 1 to 3+ line list items -->
|
||||
* <md-item>
|
||||
* <md-icon slot="start">image</md-icon>
|
||||
* <div slot="overline">Overline</div>
|
||||
* <div slot="headline">Headline</div>
|
||||
* <div="supporting-text">Supporting text</div>
|
||||
* <div="trailing-supporting-text">Trailing</div>
|
||||
* <md-icon slot="end">image</md-icon>
|
||||
* </md-item>
|
||||
* ```
|
||||
*
|
||||
* When wrapping `<md-item>`, forward the available slots to use the same slot
|
||||
* structure for the wrapping component (this is what `<md-list-item>` does).
|
||||
*
|
||||
* @example
|
||||
* ```html
|
||||
* <md-item>
|
||||
* <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>
|
||||
* <slot name="start" slot="start"></slot>
|
||||
* <slot name="end" slot="end"></slot>
|
||||
* </md-item>
|
||||
* ```
|
||||
*/
|
||||
@customElement('md-item')
|
||||
export class MdItem extends Item {
|
||||
static override styles = [styles];
|
||||
}
|
@ -28,6 +28,7 @@
|
||||
@forward './md-comp-icon' as md-comp-icon-*;
|
||||
@forward './md-comp-icon-button' as md-comp-icon-button-*;
|
||||
@forward './md-comp-input-chip' as md-comp-input-chip-*;
|
||||
@forward './md-comp-item' as md-comp-item-*;
|
||||
@forward './md-comp-linear-progress' as md-comp-linear-progress-*;
|
||||
@forward './md-comp-list' as md-comp-list-*;
|
||||
@forward './md-comp-list-item' as md-comp-list-item-*;
|
||||
|
87
tokens/_md-comp-item.scss
Normal file
87
tokens/_md-comp-item.scss
Normal file
@ -0,0 +1,87 @@
|
||||
//
|
||||
// Copyright 2023 Google LLC
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
//
|
||||
|
||||
// go/keep-sorted start
|
||||
@use 'sass:map';
|
||||
// go/keep-sorted end
|
||||
// go/keep-sorted start
|
||||
@use './md-sys-color';
|
||||
@use './md-sys-typescale';
|
||||
@use './v0_192/md-comp-list';
|
||||
@use './values';
|
||||
// go/keep-sorted end
|
||||
|
||||
$supported-tokens: (
|
||||
// go/keep-sorted start
|
||||
'label-text-color',
|
||||
'label-text-font',
|
||||
'label-text-line-height',
|
||||
'label-text-size',
|
||||
'label-text-weight',
|
||||
'overline-color',
|
||||
'overline-font',
|
||||
'overline-line-height',
|
||||
'overline-size',
|
||||
'overline-weight',
|
||||
'supporting-text-color',
|
||||
'supporting-text-font',
|
||||
'supporting-text-line-height',
|
||||
'supporting-text-size',
|
||||
'supporting-text-weight',
|
||||
'trailing-supporting-text-color',
|
||||
'trailing-supporting-text-font',
|
||||
'trailing-supporting-text-line-height',
|
||||
'trailing-supporting-text-size',
|
||||
'trailing-supporting-text-weight',
|
||||
// go/keep-sorted end
|
||||
);
|
||||
|
||||
$_default: (
|
||||
'md-sys-color': md-sys-color.values-light(),
|
||||
'md-sys-typescale': md-sys-typescale.values(),
|
||||
);
|
||||
|
||||
@function values($deps: $_default, $exclude-hardcoded-values: false) {
|
||||
$list-tokens: md-comp-list.values($deps, $exclude-hardcoded-values);
|
||||
|
||||
$tokens: (
|
||||
// go/keep-sorted start
|
||||
'label-text-color': map.get($list-tokens, 'list-item-label-text-color'),
|
||||
'label-text-font': map.get($list-tokens, 'list-item-label-text-font'),
|
||||
'label-text-line-height':
|
||||
map.get($list-tokens, 'list-item-label-text-line-height'),
|
||||
'label-text-size': map.get($list-tokens, 'list-item-label-text-size'),
|
||||
'label-text-weight': map.get($list-tokens, 'list-item-label-text-weight'),
|
||||
'overline-color': map.get($list-tokens, 'list-item-overline-color'),
|
||||
'overline-font': map.get($list-tokens, 'list-item-overline-font'),
|
||||
'overline-line-height':
|
||||
map.get($list-tokens, 'list-item-overline-line-height'),
|
||||
'overline-size': map.get($list-tokens, 'list-item-overline-size'),
|
||||
'overline-weight': map.get($list-tokens, 'list-item-overline-weight'),
|
||||
'supporting-text-color':
|
||||
map.get($list-tokens, 'list-item-supporting-text-color'),
|
||||
'supporting-text-font':
|
||||
map.get($list-tokens, 'list-item-supporting-text-font'),
|
||||
'supporting-text-line-height':
|
||||
map.get($list-tokens, 'list-item-supporting-text-line-height'),
|
||||
'supporting-text-size':
|
||||
map.get($list-tokens, 'list-item-supporting-text-size'),
|
||||
'supporting-text-weight':
|
||||
map.get($list-tokens, 'list-item-supporting-text-weight'),
|
||||
'trailing-supporting-text-color':
|
||||
map.get($list-tokens, 'list-item-trailing-supporting-text-color'),
|
||||
'trailing-supporting-text-font':
|
||||
map.get($list-tokens, 'list-item-trailing-supporting-text-font'),
|
||||
'trailing-supporting-text-line-height':
|
||||
map.get($list-tokens, 'list-item-trailing-supporting-text-line-height'),
|
||||
'trailing-supporting-text-size':
|
||||
map.get($list-tokens, 'list-item-trailing-supporting-text-size'),
|
||||
'trailing-supporting-text-weight':
|
||||
map.get($list-tokens, 'list-item-trailing-supporting-text-weight'),
|
||||
// go/keep-sorted end
|
||||
);
|
||||
|
||||
@return values.validate($tokens, $supported-tokens: $supported-tokens);
|
||||
}
|
Loading…
Reference in New Issue
Block a user