mirror of
https://github.com/rio-labs/rio.git
synced 2026-05-05 02:19:20 -05:00
add support for single/multiple selection in list view
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import { componentsByElement, componentsById } from "../componentManagement";
|
||||
import { ComponentId } from "../dataModels";
|
||||
import { ComponentBase, ComponentState, DeltaState } from "./componentBase";
|
||||
import { ComponentBase, ComponentState } from "./componentBase";
|
||||
import { CustomListItemComponent } from "./customListItem";
|
||||
import { HeadingListItemComponent } from "./headingListItem";
|
||||
import { SeparatorListItemComponent } from "./separatorListItem";
|
||||
@@ -8,9 +8,13 @@ import { SeparatorListItemComponent } from "./separatorListItem";
|
||||
export type ListViewState = ComponentState & {
|
||||
_type_: "ListView-builtin";
|
||||
children: ComponentId[];
|
||||
selection_mode: "none" | "single" | "multiple"; // Selection mode
|
||||
selected_items: number[]; // Indices of selected items
|
||||
};
|
||||
|
||||
export class ListViewComponent extends ComponentBase<ListViewState> {
|
||||
private clickHandlers: Map<number, (event: MouseEvent) => void> = new Map();
|
||||
|
||||
createElement(): HTMLElement {
|
||||
let element = document.createElement("div");
|
||||
element.classList.add("rio-list-view");
|
||||
@@ -36,14 +40,23 @@ export class ListViewComponent extends ComponentBase<ListViewState> {
|
||||
// Update the styles of the children
|
||||
this.state.children = deltaState.children;
|
||||
this.onChildGrowChanged();
|
||||
this._updateSelectionInteractivity(); // Reapply handlers after children update
|
||||
}
|
||||
|
||||
if (deltaState.selection_mode !== undefined) {
|
||||
this.state.selection_mode = deltaState.selection_mode;
|
||||
this._updateSelectionInteractivity();
|
||||
}
|
||||
if (deltaState.selected_items !== undefined) {
|
||||
this.state.selected_items = deltaState.selected_items;
|
||||
this._updateSelectionStyles();
|
||||
}
|
||||
}
|
||||
|
||||
onChildGrowChanged(): void {
|
||||
// Visually style children
|
||||
this._updateChildStyles();
|
||||
this._updateSelectionStyles();
|
||||
|
||||
// Set the children's `flex-grow`
|
||||
let hasGrowers = false;
|
||||
for (let [index, childId] of this.state.children.entries()) {
|
||||
let childComponent = componentsById[childId]!;
|
||||
@@ -154,4 +167,95 @@ export class ListViewComponent extends ComponentBase<ListViewState> {
|
||||
curChild.style.overflow = "hidden";
|
||||
}
|
||||
}
|
||||
|
||||
_clearClickHandlers(): void {
|
||||
this.element
|
||||
.querySelectorAll(".rio-custom-list-item")
|
||||
.forEach((item) => {
|
||||
const handler = this.clickHandlers.get(item as HTMLElement);
|
||||
if (handler) {
|
||||
item.removeEventListener("click", handler);
|
||||
}
|
||||
});
|
||||
this.clickHandlers.clear();
|
||||
}
|
||||
|
||||
_updateSelectionInteractivity(): void {
|
||||
// Remove all existing listeners from current DOM elements
|
||||
this.element
|
||||
.querySelectorAll(".rio-custom-list-item")
|
||||
.forEach((item, index) => {
|
||||
const oldHandler = this.clickHandlers.get(index);
|
||||
if (oldHandler) {
|
||||
item.removeEventListener("click", oldHandler);
|
||||
}
|
||||
});
|
||||
|
||||
if (this.state.selection_mode === "none") {
|
||||
this.clickHandlers.clear(); // Clear all handlers when selection is disabled
|
||||
} else {
|
||||
this.element
|
||||
.querySelectorAll(".rio-custom-list-item")
|
||||
.forEach((item, index) => {
|
||||
// Create and store a new handler for this index
|
||||
const handler = (event: MouseEvent) =>
|
||||
this._handleItemClick(index);
|
||||
item.addEventListener("click", handler);
|
||||
this.clickHandlers.set(index, handler);
|
||||
});
|
||||
// Remove handlers for indices that no longer exist
|
||||
for (const index of this.clickHandlers.keys()) {
|
||||
if (
|
||||
index >= this.element.children.length ||
|
||||
!this._isGroupedListItem(
|
||||
this.element.children[index] as HTMLElement
|
||||
)
|
||||
) {
|
||||
this.clickHandlers.delete(index);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_handleItemClick(index: number): void {
|
||||
if (this.state.selection_mode === "none") return;
|
||||
|
||||
const currentSelection = [...this.state.selected_items];
|
||||
const isSelected = currentSelection.includes(index);
|
||||
|
||||
if (this.state.selection_mode === "single") {
|
||||
this.state.selected_items = isSelected ? [] : [index];
|
||||
} else if (this.state.selection_mode === "multiple") {
|
||||
if (isSelected) {
|
||||
this.state.selected_items = currentSelection.filter(
|
||||
(i) => i !== index
|
||||
);
|
||||
} else {
|
||||
this.state.selected_items = [...currentSelection, index];
|
||||
}
|
||||
}
|
||||
|
||||
this._updateSelectionStyles();
|
||||
this._notifySelectionChange(); // Notify backend of the change
|
||||
}
|
||||
|
||||
_updateSelectionStyles(): void {
|
||||
this.element
|
||||
.querySelectorAll(".rio-custom-list-item")
|
||||
.forEach((item, index) => {
|
||||
if (this.state.selected_items.includes(index)) {
|
||||
item.classList.add("selected");
|
||||
} else {
|
||||
item.classList.remove("selected");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_notifySelectionChange(): void {
|
||||
// Send selection change to the backend
|
||||
this.sendMessageToBackend({
|
||||
type: "selectionChange",
|
||||
selected_items: this.state.selected_items,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -70,3 +70,11 @@
|
||||
.rio-list-item-ripple:hover {
|
||||
background: var(--rio-local-bg-active);
|
||||
}
|
||||
|
||||
.rio-list-view .rio-custom-list-item.selected {
|
||||
background-color: var(--rio-global-secondary-bg);
|
||||
}
|
||||
|
||||
.rio-list-view .rio-custom-list-item.selected:hover {
|
||||
background: var(--rio-global-secondary-bg-active);
|
||||
}
|
||||
|
||||
@@ -11,10 +11,23 @@ from .fundamental_component import FundamentalComponent
|
||||
__all__ = ["ListView"]
|
||||
|
||||
|
||||
# Define the SelectionChangeEvent class
|
||||
class SelectionChangeEvent:
|
||||
"""
|
||||
Event triggered when the selection in a ListView changes.
|
||||
|
||||
Attributes:
|
||||
selected_items: A list of indices of the currently selected items.
|
||||
"""
|
||||
|
||||
def __init__(self, selected_items: list[int]):
|
||||
self.selected_items = selected_items
|
||||
|
||||
|
||||
@t.final
|
||||
class ListView(FundamentalComponent):
|
||||
"""
|
||||
Vertically arranges and styles its children.
|
||||
Vertically arranges and styles its children with optional selection support.
|
||||
|
||||
Lists of items are a common pattern in user interfaces. Whether you need to
|
||||
display a list of products, messages, or any other kind of data, the
|
||||
@@ -22,7 +35,8 @@ class ListView(FundamentalComponent):
|
||||
|
||||
List views are similar to columns, in that they arrange their children
|
||||
vertically. However, they also apply a default style to their content which
|
||||
allows you to group items together in a visually distinct way.
|
||||
allows you to group items together in a visually distinct way. Additionally,
|
||||
`ListView` supports single or multiple item selection.
|
||||
|
||||
Rio ships with several components which are meant specifically to be used
|
||||
inside of `ListView`s:
|
||||
@@ -35,24 +49,33 @@ class ListView(FundamentalComponent):
|
||||
- `SeparatorListItem`: Leaves a gap between items, so you can group them
|
||||
visually.
|
||||
|
||||
|
||||
## Attributes
|
||||
|
||||
`children`: The children to display in the list.
|
||||
|
||||
`selection_mode`: Determines the selection behavior: "none" (no selection),
|
||||
"single" (one item selectable), or "multiple" (multiple items selectable).
|
||||
Defaults to "none".
|
||||
|
||||
`selected_items`: A list of indices of currently selected items. Defaults to
|
||||
an empty list.
|
||||
|
||||
`on_selection_change`: Event handler triggered when the selection changes.
|
||||
|
||||
`key`: A unique key for this component. If the key changes, the component
|
||||
will be destroyed and recreated. This is useful for components which
|
||||
maintain state across rebuilds.
|
||||
|
||||
|
||||
## Examples
|
||||
|
||||
This example will display a list of two products:
|
||||
This example will display a list of two products with single selection:
|
||||
|
||||
```python
|
||||
rio.ListView(
|
||||
rio.SimpleListItem("Product 1", key="item1"),
|
||||
rio.SimpleListItem("Product 2", key="item2"),
|
||||
rio.SimpleListItem("Item 1", key="item1"),
|
||||
rio.SimpleListItem("Item 2", key="item2"),
|
||||
selection_mode="single",
|
||||
selected_items=[0], # Preselect the first item
|
||||
)
|
||||
```
|
||||
|
||||
@@ -66,13 +89,23 @@ class ListView(FundamentalComponent):
|
||||
|
||||
class MyComponent(rio.Component):
|
||||
products: list[str] = ["Product 1", "Product 2", "Product 3"]
|
||||
selected_indices: list[int] = []
|
||||
|
||||
def on_press_heading_list_item(self, product: str) -> None:
|
||||
print(f"Selected {product}")
|
||||
|
||||
def on_selection_change(self, event: rio.SelectionChangeEvent) -> None:
|
||||
self.selected_indices = event.selected_items
|
||||
print(f"Selected indices: {self.selected_indices}")
|
||||
|
||||
def build(self) -> rio.Component:
|
||||
# First create the ListView
|
||||
result = rio.ListView()
|
||||
result = rio.ListView(
|
||||
*[rio.SimpleListItem(text=p, key=p) for p in self.products],
|
||||
selection_mode="multiple",
|
||||
selected_items=self.selected_indices,
|
||||
on_selection_change=self.on_selection_change,
|
||||
)
|
||||
|
||||
# Then add the children one by one
|
||||
for product in self.products:
|
||||
@@ -94,6 +127,9 @@ class ListView(FundamentalComponent):
|
||||
"""
|
||||
|
||||
children: list[rio.Component]
|
||||
selection_mode: t.Literal["none", "single", "multiple"]
|
||||
selected_items: list[int]
|
||||
on_selection_change: rio.EventHandler[SelectionChangeEvent]
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@@ -116,6 +152,9 @@ class ListView(FundamentalComponent):
|
||||
align_y: float | None = None,
|
||||
# SCROLLING-REWORK scroll_x: t.Literal["never", "auto", "always"] = "never",
|
||||
# SCROLLING-REWORK scroll_y: t.Literal["never", "auto", "always"] = "never",
|
||||
selection_mode: t.Literal["none", "single", "multiple"] = "none",
|
||||
selected_items: list[int] | None = None,
|
||||
on_selection_change: rio.EventHandler[SelectionChangeEvent] = None,
|
||||
) -> None:
|
||||
super().__init__(
|
||||
key=key,
|
||||
@@ -139,6 +178,9 @@ class ListView(FundamentalComponent):
|
||||
)
|
||||
|
||||
self.children = list(children)
|
||||
self.selection_mode = selection_mode
|
||||
self.selected_items = selected_items or []
|
||||
self.on_selection_change = on_selection_change
|
||||
|
||||
def add(self, child: rio.Component) -> te.Self:
|
||||
"""
|
||||
@@ -158,5 +200,27 @@ class ListView(FundamentalComponent):
|
||||
self.children.append(child)
|
||||
return self
|
||||
|
||||
async def _on_message_(self, msg: t.Any) -> None:
|
||||
"""
|
||||
Handle messages from the frontend, such as selection changes.
|
||||
"""
|
||||
# Parse the message
|
||||
assert isinstance(msg, dict), msg
|
||||
assert msg["type"] == "selectionChange", msg
|
||||
|
||||
msg_type: str = msg["type"]
|
||||
assert isinstance(msg_type, str), msg_type
|
||||
|
||||
self.selected_items = msg["selected_items"]
|
||||
|
||||
if self.on_selection_change is None:
|
||||
return
|
||||
|
||||
# Trigger the press event
|
||||
await self.call_event_handler(self.on_selection_change)
|
||||
|
||||
# Refresh the session
|
||||
await self.session._refresh()
|
||||
|
||||
|
||||
ListView._unique_id_ = "ListView-builtin"
|
||||
|
||||
Reference in New Issue
Block a user