Skip to content

Commit 616a377

Browse files
refactor: scope filtered-lists
1 parent 34a73c1 commit 616a377

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

50 files changed

+8770
-23683
lines changed

.github/workflows/test-on-pr-branch.yml

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ name: Test on PR branch
22
on: pull_request
33

44
jobs:
5-
test-on-pr-branch:
5+
unit:
66
runs-on: ubuntu-20.04
77
steps:
88
- name: Checkout
@@ -19,5 +19,37 @@ jobs:
1919
- name: Install playwright browsers
2020
run: npx playwright install --with-deps
2121

22-
- name: Run tests
23-
run: npm run test
22+
- name: Run unit tests
23+
run: npm run-script test:unit
24+
visual:
25+
runs-on: ubuntu-20.04
26+
steps:
27+
- name: Checkout
28+
uses: actions/checkout@v3
29+
30+
- name: Set up Node.js
31+
uses: actions/setup-node@v3
32+
with:
33+
node-version: '20'
34+
35+
- name: Install dependencies
36+
run: npm ci
37+
38+
- name: Install playwright browsers
39+
run: npx playwright install --with-deps
40+
41+
- name: Run visual test
42+
run: npm run-script test:visual
43+
44+
- name: Update screenshots
45+
if: failure()
46+
run: npm run test:update
47+
48+
- name: Upload failed screenshots as artifacts
49+
uses: actions/upload-artifact@v4
50+
if: failure()
51+
with:
52+
name: failed_screenshots
53+
path: |
54+
screenshots/*/failed/
55+
screenshots/*/baseline/

.github/workflows/test.yml

Lines changed: 0 additions & 24 deletions
This file was deleted.

.storybook/server.mjs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,12 @@
11
import { storybookPlugin } from '@web/dev-server-storybook';
22
import baseConfig from '../web-dev-server.config.mjs';
3+
import { polyfill } from '@web/dev-server-polyfill';
34

45
export default /** @type {import('@web/dev-server').DevServerConfig} */ ({
56
...baseConfig,
67
open: '/',
7-
plugins: [storybookPlugin({ type: 'web-components' }), ...baseConfig.plugins],
8+
plugins: [storybookPlugin(
9+
polyfill({
10+
scopedCustomElementRegistry: true,
11+
}),{ type: 'web-components' }), ...baseConfig.plugins],
812
});

ActionList.ts

