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
14 changes: 0 additions & 14 deletions etc/lime-elements.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -1733,10 +1733,6 @@ export namespace JSX {
"iconSize"?: IconSize;
"image"?: ListItem['image'];
"language"?: Languages;
"onInteract"?: (event: LimelListItemCustomEvent<{
selected: boolean;
item: ListItem;
}>) => void;
"primaryComponent"?: ListItem['primaryComponent'];
"secondaryText"?: string;
"selected"?: boolean;
Expand Down Expand Up @@ -2345,16 +2341,6 @@ export interface LimelListCustomEvent<T> extends CustomEvent<T> {
target: HTMLLimelListElement;
}

// Warning: (ae-missing-release-tag) "LimelListItemCustomEvent" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export interface LimelListItemCustomEvent<T> extends CustomEvent<T> {
// (undocumented)
detail: T;
// (undocumented)
target: HTMLLimelListItemElement;
}

// Warning: (ae-missing-release-tag) "LimelMenuCustomEvent" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
Expand Down
96 changes: 80 additions & 16 deletions src/components/list-item/examples/list-item-actions.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ListItem, MenuItem, ListSeparator } from '@limetech/lime-elements';
import { MenuItem, ListSeparator } from '@limetech/lime-elements';
import { Component, h, Host, State } from '@stencil/core';

