-
Notifications
You must be signed in to change notification settings - Fork 904
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(item): add
<md-item>
layout component
PiperOrigin-RevId: 567095805
- Loading branch information
1 parent
54fbb2e
commit ffe4f79
Showing
9 changed files
with
537 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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'}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
{ | ||
"extends": "/assets/stories/base.json", | ||
"files": { | ||
"demo.ts": { | ||
"hidden": true | ||
}, | ||
"stories.ts": {} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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]; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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; | ||
} |
Oops, something went wrong.