Lines changed: 234 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,234 @@
1+
import { css, html, TemplateResult } from 'lit';
2+
import { classMap } from 'lit/directives/class-map.js';
3+
import { property } from 'lit/decorators.js';
4+
5+
import { MdMenu, Menu } from '@scopedelement/material-web/menu/MdMenu.js';
6+
import { Icon } from '@scopedelement/material-web/icon/internal/icon.js';
7+
import { MdDivider } from '@scopedelement/material-web/divider/MdDevider.js';
8+
import { MdIcon } from '@scopedelement/material-web/icon/MdIcon.js';
9+
import { MdList } from '@scopedelement/material-web/list/MdList.js';
10+
import { MdListItem } from '@scopedelement/material-web/list/MdListItem.js';
11+
import { MdMenuItem } from '@scopedelement/material-web/menu/MdMenuItem.js';
12+
import { MdOutlinedTextField } from '@scopedelement/material-web/textfield/MdOutlinedTextField.js';
13+
14+
import { FilterListBase } from './base-list.js';
15+
16+
type Action = {
17+
icon: string;
18+
label?: string;
19+
callback: () => void;
20+
};
21+
22+
export type ActionItem = {
23+
/** The main information of the list item */
24+
headline: string;
25+
/** Supporting information rendered in a second line */
26+
supportingText?: string;
27+
/** An attached XML element */
28+
attachedElement?: Element;
29+
/** An icon rendered left to the list item content */
30+
startingIcon?: string;
31+
/** An icon rendered right to the list item content */
32+
endingIcon?: string;
33+
/** Whether to add a divider at the bottom of the item */
34+
divider?: boolean;
35+
/** Specifies additional filter terms */
36+
filtergroup?: string[];
37+
/** The action to be performed when clicking the list item */
38+
primaryAction?: () => void;
39+
/** Additional actions for the item. The first rendered is visible */
40+
actions?: Action[];
41+
};
42+
43+
function term(item: ActionItem): string {
44+
return `${item.headline} ${item.supportingText ?? ''}${
45+
item.filtergroup?.join(' ') ?? ''
46+
}`;
47+
}
48+
49+
/** TextField designed to be used for SCL element */
50+
export class ActionList extends FilterListBase {
51+
static scopedElements = {
52+
'md-outlined-text-field': MdOutlinedTextField,
53+
'md-icon': MdIcon,
54+
'md-list': MdList,
55+
'md-list-item': MdListItem,
56+
'md-divider': MdDivider,
57+
'md-menu': MdMenu,
58+
'md-menu-item': MdMenuItem,
59+
};
60+
61+
/** ListItems and potential */
62+
@property({ type: Array })
63+
items: ActionItem[] = [];
64+
65+
private renderMoreVertItem(item: ActionItem): TemplateResult {
66+
item.actions!.shift();
67+
const otherActions = item.actions!;
68+
69+
return html`
70+
<span style="position: relative">
71+
<md-list-item
72+
id="more-vert-anchor"
73+
type="button"
74+
class="${classMap({
75+
twoline: !!item.supportingText,
76+
hidden: !this.searchRegex.test(term(item)),
77+
})}"
78+
@click=${(evt: Event) => {
79+
const menu =
80+
evt.target instanceof Icon
81+
? ((evt.target.parentElement as Element)
82+
.nextElementSibling as Menu)
83+
: ((evt.target as Element).nextElementSibling as Menu);
84+
85+
menu.show();
86+
}}
87+
>
88+
<md-icon slot="start">more_vert</md-icon>
89+
</md-list-item>
90+
<md-menu id="more-vert-menu" anchor="more-vert-anchor">
91+
${otherActions.map(
92+
action => html`<md-menu-item @click=${action.callback}>
93+
<div slot="headline">${action.label}</div>
94+
<md-icon slot="start">${action.icon}</md-icon>
95+
</md-menu-item>`
96+
)}
97+
</md-menu> </span
98+
>${item.divider
99+
? html`<md-divider
100+
class="${classMap({ hidden: !this.searchRegex.test(term(item)) })}"
101+
></md-divider>`
102+
: html``}
103+
`;
104+
}
105+
106+
private renderActionItem(item: ActionItem, index = 0): TemplateResult {
107+
const action = item.actions ? item.actions[index] : null;
108+
109+
if (!action)
110+
return html` <md-list-item
111+
class="${classMap({
112+
twoline: !!item.supportingText,
113+
hidden: !this.searchRegex.test(term(item)),
114+
})}"
115+
></md-list-item
116+
>${item.divider
117+
? html`<md-divider
118+
class="${classMap({
119+
hidden: !this.searchRegex.test(term(item)),
120+
})}"
121+
></md-divider>`
122+
: html``}`;
123+
124+
return html`<md-list-item
125+
type="button"
126+
class="${classMap({
127+
twoline: !!item.supportingText,
128+
hidden: !this.searchRegex.test(term(item)),
129+
})}"
130+
@click=${action.callback}
131+
>
132+
<md-icon slot="start">${action.icon}</md-icon> </md-list-item
133+
>${item.divider
134+
? html`<md-divider
135+
class="${classMap({ hidden: !this.searchRegex.test(term(item)) })}"
136+
></md-divider>`
137+
: html``}`;
138+
}
139+
140+
private renderOtherActions(): TemplateResult {
141+
return html`<md-list>
142+
${this.items.map(item =>
143+
item.actions && item.actions?.length > 2
144+
? this.renderMoreVertItem(item)
145+
: this.renderActionItem(item, 1)
146+
)}</md-list
147+
>`;
148+
}
149+
150+
private renderFirstAction(): TemplateResult {
151+
return html`<md-list>
152+
${this.items.map(item => this.renderActionItem(item))}</md-list
153+
>`;
154+
}
155+
156+
private renderActions(): TemplateResult {
157+
return html`
158+
${this.items.some(item => item.actions && item.actions[0])
159+
? this.renderFirstAction()
160+
: html``}
161+
${this.items.some(item => item.actions && item.actions.length > 1)
162+
? this.renderOtherActions()
163+
: html``}
164+
`;
165+
}
166+
167+
private renderActionListItem(item: ActionItem): TemplateResult {
168+
return html`<md-list-item
169+
type="${item.primaryAction ? 'link' : 'text'}"
170+
class="${classMap({
171+
hidden: !this.searchRegex.test(term(item)),
172+
})}"
173+
@click="${item.primaryAction}"
174+
>
175+
<div slot="headline">${item.headline}</div>
176+
${item.supportingText
177+
? html`<div slot="headline">${item.supportingText}</div>`
178+
: html``}
179+
${item.startingIcon
180+
? html`<md-icon slot="start">${item.startingIcon}</md-icon>`
181+
: html``}
182+
${item.endingIcon
183+
? html`<md-icon slot="end">${item.endingIcon}</md-icon>`
184+
: html``} </md-list-item
185+
>${item.divider
186+
? html`<md-divider
187+
class="${classMap({ hidden: !this.searchRegex.test(term(item)) })}"
188+
></md-divider>`
189+
: html``}`;
190+
}
191+
192+
private renderListItem(item: ActionItem): TemplateResult {
193+
return this.renderActionListItem(item);
194+
}
195+
196+
render(): TemplateResult {
197+
return html`<section>
198+
${this.renderSearchField()}
199+
<div style="display: flex;">
200+
<md-list class="listitems">
201+
${this.items.map(item => this.renderListItem(item))}</md-list
202+
>
203+
${this.renderActions()}
204+
</div>
205+
</section>`;
206+
}
207+
208+
static styles = css`
209+
section {
210+
display: flex;
211+
flex-direction: column;
212+
}
213+
214+
md-outlined-text-field {
215+
background-color: var(--md-sys-color-surface, #fef7ff);
216+
--md-outlined-text-field-container-shape: 32px;
217+
padding: 8px;
218+
}
219+
220+
md-list-item.twoline {
221+
height: 72px;
222+
}
223+
224+
.listitems {
225+
flex: auto;
226+
overflow: hidden;
227+
text-overflow: ellipsis;
228+
}
229+
230+
.hidden {
231+
display: none;
232+
}
233+
`;
234+
}

0 commit comments

Comments
 (0)