Skip to content

Commit d1eec5e

Browse files
authored
Merge branch 'main' into 2887-pf-text-input-autocomplete
2 parents 3e1e8cd + 8020937 commit d1eec5e

File tree

35 files changed

+755
-320
lines changed

35 files changed

+755
-320
lines changed

.github/workflows/preview.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@ jobs:
2222
- name: POP Debug Info
2323
run: curl https://cachefly.cachefly.net/CacheFlyDebug
2424

25+
# Set up GitHub Actions caching for Wireit.
26+
- uses: google/wireit@setup-github-actions-caching/v2
27+
2528
- run: npm ci --prefer-offline
2629
- run: npm run docs
2730
- run: cat _site/components/icon/demo/custom-icon-sets/index.html

.github/workflows/release.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@ jobs:
1616
with:
1717
node-version: '20'
1818
cache: npm
19+
20+
# Set up GitHub Actions caching for Wireit.
21+
- uses: google/wireit@setup-github-actions-caching/v2
1922

2023
- run: npm ci --prefer-offline
2124
- run: npm run build

.github/workflows/tests.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,9 @@ jobs:
5555
node-version-file: '.nvmrc'
5656
cache: npm
5757

58+
# Set up GitHub Actions caching for Wireit.
59+
- uses: google/wireit@setup-github-actions-caching/v2
60+
5861
- name: Install dependencies
5962
run: npm ci --prefer-offline
6063

.github/workflows/visual-regression.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,10 @@ jobs:
3131
with:
3232
node-version: '20'
3333
cache: npm
34+
35+
# Set up GitHub Actions caching for Wireit.
36+
- uses: google/wireit@setup-github-actions-caching/v2
37+
3438
- run: npm ci --prefer-offline
3539

3640
- name: Visual Regression Tests

.nvmrc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
v20.10.0
1+
v22.13.0

core/pfe-core/CHANGELOG.md

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,59 @@
11
# @patternfly/pfe-core
22

3+
## 5.0.1
4+
### Patch Changes
5+
6+
- fefd8bb: `SlotController`: correctly report slot content after updating
7+
8+
## 5.0.0
9+
### Major Changes
10+
11+
- 0277045: Enable `connectedCallback()` and context protocol in SSR scenarios.
12+
13+
BREAKING CHANGE
14+
This change affects any element which is expected to execute in node JS when
15+
lit-ssr shims are present. By enabling the `connectedCallback()` to execute
16+
server side. Elements must ensure that their connectedCallbacks do not try to
17+
access the DOM.
18+
19+
Before:
20+
21+
```js
22+
connectedCallback() {
23+
super.connectedCallback();
24+
this.items = [...this.querySelectorAll('my-item')];
25+
}
26+
```
27+
28+
After:
29+
```js
30+
connectedCallback() {
31+
super.connectedCallback();
32+
if (!isServer) {
33+
this.items = isServer ? [] : [...this.querySelectorAll('my-item')];
34+
}
35+
}
36+
```
37+
38+
### Minor Changes
39+
40+
- 8b5b699: **SSR**: added `ssr-hint-has-slotted` and `ssr-hint-has-slotted-default` attributes to elements that use slot controller.
41+
42+
When running SSR on elements with slots, add these attributes to ensure they render correctly.
43+
44+
```html
45+
<pf-card ssr-hint-has-slotted-default
46+
ssr-hint-has-slotted="header,footer">
47+
<h2 slot="header">Header Content</h2>
48+
<p>Default content</p>
49+
<span slot="footer">Footer Content</span>
50+
</pf-card>
51+
```
52+
53+
### Patch Changes
54+
55+
- a2f3254: `ScrollSpyController`: respond to hashchange events
56+
357
## 4.0.5
458
### Patch Changes
559

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

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { ReactiveController, ReactiveElement } from 'lit';
1+
import { isServer, type ReactiveController, type ReactiveElement } from 'lit';
22

33
import { Logger } from './logger.js';
44

