Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 25 additions & 2 deletions packages/app-layout/src/vaadin-app-layout-mixin.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,24 @@ import { AriaModalController } from '@vaadin/a11y-base/src/aria-modal-controller
import { FocusTrapController } from '@vaadin/a11y-base/src/focus-trap-controller.js';
import { isKeyboardActive } from '@vaadin/a11y-base/src/focus-utils.js';
import { animationFrame } from '@vaadin/component-base/src/async.js';
import { CSSPropertyObserver } from '@vaadin/component-base/src/css-property-observer.js';
import { Debouncer } from '@vaadin/component-base/src/debounce.js';
import { I18nMixin } from '@vaadin/component-base/src/i18n-mixin.js';

CSS.registerProperty({
name: '--vaadin-app-layout-touch-optimized',
syntax: 'true | false',
inherits: true,
initialValue: 'false',
});

CSS.registerProperty({
name: '--vaadin-app-layout-drawer-overlay',
syntax: 'true | false',
inherits: true,
initialValue: 'false',
});

/**
* @typedef {import('./vaadin-app-layout.js').AppLayoutI18n} AppLayoutI18n
*/
Expand Down Expand Up @@ -180,6 +195,16 @@ export const AppLayoutMixin = (superclass) =>
this.addController(this.__focusTrapController);
this.__setAriaExpanded();

this.__cssPropertyObserver = new CSSPropertyObserver(this.$.cssPropertyObserver, (propertyName) => {
if (propertyName === '--vaadin-app-layout-touch-optimized') {
this._updateTouchOptimizedMode();
}
if (propertyName === '--vaadin-app-layout-drawer-overlay') {
this._updateOverlayMode();
}
});
this.__cssPropertyObserver.observe('--vaadin-app-layout-touch-optimized', '--vaadin-app-layout-drawer-overlay');

this.$.drawer.addEventListener('transitionstart', () => {
this.__isDrawerAnimating = true;
});
Expand Down Expand Up @@ -328,8 +353,6 @@ export const AppLayoutMixin = (superclass) =>
/** @private */
_resize() {
this._blockAnimationUntilAfterNextRender();
this._updateTouchOptimizedMode();
this._updateOverlayMode();
}

