Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 35 additions & 3 deletions .github/workflows/test-on-pr-branch.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ name: Test on PR branch
on: pull_request

jobs:
test-on-pr-branch:
unit:
runs-on: ubuntu-20.04
steps:
- name: Checkout
Expand All @@ -19,5 +19,37 @@ jobs:
- name: Install playwright browsers
run: npx playwright install --with-deps

- name: Run tests
run: npm run test
- name: Run unit tests
run: npm run-script test:unit
visual:
runs-on: ubuntu-20.04
steps:
- name: Checkout
uses: actions/checkout@v3

- name: Set up Node.js
uses: actions/setup-node@v3
with:
node-version: '20'

- name: Install dependencies
run: npm ci

- name: Install playwright browsers
run: npx playwright install --with-deps

- name: Run visual test
run: npm run-script test:visual

- name: Update screenshots
if: failure()
run: npm run test:update

- name: Upload failed screenshots as artifacts
uses: actions/upload-artifact@v4
if: failure()
with:
name: failed_screenshots
path: |
screenshots/*/failed/
screenshots/*/baseline/
24 changes: 0 additions & 24 deletions .github/workflows/test.yml

This file was deleted.

6 changes: 5 additions & 1 deletion .storybook/server.mjs
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
import { storybookPlugin } from '@web/dev-server-storybook';
import baseConfig from '../web-dev-server.config.mjs';
import { polyfill } from '@web/dev-server-polyfill';

export default /** @type {import('@web/dev-server').DevServerConfig} */ ({
...baseConfig,
open: '/',
plugins: [storybookPlugin({ type: 'web-components' }), ...baseConfig.plugins],
plugins: [storybookPlugin(
polyfill({
scopedCustomElementRegistry: true,
}),{ type: 'web-components' }), ...baseConfig.plugins],
});
234 changes: 234 additions & 0 deletions ActionList.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,234 @@
import { css, html, TemplateResult } from 'lit';
import { classMap } from 'lit/directives/class-map.js';
import { property } from 'lit/decorators.js';

import { MdMenu, Menu } from '@scopedelement/material-web/menu/MdMenu.js';
import { Icon } from '@scopedelement/material-web/icon/internal/icon.js';
import { MdDivider } from '@scopedelement/material-web/divider/MdDevider.js';
import { MdIcon } from '@scopedelement/material-web/icon/MdIcon.js';
import { MdList } from '@scopedelement/material-web/list/MdList.js';
import { MdListItem } from '@scopedelement/material-web/list/MdListItem.js';
import { MdMenuItem } from '@scopedelement/material-web/menu/MdMenuItem.js';
import { MdOutlinedTextField } from '@scopedelement/material-web/textfield/MdOutlinedTextField.js';

import { FilterListBase } from './base-list.js';

type Action = {
icon: string;
label?: string;
callback: () => void;
};

export type ActionItem = {
/** The main information of the list item */
headline: string;
/** Supporting information rendered in a second line */
supportingText?: string;
/** An attached XML element */
attachedElement?: Element;
/** An icon rendered left to the list item content */
startingIcon?: string;
/** An icon rendered right to the list item content */
endingIcon?: string;
/** Whether to add a divider at the bottom of the item */
divider?: boolean;
/** Specifies additional filter terms */
filtergroup?: string[];
/** The action to be performed when clicking the list item */
primaryAction?: () => void;
/** Additional actions for the item. The first rendered is visible */
actions?: Action[];
};

function term(item: ActionItem): string {
return `${item.headline} ${item.supportingText ?? ''}${
item.filtergroup?.join(' ') ?? ''
}`;
}