@@ -52,9 +52,13 @@ export class LightDOMController implements ReactiveController {
5252
* Returns a boolean statement of whether or not this component contains any light DOM.
5353
*/
5454
hasLightDOM(): boolean {
55-
return !!(
56-
this.host.children.length > 0
57-
|| (this.host.textContent ?? '').trim().length > 0
58-
);
55+
if (isServer) {
56+
return false;
57+
} else {
58+
return !!(
59+
this.host.children.length > 0
60+
|| (this.host.textContent ?? '').trim().length > 0
61+
);
62+
}
5963
}
6064
}

core/pfe-core/controllers/scroll-spy-controller.ts

Lines changed: 60 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { ReactiveController, ReactiveControllerHost } from 'lit';
1+
import { isServer, type ReactiveController, type ReactiveControllerHost } from 'lit';
22

33
export interface ScrollSpyControllerOptions extends IntersectionObserverInit {
44
/**
@@ -18,11 +18,13 @@ export interface ScrollSpyControllerOptions extends IntersectionObserverInit {
1818
* @default the host's root node
1919
*/
2020
rootNode?: Node;
21+
2122
/**
2223
* function to call on link children to get their URL hash (i.e. id to scroll to)
2324
* @default el => el.getAttribute('href');
2425
*/
2526
getHash?: (el: Element) => string | null;
27+
2628
/**
2729
* Optional callback for when an intersection occurs
2830
*/
@@ -33,16 +35,24 @@ export class ScrollSpyController implements ReactiveController {
3335
static #instances = new Set<ScrollSpyController>;
3436

3537
static {
36-
addEventListener('scroll', () => {
37-
if (Math.round(window.innerHeight + window.scrollY) >= document.body.scrollHeight) {
38+
if (!isServer) {
39+
addEventListener('scroll', () => {
40+
if (Math.round(window.innerHeight + window.scrollY) >= document.body.scrollHeight) {
41+
this.#instances.forEach(ssc => {
42+
ssc.#setActive(ssc.#linkChildren.at(-1));
43+
});
44+
}
45+
}, { passive: true });
46+
addEventListener('hashchange', () => {
3847
this.#instances.forEach(ssc => {
39-
ssc.#setActive(ssc.#linkChildren.at(-1));
48+
ssc.#activateHash();
4049
});
41-
}
42-
}, { passive: true });
50+
});
51+
}
4352
}
4453

4554
#tagNames: string[];
55+
4656
#activeAttribute: string;
4757

