@salesforcedevs/docs-components 1.30.1-locale-picker-2 → 1.30.1-locale-picker-final
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/modules/doc/redocReference/BACK_NAVIGATION.md +233 -0
- package/src/modules/doc/redocReference/redocReference.ts +23 -20
- package/src/modules/doc/unifiedContentLayout/unifiedContentLayout.html +2 -0
- package/src/modules/doc/unifiedContentLayout/unifiedContentLayout.ts +2 -0
package/package.json
CHANGED
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
# Redoc "← Project Title" Back Button
|
|
2
|
+
|
|
3
|
+
How the back link in the Redoc Left Nav behaves, and why each piece exists.
|
|
4
|
+
|
|
5
|
+
## Why this is not a plain `<a href>`
|
|
6
|
+
|
|
7
|
+
Switching locale in the Redoc UI is a **full page reload** (the locale picker is
|
|
8
|
+
an `<a href>` that navigates to the same page in another locale). That has two
|
|
9
|
+
consequences the back link has to deal with:
|
|
10
|
+
|
|
11
|
+
1. `document.referrer` only ever remembers **one** previous page, and each locale
|
|
12
|
+
switch overwrites it. So after a couple of locale switches the referrer no
|
|
13
|
+
longer points at the real page you came from.
|
|
14
|
+
2. The page you should go "back" to must be shown in **whatever locale you are
|
|
15
|
+
currently viewing** — not the locale it was in when you first visited it.
|
|
16
|
+
|
|
17
|
+
To solve (1) we remember the back target ourselves in `sessionStorage` (survives
|
|
18
|
+
reloads, per browser tab). To solve (2) we rewrite the stored URL's locale to the
|
|
19
|
+
current one right before navigating.
|
|
20
|
+
|
|
21
|
+
## URL shape assumption
|
|
22
|
+
|
|
23
|
+
The locale segment sits in a **different position** depending on the content
|
|
24
|
+
type:
|
|
25
|
+
|
|
26
|
+
```
|
|
27
|
+
docs content type
|
|
28
|
+
en-us (default): /docs/bot-api ← no locale segment
|
|
29
|
+
ja-jp: /docs/ja-jp/bot-api ← locale right after "docs" (index 1)
|
|
30
|
+
|
|
31
|
+
references content type
|
|
32
|
+
en-us (default): /docs/service/multi-oas-redoc/references/reference-sample.html
|
|
33
|
+
ja-jp: /docs/service/ja-jp/multi-oas-redoc/references/reference-sample.html
|
|
34
|
+
↑ locale after the product segment (index 2)
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
So: paths always start with `/docs/`, and a non-default locale is an extra
|
|
38
|
+
segment — after `docs` for docs pages, after the product segment for
|
|
39
|
+
references. The default locale `en-us` has no segment. `translateToCurrentLocale`
|
|
40
|
+
picks the index with `isDocContentType` (docs → 1, references → 2).
|
|
41
|
+
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
## The two halves of the feature
|
|
45
|
+
|
|
46
|
+
### Half 1 — Remembering where to go back to (`trackBackTarget`)
|
|
47
|
+
|
|
48
|
+
Runs once per page load, inside `connectedCallback`.
|
|
49
|
+
|
|
50
|
+
```ts
|
|
51
|
+
private trackBackTarget(): void {
|
|
52
|
+
const referrerHref = this.getSameOriginReferrerHref(); // (a)
|
|
53
|
+
if (!referrerHref) {
|
|
54
|
+
return; // (b)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const referrerUrl = new URL(referrerHref);
|
|
58
|
+
if (this.isLocaleHref(referrerUrl.pathname)) { // (c)
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
this.setBackTargetInSession(referrerHref); // (d)
|
|
63
|
+
}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
- **(a)** Get the page we navigated here from — but only if it's a real in-tab,
|
|
67
|
+
same-origin navigation (see `getSameOriginReferrerHref` below).
|
|
68
|
+
- **(b)** No usable referrer (fresh tab, opened a shared link, came from another
|
|
69
|
+
site) → store nothing. The stored target, if any, is left untouched.
|
|
70
|
+
- **(c)** If the referrer is just **this same page in another locale** (a locale
|
|
71
|
+
switch), do **not** overwrite the stored target. This is the key line: it lets
|
|
72
|
+
the real "previous page" survive across any number of locale switches.
|
|
73
|
+
- **(d)** Otherwise this was a normal navigation — remember it as the back target.
|
|
74
|
+
|
|
75
|
+
#### `getSameOriginReferrerHref`
|
|
76
|
+
|
|
77
|
+
```ts
|
|
78
|
+
private getSameOriginReferrerHref(): string | null {
|
|
79
|
+
if (this.initialHistoryLength <= 1 || !document.referrer) {
|
|
80
|
+
return null; // fresh tab / no referrer
|
|
81
|
+
}
|
|
82
|
+
try {
|
|
83
|
+
const referrerUrl = new URL(document.referrer);
|
|
84
|
+
if (referrerUrl.origin !== window.location.origin) {
|
|
85
|
+
return null; // came from another site
|
|
86
|
+
}
|
|
87
|
+
return referrerUrl.href;
|
|
88
|
+
} catch {
|
|
89
|
+
return null; // malformed referrer
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
`initialHistoryLength` is captured at mount (before Redoc pushes its own hash
|
|
95
|
+
entries). `<= 1` means this is the first entry in the tab — there is no real
|
|
96
|
+
in-tab "back".
|
|
97
|
+
|
|
98
|
+
#### `isLocaleHref`
|
|
99
|
+
|
|
100
|
+
```ts
|
|
101
|
+
private isLocaleHref(pathname: string): boolean {
|
|
102
|
+
return this.languages.some((locale) => {
|
|
103
|
+
const href = locale?.link?.href;
|
|
104
|
+
return (
|
|
105
|
+
!!href &&
|
|
106
|
+
new URL(href, window.location.origin).pathname === pathname
|
|
107
|
+
);
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
`this.languages` is the list the locale picker uses — each entry's `link.href` is
|
|
113
|
+
**this page's URL in that locale**. So a pathname is a "locale sibling" if it
|
|
114
|
+
exactly matches one of those hrefs.
|
|
115
|
+
|
|
116
|
+
### Half 2 — Going back (`onBackClick` → `getTranslatedBackTarget` → `translateToCurrentLocale`)
|
|
117
|
+
|
|
118
|
+
```ts
|
|
119
|
+
private onBackClick = (event: Event): void => {
|
|
120
|
+
event.preventDefault();
|
|
121
|
+
|
|
122
|
+
const target = this.getTranslatedBackTarget() ?? this.headerHref; // (1)
|
|
123
|
+
|
|
124
|
+
if (target) {
|
|
125
|
+
window.location.assign(target); // (2)
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
- **(1)** Work out the back target; if none, fall back to `headerHref` (the
|
|
131
|
+
parent reference page passed in by the consumer).
|
|
132
|
+
- **(2)** Navigate there.
|
|
133
|
+
|
|
134
|
+
```ts
|
|
135
|
+
private getTranslatedBackTarget(): string | null {
|
|
136
|
+
const stored = this.getBackTargetFromSession(); // (3)
|
|
137
|
+
if (stored) {
|
|
138
|
+
return this.language
|
|
139
|
+
? this.translateToCurrentLocale(stored) // (4)
|
|
140
|
+
: stored;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const referrer = this.getSameOriginReferrerHref(); // (5)
|
|
144
|
+
return referrer && !this.isLocaleHref(new URL(referrer).pathname)
|
|
145
|
+
? referrer
|
|
146
|
+
: null; // (6)
|
|
147
|
+
}
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
- **(3)** Prefer the target we stored in `sessionStorage`.
|
|
151
|
+
- **(4)** Rewrite its locale to match the locale we're viewing right now.
|
|
152
|
+
- **(5)** No stored target → fall back to the referrer.
|
|
153
|
+
- **(6)** But if that referrer is just this page in another locale, reject it
|
|
154
|
+
(returning `null`) so `onBackClick` uses `headerHref` instead. Without this we
|
|
155
|
+
could ping-pong between locales.
|
|
156
|
+
|
|
157
|
+
```ts
|
|
158
|
+
private translateToCurrentLocale(storedHref: string): string {
|
|
159
|
+
const url = new URL(storedHref, window.location.origin);
|
|
160
|
+
const localeIds = this.languages.map((loc) => loc.id);
|
|
161
|
+
const segments = url.pathname.split("/").filter(Boolean);
|
|
162
|
+
|
|
163
|
+
if (segments[0] === "docs") {
|
|
164
|
+
// Drop the existing locale segment, if any
|
|
165
|
+
if (localeIds.includes(segments[1])) {
|
|
166
|
+
segments.splice(1, 1);
|
|
167
|
+
}
|
|
168
|
+
// Add the current locale (en-us is the default, so it has none)
|
|
169
|
+
if (this.language && this.language !== "en-us") {
|
|
170
|
+
segments.splice(1, 0, this.language);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
url.pathname = "/" + segments.join("/");
|
|
175
|
+
return url.href;
|
|
176
|
+
}
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
Step by step, for a stored `/docs/intro.html` while viewing `ja-jp`:
|
|
180
|
+
|
|
181
|
+
| step | `segments` |
|
|
182
|
+
|------|-----------|
|
|
183
|
+
| split path | `["docs", "intro.html"]` |
|
|
184
|
+
| drop locale (`"intro.html"` is not a locale → no change) | `["docs", "intro.html"]` |
|
|
185
|
+
| add `ja-jp` after `docs` | `["docs", "ja-jp", "intro.html"]` |
|
|
186
|
+
| join | `/docs/ja-jp/intro.html` |
|
|
187
|
+
|
|
188
|
+
All four locale combinations:
|
|
189
|
+
|
|
190
|
+
| current locale | stored URL | result |
|
|
191
|
+
|---|---|---|
|
|
192
|
+
| ja-jp | `/docs/intro.html` | `/docs/ja-jp/intro.html` |
|
|
193
|
+
| en-us | `/docs/ja-jp/intro.html` | `/docs/intro.html` |
|
|
194
|
+
| en-us | `/docs/intro.html` | `/docs/intro.html` (unchanged) |
|
|
195
|
+
| ja-jp | `/docs/ja-jp/intro.html` | `/docs/ja-jp/intro.html` (unchanged) |
|
|
196
|
+
|
|
197
|
+
---
|
|
198
|
+
|
|
199
|
+
## Worked example — the original QA bug
|
|
200
|
+
|
|
201
|
+
> On `advanced-setup` → open `bot-api` → switch ja-jp → switch en → click back.
|
|
202
|
+
|
|
203
|
+
| step | action | `document.referrer` | `sessionStorage[redoc-back-target]` |
|
|
204
|
+
|------|--------|---------------------|--------------------------------------|
|
|
205
|
+
| 1 | land on `advanced-setup` (en) | — | (empty) |
|
|
206
|
+
| 2 | navigate to `bot-api` (en) | `advanced-setup` | **`advanced-setup`** ← stored, not a locale sibling |
|
|
207
|
+
| 3 | switch to ja-jp → `bot-api` (ja) | `bot-api` (en) | `advanced-setup` ← **kept** (referrer is a locale sibling) |
|
|
208
|
+
| 4 | switch to en → `bot-api` (en) | `bot-api` (ja) | `advanced-setup` ← **kept** again |
|
|
209
|
+
| 5 | click **back** | — | reads `advanced-setup`, current locale en-us → no rewrite → navigate to `advanced-setup` ✓ |
|
|
210
|
+
|
|
211
|
+
If at step 4 you had **stayed** in ja-jp and clicked back, step 5 would rewrite
|
|
212
|
+
`advanced-setup` → `/docs/ja-jp/.../advanced-setup`, so you land on the page in
|
|
213
|
+
the locale you're actually viewing.
|
|
214
|
+
|
|
215
|
+
---
|
|
216
|
+
|
|
217
|
+
## All cases at a glance
|
|
218
|
+
|
|
219
|
+
| situation | what happens |
|
|
220
|
+
|---|---|
|
|
221
|
+
| Stored target exists | Go to it, re-localed to the current locale |
|
|
222
|
+
| No stored target, normal same-origin referrer | Go to the referrer |
|
|
223
|
+
| No stored target, referrer is a locale sibling | Ignore it → go to `headerHref` (parent) |
|
|
224
|
+
| No stored target, no referrer (fresh tab / shared link / external) | Go to `headerHref` |
|
|
225
|
+
| Multiple locale switches in a row | Original page is preserved (locale-sibling referrers never overwrite it) |
|
|
226
|
+
|
|
227
|
+
## Tests
|
|
228
|
+
|
|
229
|
+
See `__tests__/redocReference.test.ts`:
|
|
230
|
+
|
|
231
|
+
- *adds the locale to the back target on click (en-us → ja-jp)*
|
|
232
|
+
- *removes the locale from the back target on click (ja-jp → en-us)*
|
|
233
|
+
- *falls back to headerHref when nothing is stored*
|
|
@@ -42,6 +42,8 @@ const ELEMENT_TIMEOUT = 10000;
|
|
|
42
42
|
const ELEMENT_CHECK_INTERVAL = 100;
|
|
43
43
|
const DEFAULT_PROJECT_TITLE = "All Reference";
|
|
44
44
|
const BACK_TARGET_STORAGE_KEY = "redoc-back-target";
|
|
45
|
+
const DOCS_PATH_SEGMENT = "docs";
|
|
46
|
+
const DEFAULT_LOCALE = "en-us";
|
|
45
47
|
|
|
46
48
|
export default class RedocReference extends LightningElement {
|
|
47
49
|
private _referenceConfig: ReferenceConfig = { refList: [] };
|
|
@@ -205,22 +207,25 @@ export default class RedocReference extends LightningElement {
|
|
|
205
207
|
: null;
|
|
206
208
|
}
|
|
207
209
|
|
|
208
|
-
/*
|
|
209
|
-
*
|
|
210
|
+
/* The locale segment sits after "docs" for docs pages,
|
|
211
|
+
* and after the product segment for references.
|
|
210
212
|
*/
|
|
211
213
|
private translateToCurrentLocale(storedHref: string): string {
|
|
212
214
|
const url = new URL(storedHref, window.location.origin);
|
|
213
215
|
const localeIds = this.languages.map((loc) => loc.id);
|
|
214
216
|
const segments = url.pathname.split("/").filter(Boolean);
|
|
215
217
|
|
|
216
|
-
|
|
218
|
+
// docs -> right after "docs"; references -> after the product segment.
|
|
219
|
+
const localeIndex = this.isDocContentType ? 1 : 2;
|
|
220
|
+
|
|
221
|
+
if (segments[0] === DOCS_PATH_SEGMENT) {
|
|
217
222
|
// Drop the existing locale segment, if any
|
|
218
|
-
if (localeIds.includes(segments[
|
|
219
|
-
segments.splice(
|
|
223
|
+
if (localeIds.includes(segments[localeIndex])) {
|
|
224
|
+
segments.splice(localeIndex, 1);
|
|
220
225
|
}
|
|
221
226
|
// Add the current locale
|
|
222
|
-
if (this.language && this.language !==
|
|
223
|
-
segments.splice(
|
|
227
|
+
if (this.language && this.language !== DEFAULT_LOCALE) {
|
|
228
|
+
segments.splice(localeIndex, 0, this.language);
|
|
224
229
|
}
|
|
225
230
|
}
|
|
226
231
|
|
|
@@ -533,22 +538,20 @@ export default class RedocReference extends LightningElement {
|
|
|
533
538
|
}
|
|
534
539
|
|
|
535
540
|
/**
|
|
536
|
-
* Inserts the project header
|
|
541
|
+
* Inserts the project header and LNB footer into Redoc.
|
|
537
542
|
*/
|
|
538
543
|
private insertSidebarNav(redocContainer: HTMLElement): void {
|
|
539
|
-
if (!this.showRedocHeader) {
|
|
540
|
-
return;
|
|
541
|
-
}
|
|
542
|
-
|
|
543
544
|
// Select the LNB and content area of Redoc and insert the requried header.
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
545
|
+
if (this.showRedocHeader) {
|
|
546
|
+
redocContainer
|
|
547
|
+
.querySelectorAll<HTMLElement>(".menu-content, .api-content")
|
|
548
|
+
.forEach((target) => {
|
|
549
|
+
target.insertBefore(
|
|
550
|
+
this.buildProjectHeaderDom(),
|
|
551
|
+
target.firstChild
|
|
552
|
+
);
|
|
553
|
+
});
|
|
554
|
+
}
|
|
552
555
|
|
|
553
556
|
// Locale picker
|
|
554
557
|
if (this.hasLocalePicker) {
|
|
@@ -44,6 +44,8 @@ export default class UnifiedContentLayout extends LightningElement {
|
|
|
44
44
|
@api tocAriaLevel?: string;
|
|
45
45
|
@api languages?: OptionWithLink[];
|
|
46
46
|
@api language?: string;
|
|
47
|
+
@api devCenter: any = null;
|
|
48
|
+
@api brand: any = null;
|
|
47
49
|
|
|
48
50
|
/** Optional origin URL for the footer MFE (e.g. wp-json endpoint). */
|
|
49
51
|
@api origin: string | null = null;
|