Skip to content

Commit 3018547

Browse files
committed
feat(components): list item component
1 parent ba8c36e commit 3018547

File tree

2 files changed

+101
-53
lines changed

2 files changed

+101
-53
lines changed

packages/components/src/list/item.ts

Lines changed: 42 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,19 @@
22
import {GecutDirective} from '@gecut/lit-helper/directives/directive.js';
33
import {numberUtils} from '@gecut/utilities/data-types/number.js';
44
import {directive, type PartInfo} from 'lit/directive.js';
5-
import {classMap} from 'lit/directives/class-map.js';
5+
import { classMap} from 'lit/directives/class-map.js';
66
import {ifDefined} from 'lit/directives/if-defined.js';
77
import {when} from 'lit/directives/when.js';
88
import {html, noChange, nothing} from 'lit/html.js';
99
import {literal, html as staticHtml} from 'lit/static-html.js';
1010

11-
import {divider, icon, iconButton} from '../components';
11+
import {divider, icon, gecutIconButton} from '../components';
12+
import {gecutEFO} from '../internal/events-handler';
1213

1314
import type {IconButtonContent, IconContent} from '../components';
15+
import type {EventsObject} from '../internal/events-handler';
1416
import type {RenderResult} from '@gecut/types';
17+
import type {ClassInfo} from 'lit/directives/class-map.js';
1518
import type {TemplateResult} from 'lit/html.js';
1619
import type {StaticValue} from 'lit/static-html.js';
1720

