From 48761f047854168bd584d565f58509e5dd48972f Mon Sep 17 00:00:00 2001 From: Dermot Duffy Date: Tue, 19 May 2026 22:10:02 -0700 Subject: [PATCH] refactor: Use `toggleAttribute` natively where possible (#2494) --- src/card-controller/card-element-manager.ts | 11 ++++----- src/card-controller/expand-manager.ts | 7 +----- src/card-controller/interaction-manager.ts | 7 +----- src/card-controller/style-manager.ts | 9 ++++---- src/components-lib/key-assigner-controller.ts | 3 +-- .../media-dimensions-container-controller.ts | 8 ++----- src/components-lib/media-grid-controller.ts | 5 ++-- src/components-lib/menu-controller.ts | 4 ++-- src/components-lib/status-bar-controller.ts | 6 ++--- src/components-lib/timeline/controller.ts | 23 ++++--------------- src/components/viewer/carousel.ts | 6 ++--- src/components/zoomer.ts | 3 +-- .../status-bar-controller.test.ts | 15 ++++++------ 13 files changed, 37 insertions(+), 70 deletions(-) diff --git a/src/card-controller/card-element-manager.ts b/src/card-controller/card-element-manager.ts index 6ef34ea2..7240212f 100644 --- a/src/card-controller/card-element-manager.ts +++ b/src/card-controller/card-element-manager.ts @@ -2,7 +2,6 @@ import { LitElement, ReactiveControllerHost } from 'lit'; import { ActionEventTarget } from '../action-handler-directive'; import { isCardInPanel } from '../ha/panel'; import { LovelaceCard } from '../ha/types'; -import { setOrRemoveAttribute } from '../utils/basic'; import { isBeingCasted } from '../utils/casting'; import { isAncestorInEventPath } from '../utils/event-ancestor'; import { CardMediaReviewEventTarget } from '../utils/review'; @@ -90,8 +89,8 @@ export class CardElementManager { ]); // Whether or not the card is in panel mode on the dashboard. - setOrRemoveAttribute(this._element, isCardInPanel(this._element), 'panel'); - setOrRemoveAttribute(this._element, isBeingCasted(), 'casted'); + this._element.toggleAttribute('panel', isCardInPanel(this._element)); + this._element.toggleAttribute('casted', isBeingCasted()); this._api.getFullscreenManager().connect(); @@ -165,9 +164,9 @@ export class CardElementManager { } public elementDisconnected(): void { - setOrRemoveAttribute(this._element, false, 'panel'); - setOrRemoveAttribute(this._element, false, 'tabindex'); - setOrRemoveAttribute(this._element, false, 'casted'); + this._element.toggleAttribute('panel', false); + this._element.toggleAttribute('tabindex', false); + this._element.toggleAttribute('casted', false); // Suspend issue evaluation so state changes below (e.g. clearing // mediaLoadedInfo) don't arm timers while the card is detached. Issue state diff --git a/src/card-controller/expand-manager.ts b/src/card-controller/expand-manager.ts index 6b9310aa..ce4591f3 100644 --- a/src/card-controller/expand-manager.ts +++ b/src/card-controller/expand-manager.ts @@ -1,4 +1,3 @@ -import { setOrRemoveAttribute } from '../utils/basic'; import { CardExpandAPI } from './types'; export class ExpandManager { @@ -29,11 +28,7 @@ export class ExpandManager { this._expanded = expanded; this._setConditionState(); - setOrRemoveAttribute( - this._api.getCardElementManager().getElement(), - expanded, - 'expanded', - ); + this._api.getCardElementManager().getElement().toggleAttribute('expanded', expanded); this._api.getCardElementManager().update(); } diff --git a/src/card-controller/interaction-manager.ts b/src/card-controller/interaction-manager.ts index 704302b2..2dd2d14d 100644 --- a/src/card-controller/interaction-manager.ts +++ b/src/card-controller/interaction-manager.ts @@ -1,5 +1,4 @@ import { throttle } from 'lodash-es'; -import { setOrRemoveAttribute } from '../utils/basic'; import { Timer } from '../utils/timer'; import { CardInteractionAPI } from './types'; @@ -33,11 +32,7 @@ export class InteractionManager { private _setInteraction(val: boolean): void { this._interacted = val; - setOrRemoveAttribute( - this._api.getCardElementManager().getElement(), - val, - 'interaction', - ); + this._api.getCardElementManager().getElement().toggleAttribute('interaction', val); this._api.getConditionStateManager().setState({ interaction: val }); } diff --git a/src/card-controller/style-manager.ts b/src/card-controller/style-manager.ts index b86d1a29..7da1e5cb 100644 --- a/src/card-controller/style-manager.ts +++ b/src/card-controller/style-manager.ts @@ -81,11 +81,10 @@ export class StyleManager { private _setDimmable(): void { const config = this._api.getConfigManager().getConfig(); - setOrRemoveAttribute( - this._api.getCardElementManager().getElement(), - !!config?.view.dim, - 'dimmable', - ); + this._api + .getCardElementManager() + .getElement() + .toggleAttribute('dimmable', !!config?.view.dim); } private _setMinMaxHeight(): void { diff --git a/src/components-lib/key-assigner-controller.ts b/src/components-lib/key-assigner-controller.ts index b0273d3c..34c3fbfa 100644 --- a/src/components-lib/key-assigner-controller.ts +++ b/src/components-lib/key-assigner-controller.ts @@ -1,7 +1,6 @@ import { LitElement, ReactiveController } from 'lit'; import { isEqual } from 'lodash-es'; import { KeyboardShortcut } from '../config/schema/view'; -import { setOrRemoveAttribute } from '../utils/basic'; export class KeyAssignerController implements ReactiveController { private _host: LitElement; @@ -42,7 +41,7 @@ export class KeyAssignerController implements ReactiveController { } private _setAssigning(assigning: boolean): void { this._assigning = assigning; - setOrRemoveAttribute(this._host, this._assigning, 'assigning'); + this._host.toggleAttribute('assigning', this._assigning); if (this._assigning) { this._host.addEventListener('keydown', this._keydownEventHandler); diff --git a/src/components-lib/media-dimensions-container-controller.ts b/src/components-lib/media-dimensions-container-controller.ts index 7ccfa8a5..be263eb4 100644 --- a/src/components-lib/media-dimensions-container-controller.ts +++ b/src/components-lib/media-dimensions-container-controller.ts @@ -2,11 +2,7 @@ import { ReactiveController, ReactiveControllerHost } from 'lit'; import { debounce } from 'lodash-es'; import { CameraDimensionsConfig } from '../config/schema/cameras'; import { MediaLoadedInfoEventDetail } from '../types'; -import { - aspectRatioToString, - setOrRemoveAttribute, - setOrRemoveStyleProperty, -} from '../utils/basic'; +import { aspectRatioToString, setOrRemoveStyleProperty } from '../utils/basic'; import { updateElementStyleFromMediaLayoutConfig } from '../utils/media-layout'; const ROTATED_ATTRIBUTE = 'rotated'; @@ -215,7 +211,7 @@ export class MediaDimensionsContainerController implements ReactiveController { } private _setRotation(element: HTMLElement, rotate: boolean): void { - setOrRemoveAttribute(element, rotate, ROTATED_ATTRIBUTE); + element.toggleAttribute(ROTATED_ATTRIBUTE, rotate); } private _resize(): void { diff --git a/src/components-lib/media-grid-controller.ts b/src/components-lib/media-grid-controller.ts index 0b7100f0..5b3f8ad8 100644 --- a/src/components-lib/media-grid-controller.ts +++ b/src/components-lib/media-grid-controller.ts @@ -4,7 +4,6 @@ import { ViewDisplayConfig } from '../config/schema/common/display'; import { forceReflow, getChildrenFromElement, - setOrRemoveAttribute, setOrRemoveStyleProperty, } from '../utils/basic'; import { fireAdvancedCameraCardEvent } from '../utils/fire-advanced-camera-card-event'; @@ -322,12 +321,12 @@ export class MediaGridController { private _updateSelectedStylesOnElements(): void { for (const [id, element] of this._gridContents.entries()) { - setOrRemoveAttribute(element, id === this._selected, 'selected'); + element.toggleAttribute('selected', id === this._selected); // Explicitly use an 'unselected' attribute vs a :not(selected) such that // a carousel with neither selected nor unselected will behave normally. // This matches a css selector in viewer-carousel.scss . - setOrRemoveAttribute(element, id !== this._selected, 'unselected'); + element.toggleAttribute('unselected', id !== this._selected); } } diff --git a/src/components-lib/menu-controller.ts b/src/components-lib/menu-controller.ts index ac6f941c..09c05d51 100644 --- a/src/components-lib/menu-controller.ts +++ b/src/components-lib/menu-controller.ts @@ -9,7 +9,7 @@ import type { MenuItem } from '../config/schema/elements/custom/menu/types.js'; import type { MenuConfig } from '../config/schema/menu.js'; import type { Interaction } from '../types.js'; import { getActionConfigGivenAction } from '../utils/action'; -import { arrayify, isTruthy, setOrRemoveAttribute } from '../utils/basic.js'; +import { arrayify, isTruthy } from '../utils/basic.js'; import { AutoHideState, isAutoHidden as evaluateAutoHidden } from './auto-hide.js'; export class MenuController { @@ -126,7 +126,7 @@ export class MenuController { public setExpanded(expanded: boolean): void { this._expanded = expanded; - setOrRemoveAttribute(this._host, expanded, 'expanded'); + this._host.toggleAttribute('expanded', expanded); this._host.requestUpdate(); } diff --git a/src/components-lib/status-bar-controller.ts b/src/components-lib/status-bar-controller.ts index 33b02e48..cdd1a178 100644 --- a/src/components-lib/status-bar-controller.ts +++ b/src/components-lib/status-bar-controller.ts @@ -5,7 +5,7 @@ import { ActionsConfig, StatusBarItem } from '../config/schema/actions/types'; import { STATUS_BAR_PRIORITY_DEFAULT } from '../config/schema/common/const'; import { StatusBarConfig } from '../config/schema/status-bar'; import { getActionConfigGivenAction } from '../utils/action'; -import { arrayify, setOrRemoveAttribute } from '../utils/basic'; +import { arrayify } from '../utils/basic'; import { Timer } from '../utils/timer'; import { AutoHideState, isAutoHidden as evaluateAutoHidden } from './auto-hide'; @@ -144,10 +144,10 @@ export class StatusBarController { } private _show(): void { - setOrRemoveAttribute(this._host, false, 'hide'); + this._host.toggleAttribute('hide', false); } private _hide(): void { - setOrRemoveAttribute(this._host, true, 'hide'); + this._host.toggleAttribute('hide', true); } } diff --git a/src/components-lib/timeline/controller.ts b/src/components-lib/timeline/controller.ts index 21b7a116..b07e5d8f 100644 --- a/src/components-lib/timeline/controller.ts +++ b/src/components-lib/timeline/controller.ts @@ -30,12 +30,7 @@ import { import { configDefaults } from '../../config/schema/types'; import { HomeAssistant } from '../../ha/types'; import { stopEventFromActivatingCardWideActions } from '../../utils/action'; -import { - formatDateAndTime, - isHoverableDevice, - isTruthy, - setOrRemoveAttribute, -} from '../../utils/basic'; +import { formatDateAndTime, isHoverableDevice, isTruthy } from '../../utils/basic'; import { findBestMediaTimeIndex } from '../../utils/find-best-media-time-index'; import { fireAdvancedCameraCardEvent } from '../../utils/fire-advanced-camera-card-event'; import { ViewMedia } from '../../view/item'; @@ -195,17 +190,9 @@ export class TimelineController { if (this._timelineConfig !== (options.timelineConfig ?? null)) { this._timelineConfig = options?.timelineConfig ?? null; - setOrRemoveAttribute( - this._host, - !!this._timelineConfig?.show_recordings, - 'recordings', - ); - setOrRemoveAttribute( - this._host, - this._timelineConfig?.style === 'ribbon', - 'ribbon', - ); - setOrRemoveAttribute(this._host, this._timelineConfig?.style === 'stack', 'stack'); + this._host.toggleAttribute('recordings', !!this._timelineConfig?.show_recordings); + this._host.toggleAttribute('ribbon', this._timelineConfig?.style === 'ribbon'); + this._host.toggleAttribute('stack', this._timelineConfig?.style === 'stack'); } this._thumbnailConfig = options?.thumbnailConfig ?? null; @@ -215,7 +202,7 @@ export class TimelineController { this._timelineConfig = options?.timelineConfig ?? null; this._mini = options?.mini ?? false; - setOrRemoveAttribute(this._host, this._shouldShowGroups(), 'groups'); + this._host.toggleAttribute('groups', this._shouldShowGroups()); } public async setView(viewManagerEpoch: ViewManagerEpoch | null): Promise { diff --git a/src/components/viewer/carousel.ts b/src/components/viewer/carousel.ts index 94fa65e0..3d46e7da 100644 --- a/src/components/viewer/carousel.ts +++ b/src/components/viewer/carousel.ts @@ -26,7 +26,7 @@ import { localize } from '../../localize/localize.js'; import '../../patches/ha-hls-player.js'; import viewerCarouselStyle from '../../scss/viewer-carousel.scss'; import { stopEventFromActivatingCardWideActions } from '../../utils/action.js'; -import { contentsChanged, setOrRemoveAttribute } from '../../utils/basic.js'; +import { contentsChanged } from '../../utils/basic.js'; import { CarouselSelected } from '../../utils/embla/carousel-controller.js'; import { getTextDirection } from '../../utils/text-direction.js'; import { ViewItemClassifier } from '../../view/item-classifier.js'; @@ -235,7 +235,7 @@ export class AdvancedCameraCardViewerCarousel extends LitElement { const newView = this.viewManagerEpoch?.manager.getView(); if (!newView?.context?.mediaViewer?.seek) { - setOrRemoveAttribute(this, false, 'unseekable'); + this.toggleAttribute('unseekable', false); } const oldView = this.viewManagerEpoch?.oldView; @@ -436,7 +436,7 @@ export class AdvancedCameraCardViewerCarousel extends LitElement { } const seekTimeInMedia = selectedMedia.includesTime(seek); - setOrRemoveAttribute(this, !seekTimeInMedia, 'unseekable'); + this.toggleAttribute('unseekable', !seekTimeInMedia); if (!seekTimeInMedia && !mediaPlayerController.isPaused()) { mediaPlayerController.pause(); } else if (seekTimeInMedia && mediaPlayerController.isPaused()) { diff --git a/src/components/zoomer.ts b/src/components/zoomer.ts index e2daa9fa..9091e9d0 100644 --- a/src/components/zoomer.ts +++ b/src/components/zoomer.ts @@ -8,7 +8,6 @@ import { } from 'lit'; import { customElement, property, state } from 'lit/decorators.js'; import { ZoomController } from '../components-lib/zoom/zoom-controller.js'; -import { setOrRemoveAttribute } from '../utils/basic.js'; import { PartialZoomSettings } from '../components-lib/zoom/types.js'; @customElement('advanced-camera-card-zoomer') @@ -48,7 +47,7 @@ export class AdvancedCameraCardZoomer extends LitElement { protected willUpdate(changedProps: PropertyValues): void { if (changedProps.has('_zoomed')) { - setOrRemoveAttribute(this, this._zoomed, 'zoomed'); + this.toggleAttribute('zoomed', this._zoomed); } if (changedProps.has('zoom')) { diff --git a/tests/components-lib/status-bar-controller.test.ts b/tests/components-lib/status-bar-controller.test.ts index 6eaae4be..eeb94b09 100644 --- a/tests/components-lib/status-bar-controller.test.ts +++ b/tests/components-lib/status-bar-controller.test.ts @@ -4,7 +4,6 @@ import { StatusBarConfig, statusBarConfigSchema, } from '../../src/config/schema/status-bar'; -import { setOrRemoveAttribute } from '../../src/utils/basic'; import { createInteractionActionEvent, createLitElement } from '../test-utils'; const createConfig = (config?: unknown): StatusBarConfig => { @@ -35,7 +34,7 @@ describe('StatusBarController', () => { it('should not hide when not in popup style', () => { const host = createLitElement(); - setOrRemoveAttribute(host, true, 'hide'); + host.toggleAttribute('hide', true); const controller = new StatusBarController(host); @@ -52,7 +51,7 @@ describe('StatusBarController', () => { it('should not show when in popup style', () => { const host = createLitElement(); - setOrRemoveAttribute(host, true, 'hide'); + host.toggleAttribute('hide', true); const controller = new StatusBarController(host); @@ -181,7 +180,7 @@ describe('StatusBarController', () => { describe('should deal with popup styles correctly', () => { it('should show from empty to sufficient', () => { const host = createLitElement(); - setOrRemoveAttribute(host, true, 'hide'); + host.toggleAttribute('hide', true); const controller = new StatusBarController(host); controller.setConfig( @@ -203,7 +202,7 @@ describe('StatusBarController', () => { it('should not show from empty to insufficient', () => { const host = createLitElement(); - setOrRemoveAttribute(host, true, 'hide'); + host.toggleAttribute('hide', true); const controller = new StatusBarController(host); controller.setConfig( @@ -255,13 +254,13 @@ describe('StatusBarController', () => { controller.setItems([sufficientString]); // Emulate the popup being hidden. - setOrRemoveAttribute(host, true, 'hide'); + host.toggleAttribute('hide', true); controller.setItems([sufficientIcon]); expect(host.getAttribute('hide')).toBe(null); // Emulate the popup being hidden. - setOrRemoveAttribute(host, true, 'hide'); + host.toggleAttribute('hide', true); controller.setItems([sufficientImage]); expect(host.getAttribute('hide')).toBe(null); @@ -269,7 +268,7 @@ describe('StatusBarController', () => { it('should not start popup timer when permanent items are present', () => { const host = createLitElement(); - setOrRemoveAttribute(host, true, 'hide'); + host.toggleAttribute('hide', true); const controller = new StatusBarController(host); controller.setConfig(