Skip to content

Commit

Permalink
A lot of pop-up fixes + Google Cast support!
Browse files Browse the repository at this point in the history
- [x] Pop-ups are (finally) changing color accordingly when a theme change (e.g. from dark to light). #627
- [x] Vertical stack titles are now hidden if set for a pop-up. #647
- [x] Heavy pop-ups are now closing faster on some setups.
- [x] Pop-up transitions are now smoother and more reliable on some setups.
- [x] Pop-up backdrop transitions are now smoother on some setups (if not disable it, this feature can slowdown transitions).
- [x] Pop-up auto-close timeout is now removing the hash from the URL correctly.
- [x] Added the missing haptic feedback on the pop-up close button.
- [x] Some haptic feedback were fired multiple times, this should be fixed.
- [x] Finally fixed the `Custom element doesn't exist` error on Google Nest Hub! #345 and maybe #257
  • Loading branch information
Clooos authored Aug 21, 2024
1 parent bf228cc commit 36b09bc
Show file tree
Hide file tree
Showing 6 changed files with 181 additions and 177 deletions.
66 changes: 33 additions & 33 deletions dist/bubble-card.js

Large diffs are not rendered by default.

64 changes: 45 additions & 19 deletions src/cards/pop-up/create.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { convertToRGBA } from "../../tools/style.ts";
import { addActions } from "../../tools/tap-actions.ts";
import { createElement, toggleEntity, configChanged, fireEvent } from "../../tools/utils.ts";
import { createElement, toggleEntity, configChanged, fireEvent, forwardHaptic } from "../../tools/utils.ts";
import { onUrlChange, removeHash, hideContent } from "./helpers.ts";
import styles, { backdropStyles } from "./styles.ts";

Expand All @@ -9,22 +9,31 @@ let hideBackdrop = false;
let startTouchY;
let lastTouchY;

const colorScheme = window.matchMedia('(prefers-color-scheme: dark)');

