From 0c4b0b2f758e945b55e5ee74a59968e0cfa7ec56 Mon Sep 17 00:00:00 2001 From: Serhii Kulykov Date: Fri, 2 Aug 2024 10:28:44 +0300 Subject: [PATCH] feat: add Lit renderer directive for popover (#7612) --- packages/popover/lit.d.ts | 1 + packages/popover/lit.js | 1 + packages/popover/package.json | 3 + .../popover/src/lit/renderer-directives.d.ts | 58 ++++++++++++++++ .../popover/src/lit/renderer-directives.js | 60 ++++++++++++++++ .../test/lit-renderer-directives.test.js | 69 +++++++++++++++++++ packages/popover/test/typings/lit.types.ts | 9 +++ 7 files changed, 201 insertions(+) create mode 100644 packages/popover/lit.d.ts create mode 100644 packages/popover/lit.js create mode 100644 packages/popover/src/lit/renderer-directives.d.ts create mode 100644 packages/popover/src/lit/renderer-directives.js create mode 100644 packages/popover/test/lit-renderer-directives.test.js create mode 100644 packages/popover/test/typings/lit.types.ts diff --git a/packages/popover/lit.d.ts b/packages/popover/lit.d.ts new file mode 100644 index 0000000000..06ba82bf8a --- /dev/null +++ b/packages/popover/lit.d.ts @@ -0,0 +1 @@ +export * from './src/lit/renderer-directives.js'; diff --git a/packages/popover/lit.js b/packages/popover/lit.js new file mode 100644 index 0000000000..06ba82bf8a --- /dev/null +++ b/packages/popover/lit.js @@ -0,0 +1 @@ +export * from './src/lit/renderer-directives.js'; diff --git a/packages/popover/package.json b/packages/popover/package.json index a0cf343971..67fb9b4034 100644 --- a/packages/popover/package.json +++ b/packages/popover/package.json @@ -20,6 +20,8 @@ "module": "vaadin-popover.js", "type": "module", "files": [ + "lit.d.ts", + "lit.js", "src", "theme", "vaadin-*.d.ts", @@ -37,6 +39,7 @@ "@open-wc/dedupe-mixin": "^1.3.0", "@vaadin/a11y-base": "24.5.0-alpha6", "@vaadin/component-base": "24.5.0-alpha6", + "@vaadin/lit-renderer": "24.5.0-alpha6", "@vaadin/overlay": "24.5.0-alpha6", "@vaadin/vaadin-lumo-styles": "24.5.0-alpha6", "@vaadin/vaadin-material-styles": "24.5.0-alpha6", diff --git a/packages/popover/src/lit/renderer-directives.d.ts b/packages/popover/src/lit/renderer-directives.d.ts new file mode 100644 index 0000000000..b48f9a5d2a --- /dev/null +++ b/packages/popover/src/lit/renderer-directives.d.ts @@ -0,0 +1,58 @@ +/** + * @license + * Copyright (c) 2024 Vaadin Ltd. + * This program is available under Apache License Version 2.0, available at https://vaadin.com/license/ + */ +import type { DirectiveResult } from 'lit/directive.js'; +import { LitRendererDirective, type LitRendererResult } from '@vaadin/lit-renderer'; +import type { Popover } from '../vaadin-popover.js'; + +export type PopoverLitRenderer = (popover: Popover) => LitRendererResult; + +export class PopoverRendererDirective extends LitRendererDirective { + /** + * Adds the renderer callback to the popover. + */ + addRenderer(): void; + + /** + * Runs the renderer callback on the popover. + */ + runRenderer(): void; + + /** + * Removes the renderer callback from the popover. + */ + removeRenderer(): void; +} + +/** + * A Lit directive for populating the content of the `` element. + * + * The directive accepts a renderer callback returning a Lit template and assigns it to the popover + * via the `renderer` property. The renderer is called once to populate the content when assigned + * and whenever a single dependency or an array of dependencies changes. + * It is not guaranteed that the renderer will be called immediately (synchronously) in both cases. + * + * Dependencies can be a single value or an array of values. + * Values are checked against previous values with strict equality (`===`), + * so the check won't detect nested property changes inside objects or arrays. + * When dependencies are provided as an array, each item is checked against the previous value + * at the same index with strict equality. Nested arrays are also checked only by strict + * equality. + * + * Example of usage: + * ```js + * ` html`...`)} + * >` + * ``` + * + * @param renderer the renderer callback that returns a Lit template. + * @param dependencies a single dependency or an array of dependencies + * which trigger a re-render when changed. + */ +export declare function popoverRenderer( + renderer: PopoverLitRenderer, + dependencies?: unknown, +): DirectiveResult; diff --git a/packages/popover/src/lit/renderer-directives.js b/packages/popover/src/lit/renderer-directives.js new file mode 100644 index 0000000000..c47b72611c --- /dev/null +++ b/packages/popover/src/lit/renderer-directives.js @@ -0,0 +1,60 @@ +/** + * @license + * Copyright (c) 2024 Vaadin Ltd. + * This program is available under Apache License Version 2.0, available at https://vaadin.com/license/ + */ +import { directive } from 'lit/directive.js'; +import { LitRendererDirective } from '@vaadin/lit-renderer'; + +export class PopoverRendererDirective extends LitRendererDirective { + /** + * Adds the renderer callback to the popover. + */ + addRenderer() { + this.element.renderer = (root, popover) => { + this.renderRenderer(root, popover); + }; + } + + /** + * Runs the renderer callback on the popover. + */ + runRenderer() { + this.element.requestContentUpdate(); + } + + /** + * Removes the renderer callback from the popover. + */ + removeRenderer() { + this.element.renderer = null; + } +} + +/** + * A Lit directive for populating the content of the `` element. + * + * The directive accepts a renderer callback returning a Lit template and assigns it to the popover + * via the `renderer` property. The renderer is called once to populate the content when assigned + * and whenever a single dependency or an array of dependencies changes. + * It is not guaranteed that the renderer will be called immediately (synchronously) in both cases. + * + * Dependencies can be a single value or an array of values. + * Values are checked against previous values with strict equality (`===`), + * so the check won't detect nested property changes inside objects or arrays. + * When dependencies are provided as an array, each item is checked against the previous value + * at the same index with strict equality. Nested arrays are also checked only by strict + * equality. + * + * Example of usage: + * ```js + * ` html`...`)} + * >` + * ``` + * + * @param renderer the renderer callback that returns a Lit template. + * @param dependencies a single dependency or an array of dependencies + * which trigger a re-render when changed. + */ +export const popoverRenderer = directive(PopoverRendererDirective); diff --git a/packages/popover/test/lit-renderer-directives.test.js b/packages/popover/test/lit-renderer-directives.test.js new file mode 100644 index 0000000000..f5ac1d7be3 --- /dev/null +++ b/packages/popover/test/lit-renderer-directives.test.js @@ -0,0 +1,69 @@ +import { expect } from '@esm-bundle/chai'; +import { fixtureSync, nextFrame, nextUpdate } from '@vaadin/testing-helpers'; +import sinon from 'sinon'; +import './not-animated-styles.js'; +import '../vaadin-popover.js'; +import { html, nothing, render } from 'lit'; +import { popoverRenderer } from '../lit.js'; + +async function renderOpenedPopover(container, { content }) { + render( + html` + html`${content}`, content) : nothing}> + `, + container, + ); + const popover = container.querySelector('vaadin-popover'); + await nextUpdate(popover); + return popover; +} + +describe('lit renderer directives', () => { + let container, popover, overlay; + + beforeEach(() => { + container = fixtureSync('
'); + }); + + describe('popoverRenderer', () => { + describe('basic', () => { + beforeEach(async () => { + popover = await renderOpenedPopover(container, { content: 'Content' }); + overlay = popover._overlayElement; + }); + + it('should set `renderer` property when the directive is attached', () => { + expect(popover.renderer).to.exist; + }); + + it('should unset `renderer` property when the directive is detached', async () => { + await renderOpenedPopover(container, {}); + expect(popover.renderer).not.to.exist; + }); + + it('should render the content with the renderer', () => { + expect(overlay.textContent).to.equal('Content'); + }); + + it('should re-render the content when a renderer dependency changes', async () => { + await renderOpenedPopover(container, { content: 'New Content' }); + expect(overlay.textContent).to.equal('New Content'); + }); + }); + + describe('arguments', () => { + let rendererSpy; + + beforeEach(async () => { + rendererSpy = sinon.spy(); + render(html``, container); + await nextFrame(); + popover = container.querySelector('vaadin-popover'); + }); + + it('should pass the popover instance to the renderer', () => { + expect(rendererSpy.firstCall.args[0]).to.equal(popover); + }); + }); + }); +}); diff --git a/packages/popover/test/typings/lit.types.ts b/packages/popover/test/typings/lit.types.ts new file mode 100644 index 0000000000..fe3f077819 --- /dev/null +++ b/packages/popover/test/typings/lit.types.ts @@ -0,0 +1,9 @@ +import type { DirectiveResult } from 'lit/directive.js'; +import type { PopoverLitRenderer, PopoverRendererDirective } from '../../lit.js'; +import { popoverRenderer } from '../../lit.js'; + +const assertType = (actual: TExpected) => actual; + +assertType<(renderer: PopoverLitRenderer, dependencies?: unknown) => DirectiveResult>( + popoverRenderer, +);