/** @protected */
Expand Down
1 change: 1 addition & 0 deletions packages/app-layout/src/vaadin-app-layout.js
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,7 @@ class AppLayout extends AppLayoutMixin(ElementMixin(ThemableMixin(PolylitMixin(L
<div hidden>
<slot id="touchSlot" name="navbar touch-optimized" @slotchange="${this.__onNavbarSlotChange}"></slot>
</div>
<div id="cssPropertyObserver"></div>
`;
}
}
Expand Down
10 changes: 0 additions & 10 deletions packages/app-layout/test/app-layout.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -131,16 +131,6 @@ describe('vaadin-app-layout', () => {
expect(layout.$.navbarBottom.hasAttribute('hidden')).to.be.true;
});

it('should remove hidden attribute on non-empty navbar-bottom on resize', () => {
const header = document.createElement('h1');
header.textContent = 'Header';
header.setAttribute('slot', 'navbar touch-optimized');
layout.appendChild(header);
expect(layout.$.navbarBottom.hasAttribute('hidden')).to.be.true;
window.dispatchEvent(new Event('resize'));
expect(layout.$.navbarBottom.hasAttribute('hidden')).to.be.false;
});

it('should update content offset when navbar height changes', async () => {
// Add content to navbar and measure original offset
const navbarContent = document.createElement('div');
Expand Down
60 changes: 60 additions & 0 deletions packages/component-base/src/css-property-observer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
/**
* @license
* Copyright (c) 2000 - 2025 Vaadin Ltd.
* This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
*/

/**
* WARNING: For internal use only. Do not use this class in custom components.
*
* @private
*/
export class CSSPropertyObserver extends EventTarget {
element;
callback;
properties = new Set();

constructor(element, callback) {
super();
this.element = element;
this._handleTransitionEvent = this._handleTransitionEvent.bind(this);

if (callback) {
this.addEventListener('property-changed', (event) => callback(event.detail.propertyName));
}
}

observe(...properties) {
this.connect();

const newProperties = properties.filter((property) => !this.properties.has(property));
if (newProperties.length > 0) {
newProperties.forEach((property) => this.properties.add(property));
this._updateStyles();
}
}

connect() {
this.element.addEventListener('transitionend', this._handleTransitionEvent);
}

disconnect() {
this.properties.clear();
this.element.removeEventListener('transitionend', this._handleTransitionEvent);
}

/** @protected */
_handleTransitionEvent(event) {
const { propertyName } = event;
this.dispatchEvent(new CustomEvent('property-changed', { detail: { propertyName } }));
}

/** @protected */
_updateStyles() {
this.element.style.display = 'contents';
this.element.style.transitionDuration = '1ms';
this.element.style.transitionBehavior = 'allow-discrete';
this.element.style.transitionProperty = `${[...this.properties].join(', ')}`;
this.element.style.transitionTimingFunction = 'step-end';
}
}
56 changes: 56 additions & 0 deletions packages/component-base/test/css-property-observer.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { expect } from '@vaadin/chai-plugins';
import { fixtureSync, nextFrame } from '@vaadin/testing-helpers';
import sinon from 'sinon';
import { CSSPropertyObserver } from '../src/css-property-observer.js';

CSS.registerProperty({
name: '--test-prop-0',
syntax: '<number>',
inherits: true,
initialValue: '0',
});

CSS.registerProperty({
name: '--test-prop-1',
syntax: '<number>',
inherits: true,
initialValue: '0',
});

describe('CSSPropertyObserver', () => {
let observer, element, callback;

beforeEach(async () => {
element = fixtureSync('<div></div>');
callback = sinon.spy();
observer = new CSSPropertyObserver(element, callback);
await nextFrame();
});

it('should observe CSS property changes', async () => {
observer.observe('--test-prop-0', '--test-prop-1');
await nextFrame();
expect(callback).to.be.not.called;

element.style.setProperty('--test-prop-0', '1');
await nextFrame();
expect(callback).to.be.calledOnceWith('--test-prop-0');

callback.resetHistory();

element.style.setProperty('--test-prop-1', '1');
await nextFrame();
expect(callback).to.be.calledOnceWith('--test-prop-1');
});

it('should stop observing when disconnect is called', async () => {
observer.observe('--test-prop-0');
await nextFrame();
observer.disconnect();
await nextFrame();

element.style.setProperty('--test-prop-0', '1');
await nextFrame();
expect(callback).to.be.not.called;
});
});
90 changes: 0 additions & 90 deletions packages/vaadin-themable-mixin/src/css-property-observer.js

This file was deleted.

4 changes: 2 additions & 2 deletions packages/vaadin-themable-mixin/src/lumo-injector.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@
* Copyright (c) 2021 - 2025 Vaadin Ltd.
* This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
*/
import { CSSPropertyObserver } from './css-property-observer.js';
import { injectLumoStyleSheet, removeLumoStyleSheet } from './css-utils.js';
import { parseStyleSheets } from './lumo-modules.js';
import { RootCSSPropertyObserver } from './root-css-property-observer.js';

export function getLumoInjectorPropName(lumoInjector) {
return `--_lumo-${lumoInjector.is}-inject`;
Expand Down Expand Up @@ -87,7 +87,7 @@ export class LumoInjector {
constructor(root = document) {
this.#root = root;
this.handlePropertyChange = this.handlePropertyChange.bind(this);
this.#cssPropertyObserver = CSSPropertyObserver.for(root);
this.#cssPropertyObserver = RootCSSPropertyObserver.for(root);
this.#cssPropertyObserver.addEventListener('property-changed', this.handlePropertyChange);
}

Expand Down
59 changes: 59 additions & 0 deletions packages/vaadin-themable-mixin/src/root-css-property-observer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
/**
* @license
* Copyright (c) 2021 - 2025 Vaadin Ltd.
* This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
*/
import { CSSPropertyObserver } from '@vaadin/component-base/src/css-property-observer.js';

/**
* An extension of CSSPropertyObserver that takes a Document or ShadowRoot and
* listens for changes to CSS custom properties on its `:root` or `:host` element
* via the `::before` pseudo-element.
*
* WARNING: For internal use only. Do not use this class in custom components.
*/
export class RootCSSPropertyObserver extends CSSPropertyObserver {
#root;
#styleSheet = new CSSStyleSheet();

/**
* Gets or creates the CSSPropertyObserver for the given root.
* @param {DocumentOrShadowRoot} root
* @returns {RootCSSPropertyObserver}
*/
static for(root) {
root.__cssPropertyObserver ||= new RootCSSPropertyObserver(root);
return root.__cssPropertyObserver;
}

constructor(root) {
super(root.host ?? root.documentElement);
this.#root = root;
}

connect() {
super.connect();
this.#root.adoptedStyleSheets = this.#root.adoptedStyleSheets.filter((s) => s !== this.#styleSheet);
this.#root.adoptedStyleSheets.unshift(this.#styleSheet);
}

disconnect() {
super.disconnect();
this.#root.adoptedStyleSheets = this.#root.adoptedStyleSheets.filter((s) => s !== this.#styleSheet);
}

/** @override */
_updateStyles() {
this.#styleSheet.replaceSync(`
:root::before, :host::before {
content: '' !important;
position: absolute !important;
top: -9999px !important;
left: -9999px !important;
visibility: hidden !important;
transition: 1ms allow-discrete step-end !important;
transition-property: ${[...this.properties].join(', ')} !important;
}
`);
}
}
4 changes: 2 additions & 2 deletions packages/vaadin-themable-mixin/src/theme-detector.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
* Copyright (c) 2000 - 2025 Vaadin Ltd.
* This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
*/
import { CSSPropertyObserver } from './css-property-observer.js';
import { RootCSSPropertyObserver } from './root-css-property-observer.js';

// Register CSS custom properties for observing theme changes
CSS.registerProperty({
Expand Down Expand Up @@ -43,7 +43,7 @@ export class ThemeDetector extends EventTarget {
this.#root = root;
this.#detectTheme();

this.#observer = CSSPropertyObserver.for(this.#root);
this.#observer = RootCSSPropertyObserver.for(this.#root);
this.#observer.observe('--vaadin-aura-theme');
this.#observer.observe('--vaadin-lumo-theme');
this.#observer.addEventListener('property-changed', this.#boundHandleThemeChange);
Expand Down
Loading