Skip to content

Commit 6b93a6a

Browse files
authored
feat: history on tab click and open tab based on url parameters on pfe-content-set (#797)
* feat: history on tab click and open tab based on url parameters Fixes #793 * adding ids to headers on demo page * using the pfe-clone-id attribute instead of pfe-id This makes it more clear as to what's happening with the attribute * feat: Remove the "pfe-" prefix from the history feature in pfe-tabs Fixes #802 * feat: pfe-content-set: add pfe-tab-history functionality to tab version Fixes #793 Using the `id` attribute on `pfe-content-set` instead of the `pfe-clone-id` attribute that we previously tried. Using the `id` attribute should provide some clarity to those implementing it. * updating pfe-content-set readme to be more readable
1 parent 40c8ffa commit 6b93a6a

File tree

7 files changed

+350
-10
lines changed

7 files changed

+350
-10
lines changed

elements/pfe-content-set/README.md

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,109 @@ Each header must have an attribute of `pfe-content-set--header` and each panel m
2727

2828
```
2929

30+
## Attributes
31+
32+
**`pfe-tab-history`** (observed)
33+
34+
If `pfe-content-set` renders as `pfe-tabs`, the `pfe-tab-history` attribute
35+
enables the component to update window.history and the URL to create sharable
36+
links.
37+
38+
With the `pfe-tab-history` attribute, `pfe-content-set` and elements with the
39+
`pfe-content-set--header` attribute *must* have an `id` attribute set for this
40+
to work.
41+
42+
##### Example Markup
43+
```html
44+
<pfe-content-set id="my-content-set" pfe-tab-history>
45+
<h2 pfe-content-set--header id="heading1">Heading 1</h2>
46+
<p pfe-content-set--panel id="panel1">Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore </p>
47+
<h2 pfe-content-set--header id="heading2">Heading 2</h2>
48+
<p pfe-content-set--panel id="panel2">Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore </p>
49+
</pfe-content-set>
50+
```
51+
52+
becomes
53+
54+
```html
55+
<pfe-content-set id="my-content-set" pfe-tab-history>
56+
<pfe-tabs pfe-id="my-content-set">
57+
<pfe-tab pfe-id="heading1">
58+
<h2 pfe-content-set--header id="heading1">Heading 1</h2>
59+
</pfe-tab>
60+
<pfe-tab-panel pfe-id="panel1">
61+
<p pfe-content-set--panel id="panel1">Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore </p>
62+
</pfe-tab-panel>
63+
<pfe-tab pfe-id="heading2">
64+
<h2 pfe-content-set--header id="heading2">Heading 1</h2>
65+
</pfe-tab>
66+
<pfe-tab-panel pfe-id="panel2">
67+
<p pfe-content-set--panel id="panel2">Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore </p>
68+
</pfe-tab-panel>
69+
</pfe-tabs>
70+
</pfe-content-set>
71+
```
72+
73+
Note how the `id` attributes from `pfe-content-set` and elements with the
74+
`pfe-content-set--header` attribute pass the value of the `id` attribute to
75+
its corresponding tab element and sets the `pfe-id` attribute.
76+
77+
#### How to use a URL pattern to open a specific tab
78+
79+
The URL pattern will be `?{id-of-tabs}={id-of-selected-tab}`. In the example
80+
above, selecting "Heading 2" will update the URL as follows:
81+
`?my-content-set=heading2`.
82+
83+
84+
If `pfe-content-set` renders as `pfe-tabs`, by default, `pfe-tabs` will read
85+
the URL and look for a query string parameter that matches the `pfe-id` of a
86+
`pfe-tabs` component and a value of a specific `pfe-tab`.
87+
88+
`pfe-content-set` and elements with the `pfe-content-set--header` attribute
89+
*must* have an `id` attribute set for this to work.
90+
91+
##### Example Markup
92+
```html
93+
<pfe-content-set id="my-content-set">
94+
<h2 pfe-content-set--header id="heading1">Heading 1</h2>
95+
<p pfe-content-set--panel id="panel1">Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore </p>
96+
<h2 pfe-content-set--header id="heading2">Heading 2</h2>
97+
<p pfe-content-set--panel id="panel2">Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore </p>
98+
</pfe-content-set>
99+
```
100+
101+
becomes
102+
103+
```html
104+
<pfe-content-set id="my-content-set">
105+
<pfe-tabs pfe-id="my-content-set">
106+
<pfe-tab pfe-id="heading1">
107+
<h2 pfe-content-set--header id="heading1">Heading 1</h2>
108+
</pfe-tab>
109+
<pfe-tab-panel pfe-id="panel1">
110+
<p pfe-content-set--panel id="panel1">Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore </p>
111+
</pfe-tab-panel>
112+
<pfe-tab pfe-id="heading2">
113+
<h2 pfe-content-set--header id="heading2">Heading 1</h2>
114+
</pfe-tab>
115+
<pfe-tab-panel pfe-id="panel2">
116+
<p pfe-content-set--panel id="panel2">Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore </p>
117+
</pfe-tab-panel>
118+
</pfe-tabs>
119+
</pfe-content-set>
120+
```
121+
122+
For example, `?my-content-set=heading2` would open the second tab in the
123+
code sample above. "my-content-set" is equal to the `pfe-id` of the `pfe-tabs`
124+
component and "heading2" is equal to the `pfe-id` of the second tab in the tab
125+
set.
126+
127+
In the event that a tab with the supplied id in the URL does not exist,
128+
`pfe-tabs` will fall back to the `selected-index` attribute if one is supplied
129+
in the markup, or the first tab if `selected-index` is not provided.
130+
131+
*Note:* This feature is not supported in IE11.
132+
30133
## Variants
31134

32135
### Style

elements/pfe-content-set/demo/index.html

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -331,6 +331,41 @@ <h4 pfe-content-set--header>Heading 3</h4>
331331

332332
</section>
333333

334+
<section>
335+
<h2 id="open-to-tab">Open pfe-content set to a specific tab on page load using a query string parameter</h2>
336+
<pfe-content-set id="querystring-content-set">
337+
<h4 pfe-content-set--header id="my-header-1">Heading 1</h4>
338+
<div pfe-content-set--panel id="my-panel-1">
339+
<p>Content for Heading 1</p>
340+
</div>
341+
<h4 pfe-content-set--header id="my-header-2">Heading 2</h4>
342+
<div pfe-content-set--panel id="my-panel-2">
343+
<p>Content for Heading 2</p>
344+
</div>
345+
<h4 pfe-content-set--header id="my-header-3">Heading 3</h4>
346+
<div pfe-content-set--panel id="my-panel-3">
347+
<p>Content for Heading 3</p>
348+
</div>
349+
</pfe-content-set>
350+
</section>
351+
352+
<section>
353+
<h2 id="with-tab-history">pfe-content set with tab history</h2>
354+
<pfe-content-set id="my-content-set" pfe-tab-history>
355+
<h4 pfe-content-set--header id="header-1">Heading 1</h4>
356+
<div pfe-content-set--panel id="panel-1">
357+
<p>Content for Heading 1</p>
358+
</div>
359+
<h4 pfe-content-set--header id="header-2">Heading 2</h4>
360+
<div pfe-content-set--panel id="panel-2">
361+
<p>Content for Heading 2</p>
362+
</div>
363+
<h4 pfe-content-set--header id="header-3">Heading 3</h4>
364+
<div pfe-content-set--panel id="panel-3">
365+
<p>Content for Heading 3</p>
366+
</div>
367+
</pfe-content-set>
368+
</section>
334369

335370
<section>
336371
<h3>The pfe-content-set comes with a mutation observer so that you may dynamically add headers and panels.</h3>

elements/pfe-content-set/src/pfe-content-set.js

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,16 @@ class PfeContentSet extends PFElement {
2323
return PFElement.pfeType.combo;
2424
}
2525

26+
static get cascadingAttributes() {
27+
return {
28+
"pfe-tab-history": "pfe-tabs"
29+
};
30+
}
31+
32+
static get observedAttributes() {
33+
return ["pfe-tab-history"];
34+
}
35+
2636
get isTab() {
2737
var breakpointValue;
2838
if (this.hasAttribute("pfe-breakpoint")) {
@@ -127,17 +137,25 @@ class PfeContentSet extends PFElement {
127137
const header = document.createElement("pfe-tab");
128138

129139
header.setAttribute("slot", "tab");
130-
header.appendChild(child);
131140

141+
if (child.id) {
142+
header.setAttribute("pfe-id", child.id);
143+
}
144+
145+
header.appendChild(child);
132146
tabs.appendChild(header);
133147
}
134148

135149
if (child.hasAttribute("pfe-content-set--panel")) {
136150
const panel = document.createElement("pfe-tab-panel");
137151

138152
panel.setAttribute("slot", "panel");
139-
panel.appendChild(child);
140153

154+
if (child.id) {
155+
panel.setAttribute("pfe-id", child.id);
156+
}
157+
158+
panel.appendChild(child);
141159
tabs.appendChild(panel);
142160
}
143161
});
@@ -160,6 +178,14 @@ class PfeContentSet extends PFElement {
160178
tabs.setAttribute("pfe-tab-align", this.align.value);
161179
}
162180

181+
if (this.id) {
182+
tabs.setAttribute("pfe-id", this.id);
183+
}
184+
185+
if (this.hasAttribute("pfe-tab-history")) {
186+
tabs.setAttribute("pfe-tab-history", true);
187+
}
188+
163189
if (!existingTabs) {
164190
this.appendChild(fragment);
165191
}

elements/pfe-content-set/src/pfe-content-set.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,12 @@
6060
"title": "Custom breakpoint",
6161
"type": "string",
6262
"prefixed": true
63+
},
64+
"tab-history": {
65+
"title": "Tab history",
66+
"type": "boolean",
67+
"default": false,
68+
"prefixed": true
6369
}
6470
},
6571
"required": ["orientation", "variant", "on"]

elements/pfe-content-set/test/pfe-content-set_test.html

Lines changed: 104 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,18 @@ <h2 pfe-content-set--header>Heading 1</h2>
106106
</pfe-content-set>
107107
</pfe-band>
108108

109+
<pfe-content-set id="my-content-set">
110+
<h2 pfe-content-set--header id="my-content-set-header">Heading 1</h2>
111+
<p pfe-content-set--panel id="my-content-set-panel">Panel 1</p>
112+
</pfe-content-set>
113+
114+
<pfe-content-set id="my-content-set-history" pfe-tab-history>
115+
<h2 pfe-content-set--header id="my-content-set-header">Heading 1</h2>
116+
<p pfe-content-set--panel id="my-content-set-panel">Panel 1</p>
117+
<h2 pfe-content-set--header id="historyTab2">Heading 2</h2>
118+
<p pfe-content-set--panel>Panel 2</p>
119+
</pfe-content-set>
120+
109121
<script>
110122
suite('<pfe-content-set>', () => {
111123
test('it should have the proper attributes for tabs', () => {
@@ -225,7 +237,7 @@ <h2 pfe-content-set--header>Heading 1</h2>
225237
assert.isNotNull(pfeTabs);
226238

227239
});
228-
240+
229241
test("it should set the correct \"on\" attribute from a parent component that has a pfe-color attribute", done => {
230242
const band = document.querySelector("#band");
231243
const contentSet = band.querySelector("pfe-content-set");
@@ -267,6 +279,97 @@ <h2 pfe-content-set--header>Heading 1</h2>
267279
});
268280
});
269281
});
282+
283+
suite("<pfe-content-set> with history", () => {
284+
test("it should add ids to pfe-tabs, pfe-tab, and pfe-tab-panel if pfe-id attributes are set on pfe-content-set, pfe-content-set--header, and pfe-content-set--panel", () => {
285+
const pfeContentSet = document.querySelector('#my-content-set');
286+
const header = pfeContentSet.querySelector("[pfe-content-set--header]");
287+
const content = pfeContentSet.querySelector("[pfe-content-set--panel]");
288+
289+
const tabs = pfeContentSet.querySelector("pfe-tabs");
290+
const tab1 = tabs.querySelector("pfe-tab");
291+
const panel1 = tabs.querySelector("pfe-tab-panel");
292+
293+
assert.equal(pfeContentSet.id, tabs.getAttribute("pfe-id"));
294+
assert.equal(header.id, tab1.getAttribute("pfe-id"));
295+
assert.equal(content.id, panel1.getAttribute("pfe-id"));
296+
});
297+
298+
test("if pfe-content-set displays as tabs, it should show the correct tab if there is a querystring parameter in the URL", done => {
299+
// the parameter should be equal to the id of the tabset
300+
// the value should be equal to the id of the tab you want opened
301+
const searchParams = new URLSearchParams(window.location.search)
302+
searchParams.set("fromQueryString", "fromQueryStringTab2");
303+
var newPath = window.location.pathname + '?' + searchParams.toString();
304+
history.pushState(null, '', newPath);
305+
306+
const fragment = document.createRange().createContextualFragment(`
307+
<pfe-content-set id="fromQueryString">
308+
<h2 pfe-content-set--header id="tab1">Tab 1</h2>
309+
<p pfe-content-set--panel>Panel 1</p>
310+
<h2 pfe-content-set--header id="fromQueryStringTab2">Tab 2</h2>
311+
<p pfe-content-set--panel>Panel 2</p>
312+
</pfe-content-set>
313+
`);
314+
315+
document.body.appendChild(fragment);
316+
317+
flush(() => {
318+
const contentSet = document.querySelector('#fromQueryString');
319+
const tabs = document.querySelector('[pfe-id="fromQueryString"]');
320+
const tab2 = tabs.querySelector('[pfe-id="fromQueryStringTab2"]');
321+
assert.equal(tabs.selectedIndex, 1);
322+
assert.isTrue(tab2.hasAttribute("aria-selected"));
323+
324+
document.body.removeChild(contentSet);
325+
done();
326+
});
327+
});
328+
329+
test("if pfe-content-set displays as tabs, it should open the first tab if the querystring in the URL doesn't match the id of any of the tabs", done => {
330+
const searchParams = new URLSearchParams(window.location.search)
331+
searchParams.set("fromQueryString", "iDontExist");
332+
var newPath = window.location.pathname + '?' + searchParams.toString();
333+
history.pushState(null, '', newPath);
334+
335+
const fragment = document.createRange().createContextualFragment(`
336+
<pfe-content-set id="fromQueryString">
337+
<h2 pfe-content-set--header id="tab1">Tab 1</h2>
338+
<p pfe-content-set--panel>Panel 1</p>
339+
<h2 pfe-content-set--header id="fromQueryStringTab2">Tab 2</h2>
340+
<p pfe-content-set--panel>Panel 2</p>
341+
</pfe-content-set>
342+
`);
343+
344+
document.body.appendChild(fragment);
345+
346+
flush(() => {
347+
const contentSet = document.querySelector("#fromQueryString");
348+
const tabs = document.querySelector('[pfe-id="fromQueryString"]');
349+
const tab1 = tabs.querySelector('[pfe-id="tab1"]');
350+
assert.equal(tabs.selectedIndex, 0);
351+
assert.isTrue(tab1.hasAttribute("aria-selected"));
352+
353+
document.body.removeChild(contentSet);
354+
done();
355+
});
356+
});
357+
358+
test("if pfe-content-set displays as tabs, it should update the URL on tab selection if the pfe-tab-history attribute is present", done => {
359+
const tabs = document.querySelector('[pfe-id="my-content-set-history"]');
360+
const tab2 = tabs.querySelector('[pfe-id="historyTab2"]');
361+
362+
tab2.click();
363+
364+
flush(() => {
365+
const urlSearchParams = new URLSearchParams(window.location.search);
366+
assert.equal(urlSearchParams.get("my-content-set-history"), "historyTab2");
367+
assert.equal(tabs.selectedIndex, 1);
368+
assert.isTrue(tab2.hasAttribute("aria-selected"));
369+
done();
370+
});
371+
});
372+
});
270373
</script>
271374
</body>
272375
</html>

elements/pfe-tabs/src/pfe-tabs.js

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -210,8 +210,10 @@ class PfeTabs extends PFElement {
210210
const pathname = window.location.pathname;
211211
const urlParams = new URLSearchParams(window.location.search);
212212
const hash = window.location.hash;
213+
const property = this.id || this.getAttribute("pfe-id");
214+
const value = tab.id || tab.getAttribute("pfe-id");
213215

214-
urlParams.set(`${this.id}`, tab.id);
216+
urlParams.set(property, value);
215217
history.pushState({}, "", `${pathname}?${urlParams.toString()}${hash}`);
216218
}
217219

@@ -452,12 +454,21 @@ class PfeTabs extends PFElement {
452454
// we'll give priority to the urlParams.has(`${this.id}`) attribute first
453455
// and fallback to urlParams.has(`pfe-${this.id}`) if it exists. We should
454456
// be able to remove the || part of the if statement in the future
455-
if (
456-
urlParams &&
457-
(urlParams.has(`${this.id}`) || urlParams.has(`pfe-${this.id}`))
458-
) {
459-
const id = urlParams.get(`${this.id}`) || urlParams.get(`pfe-${this.id}`);
460-
tabIndex = this._allTabs().findIndex(tab => tab.id === id);
457+
const tabsetInUrl =
458+
urlParams.has(`${this.id}`) ||
459+
urlParams.has(this.getAttribute("pfe-id")) ||
460+
urlParams.has(`pfe-${this.id}`); // remove this condition when it's no longer used in production
461+
462+
if (urlParams && tabsetInUrl) {
463+
const id =
464+
urlParams.get(`${this.id}`) ||
465+
urlParams.get(this.getAttribute("pfe-id")) ||
466+
urlParams.get(`pfe-${this.id}`); // remove this condition when it's no longer used in production
467+
468+
tabIndex = this._allTabs().findIndex(tab => {
469+
const tabId = tab.id || tab.getAttribute("pfe-id");
470+
return tabId === id;
471+
});
461472
}
462473

463474
return tabIndex;

0 commit comments

Comments
 (0)