mirror of
https://github.com/ruvnet/RuView
synced 2026-07-25 17:51:48 +00:00
feat(homecore-ui iter 2): Edit Entity modal + shadow-DOM focus delegation
CRUD increment 2/6 — clicking any state card on the Dashboard opens
the Add Entity modal in EDIT mode: pre-populated, entity_id locked,
"Save" primary button, idempotent POST to /api/states/<id> (backend
returns 200 if existed, 201 if created — same handler).
frontend/src/components/StateCard.ts:
- card div is now role="button" tabindex=0, dispatches
`hc-state-card-click` on click + Enter/Space keydown
- aria-label="Edit <entity_id>" for screen readers
- shadowRootOptions delegatesFocus=true so the outer Tab sequence
can reach the inner focusable div (caught by browser agent —
without this Tab couldn't pierce the shadow root)
frontend/src/pages/Dashboard.ts:
- new state: editingState (null = create, StateView = edit)
- _openEdit() catches `hc-state-card-click` from the grid container
- modal heading switches: "Add entity" ↔ "Edit <entity_id>"
- primary button text switches: "Create" ↔ "Save"
- EntityForm receives .editing=true so entity_id input is disabled
- submit toast reads "Updated" or "Created" depending on mode
Browser-verified end-to-end (real homecore-server :8123, 12 entities):
- Click `light.kitchen_ceiling` → modal opens with all 4 attributes
(brightness=230, color_temp_kelvin=4000, friendly_name,
supported_color_modes) pre-populated
- Change state to "off", click Save → toast "Updated
light.kitchen_ceiling = off", grid card reflects new state
- Backend curl confirms /api/states/light.kitchen_ceiling.state = "off"
- Enter key on focused card opens the modal too
- 0 console errors
Co-Authored-By: claude-flow <ruv@ruv.net>
This commit is contained in:
@@ -9,6 +9,12 @@ import type { StateView } from '../api/types.js';
|
||||
|
||||
@customElement('hc-state-card')
|
||||
export class StateCard extends LitElement {
|
||||
// `delegatesFocus` lets Tab key traversal from the light DOM reach the
|
||||
// role="button" element inside this card's shadow root. Without it the
|
||||
// user can only activate the card via mouse click or by JS-focusing the
|
||||
// inner div; with it, the natural tab sequence flows through every card.
|
||||
static shadowRootOptions = { ...LitElement.shadowRootOptions, delegatesFocus: true };
|
||||
|
||||
@property({ type: Object }) state!: StateView;
|
||||
/** Optional: icon SVG string (use `iconSvg()` from lucide.ts) */
|
||||
@property({ type: String }) iconSvg?: string;
|
||||
@@ -32,6 +38,9 @@ export class StateCard extends LitElement {
|
||||
border-color: hsl(185 80% 50% / 0.4);
|
||||
}
|
||||
|
||||
.card { cursor: pointer; }
|
||||
.card:focus-visible { outline: 2px solid var(--hc-primary, #19d4e5); outline-offset: 2px; }
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
@@ -108,7 +117,10 @@ export class StateCard extends LitElement {
|
||||
const badge = this.badgeClass(state);
|
||||
|
||||
return html`
|
||||
<div class="card" part="card">
|
||||
<div class="card" part="card" role="button" tabindex="0"
|
||||
@click=${this._onClick}
|
||||
@keydown=${(e: KeyboardEvent) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); this._onClick(); } }}
|
||||
aria-label="Edit ${entity_id}">
|
||||
<div class="header">
|
||||
${this.iconSvg
|
||||
? html`<div class="icon-wrap" .innerHTML=${this.iconSvg}></div>`
|
||||
@@ -123,6 +135,12 @@ export class StateCard extends LitElement {
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private _onClick() {
|
||||
this.dispatchEvent(new CustomEvent('hc-state-card-click', {
|
||||
detail: { state: this.state }, bubbles: true, composed: true,
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
||||
Reference in New Issue
Block a user