export function getBackdrop(context) {
if (backdrop) {
return backdrop;
}

const themeColorBackground =
getComputedStyle(document.body).getPropertyValue('--ha-card-background') ||
getComputedStyle(document.body).getPropertyValue('--card-background-color');

let themeColorBackground;
const backdropStyle = createElement('style');
backdropStyle.innerHTML = `
${backdropStyles}
.bubble-backdrop {
background-color: ${convertToRGBA(themeColorBackground, 0.7, 0.7)};
}
`;

function updateBackdropColor() {
themeColorBackground =
getComputedStyle(document.body).getPropertyValue('--ha-card-background') ||
getComputedStyle(document.body).getPropertyValue('--card-background-color');

backdropStyle.style.setProperty('--bubble-backdrop-background-color', convertToRGBA(themeColorBackground, 0.7, 0.7));
}

colorScheme.addEventListener('change', () => {
updateBackdropColor()
});

updateBackdropColor();

backdropStyle.innerHTML = backdropStyles;
document.head.appendChild(backdropStyle);

const backdropCustomStyle = createElement('style');
Expand Down Expand Up @@ -62,7 +71,10 @@ export function createHeader(context) {
context.elements.closeIcon = createElement('ha-icon', 'bubble-close-icon');
context.elements.closeIcon.icon = 'mdi:close';
context.elements.closeButton = createElement("button", "bubble-close-button close-pop-up");
context.elements.closeButton.addEventListener('click', removeHash);
context.elements.closeButton.addEventListener('click', () => {
removeHash();
forwardHaptic("selection");
});
context.elements.closeButton.appendChild(context.elements.closeIcon);

context.elements.buttonContainer = createElement('div', 'bubble-button-container');
Expand Down Expand Up @@ -120,15 +132,27 @@ export function createStructure(context) {
} else {
context.elements.customStyle = existingStyle;
}

let themeColorBackground;
const opacity = context.config.bg_opacity ?? 88;

const themeColorBackground =
getComputedStyle(document.body).getPropertyValue('--ha-card-background') ||
getComputedStyle(document.body).getPropertyValue('--card-background-color');
function updatePopupColor() {
themeColorBackground =
getComputedStyle(document.body).getPropertyValue('--ha-card-background') ||
getComputedStyle(document.body).getPropertyValue('--card-background-color');

const color = context.config.bg_color ? context.config.bg_color : themeColorBackground;
const rgbaColor = convertToRGBA(color, (opacity / 100), 1.02);

context.popUp.style.setProperty('--bubble-pop-up-background-color', rgbaColor);
}

colorScheme.addEventListener('change', () => {
updatePopupColor()
});

updatePopupColor();

const color = context.config.bg_color ? context.config.bg_color : themeColorBackground;
const opacity = context.config.bg_opacity ?? 88;
const rgbaColor = convertToRGBA(color, (opacity / 100), 1.02);
context.popUp.style.backgroundColor = rgbaColor;
context.popUp.style.setProperty('--desktop-width', context.config.width_desktop ?? '540px');

if (context.config.close_on_click) {
Expand Down Expand Up @@ -195,9 +219,11 @@ export function prepareStructure(context) {
context.popUp = context.verticalStack.querySelector('#root');
context.popUp.classList.add('bubble-pop-up', 'pop-up', 'is-popup-closed');
context.verticalStack.removeChild(context.popUp);
context.cardTitle = context.verticalStack.querySelector('.card-header');
context.elements = {};
getBackdrop(context);

if (context.cardTitle) context.cardTitle.style.display = 'none';
hideBackdrop = hideBackdrop || (context.config.hide_backdrop ?? false);

context.popUp.style.setProperty('--custom-height-offset-desktop', context.config.margin_top_desktop ?? '0px');
Expand Down
198 changes: 89 additions & 109 deletions src/cards/pop-up/helpers.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
import { getBackdrop } from "./create.ts";
import { callAction } from "../../tools/tap-actions.ts";

let hashTimeout = null;

export function clickOutside(event) {
const targets = event.composedPath();
const popupTarget = targets.find((target) => {
Expand All @@ -19,148 +17,131 @@ export function clickOutside(event) {
}

export function removeHash() {
// Check if removeHash is executed too soon after addHash
if (hashTimeout) return;

const newURL = window.location.href.split('#')[0];
history.replaceState(null, "", newURL);
history.replaceState(null, null, newURL);
window.dispatchEvent(new Event('location-changed'));
}

export function addHash(hash) {
// Clear any existing timeout to reset the timer
if (hashTimeout) {
clearTimeout(hashTimeout);
hashTimeout = null;
}

const newURL = hash.startsWith('#') ? window.location.href.split('#')[0] + hash : hash;
history.pushState(null, "", newURL);
export function addHash(hash) {
const newURL = hash.startsWith('#') ? window.location.href.split('#')[0] + hash : `#${hash}`;
history.pushState(null, null, newURL);
window.dispatchEvent(new Event('location-changed'));

// Set a timeout after addHash to prevent immediate removal
hashTimeout = setTimeout(() => {
hashTimeout = null;
}, 10);
}

export function hideContent(context, delay) {
if (context.editor) return;
return new Promise((resolve) => {
context.hideContentTimeout = setTimeout(() => {
if (context.sectionRow?.tagName.toLowerCase() === 'hui-card') {
context.sectionRow.hidden = true;
context.sectionRow.style.display = "none";

if (context.sectionRowContainer?.classList.contains('card')) {
context.sectionRowContainer.style.display = "none";
}

context.hideContentTimeout = setTimeout(() => {
let { sectionRow, sectionRowContainer } = context;

if (sectionRow?.tagName.toLowerCase() === 'hui-card') {
sectionRow.hidden = true;
sectionRow.style.display = "none";

if (sectionRowContainer?.classList.contains('card')) {
sectionRowContainer.style.display = "none";
}
resolve();
}, delay);
});
}
}, delay);
}

function displayContent(context) {
return new Promise((resolve) => {
context.popUp.style.transform = '';
let { sectionRow, sectionRowContainer, popUp } = context;

if (context.sectionRow?.tagName.toLowerCase() === 'hui-card') {
context.sectionRow.hidden = false;
context.sectionRow.style.display = "";
popUp.style.transform = '';

if (context.sectionRowContainer?.classList.contains('card')) {
context.sectionRowContainer.style.display = "";
}
if (sectionRow?.tagName.toLowerCase() === 'hui-card') {
sectionRow.hidden = false;
sectionRow.style.display = "";

if (sectionRowContainer?.classList.contains('card')) {
sectionRowContainer.style.display = "";
}
resolve();
});
}
}

function showBackdrop(context, show) {
const { showBackdrop, hideBackdrop } = getBackdrop(context);
return new Promise((resolve) => {
if (show) {
showBackdrop();
} else {
hideBackdrop();
}
resolve();
});
if (show) {
showBackdrop();
} else {
hideBackdrop();
}
}

function appendPopup(context, append) {
return new Promise((resolve) => {
if (append && context.popUp.parentNode !== context.verticalStack) {
context.verticalStack.appendChild(context.popUp);
} else if (!append) {
context.removeDomTimeout = setTimeout(() => {
if (context.popUp.parentNode === context.verticalStack) {
context.verticalStack.removeChild(context.popUp);
}
}, 340);
}
resolve();
});
let { popUp, verticalStack, removeDomTimeout } = context;

if (append && popUp.parentNode !== verticalStack) {
verticalStack.appendChild(context.popUp);
} else if (!append) {
removeDomTimeout = setTimeout(() => {
if (popUp.parentNode === verticalStack) {
verticalStack.removeChild(popUp);
}
}, 400);
}
}

function updatePopupClass(context, open) {
return new Promise((resolve) => {
let popUp = context.popUp;

requestAnimationFrame(() => {
requestAnimationFrame(() => {
requestAnimationFrame(() => {
if (open) {
context.popUp.classList.replace('is-popup-closed', 'is-popup-opened');
} else {
context.popUp.classList.replace('is-popup-opened', 'is-popup-closed');
}
resolve();
});
if (open) {
popUp.classList.replace('is-popup-closed', 'is-popup-opened');
} else {
popUp.classList.replace('is-popup-opened', 'is-popup-closed');
}
});
});
}

export async function openPopup(context) {
function updateListeners(context, add) {
let popUp = context.popUp;

if (add) {
if (!window.listenersAdded && (context.config.close_by_clicking_outside ?? true)) {
window.addEventListener('click', clickOutside);
window.listenersAdded = true;
}
popUp.addEventListener('touchstart', context.resetCloseTimeout, { passive: true });
if (context.config.close_on_click ?? false) {
popUp.addEventListener('mouseup', removeHash, { once: true });
popUp.addEventListener('touchend', removeHash, { once: true });
}
} else {
popUp.removeEventListener('touchstart', context.resetCloseTimeout);
}
}

export function openPopup(context) {
if (context.popUp.classList.contains('is-popup-opened')) return;

// Clear all existing timeouts before starting a new one
clearTimeout(context.hideContentTimeout);
clearTimeout(context.removeDomTimeout);
clearTimeout(context.closeTimeout);

await appendPopup(context, true);
showBackdrop(context, true);
appendPopup(context, true);
displayContent(context);
updatePopupClass(context, true);
updateListeners(context, true)

if (context.config.close_by_clicking_outside ?? true) {
window.addEventListener('click', clickOutside, { passive: true });
if (context.config.auto_close > 0) {
context.closeTimeout = setTimeout(() => removeHash(), context.config.auto_close);
}

document.body.style.overflow = 'hidden';

await displayContent(context);
await showBackdrop(context, true);
await updatePopupClass(context, true);

context.popUp.addEventListener('touchstart', context.resetCloseTimeout, { passive: true });

if (context.config.close_on_click ?? false) {
context.popUp.addEventListener('mouseup', removeHash, { passive: true });
context.popUp.addEventListener('touchend', removeHash, { passive: true });
}

if (context.config.auto_close > 0) {
context.closeTimeout = setTimeout(() => closePopup(context), context.config.auto_close);
}

if (context.config.open_action) {
callAction(context.popUp, context.config, 'open_action');
}
}

export async function closePopup(context) {
export function closePopup(context) {
if (!context.popUp.classList.contains('is-popup-opened')) return;

document.body.style.overflow = '';

// Clear existing timeouts
clearTimeout(context.hideContentTimeout);
clearTimeout(context.removeDomTimeout);
clearTimeout(context.closeTimeout);
Expand All @@ -169,17 +150,9 @@ export async function closePopup(context) {
showBackdrop(context, false);
hideContent(context, 300);
appendPopup(context, false);
updateListeners(context, false)

context.popUp.removeEventListener('touchstart', context.resetCloseTimeout);

if (context.config.close_by_clicking_outside ?? true) {
window.removeEventListener('click', clickOutside);
}

if (context.config.close_on_click ?? false) {
context.popUp.removeEventListener('mouseup', removeHash);
context.popUp.removeEventListener('touchend', removeHash);
}
document.body.style.overflow = '';

if (context.config.close_action) {
callAction(context, context.config, 'close_action');
Expand All @@ -197,14 +170,21 @@ export function onUrlChange(context) {
}

export function onEditorChange(context) {
const { hideBackdrop, showBackdrop } = getBackdrop(context);
const detectedEditor = context.verticalStack.host?.closest('hui-card-preview') || context.verticalStack.host?.closest('hui-card[preview][class]') || context.verticalStack.host?.getRootNode().host?.closest('hui-section[preview][class]');
const { hideBackdrop } = getBackdrop(context);
let verticalStack = context.verticalStack;

const detectedEditor =
verticalStack.host?.closest('hui-card-preview') ||
verticalStack.host?.closest('hui-card[preview][class]') ||
verticalStack.host?.getRootNode().host?.closest('hui-section[preview][class]');

if (context.editor || detectedEditor !== null) {
hideBackdrop();
window.clearTimeout(context.removeDomTimeout);
if (context.popUp.parentNode !== context.verticalStack) {
context.verticalStack.appendChild(context.popUp);

const popUp = context.popUp;
if (popUp.parentNode !== verticalStack) {
verticalStack.appendChild(popUp);
}
}
}
Loading

0 comments on commit 36b09bc

Please sign in to comment.