Skip to content

Commit 78c8e44

Browse files
eyevanabennypowersbrianferry
authored
feat(popover): add pf-popover (#2320)
* feat(pf-popover): pf-popover init * feat(pf-popover): pfe-popover init; add basic template and props * chore: remove pf-popover element * style: copy pf4 styles and template changes * chore: reorganize * feat: add essential functionality/styling * refactor: don't use properties for styling * chore: clean up * feat: get arrow positioning from controller * feat: use new arrow functionality * fix: implement feedback from office hours * refactor: undo arrow changes * docs: add jsdoc * docs: update README * chore: clean up * chore: clean up demo * refactor: apply suggestions from code review Co-authored-by: Benny Powers <[email protected]> * refactor: rename popover element; delete requested files * revert: rename after main merge * refactor: migrate to css * fix: reset lockfile * fix: rename custom css var * docs: update example * docs: add usage snippet * test: add basic unit test coverage * docs: add changelog * fix: demo formatting * feat: add arrow middleware * feat: add custom events for show and hide * feat: use dialog element; add additional props * chore: clean up * style: remove unused CSS for arrow placement * feat: ass string list converter * fix: use the new converter; move dynamic options into show * fix: removing arrow from BaseTooltip with FloatingDomController rework * chore: clean up * fix: arrow placement with left position * fix: add visibility hidden on close * chore: clean up * test: add event hander cleanup test * fix: use specific imports * fix: formatting Co-authored-by: Benny Powers <[email protected]> * refactor: render all templates inline * refactor: set fallback where used * refactor: set fallback where used * refactor: enableFlip to noFlip * refactor: set fallback where used * docs: add JSDoc to public props and methods * refactor: manage listeners with set of instances * fix: the default is flip (opposite side only) * feat: add alert severity screen reader default text * feat: add ability to override SR text * feat: allow no hide on outside click * feat: disable focus trapping * chore: clean up * fix: don't trap focus by default; add toggle * feat: leverage showModal to trap focus * revert: focus-trap attribute * chore: clean up * docs: tweak disclaimer * docs: move disclaimer to overview * docs: update elements/pf-popover/docs/pf-popover.md * fix: typo * perf(core): dedupe converter code * docs: minor pedantry * refactor: use `visually-hidden` classname * docs: fixup docs page * test: fail on a11y audit * docs: update prop docblocks, slight refactors * perf: only add one body listener --------- Co-authored-by: Benny Powers <[email protected]> Co-authored-by: Brian Ferry <[email protected]> Co-authored-by: Benny Powers <[email protected]>
1 parent 27ad7b6 commit 78c8e44

File tree

16 files changed

+1252
-25
lines changed

16 files changed

+1252
-25
lines changed
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
---
2+
"@patternfly/pfe-core": minor
3+
---
4+
Added `StringListConverter` for managing comma-separated list attributes.

.changeset/pf-popover.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
---
2+
"@patternfly/elements": minor
3+
---
4+
✨ Added `<pf-popover>`
5+
6+
```html
7+
<pf-popover heading="Popover heading" body="Popovers are triggered by click rather than hover." footer="Popover footer">
8+
<pf-button>Toggle popover</pf-button>
9+
</pf-popover>
10+
```

core/pfe-core/controllers/floating-dom-controller.ts

Lines changed: 38 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -11,22 +11,25 @@ import {
1111
offset as offsetMiddleware,
1212
shift as shiftMiddleware,
1313
flip as flipMiddleware,
14+
arrow as arrowMiddleware
1415
} from '@floating-ui/dom';
1516

1617
type Lazy<T> = T | (() => T | null | undefined);
1718

1819
interface FloatingDOMControllerOptions {
1920
content: Lazy<HTMLElement>;
2021
invoker?: Lazy<HTMLElement>;
21-
arrow?: boolean;
22-
flip?: boolean;
22+
arrow?: Lazy<HTMLElement>;
2323
shift?: boolean;
2424
padding?: number;
25+
fallbackPlacements?: Placement[];
2526
}
2627

2728
interface ShowOptions {
2829
offset?: Offset;
2930
placement?: Placement;
31+
flip?: boolean;
32+
fallbackPlacements?: Placement[];
3033
}
3134

3235
export type Anchor = '' | 'top' | 'left' | 'bottom' | 'right';
@@ -55,6 +58,11 @@ export class FloatingDOMController implements ReactiveController {
5558
return typeof content === 'function' ? content() : content;
5659
}
5760

61+
get #arrow() {
62+
const { arrow } = this.#options;
63+
return typeof arrow === 'function' ? arrow() : arrow;
64+
}
65+
5866
/** The crosswise alignment of the invoker on which to display the floating DOM */
5967
get alignment() {
6068
return this.#alignment ?? 'center';
@@ -93,33 +101,52 @@ export class FloatingDOMController implements ReactiveController {
93101
host.addController(this);
94102
this.#options = options as Required<FloatingDOMControllerOptions>;
95103
this.#options.invoker ??= host;
96-
this.#options.arrow ??= false;
97-
this.#options.flip ??= true;
98104
this.#options.shift ??= true;
99105
}
100106

101107
hostDisconnected() {
102108
this.#cleanup?.();
103109
}
104110

105-
async #update(placement: Placement = 'top', offset?: Offset) {
106-
const { flip, padding, shift } = this.#options;
111+
async #update(placement: Placement = 'top', offset?: Offset, flip = true, fallbackPlacements?: Placement[]) {
112+
const { padding, shift } = this.#options;
107113

108114
const invoker = this.#invoker;
109115
const content = this.#content;
116+
const arrow = this.#arrow;
110117
if (!invoker || !content) {
111118
return;
112119
}
113-
const { x, y, placement: _placement } = await computePosition(invoker, content, {
120+
const { x, y, placement: _placement, middlewareData } = await computePosition(invoker, content, {
114121
strategy: 'absolute',
115122
placement,
116123
middleware: [
117124
offsetMiddleware(offset),
118125
shift && shiftMiddleware({ padding }),
119-
flip && flipMiddleware({ padding }),
126+
arrow && arrowMiddleware({ element: arrow, padding: arrow.offsetHeight / 2 }),
127+
flip && flipMiddleware({ padding, fallbackPlacements }),
120128
].filter(Boolean)
121129
});
122130

131+
if (arrow) {
132+
const { x: arrowX, y: arrowY } = middlewareData.arrow || {};
133+
134+
const staticSide = {
135+
top: 'bottom',
136+
right: 'left',
137+
bottom: 'top',
138+
left: 'right',
139+
}[_placement.split('-')[0]] || '';
140+
141+
Object.assign(arrow.style, {
142+
left: arrowX != null ? `${arrowX}px` : '',
143+
top: arrowY != null && !['top'].includes(_placement) ? `${arrowY}px` : '',
144+
right: '',
145+
bottom: '',
146+
[staticSide]: `-${arrow.offsetHeight / 2}px`,
147+
});
148+
}
149+
123150
this.#placement = _placement;
124151
[this.#anchor, this.#alignment] = (this.#placement.split('-') ?? []) as [Anchor, Alignment];
125152
this.#styles = {
@@ -129,17 +156,17 @@ export class FloatingDOMController implements ReactiveController {
129156
}
130157

131158
/** Show the floating DOM */
132-
async show({ offset, placement }: ShowOptions = {}) {
159+
async show({ offset, placement, flip, fallbackPlacements }: ShowOptions = {}) {
133160
const invoker = this.#invoker;
134161
const content = this.#content;
135162
if (!invoker || !content) {
136163
return;
137164
}
138165
if (!this.#opening) {
139166
this.#opening = true;
140-
const p = this.#update(placement, offset);
167+
const p = this.#update(placement, offset, flip, fallbackPlacements);
141168
this.#cleanup ??= autoUpdate(invoker, content, () =>
142-
this.#update(placement, offset));
169+
this.#update(placement, offset, flip, fallbackPlacements));
143170
await p;
144171
this.#opening = false;
145172
}

core/pfe-core/core.ts

Lines changed: 24 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -29,22 +29,34 @@ export function trackPerformance(preference: boolean | typeof noPref = noPref) {
2929
return window.PfeConfig.trackPerformance;
3030
}
3131

32+
function makeConverter<T>(f: (x: string, type?: unknown) => T): ComplexAttributeConverter<null | T[]> {
33+
return {
34+
fromAttribute(value: string) {
35+
if (typeof value !== 'string') {
36+
return null;
37+
} else {
38+
return value.split(',').map(f);
39+
}
40+
},
41+
toAttribute(value: T[]) {
42+
return value.join(',');
43+
},
44+
};
45+
}
46+
3247
/**
3348
* A LitElement property converter which represents a list of numbers as a comma separated string
3449
* @see https://lit.dev/docs/components/properties/#conversion-converter
3550
*/
36-
export const NumberListConverter: ComplexAttributeConverter<null | number[]> = {
37-
fromAttribute(value: string) {
38-
if (typeof value !== 'string') {
39-
return null;
40-
} else {
41-
return value.split(',').map(x => x.trim()).map(x => parseInt(x, 10));
42-
}
43-
},
44-
toAttribute(value: number[]) {
45-
return value.join(',');
46-
},
47-
};
51+
export const NumberListConverter =
52+
makeConverter(x => parseInt(x?.trim(), 10));
53+
54+
/**
55+
* A LitElement property converter which represents a list of strings as a comma separated string
56+
* @see https://lit.dev/docs/components/properties/#conversion-converter
57+
*/
58+
export const StringListConverter =
59+
makeConverter(x => x.trim());
4860

4961
/**
5062
* A composed, bubbling event for UI interactions

elements/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,8 @@
5454
"./pf-tile/pf-tile.js": "./pf-tile/pf-tile.js",
5555
"./pf-timestamp/pf-timestamp.js": "./pf-timestamp/pf-timestamp.js",
5656
"./pf-tooltip/BaseTooltip.js": "./pf-tooltip/BaseTooltip.js",
57-
"./pf-tooltip/pf-tooltip.js": "./pf-tooltip/pf-tooltip.js"
57+
"./pf-tooltip/pf-tooltip.js": "./pf-tooltip/pf-tooltip.js",
58+
"./pf-popover/pf-popover.js": "./pf-popover/pf-popover.js"
5859
},
5960
"publishConfig": {
6061
"access": "public",

elements/pf-popover/README.md

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
# Popover
2+
3+
A Popover displays content in a non-modal dialog and adds contextual information or provides resources via text and links.
4+
5+
## Usage
6+
7+
Read more about popovers in the [PatternFly Elements Popover documentation](https://patternflyelements.org/components/popover)
8+
9+
## Installation
10+
11+
Load `<pf-popover>` via CDN:
12+
13+
```html
14+
<script src="https://jspm.dev/@patternfly/elements/pf-popover/pf-popover.js"></script>
15+
```
16+
17+
Or, if you are using [NPM](https://npm.im), install it
18+
19+
```bash
20+
npm install @patternfly/elements
21+
```
22+
23+
Then once installed, import it to your application:
24+
25+
```js
26+
import '@patternfly/elements/pf-popover/pf-popover.js';
27+
```
28+
29+
## Usage
30+
31+
### Basic Popover
32+
33+
```html
34+
<pf-popover heading="Popover heading"
35+
body="Popovers are triggered by click rather than hover."
36+
footer="Popover footer">
37+
<pf-button>Toggle popover</pf-button>
38+
</pf-popover>
39+
```
40+
41+
```html
42+
<pf-popover>
43+
<h3 slot="heading">Popover heading</h3>
44+
<div slot="body">Popovers are triggered by click rather than hover.</div>
45+
<span slot="footer">Popover footer</span>
46+
<pf-button>Toggle popover</pf-button>
47+
</pf-popover>
48+
```

elements/pf-popover/demo/demo.css

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
form {
2+
padding: 0 2rem;
3+
display: flex;
4+
flex-flow: row wrap;
5+
gap: 8px;
6+
}
7+
8+
form h2 {
9+
width: 100%;
10+
}
11+
12+
label {
13+
display: flex;
14+
flex-direction: column;
15+
}
16+
17+
fieldset label {
18+
display: grid;
19+
align-content: center;
20+
display: contents;
21+
}
22+
23+
select {
24+
display: block;
25+
margin-inline-end: auto;
26+
flex-basis: 100%;
27+
}
28+
29+
#position-select {
30+
margin: 1rem 0;
31+
}
32+
33+
#alert-select {
34+
margin-bottom: 1rem;
35+
}
36+
37+
#no-padding pf-popover::part(content) {
38+
--pf-c-popover__content--PaddingTop: 0px;
39+
--pf-c-popover__content--PaddingRight: 0px;
40+
--pf-c-popover__content--PaddingBottom: 0px;
41+
--pf-c-popover__content--PaddingLeft: 0px;
42+
}
43+
44+
#auto-width pf-popover::part(content) {
45+
--pf-c-popover--MaxWidth: none;
46+
--pf-c-popover--MinWidth: auto;
47+
}

0 commit comments

Comments
 (0)