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(components): popover #2109

Merged
merged 50 commits into from
Dec 7, 2023
Merged
Changes from 1 commit
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
93d48b4
feat: added post-popover and post-popup components
gfellerph Oct 18, 2023
d0170f0
fix: popoverRef should be set
gfellerph Oct 18, 2023
c96d199
fix: refactor unnecessary no pointer events
gfellerph Oct 18, 2023
75d968e
feat: improve popover base component
gfellerph Oct 20, 2023
bea295d
fix: improve event handling
gfellerph Oct 20, 2023
c9d2131
refactor: add elevation mixin for usage in components
gfellerph Oct 20, 2023
7c9747b
fix: close button should have pointer cursor
gfellerph Oct 20, 2023
d04145b
docs: add popup story
gfellerph Oct 20, 2023
12235eb
chore: add component types to cypress tests
gfellerph Oct 20, 2023
3eb06f5
test: add tests for popup
gfellerph Oct 20, 2023
d70f248
fix: remove fallbackAxisSideDirection
gfellerph Oct 20, 2023
d70cec8
refactor: rename post-popover to post-popovercontainer
gfellerph Oct 26, 2023
90eca1a
chore: fix types
gfellerph Oct 26, 2023
772f06c
refactor: rename post-popup to post-popover
gfellerph Oct 26, 2023
4d116de
test: fix specs
gfellerph Oct 26, 2023
a6d0b99
chore(header): fix linting issues
gfellerph Oct 26, 2023
2f9e621
chore: fix code smell
gfellerph Oct 26, 2023
f8eb981
chore: add changeset
gfellerph Oct 26, 2023
3e3fe64
chore: fix linting
gfellerph Oct 26, 2023
245fcb6
Merge branch 'main' into popup-component
gfellerph Oct 26, 2023
464ae8b
Merge branch 'main' into popup-component
gfellerph Oct 30, 2023
b93bb4a
fix(e2e): Revert vite 4.11 upgrade
imagoiq Oct 30, 2023
2729a93
chore: update generated files
gfellerph Oct 31, 2023
a77a84d
test: add tests for post-popovercontainer
gfellerph Oct 31, 2023
71493ae
Merge branch 'main' into popup-component
gfellerph Oct 31, 2023
5e673d1
Update packages/components/cypress/e2e/popover.cy.ts
gfellerph Nov 2, 2023
13fd523
Update packages/components/src/components/post-popovercontainer/post-…
gfellerph Nov 2, 2023
5c21efa
Update packages/components/src/components/post-popovercontainer/post-…
gfellerph Nov 2, 2023
342544d
Update packages/components/src/components/post-popovercontainer/post-…
gfellerph Nov 9, 2023
6f4f2a2
Merge branch 'main' into popup-component
gfellerph Nov 24, 2023
27436b1
update popover styles and set required props
gfellerph Nov 24, 2023
15645f7
fix firefox popovers
gfellerph Nov 24, 2023
295cf7b
Merge branch 'main' into popup-component
imagoiq Nov 30, 2023
244e2f5
Merge branch 'main' into popup-component
imagoiq Nov 30, 2023
d914021
Merge branch 'main' into popup-component
imagoiq Nov 30, 2023
e6610de
fix: stop pointerup/down propagation for firefox
gfellerph Dec 1, 2023
81e669c
Merge branch 'main' into popup-component
gfellerph Dec 4, 2023
f314814
Merge branch 'main' into popup-component
gfellerph Dec 4, 2023
778c5af
fix: keep font-size and padding flexible and context aware
gfellerph Dec 4, 2023
aacdcc0
fix: set aria-expanded when toggling
gfellerph Dec 4, 2023
aa988cb
feat: return new state when toggling popovercontainer
gfellerph Dec 4, 2023
3571b42
feat: add title option to story, wrap content
gfellerph Dec 4, 2023
ea51ecf
chore: add start:clean command to root package.json
gfellerph Dec 4, 2023
f784fef
chore: does not belong here
gfellerph Dec 5, 2023
99aa577
tests: use fixture for popover tests
gfellerph Dec 6, 2023
01d5270
tests: fix popover tests
gfellerph Dec 6, 2023
08c38c6
chore: clean up index.html
gfellerph Dec 6, 2023
5d7fb97
fix: revert popover-polyfill to 0.2.3
gfellerph Dec 6, 2023
272babc
fix: show close button in high contrast mode
gfellerph Dec 6, 2023
4b97847
Merge branch 'main' into popup-component
gfellerph Dec 6, 2023
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
Prev Previous commit
Next Next commit
refactor: rename post-popup to post-popover
gfellerph committed Oct 26, 2023
commit 772f06cd7e496919bb78008f92fecdba6b2ab1cd
42 changes: 21 additions & 21 deletions packages/components/cypress/e2e/popup.cy.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
describe('popup', () => {
describe('popover', () => {
describe('default', () => {
beforeEach(() => {
cy.getComponent('popup');
@@ -8,61 +8,61 @@ describe('popup', () => {
});

it('should show up on click', () => {
cy.get('@popup').should('not.be.visible');
cy.get('@popover').should('not.be.visible');
cy.get('@trigger').should('have.attr', 'aria-expanded', 'false');
cy.get('@trigger').click();
cy.get('@popup').should('be.visible');
cy.get('@popover').should('be.visible');
cy.get('@trigger').should('have.attr', 'aria-expanded', 'true');
// Void click light dismiss does not work in cypress for closing
});

it('should close on X click', () => {
cy.get('@trigger').click();
cy.get('@popup').should('be.visible');
cy.get('@popover').should('be.visible');
cy.get('.btn-close').click();
cy.get('@popup').should('not.be.visible');
cy.get('@popover').should('not.be.visible');
});

it('should open and close on enter', () => {
cy.get('@popup').should('not.be.visible');
cy.get('@popover').should('not.be.visible');
cy.get('@trigger').focus().type('{enter}');
cy.get('@popup').should('be.visible');
cy.get('@popover').should('be.visible');
cy.get('@trigger').type('{enter}');
cy.get('@popup').should('not.be.visible');
cy.get('@popover').should('not.be.visible');
});

// Light dismiss with esc does not work in cypress apparently
/* it('should close on esc', () => {
cy.get('@popup').should('not.be.visible');
cy.get('@popover').should('not.be.visible');
cy.get('@trigger').click();
cy.get('@popup').should('be.visible');
cy.get('@popover').should('be.visible');
cy.get('@trigger').focus().type('{esc}');
cy.get('@popup').should('not.be.visible');
cy.get('@popover').should('not.be.visible');
}); */

it('should open and close with the API', () => {
Promise.all([cy.get('@trigger'), cy.get('@popup')])
.then(([$trigger, $popup]: [JQuery<HTMLButtonElement>, JQuery<HTMLPostPopupElement>]) => [
Promise.all([cy.get('@trigger'), cy.get('@popover')])
.then(([$trigger, $popup]: [JQuery<HTMLButtonElement>, JQuery<HTMLPostPopoverElement>]) => [
$trigger.get(0),
$popup.get(0),
])
.then(([trigger, popup]: [HTMLButtonElement, HTMLPostPopupElement]) => {
cy.get('@popup').should('not.be.visible');
.then(([trigger, popup]: [HTMLButtonElement, HTMLPostPopoverElement]) => {
cy.get('@popover').should('not.be.visible');
popup.show(trigger);
cy.get('@popup').should('be.visible');
cy.get('@popover').should('be.visible');
popup.hide();
cy.get('@popup').should('not.be.visible');
cy.get('@popover').should('not.be.visible');
popup.toggle(trigger);
cy.get('@popup').should('be.visible');
cy.get('@popover').should('be.visible');
popup.toggle(trigger);
cy.get('@popup').should('not.be.visible');
cy.get('@popover').should('not.be.visible');
});
});

it('should switch position', () => {
cy.get('@popup').invoke('attr', 'placement', 'top').should('not.be.visible');
cy.get('@popover').invoke('attr', 'placement', 'top').should('not.be.visible');
cy.get('@trigger').click();
cy.get('@popup').find('[popover]').should('have.css', 'top', '76px');
cy.get('@popover').find('[popover]').should('have.css', 'top', '76px');
});
});
});
70 changes: 39 additions & 31 deletions packages/components/src/components.d.ts
Original file line number Diff line number Diff line change
@@ -83,48 +83,52 @@ export namespace Components {
*/
"scale"?: number | null;
}
interface PostPopovercontainer {
interface PostPopover {
/**
* Wheter or not to display a little pointer arrow
* Define the caption of the close button for assistive technology
*/
"arrow"?: boolean;
"closeButtonCaption": string;
/**
* Programmatically hide this tooltip
* Programmatically hide this popover
*/
"hide": () => Promise<void>;
/**
* Defines the placement of the tooltip according to the floating-ui options available at https://floating-ui.com/docs/computePosition#placement. Tooltips are automatically flipped to the opposite side if there is not enough available space and are shifted towards the viewport if they would overlap edge boundaries.
* Defines the placement of the popover according to the floating-ui options available at https://floating-ui.com/docs/computePosition#placement. Popoverss are automatically flipped to the opposite side if there is not enough available space and are shifted towards the viewport if they would overlap edge boundaries.
*/
"placement"?: Placement;
/**
* Programmatically display the tooltip
* @param target An element with [data-tooltip-target="id"] where the tooltip should be shown
* Programmatically display the popover
* @param target An element with [data-popover-target="id"] where the popover should be shown
*/
"show": (target: HTMLElement) => Promise<void>;
/**
* Toggle tooltip display
* @param target An element with [data-tooltip-target="id"] where the tooltip should be shown
* Toggle popover display
* @param target An element with [data-popover-target="id"] where the popover should be anchored to
* @param force Pass true to always show or false to always hide
*/
"toggle": (target: HTMLElement, force?: boolean) => Promise<void>;
}
interface PostPopup {
interface PostPopovercontainer {
/**
* Wheter or not to display a little pointer arrow
*/
"arrow"?: boolean;
/**
* Programmatically hide this popup
* Programmatically hide this tooltip
*/
"hide": () => Promise<void>;
/**
* Defines the placement of the popup according to the floating-ui options available at https://floating-ui.com/docs/computePosition#placement. Popups are automatically flipped to the opposite side if there is not enough available space and are shifted towards the viewport if they would overlap edge boundaries.
* Defines the placement of the tooltip according to the floating-ui options available at https://floating-ui.com/docs/computePosition#placement. Tooltips are automatically flipped to the opposite side if there is not enough available space and are shifted towards the viewport if they would overlap edge boundaries.
*/
"placement"?: Placement;
/**
* Programmatically display the popup
* @param target An element with [data-popup-target="id"] where the popup should be shown
* Programmatically display the tooltip
* @param target An element with [data-tooltip-target="id"] where the tooltip should be shown
*/
"show": (target: HTMLElement) => Promise<void>;
/**
* Toggle popup display
* @param target An element with [data-popup-target="id"] where the popup should be anchored to
* Toggle tooltip display
* @param target An element with [data-tooltip-target="id"] where the tooltip should be shown
* @param force Pass true to always show or false to always hide
*/
"toggle": (target: HTMLElement, force?: boolean) => Promise<void>;
@@ -207,18 +211,18 @@ declare global {
prototype: HTMLPostIconElement;
new (): HTMLPostIconElement;
};
interface HTMLPostPopoverElement extends Components.PostPopover, HTMLStencilElement {
}
var HTMLPostPopoverElement: {
prototype: HTMLPostPopoverElement;
new (): HTMLPostPopoverElement;
};
interface HTMLPostPopovercontainerElement extends Components.PostPopovercontainer, HTMLStencilElement {
}
var HTMLPostPopovercontainerElement: {
prototype: HTMLPostPopovercontainerElement;
new (): HTMLPostPopovercontainerElement;
};
interface HTMLPostPopupElement extends Components.PostPopup, HTMLStencilElement {
}
var HTMLPostPopupElement: {
prototype: HTMLPostPopupElement;
new (): HTMLPostPopupElement;
};
interface HTMLPostTabHeaderElement extends Components.PostTabHeader, HTMLStencilElement {
}
var HTMLPostTabHeaderElement: {
@@ -247,8 +251,8 @@ declare global {
"post-alert": HTMLPostAlertElement;
"post-collapsible": HTMLPostCollapsibleElement;
"post-icon": HTMLPostIconElement;
"post-popover": HTMLPostPopoverElement;
"post-popovercontainer": HTMLPostPopovercontainerElement;
"post-popup": HTMLPostPopupElement;
"post-tab-header": HTMLPostTabHeaderElement;
"post-tab-panel": HTMLPostTabPanelElement;
"post-tabs": HTMLPostTabsElement;
@@ -325,6 +329,16 @@ declare namespace LocalJSX {
*/
"scale"?: number | null;
}
interface PostPopover {
/**
* Define the caption of the close button for assistive technology
*/
"closeButtonCaption"?: string;
/**
* Defines the placement of the popover according to the floating-ui options available at https://floating-ui.com/docs/computePosition#placement. Popoverss are automatically flipped to the opposite side if there is not enough available space and are shifted towards the viewport if they would overlap edge boundaries.
*/
"placement"?: Placement;
}
interface PostPopovercontainer {
/**
* Wheter or not to display a little pointer arrow
@@ -336,12 +350,6 @@ declare namespace LocalJSX {
*/
"placement"?: Placement;
}
interface PostPopup {
/**
* Defines the placement of the popup according to the floating-ui options available at https://floating-ui.com/docs/computePosition#placement. Popups are automatically flipped to the opposite side if there is not enough available space and are shifted towards the viewport if they would overlap edge boundaries.
*/
"placement"?: Placement;
}
interface PostTabHeader {
/**
* The name of the panel controlled by the tab header.
@@ -374,8 +382,8 @@ declare namespace LocalJSX {
"post-alert": PostAlert;
"post-collapsible": PostCollapsible;
"post-icon": PostIcon;
"post-popover": PostPopover;
"post-popovercontainer": PostPopovercontainer;
"post-popup": PostPopup;
"post-tab-header": PostTabHeader;
"post-tab-panel": PostTabPanel;
"post-tabs": PostTabs;
@@ -392,8 +400,8 @@ declare module "@stencil/core" {
* @class PostIcon - representing a stencil component
*/
"post-icon": LocalJSX.PostIcon & JSXBase.HTMLAttributes<HTMLPostIconElement>;
"post-popover": LocalJSX.PostPopover & JSXBase.HTMLAttributes<HTMLPostPopoverElement>;
"post-popovercontainer": LocalJSX.PostPopovercontainer & JSXBase.HTMLAttributes<HTMLPostPopovercontainerElement>;
"post-popup": LocalJSX.PostPopup & JSXBase.HTMLAttributes<HTMLPostPopupElement>;
"post-tab-header": LocalJSX.PostTabHeader & JSXBase.HTMLAttributes<HTMLPostTabHeaderElement>;
"post-tab-panel": LocalJSX.PostTabPanel & JSXBase.HTMLAttributes<HTMLPostTabPanelElement>;
"post-tabs": LocalJSX.PostTabs & JSXBase.HTMLAttributes<HTMLPostTabsElement>;
Original file line number Diff line number Diff line change
@@ -14,7 +14,7 @@
@include utilities.visuallyhidden();
}

.popup-container {
.popover-container {
display: flex;
align-items: self-start;
padding: spacing.$size-micro spacing.$size-mini;
Original file line number Diff line number Diff line change
@@ -3,40 +3,45 @@ import { Placement } from '@floating-ui/dom';

import { version } from '../../../package.json';
@Component({
tag: 'post-popup',
styleUrl: 'post-popup.scss',
tag: 'post-popover',
styleUrl: 'post-popover.scss',
shadow: true,
})
export class PostPopup {
export class PostPopover {
private popoverRef: HTMLPostPopovercontainerElement;
private localTogglePopup: (e: Event) => Promise<void>;
private localEnterTogglePopup: (e: KeyboardEvent) => void;
private localTouchTogglePopup: (e: TouchEvent) => void;
private localTogglePopover: (e: Event) => Promise<void>;
private localEnterTogglePopover: (e: KeyboardEvent) => void;
private localTouchTogglePopover: (e: TouchEvent) => void;
private currentTarget: HTMLElement;

@Element() host: HTMLPostPopupElement;
@Element() host: HTMLPostPopoverElement;

/**
* Defines the placement of the popup according to the floating-ui options available at https://floating-ui.com/docs/computePosition#placement.
* Popups are automatically flipped to the opposite side if there is not enough available space and are shifted
* Defines the placement of the popover according to the floating-ui options available at https://floating-ui.com/docs/computePosition#placement.
* Popoverss are automatically flipped to the opposite side if there is not enough available space and are shifted
* towards the viewport if they would overlap edge boundaries.
*/
@Prop() readonly placement?: Placement = 'right-end';

/**
* Define the caption of the close button for assistive technology
*/
@Prop() readonly closeButtonCaption: string;
Copy link
Contributor

Choose a reason for hiding this comment

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

It would be great if you can display it as required in Storybook docs.

Copy link
Member Author

Choose a reason for hiding this comment

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

Done


constructor() {
this.localTogglePopup = e => this.toggle(e.target as HTMLElement);
this.localEnterTogglePopup = e => {
this.localTogglePopover = e => this.toggle(e.target as HTMLElement);
this.localEnterTogglePopover = e => {
if (e.key === 'Enter') this.toggle(e.target as HTMLElement);
};
this.localTouchTogglePopup = e => {
this.localTouchTogglePopover = e => {
e.preventDefault();
this.toggle(e.target as HTMLElement);
};
}

connectedCallback() {
if (!this.triggers) {
throw new Error(`No trigger found for <post-popup popup-id="${this.host.id}`);
throw new Error(`No trigger found for <post-popover popover-id="${this.host.id}`);
}

// As long as cross-shadow-boundary [popovertarget] and button.popoverTargetElement are not working
@@ -46,25 +51,25 @@ export class PostPopup {
// https://stackoverflow.com/questions/77324143/popovertargetelement-does-not-cross-shadow-boundaries?noredirect=1#comment136318281_77324143
this.triggers.forEach(trigger => {
// See this.onToggle for one time mouse event listener
trigger.addEventListener('mouseup', this.localTogglePopup, { once: true });
trigger.addEventListener('keypress', this.localEnterTogglePopup);
trigger.addEventListener('touch', this.localTouchTogglePopup, { once: true });
trigger.addEventListener('mouseup', this.localTogglePopover, { once: true });
trigger.addEventListener('keypress', this.localEnterTogglePopover);
trigger.addEventListener('touch', this.localTouchTogglePopover, { once: true });
trigger.setAttribute('aria-expanded', 'false');
});
}

disconnectedCallback() {
this.triggers.forEach(trigger => {
trigger.removeEventListener('mouseup', this.localTogglePopup);
trigger.removeEventListener('keypress', this.localEnterTogglePopup);
trigger.removeEventListener('touch', this.localTouchTogglePopup);
trigger.removeEventListener('mouseup', this.localTogglePopover);
trigger.removeEventListener('keypress', this.localEnterTogglePopover);
trigger.removeEventListener('touch', this.localTouchTogglePopover);
trigger.removeAttribute('aria-expanded');
});
}

/**
* Programmatically display the popup
* @param target An element with [data-popup-target="id"] where the popup should be shown
* Programmatically display the popover
* @param target An element with [data-popover-target="id"] where the popover should be shown
*/
@Method()
async show(target: HTMLElement) {
@@ -74,7 +79,7 @@ export class PostPopup {
}

/**
* Programmatically hide this popup
* Programmatically hide this popover
*/
@Method()
async hide() {
@@ -83,8 +88,8 @@ export class PostPopup {
}

/**
* Toggle popup display
* @param target An element with [data-popup-target="id"] where the popup should be anchored to
* Toggle popover display
* @param target An element with [data-popover-target="id"] where the popover should be anchored to
* @param force Pass true to always show or false to always hide
*/
@Method()
@@ -94,7 +99,7 @@ export class PostPopup {
}

private get triggers() {
return document.querySelectorAll(`[data-popup-target="${this.host.id}"]`);
return document.querySelectorAll(`[data-popover-target="${this.host.id}"]`);
}

/**
@@ -112,8 +117,8 @@ export class PostPopup {
if (!e.detail) {
window.requestAnimationFrame(() => {
this.triggers.forEach(trigger => {
trigger.addEventListener('mouseup', this.localTogglePopup, { once: true });
trigger.addEventListener('touch', this.localTouchTogglePopup, { once: true });
trigger.addEventListener('mouseup', this.localTogglePopover, { once: true });
trigger.addEventListener('touch', this.localTouchTogglePopover, { once: true });
});
});

@@ -134,12 +139,12 @@ export class PostPopup {
ref={e => (this.popoverRef = e)}
onPostPopoverToggled={e => this.onToggle(e)}
>
<div class="popup-container">
<div class="popover-container">
<div>
<slot></slot>
</div>
<button class="btn-close" onClick={() => this.hide()}>
<span class="visually-hidden">Collapse popup</span>
<span class="visually-hidden">{this.closeButtonCaption}</span>
</button>
</div>
</post-popovercontainer>
Loading