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

chore(components): added optional safe space to post-popovercontainer #4436

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
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
5 changes: 5 additions & 0 deletions .changeset/gold-beans-scream.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@swisspost/design-system-components': minor
---

Added optional safe triangle and trapeziod to `post-popovercontainer` to improve accessability.
8 changes: 8 additions & 0 deletions packages/components/src/components.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -379,6 +379,10 @@ export namespace Components {
* 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;
/**
* Enables a safespace through which the cursor can be moved without the popover being disabled
*/
"safeSpace"?: 'triangle' | 'trapezoid';
/**
* Programmatically display the tooltip
* @param target An element with [data-tooltip-target="id"] where the tooltip should be shown
Expand Down Expand Up @@ -1199,6 +1203,10 @@ declare namespace LocalJSX {
* 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;
/**
* Enables a safespace through which the cursor can be moved without the popover being disabled
*/
"safeSpace"?: 'triangle' | 'trapezoid';
}
interface PostRating {
/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,39 @@
// Keeps the little arrow visible
overflow: visible;

// Safe space overlay styles
&[data-safe-space]::after {
content: '';
position: fixed;
inset: 0;
z-index: calc(#{commons.$zindex-popover} - 1);
pointer-events: auto;
background: transparent;
}

&[data-safe-space='triangle']::after {
clip-path: polygon(
var(--safe-space-cursor-x, 0) var(--safe-space-cursor-y, 0),
var(--safe-space-popover-x-start, var(--safe-space-popover-x, 0))
var(--safe-space-popover-y, var(--safe-space-popover-y-start, 0)),
var(--safe-space-popover-x-end, var(--safe-space-popover-x, 0))
var(--safe-space-popover-y, var(--safe-space-popover-y-end, 0))
);
}

&[data-safe-space='trapezoid']::after {
clip-path: polygon(
var(--safe-space-trigger-x-start, var(--safe-space-trigger-x, 0))
var(--safe-space-trigger-y, var(--safe-space-trigger-y-start, 0)),
var(--safe-space-trigger-x-end, var(--safe-space-trigger-x, 0))
var(--safe-space-trigger-y, var(--safe-space-trigger-y-end, 0)),
var(--safe-space-popover-x-end, var(--safe-space-popover-x, 0))
var(--safe-space-popover-y, var(--safe-space-popover-y-end, 0)),
var(--safe-space-popover-x-start, var(--safe-space-popover-x, 0))
var(--safe-space-popover-y, var(--safe-space-popover-y-start, 0))
);
}

.arrow {
$arrow-size: 0.5825rem;
position: absolute;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,6 @@ import '@oddbird/popover-polyfill';

import { version } from '@root/package.json';

const SIDE_MAP = {
top: 'bottom',
right: 'left',
bottom: 'top',
left: 'right',
};

interface PopoverElement {
showPopover: () => void;
hidePopover: () => void;
Expand Down Expand Up @@ -69,13 +62,30 @@ export class PostPopovercontainer {
*/
@Prop() readonly arrow?: boolean = false;

/**
* Enables a safespace through which the cursor can be moved without the popover being disabled
*/
@Prop() readonly safeSpace?: 'triangle' | 'trapezoid';

// New method for safe space
private mouseTrackingHandler = (event: MouseEvent) => {
if (!this.safeSpace || !this.host.matches(':where(:popover-open, .popover-open)')) return;

this.host.style.setProperty('--safe-space-cursor-x', `${event.clientX}px`);
this.host.style.setProperty('--safe-space-cursor-y', `${event.clientY}px`);
};

componentDidLoad() {
this.host.setAttribute('popover', '');
this.host.addEventListener('beforetoggle', this.handleToggle.bind(this));
window.addEventListener('mousemove', this.mouseTrackingHandler);
}

disconnectedCallback() {
if (typeof this.clearAutoUpdate === 'function') this.clearAutoUpdate();
if (typeof this.clearAutoUpdate === 'function') {
this.clearAutoUpdate();
}
window.removeEventListener('mousemove', this.mouseTrackingHandler);
}

/**
Expand Down Expand Up @@ -149,15 +159,45 @@ export class PostPopovercontainer {
}

private async calculatePosition() {
const { x, y, middlewareData, placement } = await this.computeMainPosition();
const currentPlacement = placement.split('-')[0];

// Position popover
this.host.style.left = `${x}px`;
this.host.style.top = `${y}px`;

// Position arrow if enabled
if (this.arrow && middlewareData.arrow) {
const { x: arrowX, y: arrowY } = middlewareData.arrow;
const staticSide = {
top: 'bottom',
right: 'left',
bottom: 'top',
left: 'right',
}[currentPlacement];

if (staticSide) {
Object.assign(this.arrowRef.style, {
left: arrowX ? `${arrowX}px` : '',
top: arrowY ? `${arrowY}px` : '',
[staticSide]: '-4px',
});
}
}

// Handle safe space if enabled
if (this.safeSpace && this.eventTarget) {
await this.updateSafeSpaceBoundaries(currentPlacement);
}
}

private async computeMainPosition() {
const gap = this.edgeGap;
const middleware = [
flip(),
inline(),
shift({
padding: gap,

// Prevents shifting away from the anchor too far, while shifting as far as possible
// https://floating-ui.com/docs/shift#limiter
limiter: limitShift({
offset: 32,
}),
Expand All @@ -169,51 +209,103 @@ export class PostPopovercontainer {
});
},
}),
offset(this.arrow ? gap + 4 : gap), // 4px outside of element to account for focus outline + ~arrow size
offset(this.arrow ? gap + 4 : gap),
];

if (this.arrow) {
middleware.push(arrow({ element: this.arrowRef, padding: gap }));
}

const {
x,
y,
middlewareData,
placement: currentPlacement,
} = await computePosition(this.eventTarget, this.host, {
return computePosition(this.eventTarget, this.host, {
placement: this.placement || 'top',
strategy: 'fixed',
middleware,
});
}

// Tooltip
this.host.style.left = `${x}px`;
this.host.style.top = `${y}px`;
private async updateSafeSpaceBoundaries(currentPlacement: string) {
const targetRect = this.eventTarget.getBoundingClientRect();
const popoverRect = this.host.getBoundingClientRect();
const isVertical = currentPlacement === 'top' || currentPlacement === 'bottom';

// Arrow
if (this.arrow) {
// Tutorial: https://codesandbox.io/s/mystifying-kare-ee3hmh?file=/src/index.js
const side = currentPlacement.split('-')[0];
const { x: arrowX, y: arrowY } = middlewareData.arrow;
const staticSide = SIDE_MAP[side];
const offsetBorderLineJoin = 2;

Object.assign(this.arrowRef.style, {
top: arrowY ? `${arrowY}px` : '',
left: arrowX ? `${arrowX}px` : '',
[staticSide]: `${-this.arrowRef.offsetWidth / 2 - offsetBorderLineJoin}px`,
});

// Add position as a class to be able to style arrow for HCM
this.arrowRef.classList.remove(...Object.values(SIDE_MAP));
this.arrowRef.classList.add(staticSide);
// Helper function to get positioning data based on placement
const getPositioningData = (placement: string, popoverRect: DOMRect, targetRect: DOMRect) => {
if (placement === 'top' || placement === 'bottom') {
return {
popover: {
y: placement === 'top' ? popoverRect.bottom : popoverRect.top,
xStart: popoverRect.left,
xEnd: popoverRect.right,
},
trigger: {
y: placement === 'top' ? targetRect.top : targetRect.bottom,
xStart: targetRect.left,
xEnd: targetRect.right,
},
};
} else {
// left or right
return {
popover: {
x: placement === 'left' ? popoverRect.right : popoverRect.left,
yStart: popoverRect.top,
yEnd: popoverRect.bottom,
},
trigger: {
x: placement === 'left' ? targetRect.left : targetRect.right,
yStart: targetRect.top,
yEnd: targetRect.bottom,
},
};
}
};

const posData = getPositioningData(currentPlacement, popoverRect, targetRect);

// Clear previous values
const propertiesToClear = [
'--safe-space-popover-x',
'--safe-space-popover-y',
'--safe-space-popover-x-start',
'--safe-space-popover-x-end',
'--safe-space-popover-y-start',
'--safe-space-popover-y-end',
'--safe-space-trigger-x',
'--safe-space-trigger-y',
'--safe-space-trigger-x-start',
'--safe-space-trigger-x-end',
'--safe-space-trigger-y-start',
'--safe-space-trigger-y-end',
];

propertiesToClear.forEach(prop => {
this.host.style.removeProperty(prop);
});

if (isVertical) {
// For top/bottom placement
this.host.style.setProperty('--safe-space-popover-y', `${posData.popover.y}px`);
this.host.style.setProperty('--safe-space-popover-x-start', `${posData.popover.xStart}px`);
this.host.style.setProperty('--safe-space-popover-x-end', `${posData.popover.xEnd}px`);

this.host.style.setProperty('--safe-space-trigger-y', `${posData.trigger.y}px`);
this.host.style.setProperty('--safe-space-trigger-x-start', `${posData.trigger.xStart}px`);
this.host.style.setProperty('--safe-space-trigger-x-end', `${posData.trigger.xEnd}px`);
} else {
// For left/right placement
this.host.style.setProperty('--safe-space-popover-x', `${posData.popover.x}px`);
this.host.style.setProperty('--safe-space-popover-y-start', `${posData.popover.yStart}px`);
this.host.style.setProperty('--safe-space-popover-y-end', `${posData.popover.yEnd}px`);

this.host.style.setProperty('--safe-space-trigger-x', `${posData.trigger.x}px`);
this.host.style.setProperty('--safe-space-trigger-y-start', `${posData.trigger.yStart}px`);
this.host.style.setProperty('--safe-space-trigger-y-end', `${posData.trigger.yEnd}px`);
}
}

render() {
return (
<Host data-version={version}>
<Host data-version={version} data-safe-space={this.safeSpace}>
{this.arrow && (
<span
class="arrow"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,12 @@

## Properties

| Property | Attribute | Description | Type | Default |
| ----------- | ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------- |
| `arrow` | `arrow` | Wheter or not to display a little pointer arrow | `boolean` | `false` |
| `edgeGap` | `edge-gap` | Gap between the edge of the page and the popover | `number` | `8` |
| `placement` | `placement` | 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. | `"bottom" \| "bottom-end" \| "bottom-start" \| "left" \| "left-end" \| "left-start" \| "right" \| "right-end" \| "right-start" \| "top" \| "top-end" \| "top-start"` | `'top'` |
| Property | Attribute | Description | Type | Default |
| ----------- | ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------- |
| `arrow` | `arrow` | Wheter or not to display a little pointer arrow | `boolean` | `false` |
| `edgeGap` | `edge-gap` | Gap between the edge of the page and the popover | `number` | `8` |
| `placement` | `placement` | 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. | `"bottom" \| "bottom-end" \| "bottom-start" \| "left" \| "left-end" \| "left-start" \| "right" \| "right-end" \| "right-start" \| "top" \| "top-end" \| "top-start"` | `'top'` |
| `safeSpace` | `safe-space` | Enables a safespace through which the cursor can be moved without the popover being disabled | `"trapezoid" \| "triangle"` | `undefined` |


## Events
Expand Down
Loading