Merge pull request #5275 from material-components:stories-page

PiperOrigin-RevId: 590302109
This commit is contained in:
Copybara-Service 2023-12-12 12:48:15 -08:00
commit 8e75343bbd
8 changed files with 402 additions and 28 deletions

View File

@ -51,7 +51,7 @@ function playgroundExample(eleventyConfig) {
<md-icon aria-hidden="true">expand_more</md-icon>
<md-icon aria-hidden="true" slot="selected">expand_less</md-icon>
</md-outlined-icon-button>
Expand interactive demo.
View interactive demo inline.
</summary>
<lit-island on:visible import="/js/hydration-entrypoints/playground-elements.js" class="example" aria-hidden="true">
<playground-project
@ -68,6 +68,7 @@ function playgroundExample(eleventyConfig) {
><md-circular-progress indeterminate></md-circular-progress></playground-file-editor>
</lit-island>
</details>
<p><a href="./stories/" target="_blank">Open interactive demo in new tab.</a></p>
`;
});
}

View File

@ -0,0 +1,34 @@
/**
* @license
* Copyright 2023 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
#dragbar {
max-width: 100%;
max-height: 100%;
}
#editor {
margin-block: 0;
height: 100%;
box-sizing: border-box;
}
#editor-wrapper {
height: 100%;
overflow: hidden;
}
body {
height: 100dvh;
}
#preview {
position: relative;
}
#preview md-circular-progress {
inset: 50%;
transform: translate(-50%, -50%);
}

View File

@ -50,6 +50,7 @@
/* Formats the code boxes themselves */
.example playground-file-editor,
playground-file-editor,
pre[class*='language-'] {
padding: var(--__code-block-font-size);
/* Remove the extra hard coded 3px from line number padding. */

View File

@ -0,0 +1,97 @@
---js
{
pagination: {
data: "collections.component",
size: 1,
alias: "component",
before: components => {
// remove any components that don't have a dirname
return components.filter(component => component.data.dirname)
}
},
permalink: "components/{{component.data.page.fileSlug}}/stories/index.html",
fullHeightContent: "true",
collections: ["stories"],
eleventyComputed: {
dirname: ({component}) => component.data.dirname,
}
}
---
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta
name="viewport"
content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"
/>
<title>Material Web - Stories {{component.data.name}}</title>
<!-- Set the color of the url bar on mobile to match theme -->
<meta name="theme-color" content="#251f16" />
<link
href="/images/favicon.svg"
rel="icon"
sizes="any"
type="image/svg+xml"
/>
<!-- Inlines the global css in site/css/global.css -->
{% inlinecss "global.css" %}
<!-- MUST be loaded before any lit bundle. allows hydration of SSRd components -->
<script type="module" src="/js/ssr-utils/lit-hydrate-support.js"></script>
<!-- Inlines the material theming logic since we want to prevent FOUC -->
{% inlinejs "inline/apply-saved-theme.js" %}
<!-- Needed for intializing theme if this is the first page they ever visit -->
<script type="module" src="/js/pages/global.js"></script>
<noscript>
<link rel="stylesheet" href="/css/no-js.css" />
</noscript>
<link
rel="stylesheet"
href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined&display=swap"
/>
<!-- If JS is disabled just show the contents without the polyfill -->
<noscript
><style>
body[dsd-pending] {
display: block !important;
}
</style>
</noscript>
<!-- Syncs theme with playground -->
<script type="module" src="/js/pages/components.js"></script>
<script type="module" src="/js/pages/stories.js"></script>
<script
type="module"
src="/js/hydration-entrypoints/playground-elements.js"
></script>
<link rel="stylesheet" href="/css/syntax-highlight.css" />
<link rel="stylesheet" href="/css/stories.css" />
</head>
<!-- dsd-pending hides body until the polyfill has run on browsers that do not support DSD -->
<body dsd-pending>
<!-- Inlines the declarative shadow dom polyfill for FF since it's performance sensitive -->
{% inlinejs "ssr-utils/dsd-polyfill.js" %}
<playground-project
id="project"
project-src="/assets/stories/{{dirname}}/project.json"
>
</playground-project>
<drag-playground id="dragbar">
<playground-preview id="preview" project="project" slot="preview">
<md-circular-progress indeterminate></md-circular-progress>
</playground-preview>
<div slot="editor" id="editor-wrapper">
<playground-file-editor
id="editor"
project="project"
filename="stories.ts"
line-numbers
aria-hidden="true"
>
<md-circular-progress indeterminate></md-circular-progress>
</playground-file-editor>
</div>
</drag-playground>
</body>
</html>

View File

@ -0,0 +1,216 @@
/**
* @license
* Copyright 2023 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { LitElement, css, html } from 'lit';
import { customElement, state, query } from 'lit/decorators.js';
import { classMap } from 'lit/directives/class-map.js';
import { styleMap } from 'lit/directives/style-map.js';
import '@material/web/icon/icon.js';
/**
* A playground preview + editor with a draggable handle.
*/
@customElement('drag-playground')
export class DragPlayground extends LitElement {
static styles = css`
:host {
display: block;
--_drag-bar-height: 24px;
--_drag-bar-border-width: 1px;
--_half-drag-bar-height: calc(
(var(--_drag-bar-height) / 2) + var(--_drag-bar-border-width)
);
}
#wrapper {
display: flex;
flex-direction: column;
}
:host,
#wrapper,
::slotted(*) {
height: 100%;
}
slot {
display: block;
overflow: hidden;
}
[name='preview'] {
height: max(
calc(
100% - var(--editor-percentage, 0%) - var(--_half-drag-bar-height)
),
0px
);
}
[name='editor'] {
height: max(
calc(var(--editor-percentage, 0px) - var(--_half-drag-bar-height)),
0px
);
}
#drag-bar {
touch-action: none;
background-color: var(--md-sys-color-surface-container);
color: var(--md-sys-color-on-surface);
border: var(--_drag-bar-border-width) solid var(--md-sys-color-outline);
border-radius: 12px;
height: var(--_drag-bar-height);
display: flex;
justify-content: center;
align-items: center;
-webkit-user-select: none;
user-select: none;
}
#drag-bar:hover {
background-color: var(--md-sys-color-surface-container-high);
cursor: grab;
}
#drag-bar.isDragging {
background-color: var(--md-sys-color-inverse-surface);
color: var(--md-sys-color-inverse-on-surface);
cursor: grabbing;
}
`;
/**
* Whether or not we are in the "dragging" state.
*/
@state() private isDragging = false;
/**
* The percentage of the editor height.
*/
@state() private editorHeightPercent = 0;
@query('#wrapper') private wrapperEl!: HTMLElement;
/**
* A set of pointer IDs in the case that the user is dragging with multiple
* pointers.
*/
private pointerIds = new Set<number>();
render() {
return html`<div
id="wrapper"
style=${styleMap({
'--editor-percentage': `${this.editorHeightPercent}%`,
})}
>
<slot name="preview"></slot>
<div
id="drag-bar"
tabindex="0"
role="slider"
aria-orientation="vertical"
aria-valuemax="100"
aria-valuemin="0"
aria-valuenow="${this.editorHeightPercent}"
aria-valuetext="${this.editorHeightPercent} percent"
aria-label="Editor height"
@focus=${this.onFocus}
@blur=${this.onBlur}
@keydown=${this.onKeydown}
@pointerdown=${this.onPointerdown}
@pointerup=${this.onPointerup}
@pointermove=${this.onPointermove}
class=${classMap({
isDragging: this.isDragging,
})}
>
<md-icon>drag_handle</md-icon>
</div>
<slot name="editor"></slot>
</div>`;
}
private onFocus() {
this.isDragging = true;
}
private onBlur() {
this.isDragging = false;
}
private onKeydown(event: KeyboardEvent) {
const { key } = event;
switch (key) {
case 'ArrowRight':
case 'ArrowUp':
this.editorHeightPercent = Math.min(this.editorHeightPercent + 1, 100);
break;
case 'ArrowLeft':
case 'ArrowDown':
this.editorHeightPercent = Math.max(this.editorHeightPercent - 1, 0);
break;
case 'PageUp':
this.editorHeightPercent = Math.min(this.editorHeightPercent + 10, 100);
break;
case 'PageDown':
this.editorHeightPercent = Math.max(this.editorHeightPercent - 10, 0);
break;
case 'Home':
this.editorHeightPercent = 0;
break;
case 'End':
this.editorHeightPercent = 100;
break;
default:
break;
}
}
private onPointerdown(event: PointerEvent) {
this.isDragging = true;
if (this.pointerIds.has(event.pointerId)) return;
this.pointerIds.add(event.pointerId);
(event.target as HTMLElement).setPointerCapture(event.pointerId);
}
private onPointerup(event: PointerEvent) {
this.pointerIds.delete(event.pointerId);
(event.target as HTMLElement).releasePointerCapture(event.pointerId);
if (this.pointerIds.size === 0) {
this.isDragging = false;
}
}
private onPointermove(event: PointerEvent) {
if (!this.isDragging) return;
const { clientY: mouseY } = event;
const { top: wrapperTop, bottom: wrapperBottom } =
this.wrapperEl.getBoundingClientRect();
// The height of the wrapper
const height = wrapperBottom - wrapperTop;
// Calculate the percentage of the editor height in which the pointer is
// located
const editorHeightPercent = 100 - ((mouseY - wrapperTop) / height) * 100;
// Clamp the percentage between 0 and 100
this.editorHeightPercent = Math.min(Math.max(editorHeightPercent, 0), 100);
}
}
declare global {
interface HTMLElementTagNameMap {
'drag-playground': DragPlayground;
}
}

View File

@ -0,0 +1,7 @@
/**
* @license
* Copyright 2023 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import '../components/drag-playground.js';

View File

@ -12,5 +12,6 @@ import './components/catalog-component-header-title.js';
import './components/nav-drawer.js';
import './components/theme-changer.js';
import './components/top-app-bar.js';
import './components/drag-playground.js';
// 🤫
import '@material/web/labs/item/item.js';

View File

@ -13,11 +13,11 @@ import '@material/web/select/filled-select.js';
import '@material/web/select/select-option.js';
import '@material/web/textfield/filled-text-field.js';
import {css, html, LitElement} from 'lit';
import {customElement, property} from 'lit/decorators.js';
import {StyleInfo, styleMap} from 'lit/directives/style-map.js';
import { css, html, LitElement } from 'lit';
import { customElement, property } from 'lit/decorators.js';
import { StyleInfo, styleMap } from 'lit/directives/style-map.js';
import {Knob, KnobUi} from '../knobs.js';
import { Knob, KnobUi } from '../knobs.js';
/**
* A boolean Knob UI.
@ -36,7 +36,8 @@ export function boolInput(): KnobUi<boolean> {
touch-target="none"
style="margin-inline-end: 16px;"
.checked=${!!knob.latestValue}
@change="${valueChanged}">
@change="${valueChanged}"
>
</md-checkbox>
${knob.name}
</label>
@ -51,7 +52,7 @@ export function boolInput(): KnobUi<boolean> {
*/
@customElement('knob-color-selector')
export class KnobColorSelector extends LitElement {
static override styles = css`
static styles = css`
:host {
display: inline-block;
position: relative;
@ -111,7 +112,7 @@ export class KnobColorSelector extends LitElement {
private internalValue = '';
@property({type: Boolean}) hasAlpha = false;
@property({ type: Boolean }) hasAlpha = false;
set value(val: string) {
const oldVal = this.internalValue;
@ -119,12 +120,12 @@ export class KnobColorSelector extends LitElement {
this.requestUpdate('value', oldVal);
}
@property({type: String, reflect: true})
@property({ type: String, reflect: true })
get value() {
return this.internalValue;
}
override render() {
render() {
return html`<span id="wrapper">
<span>
${this.hasAlpha ? this.renderTextInput() : this.renderColorInput()}
@ -132,7 +133,8 @@ export class KnobColorSelector extends LitElement {
<md-outlined-button
@click=${() => {
this.hasAlpha = !this.hasAlpha;
}}>
}}
>
${this.hasAlpha ? 'rgba' : 'rgb'}
</md-outlined-button>
</span>`;
@ -143,7 +145,8 @@ export class KnobColorSelector extends LitElement {
style=${styleMap(sharedTextFieldStyles)}
.value=${this.value}
@change=${this.propagateEvt}
@input=${this.onInput}></md-filled-text-field>`;
@input=${this.onInput}
></md-filled-text-field>`;
}
private renderColorInput() {
@ -158,7 +161,8 @@ export class KnobColorSelector extends LitElement {
id="color-picker"
.value=${this.value}
@change=${this.propagateEvt}
@input=${this.onInput} />
@input=${this.onInput}
/>
`;
}
@ -176,17 +180,17 @@ export class KnobColorSelector extends LitElement {
this.dispatchEvent(newEvt);
}
override click() {
click() {
const input = this.renderRoot!.querySelector(
'input,md-filled-text-field',
'input,md-filled-text-field'
) as HTMLElement;
input.click();
input.focus();
}
override focus() {
focus() {
const input = this.renderRoot!.querySelector(
'input,md-filled-text-field',
'input,md-filled-text-field'
) as HTMLElement;
input.focus();
}
@ -226,7 +230,8 @@ export function colorPicker(opts?: ColorPickerOpts): KnobUi<string> {
<knob-color-selector
.value="${knob.latestValue ?? ''}"
.hasAlpha="${config.hasAlpha}"
@input=${valueChanged}></knob-color-selector>
@input=${valueChanged}
></knob-color-selector>
${knob.name}
</label>
</div>
@ -245,7 +250,7 @@ const sharedTextFieldStyles: StyleInfo = {
'--md-filled-field-trailing-space': '8px',
'--md-filled-field-top-space': '4px',
'--md-filled-field-bottom-space': '4px',
'width': '150px',
width: '150px',
'min-width': '150px',
};
@ -272,7 +277,8 @@ export function textInput<T>(options?: TextInputOptions<T>): KnobUi<T> {
<md-filled-text-field
style=${styleMap(sharedTextFieldStyles)}
.value="${(knob.latestValue ?? '') as unknown as string}"
@input="${valueChanged}"></md-filled-text-field>
@input="${valueChanged}"
></md-filled-text-field>
${knob.name}
</label>
</div>
@ -313,7 +319,8 @@ export function numberInput(opts?: NumberInputOpts): KnobUi<number> {
type="number"
step="${config.step}"
.value="${knob.latestValue ? knob.latestValue.toString() : '0'}"
@input="${valueChanged}"></md-filled-text-field>
@input="${valueChanged}"
></md-filled-text-field>
${knob.name}
</label>
</div>
@ -333,7 +340,7 @@ export function button(): KnobUi<number> {
const count = knob.latestValue ?? 0;
onChange(count + 1);
};
const styles = styleMap({display: 'inline-block'});
const styles = styleMap({ display: 'inline-block' });
return html`
<md-outlined-button outlined @click=${onClick} style=${styles}>
${knob.name}
@ -344,7 +351,10 @@ export function button(): KnobUi<number> {
}
interface RadioSelectorConfig<T extends string> {
readonly options: ReadonlyArray<{readonly value: T; readonly label: string}>;
readonly options: ReadonlyArray<{
readonly value: T;
readonly label: string;
}>;
readonly name: string;
}
@ -365,7 +375,8 @@ export function radioSelector<T extends string>({
name="${name}"
value="${value}"
@change="${valueChanged}"
?checked="${knob.latestValue === option.value}"></md-radio>
?checked="${knob.latestValue === option.value}"
></md-radio>
${option.label}
</label>`;
});
@ -375,7 +386,10 @@ export function radioSelector<T extends string>({
}
interface SelectDropdownConfig<T extends string> {
readonly options: ReadonlyArray<{readonly value: T; readonly label: string}>;
readonly options: ReadonlyArray<{
readonly value: T;
readonly label: string;
}>;
}
/** A select dropdown Knob UI. */
@ -391,14 +405,17 @@ export function selectDropdown<T extends string>({
return html`<md-select-option
?selected="${knob.latestValue === option.value}"
.value="${option.value}"
.headline=${option.label}></md-select-option>`;
>
<div slot="headline">${option.label}</div>
</md-select-option>`;
});
return html`
<label>
<md-filled-select
@change="${valueChanged}"
menu-positioning="fixed"
style=${styleMap(sharedTextFieldStyles)}>
style=${styleMap(sharedTextFieldStyles)}
>
${listItems}
</md-filled-select>
${knob.name}
@ -415,7 +432,7 @@ export function selectDropdown<T extends string>({
export function cssCustomProperty(
knob: Knob<string>,
val: string,
containerOfRenderedStory: HTMLElement,
containerOfRenderedStory: HTMLElement
) {
const value = knob.isUnset ? knob.defaultValue : val;
if (value) {