Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(vwc-popup): change anchorEl to anchorId #1214

Merged
merged 24 commits into from
Feb 7, 2022
Merged
Show file tree
Hide file tree
Changes from 10 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
77 changes: 55 additions & 22 deletions components/popup/src/vwc-popup-base.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,29 @@
import {
PropertyValues, html, LitElement, property, query, TemplateResult
html, LitElement, property, query, TemplateResult, PropertyValues
} from 'lit-element';
import { ClassInfo, classMap } from 'lit-html/directives/class-map.js';
import { nothing } from 'lit-html';
import { computePosition, offset, shift, flip, arrow } from '@floating-ui/dom';
import type { Placement, Strategy, Padding } from '@floating-ui/core';

export class VWCPopupBase extends LitElement {
private get PADDING(): Padding { return 0; };
private get DISTANCE(): number { return 12; };
private get DELAY(): number { return 300; };

private onResizeWindow = this.updatePosition.bind(this);
@query('.popup-wrapper') protected popupEl!: HTMLElement;
@query('.popup-arrow') protected arrowEl!: HTMLElement;
protected padding: Padding = 0;
protected distance = 12;
private anchorEl: Element | null | undefined;
@query('.popup-wrapper')
private popupEl!: HTMLElement;
@query('.popup-arrow')
private arrowEl!: HTMLElement;

private initialDisplayState = false;
private get middleware(): Array<any> {
return (
this.arrow ? [flip(), shift({ padding: this.PADDING }), arrow({ element: this.arrowEl, padding: this.PADDING }), offset(this.DISTANCE)]
: [flip(), shift({ padding: this.PADDING })]);
};

/**
* @prop open - indicates whether the popup is open
Expand All @@ -22,12 +34,12 @@ export class VWCPopupBase extends LitElement {
open = false;

/**
* @prop anchor - the anchor of the popup
* accepts Element
* @prop anchor - ID reference to element in the popup’s owner document.
* accepts string
* @public
* */
@property({ type: Object })
anchor: HTMLElement | null = null;
@property({ type: String })
anchor = '';

/**
* @prop dismissible - adds close button to the popup
Expand Down Expand Up @@ -80,6 +92,17 @@ export class VWCPopupBase extends LitElement {
@property({ type: Boolean, reflect: true })
alternate?: boolean;

/**
* Gets the anchor element by id
*/
private getAnchorById(): HTMLElement | null {
const rootNode = this.getRootNode();
if (rootNode instanceof ShadowRoot) {
return rootNode.getElementById(this.anchor);
}
return document.getElementById(this.anchor);
};

/**
* Opens the popup
* @public
Expand All @@ -98,23 +121,36 @@ export class VWCPopupBase extends LitElement {

override connectedCallback(): void {
super.connectedCallback();
document.addEventListener('scroll', this.updatePosition);
// Save the initial display state so that it can be restored when the positioning is complete
this.initialDisplayState = this.open;
this.hide();
this.anchorEl = this.getAnchorById();
this.anchorEl?.addEventListener('change', this.updatePosition);
rinaok marked this conversation as resolved.
Show resolved Hide resolved
window.addEventListener('scroll', this.updatePosition);
yinonov marked this conversation as resolved.
Show resolved Hide resolved
window.addEventListener('resize', this.onResizeWindow);
}

override disconnectedCallback(): void {
super.disconnectedCallback();
document.removeEventListener('scroll', this.updatePosition);
this.anchorEl?.removeEventListener('change', this.updatePosition);
window.removeEventListener('scroll', this.updatePosition);
window.removeEventListener('resize', this.onResizeWindow);
}

protected override firstUpdated(changedProperties: PropertyValues): void {
super.firstUpdated(changedProperties);
this.updatePosition();
protected override firstUpdated(_changedProperties: PropertyValues): void {
super.firstUpdated(_changedProperties);
// For proper positioning, show the popup after a delay when first updated
setTimeout(() => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now I see why you have this delay.
It should be documented in the tests:
it("should delay first position by 300ms")
Or something like that. How did you get to 300ms BTW?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@YonatanKra Having to set timeout, I chose 300 milliseconds because I saw FAST had a 300 millisecond delay for their tooltip to appear.
Would you suggest a time of less than 300 milliseconds?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As I said, we can go with 300ms, it just needs to be documented (in tests) as suggested. Why not 100ms (which is the maximum time for user interaction response)? Would it work then?
I guess 300ms is a heuristic because other factors might delay the page layout calculation (like long running JS or longer running calculations before the component loaded).
So 100ms will not be enough.
By the way, we'd might want to add our own "ready" promise on this component and then use it in the tests. This might be a handy API.
I just don't feel good enough with arbitrary numbers going around.
In either case (arbitrary or not), we need to document this in the tests as this is part of the API exposed to the user (expect the component to finish rendering in 300ms).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thinking about it, I think we can "hide" this setTimeout behind an updateComplete message. Something like that in the firstUpdated function:

this.updateComplete = new Promise(res => setTimeout(() => {
      this.open = this.initialDisplayState;
			this.updatePosition();
			res();
}, this.DELAY));

I think that's the right syntax. Then the updateComplete will reflect the 300ms wait.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This way, the 300ms becomes an implementation details and not an unexpected side effect.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@YonatanKra updateComplete is read-only

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correct me if i'm wrong @rinaok but the denounce is required for document to be ready. The popup component has no way of knowing that

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@YonatanKra updateComplete is read-only

I did something with: _getUpdateComplete. Might help but seems an overkill.
If 300ms is a must then we need to document it in the tests (e.g. should run positionUpdate on load after 300ms delay).

Copy link
Contributor

@yinonov yinonov Jan 30, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@rinaok if the intersection observer solves it, we may wanna create a single intersectionObserver instance and reuse it.
notice it is being used as a service here
https://github.com/microsoft/fast/blob/master/packages/web-components/fast-foundation/src/utilities/intersection-service.ts

we can actually might as well use that service 🤔

this.open = this.initialDisplayState;
this.updatePosition();
}, this.DELAY);
}

protected override updated(changes: Map<string, boolean>): void {
super.updated(changes);
if (changes.has('anchor')) {
this.anchorEl = this.getAnchorById();
}
this.updatePosition();
}

Expand All @@ -123,19 +159,16 @@ export class VWCPopupBase extends LitElement {
* @public
*/
async updatePosition() {
if (!this.open || !this.anchor) {
if (!this.open || !this.anchorEl) {
return;
}

const middleware = [flip(), shift({ padding: this.padding })];
this.arrow ? middleware.push(arrow({ element: this.arrowEl, padding: this.padding }), offset(this.distance)) : nothing;
const positionData = await computePosition(this.anchor, this.popupEl, {
const positionData = await computePosition(this.anchorEl, this.popupEl, {
placement: this.corner,
strategy: this.strategy,
middleware: middleware
middleware: this.middleware
});
this.assignPopupPosition(positionData);
this.arrow ? this.assignArrowPosition(positionData) : nothing;
if (this.arrow) { this.assignArrowPosition(positionData); }
}

private assignPopupPosition(data: any): void {
Expand Down Expand Up @@ -187,7 +220,7 @@ export class VWCPopupBase extends LitElement {

return html`
<div class="popup-wrapper">
<vwc-elevation dp="2" >
<vwc-elevation dp="2">
<div class="popup ${classMap(this.getRenderClasses())}" aria-hidden=${aria} part=${part}>
<div class="popup-content">
<slot></slot>
Expand Down
8 changes: 2 additions & 6 deletions components/popup/stories/popup.stories.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,18 +34,14 @@ const Template = args => html`
}
</style>
<div class="popup-wrapper">
<vwc-button id="button" layout="outlined" outlined aria-haspopup="true" aria-describedby="popup" @click=${onClick}>Click to open popup</vwc-button>
<vwc-popup id="popup" ...=${spread(args)}>
<vwc-button id="buttonAnchor" layout="outlined" outlined aria-haspopup="true" aria-describedby="popup" @click=${onClick}>Click to open popup</vwc-button>
<vwc-popup id="popup" open anchor="buttonAnchor" ...=${spread(args)}>
<div class="content">
<vwc-text font-face="body-1-bold" tight><p class="line">Popup title</p></vwc-text>
<vwc-text font-face="body-2" tight>Lorem ipsum dolor sit amet, consectetur adipiscing elit.</vwc-text>
</div>
</vwc-popup>
</div>

<script>
popup.anchor = button;
</script>
`;

export const Basic = Template.bind({});
Expand Down
4 changes: 2 additions & 2 deletions components/popup/test/popup.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,9 @@ describe('popup', () => {
);

await actualElement.updateComplete;
expect(actualElement.anchor, 'anchor should be null')
expect(actualElement.anchor, 'anchor should be ""')
rinaok marked this conversation as resolved.
Show resolved Hide resolved
.to
.equal(null);
.equal("");
expect(actualElement.open, 'open should be false')
.to
.equal(false);
Expand Down
Binary file modified ui-tests/snapshots/vwc-popup.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
80 changes: 35 additions & 45 deletions ui-tests/tests/vwc-popup/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,51 +5,41 @@ import '@vonage/vwc-text';
export async function createElementVariations(wrapper) {
const textElementWrapper = document.createElement('div');
textElementWrapper.innerHTML = `
<style>
.popup-wrapper {
width: 100%;
height: 400px;
display: flex;
align-items: center;
justify-content: center;
}
.content {
width: 166px;
text-align: left;
padding: 1rem;
}
.line {
border-bottom: 1px solid var(--vvd-color-neutral-40);
padding-bottom: 0.5rem;
margin-bottom: 0.5rem;
}
</style>
<div class="popup-wrapper">
<vwc-button id="button" layout="outlined" outlined aria-describedby="popup">Click to open popup</vwc-button>
<vwc-popup id="popup" corner="bottom" arrow dismissible>
<div class="content">
<style>
.popup-wrapper {
width: 100%;
height: 400px;
display: flex;
align-items: center;
justify-content: center;
background-color: var(--vvd-color-neutral-10);
}
.content {
width: 200px;
text-align: left;
padding: 1rem;
}
.line {
border-bottom: 1px solid var(--vvd-color-neutral-40);
padding-bottom: 0.5rem;
margin-bottom: 0.5rem;
}
</style>
<div class="popup-wrapper">
<vwc-button id="buttonAnchor" layout="outlined" outlined aria-haspopup="true" aria-describedby="popup">Click to open popup</vwc-button>
<vwc-popup id="popup" open corner="left" dismissible anchor="buttonAnchor">
<div class="content">
<vwc-text font-face="body-1-bold" tight><p class="line">Popup title</p></vwc-text>
<vwc-text font-face="body-2" tight>Lorem ipsum dolor sit amet, consectetur adipiscing elit.</vwc-text>
</div>
</vwc-popup>
</div>`;

wrapper.appendChild(textElementWrapper);
const button = document.getElementById("button");
button.addEventListener("click", onClick);
const popup = document.getElementById('popup');
await popup.updateComplete;
onClick();
return popup.updateComplete;
}
<vwc-text font-face="body-2" tight>Lorem ipsum dolor sit amet, consectetur adipiscing elit.</vwc-text>
</div>
</vwc-popup>
<vwc-popup arrow alternate corner="right" id="popup" open anchor="buttonAnchor">
<div class="content">
<vwc-text font-face="body-1-bold" tight><p class="line">Popup title</p></vwc-text>
<vwc-text font-face="body-2" tight>Lorem ipsum dolor sit amet, consectetur adipiscing elit.</vwc-text>
</div>
</vwc-popup>
</div>`;
wrapper.appendChild(textElementWrapper);

function onClick() {
const popup = document.querySelector("vwc-popup");
const button = document.querySelector("#button");
if (popup.open) {
popup.hide();
} else {
popup.anchor = button;
popup.show();
}
}