Files
advanced-camera-card/src/components/drawer.ts
T
Dermot Duffy e830db1a77 Complete carousel refactor.
Reduces one layer of DOM nesting for simplication, uses the latest Embla
version, unittests for everything.
2023-09-04 16:25:32 -07:00

159 lines
4.8 KiB
TypeScript

import {
CSSResultGroup,
html,
LitElement,
PropertyValues,
TemplateResult,
unsafeCSS,
} from 'lit';
import { customElement, property } from 'lit/decorators.js';
import { createRef, ref, Ref } from 'lit/directives/ref.js';
import 'side-drawer';
import { SideDrawer } from 'side-drawer';
import drawerInjectStyle from '../scss/drawer-inject.scss';
import drawerStyle from '../scss/drawer.scss';
import { stopEventFromActivatingCardWideActions } from '../utils/action';
import { getChildrenFromElement, isHoverableDevice } from '../utils/basic';
export interface DrawerIcons {
open?: string;
closed?: string;
}
@customElement('frigate-card-drawer')
export class FrigateCardDrawer extends LitElement {
@property({ attribute: true, reflect: true })
public location: 'left' | 'right' = 'left';
@property({ attribute: true, reflect: true, type: Boolean })
public control = true;
@property({ type: Boolean, reflect: true, attribute: true })
public open = false;
@property({ attribute: false })
public icons?: DrawerIcons;
// The 'empty' attribute is used in the styling to change the drawer
// visibility and that of all descendants if there is no content. Styling is
// used rather than display or hidden in order to ensure the contents continue
// to have a measurable size.
@property({ type: Boolean, reflect: true, attribute: true })
public empty = true;
protected _refDrawer: Ref<HTMLElement & { open: boolean }> = createRef();
protected _refSlot: Ref<HTMLSlotElement> = createRef();
protected _resizeObserver = new ResizeObserver(() => this._hideDrawerIfNecessary());
protected readonly _isHoverableDevice = isHoverableDevice();
/**
* Called on the first update.
* @param changedProps The changed properties.
*/
protected firstUpdated(changedProps: PropertyValues): void {
super.firstUpdated(changedProps);
// The `side-drawer` component (and the material drawer for that matter)
// only do fixed drawers (i.e. a drawer for the whole viewport). Hackily
// override the style to customize the drawer to be absolute within the div.
const style = document.createElement('style');
style.innerHTML = drawerInjectStyle;
this._refDrawer.value?.shadowRoot?.appendChild(style);
}
/**
* Called when the slotted children in the drawer change.
*/
protected _slotChanged(): void {
const children = this._refSlot.value
? getChildrenFromElement(this._refSlot.value)
: [];
// Watch all slot children for size changes.
this._resizeObserver.disconnect();
for (const child of children) {
this._resizeObserver.observe(child);
}
this._hideDrawerIfNecessary();
}
/**
* Hide the drawer if there is nothing to show.
* @returns
*/
protected _hideDrawerIfNecessary(): void {
if (!this._refDrawer.value) {
return;
}
const children = this._refSlot.value
? getChildrenFromElement(this._refSlot.value)
: null;
this.empty =
!children ||
!children.length ||
children.every((element) => {
const box = element.getBoundingClientRect();
return !box.width || !box.height;
});
}
protected render(): TemplateResult {
return html`
<side-drawer
${ref(this._refDrawer)}
location="${this.location}"
?open=${this.open}
@mouseleave=${() => {
if (this.open) {
this.open = false;
}
}}
>
${this.control
? html`
<div
class="control-surround"
@click=${(ev: Event) => {
stopEventFromActivatingCardWideActions(ev);
this.open = !this.open;
}}
>
<ha-icon
class="control"
icon="${this.open
? this.icons?.open ?? 'mdi:menu-open'
: this.icons?.closed ?? 'mdi:menu'}"
@mouseenter=${() => {
// Only open the drawer on mousenter when the device
// supports hover (otherwise iOS may end up passing on
// subsequent click events to a different element, see:
// https://github.com/dermotduffy/frigate-hass-card/issues/801
if (this._isHoverableDevice && !this.open) {
this.open = true;
}
}}
>
</ha-icon>
</div>
`
: ''}
<slot ${ref(this._refSlot)} @slotchange=${() => this._slotChanged()}></slot>
</side-drawer>
`;
}
static get styles(): CSSResultGroup {
return unsafeCSS(drawerStyle);
}
}
declare global {
interface HTMLElementTagNameMap {
'frigate-card-drawer': FrigateCardDrawer;
'side-drawer': SideDrawer;
}
}