Skip to content

Commit

Permalink
feat: implement basic popover logic, move mixins from tooltip (#7409)
Browse files Browse the repository at this point in the history
  • Loading branch information
web-padawan authored May 14, 2024
1 parent d247e95 commit fe73eed
Show file tree
Hide file tree
Showing 19 changed files with 920 additions and 176 deletions.
47 changes: 47 additions & 0 deletions dev/popover.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Popover</title>
<script type="module" src="./common.js"></script>

<script type="module">
import '@vaadin/button';
import '@vaadin/horizontal-layout';
import '@vaadin/popover';
import '@vaadin/text-field';
</script>
</head>

<body>
<vaadin-button id="button">Discount</vaadin-button>

<vaadin-popover for="button" position="bottom-start"></vaadin-popover>

<script type="module">
const popover = document.querySelector('vaadin-popover');

popover.renderer = (root) => {
if (root.firstChild) {
return;
}

const layout = document.createElement('vaadin-horizontal-layout');
layout.setAttribute('theme', 'spacing-s');
layout.style.alignItems = 'baseline';

const field = document.createElement('vaadin-text-field');
field.label = 'Discount code';

const button = document.createElement('vaadin-button');
button.textContent = 'Apply';

layout.append(field, button);

root.append(layout);
};
</script>
</body>
</html>
4 changes: 3 additions & 1 deletion packages/popover/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,14 +36,16 @@
"dependencies": {
"@open-wc/dedupe-mixin": "^1.3.0",
"@vaadin/component-base": "24.5.0-alpha0",
"@vaadin/overlay": "24.5.0-alpha0",
"@vaadin/vaadin-lumo-styles": "24.5.0-alpha0",
"@vaadin/vaadin-material-styles": "24.5.0-alpha0",
"@vaadin/vaadin-themable-mixin": "24.5.0-alpha0",
"lit": "^3.0.0"
},
"devDependencies": {
"@esm-bundle/chai": "^4.3.4",
"@vaadin/testing-helpers": "^0.6.0"
"@vaadin/testing-helpers": "^0.6.0",
"sinon": "^13.0.2"
},
"web-types": [
"web-types.json",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,14 @@ import { OverlayMixin } from '@vaadin/overlay/src/vaadin-overlay-mixin.js';
import { PositionMixin } from '@vaadin/overlay/src/vaadin-overlay-position-mixin.js';

/**
* A mixin providing common tooltip overlay functionality.
* A mixin providing common popover overlay functionality.
*
* @polymerMixin
* @mixes PositionMixin
* @mixes OverlayMixin
*/
export const TooltipOverlayMixin = (superClass) =>
class TooltipOverlayMixinClass extends PositionMixin(OverlayMixin(superClass)) {
export const PopoverOverlayMixin = (superClass) =>
class PopoverOverlayMixinClass extends PositionMixin(OverlayMixin(superClass)) {
static get properties() {
return {
position: {
Expand All @@ -30,13 +30,13 @@ export const TooltipOverlayMixin = (superClass) =>
* @return {string}
*/
get _tagNamePrefix() {
return 'vaadin-tooltip';
return 'vaadin-popover';
}

requestContentUpdate() {
super.requestContentUpdate();

// Copy custom properties from the tooltip
// Copy custom properties from the owner
if (this.positionTarget && this.owner) {
const style = getComputedStyle(this.owner);
['top', 'bottom', 'start', 'end'].forEach((prop) => {
Expand All @@ -59,7 +59,7 @@ export const TooltipOverlayMixin = (superClass) =>
return;
}

// Center the tooltip overlay horizontally
// Center the overlay horizontally
if (this.position === 'bottom' || this.position === 'top') {
const targetRect = this.positionTarget.getBoundingClientRect();
const overlayRect = this.$.overlay.getBoundingClientRect();
Expand All @@ -81,7 +81,7 @@ export const TooltipOverlayMixin = (superClass) =>
}
}

// Center the tooltip overlay vertically
// Center the overlay vertically
if (this.position === 'start' || this.position === 'end') {
const targetRect = this.positionTarget.getBoundingClientRect();
const overlayRect = this.$.overlay.getBoundingClientRect();
Expand Down
67 changes: 67 additions & 0 deletions packages/popover/src/vaadin-popover-overlay.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
/**
* @license
* Copyright (c) 2024 Vaadin Ltd.
* This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
*/
import { css, html, LitElement } from 'lit';
import { defineCustomElement } from '@vaadin/component-base/src/define.js';
import { DirMixin } from '@vaadin/component-base/src/dir-mixin.js';
import { PolylitMixin } from '@vaadin/component-base/src/polylit-mixin.js';
import { overlayStyles } from '@vaadin/overlay/src/vaadin-overlay-styles.js';
import { ThemableMixin } from '@vaadin/vaadin-themable-mixin/vaadin-themable-mixin.js';
import { PopoverOverlayMixin } from './vaadin-popover-overlay-mixin.js';

/**
* An element used internally by `<vaadin-popover>`. Not intended to be used separately.
*
* @customElement
* @extends HTMLElement
* @mixes DirMixin
* @mixes PopoverOverlayMixin
* @mixes ThemableMixin
* @private
*/
class PopoverOverlay extends PopoverOverlayMixin(DirMixin(ThemableMixin(PolylitMixin(LitElement)))) {
static get is() {
return 'vaadin-popover-overlay';
}

static get styles() {
return [
overlayStyles,
css`
:host([position^='top'][top-aligned]) [part='overlay'],
:host([position^='bottom'][top-aligned]) [part='overlay'] {
margin-top: var(--vaadin-popover-offset-top, 0);
}
:host([position^='top'][bottom-aligned]) [part='overlay'],
:host([position^='bottom'][bottom-aligned]) [part='overlay'] {
margin-bottom: var(--vaadin-popover-offset-bottom, 0);
}
:host([position^='start'][start-aligned]) [part='overlay'],
:host([position^='end'][start-aligned]) [part='overlay'] {
margin-inline-start: var(--vaadin-popover-offset-start, 0);
}
:host([position^='start'][end-aligned]) [part='overlay'],
:host([position^='end'][end-aligned]) [part='overlay'] {
margin-inline-end: var(--vaadin-popover-offset-end, 0);
}
`,
];
}

/** @protected */
render() {
return html`
<div id="backdrop" part="backdrop" hidden ?hidden="${!this.withBackdrop}"></div>
<div part="overlay" id="overlay">
<div part="content" id="content"><slot></slot></div>
</div>
`;
}
}

defineCustomElement(PopoverOverlay);
37 changes: 37 additions & 0 deletions packages/popover/src/vaadin-popover-position-mixin.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/**
* @license
* Copyright (c) 2024 Vaadin Ltd.
* This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
*/
import type { Constructor } from '@open-wc/dedupe-mixin';

export type PopoverPosition =
| 'bottom-end'
| 'bottom-start'
| 'bottom'
| 'end-bottom'
| 'end-top'
| 'end'
| 'start-bottom'
| 'start-top'
| 'start'
| 'top-end'
| 'top-start'
| 'top';

/**
* A mixin providing popover position functionality.
*/
export declare function PopoverPositionMixin<T extends Constructor<HTMLElement>>(
base: T,
): Constructor<PopoverPositionMixinClass> & T;

export declare class PopoverPositionMixinClass {
/**
* Position of the overlay with respect to the target.
* Supported values: `top-start`, `top`, `top-end`,
* `bottom-start`, `bottom`, `bottom-end`, `start-top`,
* `start`, `start-bottom`, `end-top`, `end`, `end-bottom`.
*/
position: PopoverPosition;
}
67 changes: 67 additions & 0 deletions packages/popover/src/vaadin-popover-position-mixin.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
/**
* @license
* Copyright (c) 2024 Vaadin Ltd.
* This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
*/

/**
* A mixin providing popover position functionality.
*
* @polymerMixin
*/
export const PopoverPositionMixin = (superClass) =>
class PopoverPositionMixinClass extends superClass {
static get properties() {
return {
/**
* Position of the overlay with respect to the target.
* Supported values: `top-start`, `top`, `top-end`,
* `bottom-start`, `bottom`, `bottom-end`, `start-top`,
* `start`, `start-bottom`, `end-top`, `end`, `end-bottom`.
*/
position: {
type: String,
},

/**
* Default value used when `position` property is not set.
* @protected
*/
_position: {
type: String,
value: 'bottom',
},

/** @private */
__effectivePosition: {
type: String,
computed: '__computePosition(position, _position)',
},
};
}

/** @protected */
__computeHorizontalAlign(position) {
return ['top-end', 'bottom-end', 'start-top', 'start', 'start-bottom'].includes(position) ? 'end' : 'start';
}

/** @protected */
__computeNoHorizontalOverlap(position) {
return ['start-top', 'start', 'start-bottom', 'end-top', 'end', 'end-bottom'].includes(position);
}

/** @protected */
__computeNoVerticalOverlap(position) {
return ['top-start', 'top-end', 'top', 'bottom-start', 'bottom', 'bottom-end'].includes(position);
}

/** @protected */
__computeVerticalAlign(position) {
return ['top-start', 'top-end', 'top', 'start-bottom', 'end-bottom'].includes(position) ? 'bottom' : 'top';
}

/** @private */
__computePosition(position, defaultPosition) {
return position || defaultPosition;
}
};
35 changes: 35 additions & 0 deletions packages/popover/src/vaadin-popover-target-mixin.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/**
* @license
* Copyright (c) 2024 Vaadin Ltd.
* This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
*/
import type { Constructor } from '@open-wc/dedupe-mixin';

/**
* A mixin providing popover target functionality.
*/
export declare function PopoverTargetMixin<T extends Constructor<HTMLElement>>(
base: T,
): Constructor<PopoverTargetMixinClass> & T;

export declare class PopoverTargetMixinClass {
/**
* The id of the element to be used as `target` value.
* The element should be in the DOM by the time when
* the attribute is set, otherwise a warning is shown.
*/
for: string | undefined;

/**
* Reference to the DOM element used both to trigger the overlay
* by user interaction and to visually position it on the screen.
*
* Defaults to an element referenced with `for` attribute, in
* which case it must be located in the same shadow scope.
*/
target: HTMLElement | undefined;

protected _addTargetListeners(target: HTMLElement): void;

protected _removeTargetListeners(target: HTMLElement): void;
}
Loading

0 comments on commit fe73eed

Please sign in to comment.