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
14 changes: 12 additions & 2 deletions packages/components/cypress/e2e/popover.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,19 @@ describe('popover', { baseUrl: null, includeShadowDom: true }, () => {
cy.get('post-popover[data-hydrated]');

// Aria-expanded is set by the web component, therefore it's a good measure to indicate the component is ready
cy.get('[data-popover-target="popover-one"][aria-expanded]').as('trigger');
cy.get('post-popover-trigger[data-hydrated][for="popover-one"]')
.children()
.first()
.as('trigger');
cy.get('#testtext').as('popover');
});

it('should contain an HTML element inside the trigger, not just plain text', () => {
cy.get('post-popover-trigger[data-hydrated][for="popover-one"]')
.children()
.should('have.length.at.least', 1);
});

it('should show up on click', () => {
cy.get('@popover').should('not.be.visible');
cy.get('@trigger').should('have.attr', 'aria-expanded', 'false');
Expand Down Expand Up @@ -119,7 +128,8 @@ describe('popover', { baseUrl: null, includeShadowDom: true }, () => {
cy.get('post-popover[data-hydrated]');

// Aria-expanded is set by the web component, therefore it's a good measure to indicate the component is ready
cy.get('[data-popover-target="popover-one"][aria-expanded]').as('trigger');

cy.get('post-popover-trigger[data-hydrated][for="popover-one"]').as('trigger');

cy.injectAxe();
});
Expand Down
12 changes: 10 additions & 2 deletions packages/components/cypress/e2e/popovercontainer.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,17 @@ describe('popovercontainer', { baseUrl: null, includeShadowDom: true }, () => {
const selector = isPopoverSupported() ? ':popover-open' : '.\\:popover-open';

beforeEach(() => {
// There is no dedicated docs page for the popovercontainer
cy.visit('./cypress/fixtures/post-popover.test.html');
cy.get('[data-popover-target="popover-one"][aria-expanded]').as('trigger');

// Ensure the component is hydrated, which is necessary to ensure the component is ready for interaction
cy.get('post-popover[data-hydrated]');

// Aria-expanded is set by the web component, therefore it's a good measure to indicate the component is ready
cy.get('post-popover-trigger[data-hydrated][for="popover-one"]')
.children()
.first()
.as('trigger');

cy.get('#testtext').as('container');
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@
<script src="../../dist/post-components/post-components.esm.js" type="module"></script>
</head>
<body>
<button data-popover-target="popover-one">toggle</button>
<post-popover-trigger for="popover-one">
<div class="btn btn-secondary">Popover Trigger</div>
</post-popover-trigger>
<post-popover id="popover-one" close-button-caption="Close Popover">
<p id="testtext">This is a test</p>
</post-popover>
Expand Down
29 changes: 25 additions & 4 deletions packages/components/src/components.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -364,16 +364,22 @@ export namespace Components {
"placement"?: Placement;
/**
* Programmatically display the popover
* @param target An element with [data-popover-target="id"] where the popover should be shown
* @param target A <post-popover-trigger> component that controls the popover
*/
"show": (target: HTMLElement) => Promise<void>;
/**
* Toggle popover display
* @param target An element with [data-popover-target="id"] where the popover should be anchored to
* @param target A <post-popover-trigger> component that controls the popover
* @param force Pass true to always show or false to always hide
*/
"toggle": (target: HTMLElement, force?: boolean) => Promise<void>;
}
interface PostPopoverTrigger {
/**
* ID of the popover element that this trigger is linked to. Used to open and close the popover.
*/
"for": string;
}
interface PostPopovercontainer {
/**
* Animation style
Expand Down Expand Up @@ -410,12 +416,12 @@ export namespace Components {
"safeSpace"?: 'triangle' | 'trapezoid';
/**
* Programmatically display the popovercontainer
* @param target An element with [data-popover-target="id"] where the popovercontainer should be shown
* @param target A <post-popover-trigger> component that controls the popover
*/
"show": (target: HTMLElement) => Promise<void>;
/**
* Toggle popovercontainer display
* @param target An element with [data-popover-target="id"] where the popovercontainer should be shown
* @param target A <post-popover-trigger> component that controls the popover
* @param force Pass true to always show or false to always hide
*/
"toggle": (target: HTMLElement, force?: boolean) => Promise<boolean>;
Expand Down Expand Up @@ -812,6 +818,12 @@ declare global {
prototype: HTMLPostPopoverElement;
new (): HTMLPostPopoverElement;
};
interface HTMLPostPopoverTriggerElement extends Components.PostPopoverTrigger, HTMLStencilElement {
}
var HTMLPostPopoverTriggerElement: {
prototype: HTMLPostPopoverTriggerElement;
new (): HTMLPostPopoverTriggerElement;
};
interface HTMLPostPopovercontainerElementEventMap {
"postToggle": boolean;
}
Expand Down Expand Up @@ -922,6 +934,7 @@ declare global {
"post-menu-item": HTMLPostMenuItemElement;
"post-menu-trigger": HTMLPostMenuTriggerElement;
"post-popover": HTMLPostPopoverElement;
"post-popover-trigger": HTMLPostPopoverTriggerElement;
"post-popovercontainer": HTMLPostPopovercontainerElement;
"post-rating": HTMLPostRatingElement;
"post-tab-header": HTMLPostTabHeaderElement;
Expand Down Expand Up @@ -1241,6 +1254,12 @@ declare namespace LocalJSX {
*/
"placement"?: Placement;
}
interface PostPopoverTrigger {
/**
* ID of the popover element that this trigger is linked to. Used to open and close the popover.
*/
"for": string;
}
interface PostPopovercontainer {
/**
* Animation style
Expand Down Expand Up @@ -1400,6 +1419,7 @@ declare namespace LocalJSX {
"post-menu-item": PostMenuItem;
"post-menu-trigger": PostMenuTrigger;
"post-popover": PostPopover;
"post-popover-trigger": PostPopoverTrigger;
"post-popovercontainer": PostPopovercontainer;
"post-rating": PostRating;
"post-tab-header": PostTabHeader;
Expand Down Expand Up @@ -1447,6 +1467,7 @@ declare module "@stencil/core" {
"post-menu-item": LocalJSX.PostMenuItem & JSXBase.HTMLAttributes<HTMLPostMenuItemElement>;
"post-menu-trigger": LocalJSX.PostMenuTrigger & JSXBase.HTMLAttributes<HTMLPostMenuTriggerElement>;
"post-popover": LocalJSX.PostPopover & JSXBase.HTMLAttributes<HTMLPostPopoverElement>;
"post-popover-trigger": LocalJSX.PostPopoverTrigger & JSXBase.HTMLAttributes<HTMLPostPopoverTriggerElement>;
"post-popovercontainer": LocalJSX.PostPopovercontainer & JSXBase.HTMLAttributes<HTMLPostPopovercontainerElement>;
"post-rating": LocalJSX.PostRating & JSXBase.HTMLAttributes<HTMLPostRatingElement>;
"post-tab-header": LocalJSX.PostTabHeader & JSXBase.HTMLAttributes<HTMLPostTabHeaderElement>;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
:host {
cursor: pointer;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
import { Component, h, Host, Prop, Watch, Element, State } from '@stencil/core';
import { version } from '@root/package.json';
import isFocusable from 'ally.js/is/focusable';
import { checkRequiredAndType } from '@/utils';

@Component({
tag: 'post-popover-trigger',
styleUrl: 'post-popover-trigger.scss',
shadow: true,
})
export class PostPopoverTrigger {
@Element() host: HTMLPostPopoverTriggerElement;

/**
* Reference to the element inside the host that will act as the trigger.
*/
private trigger: HTMLElement;

/**
* ID of the popover element that this trigger is linked to. Used to open and close the popover.
*/
@Prop({ reflect: true }) for!: string;

/**
* Manages the accessibility attribute `aria-expanded` to indicate whether the associated popover is expanded or collapsed.
*/
@State() ariaExpanded: boolean = false;

/**
* Watch for changes to the `for` property to validate its type and ensure it is a string.
* @param forValue - The new value of the `for` property.
*/
@Watch('for')
validateFor() {
checkRequiredAndType(this, 'for', 'string');
}

//this gets the associated popover element to the trigger based on 'for'
private get popover(): HTMLPostPopoverElement | null {
const ref = document.getElementById(this.for);
return ref?.localName === 'post-popover' ? (ref as HTMLPostPopoverElement) : null;
}

private handleToggle() {
if (this.popover) {
this.popover.toggle(this.host);
if (this.ariaExpanded === false) {
this.trigger.focus();
}
} else {
console.warn(`No post-popover found with ID: ${this.for}`);
}
}

private readonly handleKeyDown = (e: KeyboardEvent) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
this.handleToggle();
}
};

// when the content of the trigger changes
private handleSlotChange() {
this.setupTrigger();
}

// setup the trigger to get the correct aria attributes
private setupTrigger() {
this.trigger = this.host.querySelector('*');
if (this.trigger) {
this.trigger.setAttribute('aria-expanded', this.ariaExpanded.toString());

// check if its not focusable and add aria role and tabindex
if (!isFocusable(this.trigger)) {
this.trigger.setAttribute('tabindex', '0');
this.trigger.setAttribute('role', 'button');
}

// set aria attributes
this.trigger.setAttribute('ariahaspopup', 'true');

this.trigger.setAttribute('ariacontrols', this.for);

// add event listeners for click and keydown
this.trigger.addEventListener('click', () => {
this.handleToggle();
});
this.trigger.addEventListener('keydown', this.handleKeyDown);

// Listen to the `toggle` event emitted by the `post-popover` component
if (this.popover && this.trigger) {
this.popover.addEventListener('postToggle', (event: CustomEvent<boolean>) => {
this.ariaExpanded = event.detail;
this.trigger.setAttribute('aria-expanded', this.ariaExpanded.toString());
});
}
} else {
console.warn(
'No content found in the post-popover-trigger slot. Please insert a focusable element or content that can receive focus.',
);
}
}

componentDidLoad() {
this.validateFor();
}

disconnectedCallback() {
// remove event listeners
this.trigger.removeEventListener('click', () => {
this.handleToggle();
});
this.trigger.removeEventListener('keydown', this.handleKeyDown);
}

render() {
return (
<Host data-version={version}>
<slot onSlotchange={() => this.handleSlotChange()}></slot>
</Host>
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# post-popover-trigger



<!-- Auto Generated Below -->


## Properties

| Property | Attribute | Description | Type | Default |
| ------------------ | --------- | --------------------------------------------------------------------------------------------- | -------- | ----------- |
| `for` _(required)_ | `for` | ID of the popover element that this trigger is linked to. Used to open and close the popover. | `string` | `undefined` |


----------------------------------------------

*Built with [StencilJS](https://stenciljs.com/)*
Loading