4858
#io?: IntersectionObserver;
@@ -57,17 +67,28 @@ export class ScrollSpyController implements ReactiveController {
5767
#intersected = false;
5868

5969
#root: ScrollSpyControllerOptions['root'];
70+
6071
#rootMargin?: string;
72+
6173
#threshold: number | number[];
62-
#intersectingElements: Element[] = [];
6374

64-
#getRootNode: () => Node;
75+
#intersectingTargets = new Set<Element>();
76+
77+
#linkTargetMap = new Map<Element, Element | null>();
78+
79+
#getRootNode: () => Node | null;
80+
6581
#getHash: (el: Element) => string | null;
82+
6683
#onIntersection?: () => void;
6784

6885
get #linkChildren(): Element[] {
69-
return Array.from(this.host.querySelectorAll(this.#tagNames.join(',')))
70-
.filter(this.#getHash);
86+
if (isServer) {
87+
return [];
88+
} else {
89+
return Array.from(this.host.querySelectorAll(this.#tagNames.join(',')))
90+
.filter(this.#getHash);
91+
}
7192
}
7293

7394
get root(): Element | Document | null | undefined {
@@ -110,7 +131,7 @@ export class ScrollSpyController implements ReactiveController {
110131
this.#rootMargin = options.rootMargin;
111132
this.#activeAttribute = options.activeAttribute ?? 'active';
112133
this.#threshold = options.threshold ?? 0.85;
113-
this.#getRootNode = () => options.rootNode ?? host.getRootNode();
134+
this.#getRootNode = () => options.rootNode ?? host.getRootNode?.() ?? null;
114135
this.#getHash = options?.getHash ?? ((el: Element) => el.getAttribute('href'));
115136
this.#onIntersection = options?.onIntersection;
116137
}
@@ -132,12 +153,16 @@ export class ScrollSpyController implements ReactiveController {
132153
if (rootNode instanceof Document || rootNode instanceof ShadowRoot) {
133154
const { rootMargin, threshold, root } = this;
134155
this.#io = new IntersectionObserver(r => this.#onIo(r), { root, rootMargin, threshold });
135-
this.#linkChildren
136-
.map(x => this.#getHash(x))
137-
.filter((x): x is string => !!x)
138-
.map(x => rootNode.getElementById(x.replace('#', '')))
139-
.filter((x): x is HTMLElement => !!x)
140-
.forEach(target => this.#io?.observe(target));
156+
for (const link of this.#linkChildren) {
157+
const id = this.#getHash(link)?.replace('#', '');
158+
if (id) {
159+
const target = document.getElementById(id);
160+
if (target) {
161+
this.#io?.observe(target);
162+
this.#linkTargetMap.set(link, target);
163+
}
164+
}
165+
}
141166
}
142167
}
143168

@@ -155,6 +180,17 @@ export class ScrollSpyController implements ReactiveController {
155180
}
156181
}
157182

183+
async #activateHash() {
184+
const links = this.#linkChildren;
185+
const { hash } = location;
186+
if (!hash) {
187+
this.setActive(links.at(0) ?? null);
188+
} else {
189+
await this.#nextIntersection();
190+
this.setActive(links.find(x => this.#getHash(x) === hash) ?? null);
191+
}
192+
}
193+
158194
async #nextIntersection() {
159195
this.#intersected = false;
160196
// safeguard the loop
@@ -178,13 +214,15 @@ export class ScrollSpyController implements ReactiveController {
178214
this.#setActive(last ?? this.#linkChildren.at(0));
179215
}
180216
this.#intersected = true;
181-
this.#intersectingElements =
182-
entries
183-
.filter(x => x.isIntersecting)
184-
.map(x => x.target);
217+
this.#intersectingTargets.clear();
218+
for (const entry of entries) {
219+
if (entry.isIntersecting) {
220+
this.#intersectingTargets.add(entry.target);
221+
}
222+
}
185223
if (this.#initializing) {
186224
const ints = entries?.filter(x => x.isIntersecting) ?? [];
187-
if (this.#intersectingElements) {
225+
if (this.#intersectingTargets.size > 0) {
188226
const [{ target = null } = {}] = ints;
189227
const { id } = target ?? {};
190228
if (id) {
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import type { ReactiveElement } from 'lit';
2+
import {
3+
type SlotControllerArgs,
4+
type SlotControllerPublicAPI,
5+
} from './slot-controller.js';
6+
7+
export class SlotController implements SlotControllerPublicAPI {
8+
public static default = Symbol('default slot') satisfies symbol as symbol;
9+
10+
/** @deprecated use `default` */
11+
public static anonymous: symbol = this.default;
12+
13+
static attribute = 'ssr-hint-has-slotted' as const;
14+
15+
static anonymousAttribute = 'ssr-hint-has-slotted-default' as const;
16+
17+
constructor(public host: ReactiveElement, ..._: SlotControllerArgs) {
18+
host.addController(this);
19+
}
20+
21+
hostConnected?(): Promise<void>;
22+
23+
private fromAttribute(slots: string | null) {
24+
return (slots ?? '')
25+
.split(/[, ]/)
26+
.map(x => x.trim());
27+
}
28+
29+
getSlotted<T extends Element = Element>(..._: string[]): T[] {
30+
return [];
31+
}
32+
33+
hasSlotted(...names: (string | null)[]): boolean {
34+
const attr = this.host.getAttribute(SlotController.attribute);
35+
const anon = this.host.hasAttribute(SlotController.anonymousAttribute);
36+
const hints = new Set(this.fromAttribute(attr));
37+
return names.every(x => x === null ? anon : hints.has(x));
38+
}
39+
40+
isEmpty(...names: (string | null)[]): boolean {
41+
return !this.hasSlotted(...names);
42+
}
43+
}

0 commit comments

Comments
 (0)