/** TextField designed to be used for SCL element */
export class ActionList extends FilterListBase {
static scopedElements = {
'md-outlined-text-field': MdOutlinedTextField,
'md-icon': MdIcon,
'md-list': MdList,
'md-list-item': MdListItem,
'md-divider': MdDivider,
'md-menu': MdMenu,
'md-menu-item': MdMenuItem,
};

/** ListItems and potential */
@property({ type: Array })
items: ActionItem[] = [];

private renderMoreVertItem(item: ActionItem): TemplateResult {
item.actions!.shift();
const otherActions = item.actions!;

return html`
<span style="position: relative">
<md-list-item
id="more-vert-anchor"
type="button"
class="${classMap({
twoline: !!item.supportingText,
hidden: !this.searchRegex.test(term(item)),
})}"
@click=${(evt: Event) => {
const menu =
evt.target instanceof Icon
? ((evt.target.parentElement as Element)
.nextElementSibling as Menu)
: ((evt.target as Element).nextElementSibling as Menu);

menu.show();
}}
>
<md-icon slot="start">more_vert</md-icon>
</md-list-item>
<md-menu id="more-vert-menu" anchor="more-vert-anchor">
${otherActions.map(
action => html`<md-menu-item @click=${action.callback}>
<div slot="headline">${action.label}</div>
<md-icon slot="start">${action.icon}</md-icon>
</md-menu-item>`
)}
</md-menu> </span
>${item.divider
? html`<md-divider
class="${classMap({ hidden: !this.searchRegex.test(term(item)) })}"
></md-divider>`
: html``}
`;
}

private renderActionItem(item: ActionItem, index = 0): TemplateResult {
const action = item.actions ? item.actions[index] : null;

if (!action)
return html` <md-list-item
class="${classMap({
twoline: !!item.supportingText,
hidden: !this.searchRegex.test(term(item)),
})}"
></md-list-item
>${item.divider
? html`<md-divider
class="${classMap({
hidden: !this.searchRegex.test(term(item)),
})}"
></md-divider>`
: html``}`;

return html`<md-list-item
type="button"
class="${classMap({
twoline: !!item.supportingText,
hidden: !this.searchRegex.test(term(item)),
})}"
@click=${action.callback}
>
<md-icon slot="start">${action.icon}</md-icon> </md-list-item
>${item.divider
? html`<md-divider
class="${classMap({ hidden: !this.searchRegex.test(term(item)) })}"
></md-divider>`
: html``}`;
}

private renderOtherActions(): TemplateResult {
return html`<md-list>
${this.items.map(item =>
item.actions && item.actions?.length > 2
? this.renderMoreVertItem(item)
: this.renderActionItem(item, 1)
)}</md-list
>`;
}

private renderFirstAction(): TemplateResult {
return html`<md-list>
${this.items.map(item => this.renderActionItem(item))}</md-list
>`;
}

private renderActions(): TemplateResult {
return html`
${this.items.some(item => item.actions && item.actions[0])
? this.renderFirstAction()
: html``}
${this.items.some(item => item.actions && item.actions.length > 1)
? this.renderOtherActions()
: html``}
`;
}

private renderActionListItem(item: ActionItem): TemplateResult {
return html`<md-list-item
type="${item.primaryAction ? 'link' : 'text'}"
class="${classMap({
hidden: !this.searchRegex.test(term(item)),
})}"
@click="${item.primaryAction}"
>
<div slot="headline">${item.headline}</div>
${item.supportingText
? html`<div slot="headline">${item.supportingText}</div>`
: html``}
${item.startingIcon
? html`<md-icon slot="start">${item.startingIcon}</md-icon>`
: html``}
${item.endingIcon
? html`<md-icon slot="end">${item.endingIcon}</md-icon>`
: html``} </md-list-item
>${item.divider
? html`<md-divider
class="${classMap({ hidden: !this.searchRegex.test(term(item)) })}"
></md-divider>`
: html``}`;
}

private renderListItem(item: ActionItem): TemplateResult {
return this.renderActionListItem(item);
}

render(): TemplateResult {
return html`<section>
${this.renderSearchField()}
<div style="display: flex;">
<md-list class="listitems">
${this.items.map(item => this.renderListItem(item))}</md-list
>
${this.renderActions()}
</div>
</section>`;
}

static styles = css`
section {
display: flex;
flex-direction: column;
}

md-outlined-text-field {
background-color: var(--md-sys-color-surface, #fef7ff);
--md-outlined-text-field-container-shape: 32px;
padding: 8px;
}

md-list-item.twoline {
height: 72px;
}

.listitems {
flex: auto;
overflow: hidden;
text-overflow: ellipsis;
}

.hidden {
display: none;
}
`;
}
Loading