Initial submenu support.

This commit is contained in:
Dermot Duffy
2021-12-17 18:08:07 -08:00
parent 85fd0eebdd
commit 93ff4d8445
8 changed files with 153 additions and 10 deletions
+1
View File
@@ -17,6 +17,7 @@
"dependencies": { "dependencies": {
"@cycjimmy/jsmpeg-player": "^5.0.1", "@cycjimmy/jsmpeg-player": "^5.0.1",
"@material/image-list": "^12.0.0", "@material/image-list": "^12.0.0",
"@material/rtl": "^13.0.0",
"custom-card-helpers": "^1.8.0", "custom-card-helpers": "^1.8.0",
"dayjs": "^1.10.7", "dayjs": "^1.10.7",
"dlv": "github:developit/dlv", "dlv": "github:developit/dlv",
+4
View File
@@ -9,6 +9,7 @@ import {
MenuIcon, MenuIcon,
MenuStateIcon, MenuStateIcon,
PictureElements, PictureElements,
MenuSubmenu,
} from '../types.js'; } from '../types.js';
import { import {
dispatchErrorMessageEvent, dispatchErrorMessageEvent,
@@ -339,3 +340,6 @@ export class FrigateCardElementsMenuIcon extends FrigateCardElementsBaseMenuIcon
@customElement('frigate-card-menu-state-icon') @customElement('frigate-card-menu-state-icon')
export class FrigateCardElementsMenuStateIcon extends FrigateCardElementsBaseMenuIcon<MenuStateIcon> {} export class FrigateCardElementsMenuStateIcon extends FrigateCardElementsBaseMenuIcon<MenuStateIcon> {}
@customElement('frigate-card-menu-submenu')
export class FrigateCardElementsMenuSubmenu extends FrigateCardElementsBaseMenuIcon<MenuSubmenu> {}
+24 -4
View File
@@ -14,7 +14,10 @@ import { StyleInfo, styleMap } from 'lit/directives/style-map.js';
import { actionHandler } from '../action-handler-directive.js'; import { actionHandler } from '../action-handler-directive.js';
import './submenu.js';
import type { import type {
Actions,
ExtendedHomeAssistant, ExtendedHomeAssistant,
MenuButton, MenuButton,
MenuConfig, MenuConfig,
@@ -61,18 +64,26 @@ export class FrigateCardMenu extends LitElement {
* @param ev The action event. * @param ev The action event.
* @param button The button configuration. * @param button The button configuration.
*/ */
protected _actionHandler(ev: CustomEvent, button: MenuButton): void { protected _actionHandler(ev: CustomEvent<{action: string, config?: Actions}>, config?: Actions): void {
if (!ev) { if (!ev) {
return; return;
} }
// If the event itself contains a configuration then use that. This is
// useful in cases where the registration of the event handler does not have
// access to the actual desired configuration (e.g. action events generated
// by a submenu).
if (ev.detail.config) {
config = ev.detail.config;
}
// These interactions should only be handled by the card, as nothing // These interactions should only be handled by the card, as nothing
// upstream has the user-provided configuration. // upstream has the user-provided configuration.
ev.stopPropagation(); ev.stopPropagation();
const interaction: string = ev.detail.action; const interaction: string = ev.detail.action;
const action = getActionConfigGivenAction(interaction, button); const action = getActionConfigGivenAction(interaction, config);
if (!action || !interaction) { if (!config || !action || !interaction) {
return; return;
} }
@@ -92,7 +103,7 @@ export class FrigateCardMenu extends LitElement {
// Collapse menu after the user clicks on something. // Collapse menu after the user clicks on something.
this.expand = false; this.expand = false;
handleAction(this, this.hass as HomeAssistant, button, interaction); handleAction(this, this.hass as HomeAssistant, config, interaction);
} }
/** /**
@@ -169,6 +180,15 @@ export class FrigateCardMenu extends LitElement {
button: true, button: true,
}; };
if (button.type == 'custom:frigate-card-menu-submenu') {
return html`
<frigate-card-submenu
.submenu=${button}
@action=${this._actionHandler.bind(this)}
>
</frigate-card-submenu>`;
}
// TODO: Upon a safe distance from the release of HA 2021.11 these // TODO: Upon a safe distance from the release of HA 2021.11 these
// attributes can be removed from the <ha-icon-button>. // attributes can be removed from the <ha-icon-button>.
// - icon (replaced with the embedded <ha-icon>) // - icon (replaced with the embedded <ha-icon>)
+60
View File
@@ -0,0 +1,60 @@
import { CSSResultGroup, html, LitElement, TemplateResult, unsafeCSS } from 'lit';
import { customElement, property } from 'lit/decorators';
import { actionHandler } from '../action-handler-directive.js';
import { MenuSubmenu } from '../types.js';
import submenuStyle from '../scss/submenu.scss';
import { hasAction } from 'custom-card-helpers';
@customElement('frigate-card-submenu')
export class FrigateCardSubmenu extends LitElement {
@property({ attribute: false })
public submenu?: MenuSubmenu;
protected render(): TemplateResult {
if (!this.submenu) {
return html``;
}
return html`
<ha-button-menu corner="BOTTOM_LEFT">
<ha-icon-button
class="button"
slot="trigger"
.label=${this.submenu.title || ''}
.actionHandler=${actionHandler({
hasHold: hasAction(this.submenu.hold_action),
hasDoubleClick: hasAction(this.submenu.double_tap_action),
})}
>
<ha-icon icon="${this.submenu.icon}"></ha-icon>
</ha-icon-button>
${this.submenu.items.map(
(item) => html`
<mwc-list-item
graphic="icon"
aria-label="${item.title || ''}"
@action=${(ev) => {
// Attach the action config so ascendants have access to it.
ev.detail.config = item;
}}
.actionHandler=${actionHandler({
hasHold: hasAction(item.hold_action),
hasDoubleClick: hasAction(item.double_tap_action),
})}
>
${item.title || ''}
${item.icon
? html` <ha-icon slot="graphic" icon="${item.icon}"> </ha-icon>`
: ``}
</mwc-list-item>
`,
)}
</ha-button-menu>
`;
}
static get styles(): CSSResultGroup {
return unsafeCSS(submenuStyle);
}
}
+3 -1
View File
@@ -61,7 +61,9 @@ ha-card {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
margin: auto; margin: auto;
overflow: hidden;
// Some elements (such as menus) may need to extend beyond the card boundary.
overflow: visible;
width: 100%; width: 100%;
height: 100%; height: 100%;
position: relative; position: relative;
+39 -3
View File
@@ -8,35 +8,71 @@
.frigate-card-menu { .frigate-card-menu {
z-index: 1; z-index: 1;
height: calc(var(--frigate-card-menu-button-size) + 6px);
overflow: hidden;
/* Menu div itself does not handle click events. Without this, in overlay /* Menu div itself does not handle click events. Without this, in overlay
mode, the menu div prevents clicking on gallery items 'behind' the overlay. mode, the menu div prevents clicking on gallery items 'behind' the overlay.
*/ */
pointer-events: none; pointer-events: none;
display: flex;
flex-direction: row;
flex-wrap: wrap;
} }
.frigate-card-menu.overlay-hidden { .frigate-card-menu.overlay-hidden {
position: absolute; position: absolute;
overflow: hidden;
width: calc(var(--frigate-card-menu-button-size) + 6px); width: calc(var(--frigate-card-menu-button-size) + 6px);
height: calc(var(--frigate-card-menu-button-size) + 6px);
} }
.frigate-card-menu.overlay-hidden.left, .frigate-card-menu.overlay-hidden.top { .frigate-card-menu.overlay-hidden.left {
left: 0px;
top: 0px;
// Awful hack: Flexbox column wrapping doesn't work properly in most major
// browsers -- the element boundary does not expand to cover the full wrapped
// content as it should. This results in the wrapped 'content' appearing
// outside the element background (the linear gradient in this case). The
// workaround is to use flex row direction for both rows & columns, and use
// vertical-lr/vertical-rl as the writing mode for columns -- resetting
// writing-mode for descendant elements.
//
// For more information see the Chromium bug as an example:
// - https://bugs.chromium.org/p/chromium/issues/detail?id=507397
writing-mode: vertical-lr;
}
.frigate-card-menu.overlay-hidden.top {
left: 0px; left: 0px;
top: 0px; top: 0px;
} }
.frigate-card-menu.overlay-hidden.right { .frigate-card-menu.overlay-hidden.right {
right: 0px; right: 0px;
top: 0px; top: 0px;
// See "Awful hack" above.
writing-mode: vertical-rl;
} }
.frigate-card-menu.overlay-hidden.left > *,.frigate-card-menu.overlay-hidden.right > * {
// See "Awful hack" above.
writing-mode: horizontal-tb;
}
.frigate-card-menu.overlay-hidden.bottom { .frigate-card-menu.overlay-hidden.bottom {
left: 0px; left: 0px;
bottom: 0px; bottom: 0px;
// If the menu has more content that allows, "wrap upwards" to keep the
// Frigate button in the same place.
flex-wrap: wrap-reverse;
} }
.frigate-card-menu.overlay-hidden.expanded-horizontal { .frigate-card-menu.overlay-hidden.expanded-horizontal {
width: 100%; width: 100%;
height: auto;
overflow: visible;
background: linear-gradient(90deg, rgba(0,0,0,0.3), rgba(0,0,0,0)); background: linear-gradient(90deg, rgba(0,0,0,0.3), rgba(0,0,0,0));
} }
.frigate-card-menu.overlay-hidden.expanded-vertical { .frigate-card-menu.overlay-hidden.expanded-vertical {
height: 100%; height: 100%;
width: auto;
overflow: visible;
background: linear-gradient(180deg, rgba(0,0,0,0.3), rgba(0,0,0,0)); background: linear-gradient(180deg, rgba(0,0,0,0.3), rgba(0,0,0,0));
} }
/* Full above/below menu */ /* Full above/below menu */
+6
View File
@@ -0,0 +1,6 @@
@use './button.scss';
:host {
z-index: 20;
pointer-events: auto;
}
+16 -2
View File
@@ -198,7 +198,7 @@ const serviceCallButtonSchema = elementsBaseSchema.merge(
}), }),
); );
// https://www.home-assistant.io/lovelace/picture-elements/#icon // https://www.home-assistant.io/lovelace/picture-elements/#icon-element
const iconSchema = elementsBaseSchema.merge( const iconSchema = elementsBaseSchema.merge(
z.object({ z.object({
type: z.literal('icon'), type: z.literal('icon'),
@@ -269,6 +269,15 @@ export const menuStateIconSchema = stateIconSchema.merge(
); );
export type MenuStateIcon = z.infer<typeof menuStateIconSchema>; export type MenuStateIcon = z.infer<typeof menuStateIconSchema>;
export const menuSubmenuSchema = iconSchema.merge(
z.object({
type: z.literal('custom:frigate-card-menu-submenu'),
// Menu items don't strictly require their own icon.
items: iconSchema.omit({ type: true }).partial({ icon: true }).array(),
}),
);
export type MenuSubmenu = z.infer<typeof menuSubmenuSchema>;
const frigateCardConditionSchema = z.object({ const frigateCardConditionSchema = z.object({
view: z.string().array().optional(), view: z.string().array().optional(),
fullscreen: z.boolean().optional(), fullscreen: z.boolean().optional(),
@@ -285,6 +294,7 @@ export type FrigateConditional = z.infer<typeof frigateConditionalSchema>;
const pictureElementSchema = z.union([ const pictureElementSchema = z.union([
menuStateIconSchema, menuStateIconSchema,
menuIconSchema, menuIconSchema,
menuSubmenuSchema,
frigateConditionalSchema, frigateConditionalSchema,
stateBadgeIconSchema, stateBadgeIconSchema,
stateIconSchema, stateIconSchema,
@@ -586,7 +596,11 @@ export const frigateCardConfigDefaults = {
event_viewer: viewerConfigDefault, event_viewer: viewerConfigDefault,
}; };
const menuButtonSchema = z.union([menuIconSchema, menuStateIconSchema]); const menuButtonSchema = z.union([
menuIconSchema,
menuStateIconSchema,
menuSubmenuSchema,
]);
export type MenuButton = z.infer<typeof menuButtonSchema>; export type MenuButton = z.infer<typeof menuButtonSchema>;
export interface ExtendedHomeAssistant { export interface ExtendedHomeAssistant {
hassUrl(path?): string; hassUrl(path?): string;