Skip to content

Commit 2af18fa

Browse files
committed
feat(list-item): add new component
1 parent a3d78d4 commit 2af18fa

13 files changed

+990
-2
lines changed
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
import { ListItem, MenuItem, ListSeparator } from '@limetech/lime-elements';
2+
import { Component, h, Host, State } from '@stencil/core';
3+
4+
/**
5+
* List item with action menu
6+
*
7+
* This example shows how a list item can have an action menu
8+
* with multiple actions that can be triggered by the user.
9+
*
10+
* ⚠️ _TODO: verify that this implementation is correct_ 👇
11+
* The consumer (e.g. `limel-list`) component should handle the interaction with the action menu.
12+
* - Action menu items are `MenuItem` objects where `value` contains the actual `Action`
13+
* - **Event bubbling:** `limel-menu` emits `select` events with the `MenuItem` as `event.detail`
14+
* - **Natural forwarding:** The select event bubbles up and `limel-list` re-emits it
15+
* - **Value extraction:** the final consumer of `limel-list`
16+
* will eventually extract `event.detail.value` to get the actual `Action`
17+
*/
18+
@Component({
19+
tag: 'limel-example-list-item-actions',
20+
shadow: true,
21+
styleUrl: 'list-item-basic.scss',
22+
})
23+
export class ListItemActionsExample {
24+
@State()
25+
private lastInteraction: string = '';
26+
27+
@State()
28+
private selectedItems: Set<number> = new Set();
29+
30+
private actionItems: Array<MenuItem | ListSeparator> = [
31+
{ text: 'Edit item', value: 'edit', icon: 'edit' },
32+
{ text: 'Duplicate item', value: 'duplicate', icon: 'copy' },
33+
{ text: 'Share item', value: 'share', icon: 'share' },
34+
{ separator: true },
35+
{
36+
text: 'Delete item',
37+
value: 'delete',
38+
icon: 'trash',
39+
disabled: false,
40+
},
41+
];
42+
43+
public render() {
44+
return (
45+
<Host>
46+
<ul>
47+
<limel-list-item
48+
value={1}
49+
selectable={true}
50+
selected={this.selectedItems.has(1)}
51+
text="King of Tokyo"
52+
secondaryText="A fun dice game for 2-6 players"
53+
icon="gorilla"
54+
actions={this.actionItems}
55+
onInteract={this.onListItemInteraction}
56+
/>
57+
<limel-list-item
58+
value={2}
59+
selectable={true}
60+
selected={this.selectedItems.has(2)}
61+
text="Pandemic"
62+
secondaryText="Cooperative board game where players work together to save the world"
63+
icon="virus"
64+
actions={this.actionItems}
65+
onInteract={this.onListItemInteraction}
66+
/>
67+
</ul>
68+
<limel-example-value
69+
label="Last interaction"
70+
value={this.lastInteraction}
71+
/>
72+
</Host>
73+
);
74+
}
75+
76+
private onListItemInteraction = (
77+
event: CustomEvent<{ selected: boolean; item: ListItem }>
78+
) => {
79+
const itemValue = event.detail.item.value as number;
80+
const isSelected = event.detail.selected;
81+
82+
if (isSelected) {
83+
this.selectedItems = new Set([...this.selectedItems, itemValue]);
84+
} else {
85+
this.selectedItems = new Set(
86+
[...this.selectedItems].filter((id) => id !== itemValue)
87+
);
88+
}
89+
90+
this.lastInteraction = `Item "${event.detail.item.text}" interaction: selected=${isSelected}`;
91+
};
92+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
:host {
2+
display: flex;
3+
flex-direction: column;
4+
padding: 1rem;
5+
border-radius: 0.25rem;
6+
7+
background: url("data:image/svg+xml;charset=utf-8, <svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8' style='fill-rule:evenodd;'><path fill='rgba(186,186,192,0.16)' d='M0 0h4v4H0zM4 4h4v4H4z'/></svg>");
8+
background-size: 0.5rem;
9+
}
10+
11+
ul {
12+
list-style: none;
13+
padding: 0;
14+
margin: 0;
15+
overflow: hidden;
16+
17+
&.is-resizable {
18+
resize: horizontal;
19+
max-width: 100%;
20+
min-width: 10rem;
21+
22+
&::after {
23+
content: 'Resize me ⤵';
24+
font-size: 0.75rem;
25+
position: absolute;
26+
right: 0.25rem;
27+
bottom: 0.25rem;
28+
}
29+
}
30+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { Component, h } from '@stencil/core';
2+
3+
/**
4+
* Basic list item
5+
*
6+
* This example demonstrates the basic usage of the `limel-list-item` component
7+
* with text and secondary text.
8+
*
9+
* :::important
10+
* The list items are not focusable by default.
11+
* The consumer should handle the focusability of the list items
12+
* by dynamically setting and altering the `tabindex` attribute.
13+
* :::
14+
*/
15+
@Component({
16+
tag: 'limel-example-list-item-basic',
17+
shadow: true,
18+
styleUrl: 'list-item-basic.scss',
19+
})
20+
export class ListItemBasicExample {
21+
public render() {
22+
return (
23+
<ul>
24+
<limel-list-item
25+
value={1}
26+
tabindex="0"
27+
text="Basic List Item"
28+
/>
29+
<limel-list-item
30+
value={2}
31+
tabindex="0"
32+
text="This is the `text`"
33+
secondaryText="This is the `secondaryText`"
34+
/>
35+
</ul>
36+
);
37+
}
38+
}
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
import { ListItem } from '@limetech/lime-elements';
2+
import { Component, h, Host, State } from '@stencil/core';
3+
4+
/**
5+
* Checkbox list items
6+
*
7+
* This example shows how list items can be displayed as checkboxes.
8+
* Checkboxes allow users to select multiple options from a group.
9+
*
10+
* :::important
11+
* - The consumer component should set `role="group"` for the `ul` or
12+
* the container of the `limel-list-item`s
13+
* :::
14+
*
15+
* :::note
16+
* - The checkboxes are purely visual - the selection logic
17+
* is handled by the parent component through the interact events.
18+
* :::
19+
*/
20+
@Component({
21+
tag: 'limel-example-list-item-checkbox',
22+
shadow: true,
23+
styleUrl: 'list-item-basic.scss',
24+
})
25+
export class ListItemCheckboxExample {
26+
@State()
27+
private selectedValues: Set<number> = new Set([2]); // Pre-select second item
28+
29+
@State()
30+
private lastInteraction: string = '';
31+
32+
private items = [
33+
{
34+
value: 1,
35+
text: 'Email notifications',
36+
secondaryText: 'Receive updates via email',
37+
},
38+
{
39+
value: 2,
40+
text: 'Push notifications',
41+
secondaryText: 'Receive updates on your device',
42+
},
43+
{
44+
value: 3,
45+
text: 'SMS notifications',
46+
secondaryText: 'Receive updates via text message',
47+
},
48+
{
49+
value: 4,
50+
text: 'Newsletter',
51+
secondaryText: 'Weekly product updates and tips',
52+
},
53+
];
54+
55+
public render() {
56+
return (
57+
<Host>
58+
<div role="group" aria-labelledby="notification-heading">
59+
{this.items.map((item) => (
60+
<limel-list-item
61+
key={item.value}
62+
value={item.value}
63+
text={item.text}
64+
secondaryText={item.secondaryText}
65+
type="checkbox"
66+
selectable={true}
67+
selected={this.selectedValues.has(item.value)}
68+
onInteract={this.onListItemInteraction}
69+
/>
70+
))}
71+
</div>
72+
<limel-example-value
73+
label="Last interaction"
74+
value={this.lastInteraction}
75+
/>
76+
</Host>
77+
);
78+
}
79+
80+
private onListItemInteraction = (
81+
event: CustomEvent<{ selected: boolean; item: ListItem }>
82+
) => {
83+
const itemValue = event.detail.item.value as number;
84+
const isSelected = event.detail.selected;
85+
86+
// For checkboxes, toggle the selection state
87+
if (isSelected) {
88+
this.selectedValues = new Set([...this.selectedValues, itemValue]);
89+
} else {
90+
this.selectedValues = new Set(
91+
[...this.selectedValues].filter((id) => id !== itemValue)
92+
);
93+
}
94+
95+
this.lastInteraction = `${isSelected ? 'Selected' : 'Deselected'} "${event.detail.item.text}"`;
96+
};
97+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import { Component, h } from '@stencil/core';
2+
3+
/**
4+
* With icons
5+
*/
6+
@Component({
7+
tag: 'limel-example-list-item-icon',
8+
shadow: true,
9+
styleUrl: 'list-item-basic.scss',
10+
})
11+
export class ListItemIconExample {
12+
public render() {
13+
return (
14+
<ul>
15+
<limel-list-item
16+
value={1}
17+
tabindex="0"
18+
text="Santa Hat"
19+
secondaryText="Santa's favorite"
20+
icon={{
21+
name: 'santas_hat',
22+
title: 'Icon of Santa Hat',
23+
color: 'rgb(var(--color-coral-default))',
24+
}}
25+
/>
26+
<limel-list-item
27+
value={2}
28+
tabindex="0"
29+
text="Party Hat"
30+
secondaryText="For the party animals"
31+
icon={{
32+
name: 'party_hat',
33+
title: 'Icon of Party Hat',
34+
color: 'rgb(var(--color-white))',
35+
backgroundColor: 'rgb(var(--color-pink-default))',
36+
}}
37+
/>
38+
</ul>
39+
);
40+
}
41+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
:host(limel-example-list-item-interactive) {
2+
display: flex;
3+
flex-direction: column;
4+
gap: 1rem;
5+
6+
padding: 1rem;
7+
}
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import { Component, h, State } from '@stencil/core';
2+
3+
/**
4+
* Interactive list item example
5+
*
6+
* This example demonstrates the interactive features of the `limel-list-item` component
7+
* including selectable, selected, and disabled states.
8+
*
9+
* A list item shows a visual feedback when hovered.
10+
* Once clicked, it emits an event with details about the item.
11+
*
12+
* However, only when the the item is `selectable` a user can click on it
13+
* (or focus and press the Enter or Space key) to make it selected.
14+
*
15+
* A `selected` item will both visually indicate that it is selected
16+
* and also informs assistive technology about its state.
17+
*
18+
* Needless to say that a `disabled` item cannot be selected or interacted with.
19+
*/
20+
@Component({
21+
tag: 'limel-example-list-item-interactive',
22+
shadow: true,
23+
styleUrl: 'list-item-interactive.scss',
24+
})
25+
export class ListItemInteractiveExample {
26+
@State()
27+
private disabled = false;
28+
29+
@State()
30+
private selected = false;
31+
32+
@State()
33+
private selectable = true;
34+
35+
@State()
36+
private lastEvent: string = 'No events yet';
37+
38+
public render() {
39+
return [
40+
<limel-list-item
41+
text="Interactive List Item"
42+
secondaryText="Click me if I'm not disabled and selectable"
43+
icon="star"
44+
disabled={this.disabled}
45+
selected={this.selected}
46+
selectable={this.selectable}
47+
onInteract={this.onInteract}
48+
/>,
49+
<limel-example-controls>
50+
<limel-switch
51+
label="Disabled"
52+
value={this.disabled}
53+
onChange={this.setDisabled}
54+
/>
55+
<limel-switch
56+
label="Selected"
57+
value={this.selected}
58+
onChange={this.setSelected}
59+
/>
60+
<limel-switch
61+
label="Selectable"
62+
value={this.selectable}
63+
onChange={this.setSelectable}
64+
/>
65+
</limel-example-controls>,
66+
<limel-example-value label="Last event" value={this.lastEvent} />,
67+
];
68+
}
69+
70+
private onInteract = (event: CustomEvent) => {
71+
this.lastEvent = `Item clicked - Selected: ${event.detail.selected}`;
72+
this.selected = event.detail.selected; // Update the state to reflect the new selection
73+
console.log('List item interacted:', event.detail);
74+
};
75+
76+
private setDisabled = (event: CustomEvent<boolean>) => {
77+
this.disabled = event.detail;
78+
};
79+
80+
private setSelected = (event: CustomEvent<boolean>) => {
81+
this.selected = event.detail;
82+
};
83+
84+
private setSelectable = (event: CustomEvent<boolean>) => {
85+
this.selectable = event.detail;
86+
};
87+
}

0 commit comments

Comments
 (0)