/**
Expand All @@ -15,7 +15,9 @@ import { Component, h, Host, State } from '@stencil/core';
* will eventually extract `event.detail.value` to get the actual `Action`
*
* :::note
* The action menu of the disabled items remains enabled!
* Disabled list items keep their action menu enabled so actions remain
* accessible (e.g., for contextual info). The example guards against
* toggling selection when disabled.
* :::
*/
@Component({
Expand Down Expand Up @@ -44,29 +46,29 @@ export class ListItemActionsExample {
public render() {
return (
<Host>
<ul>
<ul onClick={this.onClick} onKeyDown={this.onKeyDown}>
<limel-list-item
tabIndex={0}
data-value={1}
value={1}
type="option"
selected={this.selectedItems.has(1)}
text="King of Tokyo"
secondaryText="A fun dice game for 2-6 players"
icon="gorilla"
actions={this.actionItems}
onInteract={this.onListItemInteraction}
disabled={this.disabled}
/>
<limel-list-item
tabIndex={0}
data-value={2}
value={2}
type="option"
selected={this.selectedItems.has(2)}
text="Pandemic"
secondaryText="Cooperative board game where players work together to save the world"
icon="virus"
actions={this.actionItems}
onInteract={this.onListItemInteraction}
disabled={this.disabled}
/>
</ul>
Expand All @@ -85,21 +87,83 @@ export class ListItemActionsExample {
);
}

private onListItemInteraction = (
event: CustomEvent<{ selected: boolean; item: ListItem }>
) => {
const itemValue = event.detail.item.value as number;
const isSelected = event.detail.selected;

if (isSelected) {
this.selectedItems = new Set([...this.selectedItems, itemValue]);
} else {
private toggleValue = (value: number, text: string) => {
const selected = this.selectedItems.has(value);
if (selected) {
this.selectedItems = new Set(
[...this.selectedItems].filter((id) => id !== itemValue)
[...this.selectedItems].filter((id) => id !== value)
);
this.lastInteraction = `Deselected "${text}"`;
} else {
this.selectedItems = new Set([...this.selectedItems, value]);
this.lastInteraction = `Selected "${text}"`;
}
};

this.lastInteraction = `Item "${event.detail.item.text}" interaction: selected=${isSelected}`;
private onClick = (event: MouseEvent) => {
// If the entire example is disabled, ignore item clicks
if (this.disabled) {
return;
}
const host = (event.target as HTMLElement).closest('limel-list-item');
if (!host) {
return;
}
// Skip if clicking the action menu trigger or inside the menu
if (
(event.target as HTMLElement).closest('.action-menu-trigger') ||
(event.target as HTMLElement).closest('limel-menu')
) {
return;
}
// Respect per-item disabled state (attribute reflected by the component)
if (
host.hasAttribute('disabled') ||
host.getAttribute('aria-disabled') === 'true'
) {
return;
}
const value = Number((host as HTMLElement).dataset.value);
const text = host.getAttribute('text') || '';
this.toggleValue(value, text);
};

private onKeyDown = (event: KeyboardEvent) => {
if (this.disabled) {
return;
}
const isEnter = event.key === 'Enter';
const isSpace =
event.key === ' ' ||
event.key === 'Space' ||
event.key === 'Spacebar' ||
event.code === 'Space';
if (!isEnter && !isSpace) {
return;
}
if (event.repeat) {
return;
}
if (isSpace) {
event.preventDefault();
}
const focused = (event.target as HTMLElement).closest(
'limel-list-item'
);
if (!focused) {
return;
}
if (
(event.target as HTMLElement).closest('.action-menu-trigger') ||
(event.target as HTMLElement).closest('limel-menu') ||
focused.hasAttribute('disabled') ||
focused.getAttribute('aria-disabled') === 'true'
) {
return;
}
const value = Number((focused as HTMLElement).dataset.value);
const text = focused.getAttribute('text') || '';
this.toggleValue(value, text);
};

private setDisabled = (event: CustomEvent<boolean>) => {
Expand Down
75 changes: 56 additions & 19 deletions src/components/list-item/examples/list-item-checkbox.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { ListItem } from '@limetech/lime-elements';
import { Component, h, Host, State } from '@stencil/core';

const NOTIFICATION_ICON = {
Expand All @@ -15,13 +14,14 @@ const NOTIFICATION_ICON = {
* Checkboxes allow users to select multiple options from a group.
*
* :::important
* - The consumer component should set `role="group"` for the `ul` or
* the container of the `limel-list-item`s
* - Apply `role="group"` to the container for accessibility.
* - Selection logic is fully managed by the parent example via click and
* key delegation; each item receives `selected` based on `selectedValues`.
* :::
*
* :::note
* - The checkboxes are purely visual - the selection logic
* is handled by the parent component through the interact events.
* The checkboxes are presentational only. For production usage prefer
* a container component (`limel-list type="checkbox"`) to centralize state.
* :::
*/
@Component({
Expand Down Expand Up @@ -73,16 +73,21 @@ export class ListItemCheckboxExample {
public render() {
return (
<Host>
<ul role="group" aria-label="Notification preferences">
<ul
role="group"
aria-label="Notification preferences"
onClick={this.onClick}
onKeyDown={this.onKeyDown}
>
{this.items.map((item) => (
<limel-list-item
key={item.value}
data-value={item.value}
value={item.value}
text={item.text}
secondaryText={item.secondaryText}
type="checkbox"
selected={this.selectedValues.has(item.value)}
onInteract={this.onListItemInteraction}
icon={this.icon}
badgeIcon={this.badgeIcon}
/>
Expand All @@ -108,22 +113,54 @@ export class ListItemCheckboxExample {
);
}

private onListItemInteraction = (
event: CustomEvent<{ selected: boolean; item: ListItem }>
) => {
const itemValue = event.detail.item.value as number;
const isSelected = event.detail.selected;

// For checkboxes, toggle the selection state
if (isSelected) {
this.selectedValues = new Set([...this.selectedValues, itemValue]);
} else {
private toggleValue = (value: number, text: string) => {
const selected = this.selectedValues.has(value);
if (selected) {
this.selectedValues = new Set(
[...this.selectedValues].filter((id) => id !== itemValue)
[...this.selectedValues].filter((id) => id !== value)
);
this.lastInteraction = `Deselected "${text}"`;
} else {
this.selectedValues = new Set([...this.selectedValues, value]);
this.lastInteraction = `Selected "${text}"`;
}
};

private onClick = (event: MouseEvent) => {
const host = (event.target as HTMLElement).closest('limel-list-item');
if (!host) {
return;
}
const value = Number((host as HTMLElement).dataset.value);
const text = host.getAttribute('text') || '';
this.toggleValue(value, text);
};

this.lastInteraction = `${isSelected ? 'Selected' : 'Deselected'} "${event.detail.item.text}"`;
private onKeyDown = (event: KeyboardEvent) => {
const isEnter = event.key === 'Enter';
const isSpace =
event.key === ' ' ||
event.key === 'Space' ||
event.key === 'Spacebar' ||
event.code === 'Space';
if (!isEnter && !isSpace) {
return;
}
if (event.repeat) {
return;
}
if (isSpace) {
event.preventDefault();
}
const focused = (event.target as HTMLElement).closest(
'limel-list-item'
);
if (!focused) {
return;
}
const value = Number((focused as HTMLElement).dataset.value);
const text = focused.getAttribute('text') || '';
this.toggleValue(value, text);
};

private setBadgeIcon = (event: CustomEvent<boolean>) => {
Expand Down
71 changes: 60 additions & 11 deletions src/components/list-item/examples/list-item-interactive.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,23 @@ import { Component, h, Host, State } from '@stencil/core';
* Interactive list item example
*
* A list item with the default type (`type="listitem"`) shows a simpler
* visual feedback when hovered. Once it is clicked, it emits an event
* with details about the item.
* visual feedback when hovered.
*
* However, certain item `type`s are "selectable";
* for instance `option`, `radio` and `checkbox`.
* However, certain item `type`s are "selectable"; for instance `option`, `radio` and `checkbox`.
* When users click them (or focus and press <kbd>Enter</kbd> or <kbd>Space</kbd>)
* these items toggle their selection.
* these items must toggle their selection.
*
* This example demonstrates manual selection handling. Note that
* `limel-list-item` does not emit its own `interact` event. The consumer
* should listen for native `click` events and handle keyboard events.
*
* The component is purely presentational; selection state is passed in
* via the `selected` prop and updated by the parent example using
* native `click` and keyboard events.
*
* Item `type`s that are "selectable" (`option`, `radio`, `checkbox`)
* are expected to be managed by a container component like `limel-list`.
* For standalone demo purposes we toggle `selected` ourselves.
*
* A `selected` item will both visually indicate that it is selected
* and also informs assistive technology about its state, using proper ARIA attributes.
Expand Down Expand Up @@ -50,7 +60,7 @@ export class ListItemInteractiveExample {

public render() {
return (
<Host>
<Host onKeyDown={this.onHostKeyDown}>
<limel-list-item
tabindex={0}
text="Interactive List Item, with `type='option'`"
Expand All @@ -59,7 +69,7 @@ export class ListItemInteractiveExample {
disabled={this.disabled}
selected={this.selected}
type="option"
onInteract={this.onInteract}
onClick={this.onItemClick}
/>
<limel-example-controls>
<limel-switch
Expand All @@ -81,10 +91,49 @@ export class ListItemInteractiveExample {
);
}

private onInteract = (event: CustomEvent) => {
this.lastEvent = `Item clicked - Selected: ${event.detail.selected}`;
this.selected = event.detail.selected;
console.log('List item interacted:', event.detail);
private toggleSelection = () => {
if (this.disabled) {
return;
}
this.selected = !this.selected;
this.lastEvent = `Item ${this.selected ? 'selected' : 'deselected'}`;
};

private onItemClick = (event: MouseEvent) => {
const target = event.target as HTMLElement;
if (
target.closest('.action-menu-trigger') ||
target.closest('limel-menu')
) {
return; // ignore action menu clicks
}
this.toggleSelection();
};

private onHostKeyDown = (event: KeyboardEvent) => {
if (this.disabled) {
return;
}
const isEnter = event.key === 'Enter';
const isSpace =
event.key === ' ' ||
event.key === 'Space' ||
event.key === 'Spacebar' ||
event.code === 'Space';
if (!isEnter && !isSpace) {
return;
}
if (event.repeat) {
return;
}
if (isSpace) {
event.preventDefault();
}
// Ensure the focused element is our list item before toggling
const active = document.activeElement as HTMLElement | null;
if (active && active.tagName.toLowerCase() === 'limel-list-item') {
this.toggleSelection();
}
};

private setDisabled = (event: CustomEvent<boolean>) => {
Expand Down
Loading