@@ -23,18 +26,18 @@ export interface ItemImageSlotContent {
2326
export type HtmlString = string | TemplateResult;
2427
export type ItemSlutContent =
2528
| {
26-
type: 'avatar:character';
29+
element: 'avatar:character';
2730
character: string;
2831
}
2932
| (ItemImageSlotContent & {
30-
type: 'avatar:image';
33+
element: 'avatar:image';
3134
})
3235
| (ItemImageSlotContent & {
33-
type: 'image';
36+
element: 'image';
3437
})
35-
| {type: 'template'; template: TemplateResult}
36-
| (IconContent & {type: 'icon'})
37-
| (IconButtonContent & {type: 'icon-button'});
38+
| {element: 'template'; template: TemplateResult}
39+
| (IconContent & {element: 'icon'})
40+
| (IconButtonContent & {element: 'icon-button'});
3841

3942
export interface ItemContent {
4043
headline: HtmlString;
@@ -47,8 +50,7 @@ export interface ItemContent {
4750
href?: string;
4851
target?: '_blank' | '_parent' | '_self' | '_top';
4952

50-
onClick?: (event: MouseEvent) => void;
51-
onDblClick?: (event: MouseEvent) => void;
53+
events?: EventsObject;
5254

5355
disabled?: boolean;
5456
divider?: boolean;
@@ -59,23 +61,20 @@ export interface ItemContent {
5961

6062
export class GecutItemDirective extends GecutDirective {
6163
constructor(partInfo: PartInfo) {
62-
super(partInfo, 'gecut-button');
64+
super(partInfo, 'gecut-list-item');
6365
}
6466

6567
protected content?: ItemContent;
6668
protected type: 'link' | 'text' | 'button' = 'text';
6769

68-
protected $rootClassName =
69-
'relative flex flex-col list-none group w-full bg-surface text-onSurface overflow-hidden [&[interactive]]:focus-ring-inner rounded-lg disabled:cursor-default disabled:pointer-events-none select-none [&[interactive]]:hover:stateHover-onSurface [&[interactive]]:active:stateActive-onSurface';
70-
7170
render(content?: ItemContent): unknown {
7271
this.log.methodArgs?.('render', content);
7372

7473
if (content === undefined) return noChange;
7574

7675
this.content = content;
7776

78-
if (content.onClick || content.onDblClick) this.type = 'button';
77+
if (content.events) this.type = 'button';
7978
if (content.href) this.type = 'link';
8079

8180
return this.renderItem();
@@ -104,44 +103,33 @@ export class GecutItemDirective extends GecutDirective {
104103

105104
return staticHtml`
106105
<${tag}
107-
class=${classMap({[this.$rootClassName]: true, ...this.getRenderClasses()})}
106+
class=${classMap(this.getRenderClasses())}
108107
tabindex="${this.content.disabled || !isInteractive ? -1 : 0}"
109108
role="listitem"
110109
href=${ifDefined(this.content.href)}
111110
target=${ifDefined(this.content.target)}
112111
?disabled=${this.content.disabled}
113-
?sttl=${this.content.supportingTextTwoLine}
114-
?interactive=${isInteractive}
115-
?multiline=${this.content.headline && this.content.supportingText}
116-
?divider=${this.content.divider}
117-
@click=${this.content.onClick}
118-
@dblclick=${this.content.onDblClick}
112+
${gecutEFO(this.content.events)}
119113
>${this.renderBody()}</${tag}>
120114
`;
121115
}
122116
protected renderBody() {
123117
if (!this.content) return nothing;
124118

125119
return html`
126-
<div class="flex gap-4 py-3 px-4">
127-
<div class="empty:hidden flex items-center group-[[sttl]]:items-start shrink-0">
128-
${this.renderSlot('leading')}
129-
</div>
120+
<div class="gecut-list-item-body">
121+
<div class="gecut-list-item-leading">${this.renderSlot('leading')}</div>
130122
131-
<div class="flex flex-col min-h-8 grow justify-center group-[[multiline]]:min-h-12">
132-
<p class="text-onSurface text-bodyLarge text-start line-clamp-1">${this.content.headline}</p>
133-
<p class="text-onSurfaceVariant text-bodyMedium line-clamp-1 text-start group-[[sttl]]:line-clamp-2">
134-
${this.content.supportingText}
135-
</p>
123+
<div class="gecut-list-item-content">
124+
<p class="gecut-list-item-headline">${this.content.headline}</p>
125+
<p class="gecut-list-item-supporting-text">${this.content.supportingText}</p>
136126
</div>
137127
138-
<div class="empty:hidden flex justify-center items-center shrink-0 gap-4">
128+
<div class="gecut-list-item-trailing">
139129
${when(
140130
this.content.trailingSupportingText,
141131
() => html`
142-
<p class="text-onSurfaceVariant text-labelSmall">
143-
${this.renderItemTrailingSupportingText()}
144-
</p>
132+
<p class="gecut-list-item-trailing-supporting-text">${this.renderItemTrailingSupportingText()}</p>
145133
`,
146134
)}
147135
${this.renderSlot('trailing')}
@@ -161,18 +149,14 @@ export class GecutItemDirective extends GecutDirective {
161149

162150
if (!this.content || !content) return nothing;
163151

164-
switch (content.type) {
152+
switch (content.element) {
165153
case 'avatar:character':
166-
return html`
167-
<div
168-
class="bg-tertiaryContainer text-onTertiaryContainer uppercase size-10 flex items-center justify-center text-bodyLarge rounded-full"
169-
>
170-
${content.character}
171-
</div>
172-
`;
154+
return html`<div class="gecut-list-item-slot-avatar-character">${content.character}</div>`;
173155
case 'avatar:image':
174156
return nothing;
175157
case 'image': {
158+
// TODO: Write a Image LazyLoad Component
159+
176160
const image = new Image();
177161
const lazyLoadImage = new Image();
178162

@@ -192,12 +176,12 @@ export class GecutItemDirective extends GecutDirective {
192176

193177
lazyLoadImage.src = content.source;
194178

195-
return html`${image}`;
179+
return html`<div class="gecut-list-item-slot-thumbnail">${image}</div>`;
196180
}
197181
case 'icon':
198182
return icon(content);
199183
case 'icon-button':
200-
return iconButton(content);
184+
return gecutIconButton(content);
201185
case 'template':
202186
return content.template;
203187
}
@@ -220,6 +204,19 @@ export class GecutItemDirective extends GecutDirective {
220204
}
221205
}
222206
}
207+
208+
protected override getRenderClasses(): ClassInfo {
209+
if (!this.content) return super.getRenderClasses();
210+
211+
return {
212+
...super.getRenderClasses(),
213+
214+
'supporting-text-two-line': this.content.supportingTextTwoLine ?? false,
215+
interactive: this.type !== 'text',
216+
multiline: (!!this.content.headline && !!this.content.supportingText) ?? false,
217+
divider: this.content.divider ?? false,
218+
};
219+
}
223220
}
224221

225222
export const gecutItem = directive(GecutItemDirective);
Lines changed: 59 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,80 @@
1-
/* eslint-disable max-len */
21
import {GecutDirective} from '@gecut/lit-helper/directives/directive.js';
32
import {directive} from 'lit/directive.js';
3+
import {classMap, type ClassInfo} from 'lit/directives/class-map.js';
4+
import {repeat} from 'lit/directives/repeat.js';
45
import {html, noChange} from 'lit/html.js';
56

6-
import type {ItemContent} from './item';
7+
import {gecutItem, type ItemContent} from './item';
8+
79
import type {PartInfo} from 'lit/directive.js';
810

11+
export type KeyFn<T> = (item: T, index: number) => unknown;
12+
export type ItemTemplate<T> = (item: T, index: number) => ItemContent;
13+
export interface GecutListDirectiveFn {
14+
<T>(content: ListContent, items: Iterable<T>, template?: ItemTemplate<T>): unknown;
15+
<T>(content: ListContent, items: Iterable<T>, template: ItemTemplate<T>): unknown;
16+
<T>(content: ListContent, items: Iterable<T>, keyFn: KeyFn<T> | ItemTemplate<T>, template: ItemTemplate<T>): unknown;
17+
}
918
export interface ListContent {
1019
scrollable?: boolean;
1120
box?: 'elevated' | 'filled' | 'outlined';
12-
items: ItemContent[];
21+
fade?: 'auto' | 'top' | 'bottom' | boolean;
1322
}
1423

1524
export class GecutListDirective extends GecutDirective {
1625
constructor(partInfo: PartInfo) {
17-
super(partInfo, 'gecut-button');
26+
super(partInfo, 'gecut-list');
1827
}
1928

20-
render(content?: ListContent): unknown {
21-
this.log.methodArgs?.('render', content);
29+
protected content?: ListContent;
30+
31+
render<T>(content: ListContent, items: Iterable<T>, template: ItemTemplate<T>): unknown;
32+
render<T>(
33+
content: ListContent,
34+
items: Iterable<T>,
35+
keyFn: KeyFn<T> | ItemTemplate<T>,
36+
template: ItemTemplate<T>,
37+
): unknown;
38+
render<T>(
39+
content: ListContent,
40+
items: Iterable<T>,
41+
keyFnOrTemplate: KeyFn<T> | ItemTemplate<T>,
42+
template?: ItemTemplate<T>,
43+
): unknown {
44+
this.log.methodArgs?.('render', {content, items});
2245

2346
if (content === undefined) return noChange;
2447

25-
return html``;
48+
this.content = content;
49+
50+
return html`
51+
<div class=${classMap(this.getRenderClasses())}>
52+
<div class="gecut-list-body">
53+
${repeat(items, keyFnOrTemplate, (item, index) =>
54+
gecutItem(template?.(item, index) || (keyFnOrTemplate(item, index) as ItemContent)),
55+
)}
56+
</div>
57+
</div>
58+
`;
59+
}
60+
61+
protected override getRenderClasses(): ClassInfo {
62+
if (!this.content) return super.getRenderClasses();
63+
64+
return {
65+
...super.getRenderClasses(),
66+
67+
'gecut-card-elevated': this.content.box === 'elevated',
68+
'gecut-card-filled': this.content.box === 'filled',
69+
'gecut-card-outlined': this.content.box === 'outlined',
70+
71+
card: this.content.box != null,
72+
scrollable: this.content.scrollable ?? false,
73+
74+
'top-fade': this.content.fade === 'top' || this.content.fade === true,
75+
'bottom-fade': this.content.fade === 'bottom' || this.content.fade === true,
76+
};
2677
}
2778
}
2879

29-
export const gecutList = directive(GecutListDirective);
80+
export const gecutList = directive(GecutListDirective) as GecutListDirectiveFn;

0 commit comments

Comments
 (0)