@salesforcedevs/dx-components 1.2.3 → 1.2.6-avatar-button-2
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/lwc.config.json +1 -0
- package/package.json +10 -4
- package/src/assets/svg/login-widget-bg.png +0 -0
- package/src/modules/dx/avatarButton/avatarButton.css +129 -0
- package/src/modules/dx/avatarButton/avatarButton.html +165 -0
- package/src/modules/dx/avatarButton/avatarButton.ts +371 -0
- package/src/modules/dx/button/button.css +0 -6
- package/src/modules/dx/featuresList/featuresList.css +1 -1
- package/src/modules/dx/header/header.html +3 -0
- package/src/modules/dx/header/header.ts +4 -0
- package/src/modules/dx/hr/hr.css +0 -1
- package/src/modules/dx/imageAndLabel/imageAndLabel.css +0 -1
- package/src/modules/dx/logo/logo.css +0 -1
- package/src/modules/dx/logo/logo.ts +1 -1
- package/src/modules/dx/metadataBadge/metadataBadge.css +0 -8
- package/src/modules/dx/pagination/pagination.css +1 -2
- package/src/modules/dx/popover/popover.css +15 -0
- package/src/modules/dx/popover/popover.html +1 -0
- package/src/modules/dx/popover/popover.ts +76 -30
- package/src/modules/dx/tab/tab.css +0 -4
- package/src/modules/dxHelpers/commonHeader/commonHeader.css +4 -3
- package/yarn-error.log +19802 -0
- package/LICENSE +0 -12
|
@@ -0,0 +1,371 @@
|
|
|
1
|
+
import { api, LightningElement } from "lwc";
|
|
2
|
+
import Cookies from "js-cookie";
|
|
3
|
+
import { track } from "dxUtils/analytics";
|
|
4
|
+
import defaults from "lodash.defaults";
|
|
5
|
+
import escapeHtml from "escape-html";
|
|
6
|
+
|
|
7
|
+
// TBID Page URLs:
|
|
8
|
+
const TBID_BASE_URL =
|
|
9
|
+
process.env.TBID_BASE_URL ||
|
|
10
|
+
"https://dev1-trailblazer-identity.cs192.force.com";
|
|
11
|
+
const TBID_SETTINGS_URL = `${TBID_BASE_URL}/settings`;
|
|
12
|
+
const TBID_PROFILE_URL = `${TBID_BASE_URL}/id`;
|
|
13
|
+
const TBID_IFRAME_URL = `${TBID_BASE_URL}/secur/logout.jsp`;
|
|
14
|
+
|
|
15
|
+
// API URLs:
|
|
16
|
+
const TBID_API_BASE_URL = `${
|
|
17
|
+
process.env.BASE_URL || "https://developer.salesforce.com"
|
|
18
|
+
}/tbid`;
|
|
19
|
+
const TBID_API_LOGOUT_URL = `${TBID_API_BASE_URL}/logout`;
|
|
20
|
+
const TBID_API_USERINFO_URL = `${TBID_API_BASE_URL}/userinfo`;
|
|
21
|
+
const TBID_API_LOGIN_URL = `${TBID_API_BASE_URL}/dologin`;
|
|
22
|
+
const TBID_API_TOKEN_URL = `${TBID_API_BASE_URL}/token`;
|
|
23
|
+
const TBID_API_PLATFORM_EVENTS_URL = `${TBID_API_BASE_URL}/platform-events`;
|
|
24
|
+
|
|
25
|
+
declare global {
|
|
26
|
+
interface Window {
|
|
27
|
+
SFIDWidget?: {
|
|
28
|
+
logout(): void;
|
|
29
|
+
login(): void;
|
|
30
|
+
openid_response: any;
|
|
31
|
+
disabled: boolean;
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface UserInfo {
|
|
37
|
+
avatarImgSrc?: string;
|
|
38
|
+
company?: string;
|
|
39
|
+
firstName?: string;
|
|
40
|
+
id?: string;
|
|
41
|
+
lastName?: string;
|
|
42
|
+
orgId?: string;
|
|
43
|
+
relationship?: string;
|
|
44
|
+
role?: string;
|
|
45
|
+
username?: string;
|
|
46
|
+
fullName?: string;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
interface EventSourceEvent extends Event {
|
|
50
|
+
id: string;
|
|
51
|
+
retry?: number;
|
|
52
|
+
data: string;
|
|
53
|
+
event?: string;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const enum PlatformEventsType {
|
|
57
|
+
UserDataChange = "UserDataChange",
|
|
58
|
+
UserPhotoChange = "UserPhotoChange",
|
|
59
|
+
UserMerge = "UserMerge"
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const trackableUserInfo = new Set([
|
|
63
|
+
"company",
|
|
64
|
+
"id",
|
|
65
|
+
"orgId",
|
|
66
|
+
"relationship",
|
|
67
|
+
"role"
|
|
68
|
+
]);
|
|
69
|
+
|
|
70
|
+
export default class AvatarButton extends LightningElement {
|
|
71
|
+
@api size: "small" | "medium" | "large" = "medium";
|
|
72
|
+
|
|
73
|
+
private userInfo: UserInfo = {};
|
|
74
|
+
private isLoading = false;
|
|
75
|
+
private settingsUrl = TBID_SETTINGS_URL;
|
|
76
|
+
private profileUrl = TBID_PROFILE_URL;
|
|
77
|
+
private eventSource?: EventSource;
|
|
78
|
+
private _hasRendered = false;
|
|
79
|
+
private _didReceiveUserInfo = false;
|
|
80
|
+
|
|
81
|
+
private get loginUrl() {
|
|
82
|
+
return `${TBID_API_LOGIN_URL}?startURL=${encodeURIComponent(
|
|
83
|
+
window.location.href
|
|
84
|
+
)}`;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
private get isLoggedIn() {
|
|
88
|
+
return this._didReceiveUserInfo;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
connectedCallback() {
|
|
92
|
+
const isLoginSuccessRedirect =
|
|
93
|
+
Cookies.get("tbidLoginSuccess") === "true";
|
|
94
|
+
|
|
95
|
+
if (isLoginSuccessRedirect) {
|
|
96
|
+
this.isLoading = true;
|
|
97
|
+
Cookies.remove("tbidLoginSuccess"); // cleanup
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
window.addEventListener("tbid-login", this.handleSsoLogin);
|
|
101
|
+
window.addEventListener("tbid-logout", this.handleSsoLogout);
|
|
102
|
+
|
|
103
|
+
const isWidgetDisabled = window.SFIDWidget?.disabled;
|
|
104
|
+
const isWidgetLoggedIn = window.SFIDWidget?.openid_response;
|
|
105
|
+
|
|
106
|
+
// If the component loads and (1) we are logging in and (2) the embedded login widget script
|
|
107
|
+
// is in the DOM but (3) the widget has either not loaded or not completed login yet, we
|
|
108
|
+
// defer the userInfo request in order to allow the SFIDWidget the time to trigger it (via
|
|
109
|
+
// it's own login handler) after SSO login. If any of (1) - (3) are false, then we request
|
|
110
|
+
// user info immediately. The check prevents duplicate requests.
|
|
111
|
+
if (
|
|
112
|
+
this.isLoading &&
|
|
113
|
+
(!window.SFIDWidget || (!isWidgetDisabled && !isWidgetLoggedIn)) &&
|
|
114
|
+
document.querySelector('script[src*="authProviderEmbeddedLogin"]')
|
|
115
|
+
) {
|
|
116
|
+
setTimeout(() => {
|
|
117
|
+
if (!window.SFIDWidget?.openid_response) {
|
|
118
|
+
// SFIDWidget not responding in time; handle login ourselves.
|
|
119
|
+
this.trackLoginSuccess();
|
|
120
|
+
this.requestUserInfo();
|
|
121
|
+
}
|
|
122
|
+
}, 1000);
|
|
123
|
+
} else {
|
|
124
|
+
if (this.isLoading) {
|
|
125
|
+
// This is a successful login; track it.
|
|
126
|
+
this.trackLoginSuccess();
|
|
127
|
+
}
|
|
128
|
+
this.requestUserInfo();
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
disconnectedCallback() {
|
|
133
|
+
window.removeEventListener("tbid-login", this.handleSsoLogin);
|
|
134
|
+
window.removeEventListener("tbid-logout", this.handleSsoLogout);
|
|
135
|
+
this.teardownEventSource();
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
private handleUserDataChange = ({ data }: EventSourceEvent) => {
|
|
139
|
+
let parsedEventData: any;
|
|
140
|
+
|
|
141
|
+
try {
|
|
142
|
+
parsedEventData = JSON.parse(data);
|
|
143
|
+
} catch (ex) {
|
|
144
|
+
console.error(
|
|
145
|
+
`Unparseable ${PlatformEventsType.UserDataChange} data received`
|
|
146
|
+
);
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
this.updateAvatarWithUserInfo(parsedEventData);
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
private setupEventSource = () => {
|
|
154
|
+
// subscribe to platform events
|
|
155
|
+
this.eventSource = new EventSource(TBID_API_PLATFORM_EVENTS_URL);
|
|
156
|
+
this.eventSource.addEventListener(
|
|
157
|
+
PlatformEventsType.UserDataChange,
|
|
158
|
+
// eslint-disable-next-line no-undef
|
|
159
|
+
this.handleUserDataChange as EventListener
|
|
160
|
+
);
|
|
161
|
+
this.eventSource.addEventListener(
|
|
162
|
+
PlatformEventsType.UserMerge,
|
|
163
|
+
this.handleSsoLogout
|
|
164
|
+
);
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
private teardownEventSource = () => {
|
|
168
|
+
if (!this.eventSource) {
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
this.eventSource.removeEventListener(
|
|
173
|
+
PlatformEventsType.UserDataChange,
|
|
174
|
+
// eslint-disable-next-line no-undef
|
|
175
|
+
this.handleUserDataChange as EventListener
|
|
176
|
+
);
|
|
177
|
+
this.eventSource.removeEventListener(
|
|
178
|
+
PlatformEventsType.UserMerge,
|
|
179
|
+
this.handleSsoLogout
|
|
180
|
+
);
|
|
181
|
+
this.eventSource.close();
|
|
182
|
+
this.eventSource = undefined;
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
private platformEventsSubscribe = () => {
|
|
186
|
+
this.teardownEventSource();
|
|
187
|
+
this.setupEventSource();
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
// This handles logout from within this component, rather than from SSO via the SFIDWidget.
|
|
191
|
+
private handleLogout = (isSsoLogout: boolean) => {
|
|
192
|
+
this._didReceiveUserInfo = false;
|
|
193
|
+
this.isLoading = false;
|
|
194
|
+
this.updateAvatarWithUserInfo({}); // clear old info
|
|
195
|
+
this.teardownEventSource();
|
|
196
|
+
|
|
197
|
+
if (!isSsoLogout && window.SFIDWidget?.openid_response) {
|
|
198
|
+
// If the SFIDWidget is around and has an access token, and if logout was not *already*
|
|
199
|
+
// triggered by SSO logout, defer to the SFIDWidget. This will ensure that the SSO
|
|
200
|
+
// session is logged out as well.
|
|
201
|
+
window.SFIDWidget.logout();
|
|
202
|
+
} else {
|
|
203
|
+
// Always clear the session token; if not SSO logout, this will also revoke the token with
|
|
204
|
+
// TBID; if SSO logout, that step is already taken care of. No need to await this.
|
|
205
|
+
fetch(`${TBID_API_LOGOUT_URL}?isSsoLogout=${isSsoLogout}`, {
|
|
206
|
+
method: "DELETE",
|
|
207
|
+
credentials: "same-origin"
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
if (!isSsoLogout) {
|
|
211
|
+
// Dropping an iframe is required to fully get TBID to destroy the session; this is
|
|
212
|
+
// a TBID issue and they have requested that we do this for now.
|
|
213
|
+
const ifr = document.createElement("iframe");
|
|
214
|
+
ifr.setAttribute("src", TBID_IFRAME_URL);
|
|
215
|
+
document.body.appendChild(ifr);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
// This will only be called for "seamless SSO" login by the embedded login widget (SFIDWidget) on the website, if it exists.
|
|
221
|
+
private handleSsoLogout = this.handleLogout.bind(this, true);
|
|
222
|
+
|
|
223
|
+
private handleComponentLogout = () => {
|
|
224
|
+
this.trackLogoutClick();
|
|
225
|
+
this.handleLogout(false);
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
// This will only be called for "seamless SSO" login by the embedded login widget (SFIDWidget) on the website, if it exists.
|
|
229
|
+
private handleSsoLogin = async (event: Event) => {
|
|
230
|
+
const { userInfo } = (event as CustomEvent).detail;
|
|
231
|
+
const token = userInfo?.access_token;
|
|
232
|
+
|
|
233
|
+
// `token` should always be defined if an SSO login occurs, but we check for extra safety
|
|
234
|
+
if (token) {
|
|
235
|
+
this.isLoading = true;
|
|
236
|
+
// If an SSO login occurred and we have an SSO access token, we want to start using
|
|
237
|
+
// it rather than some other non-linked access token, for the "seamless SSO"
|
|
238
|
+
// experience
|
|
239
|
+
try {
|
|
240
|
+
await fetch(TBID_API_TOKEN_URL, {
|
|
241
|
+
method: "POST",
|
|
242
|
+
headers: {
|
|
243
|
+
"Content-Type": "application/json"
|
|
244
|
+
},
|
|
245
|
+
body: JSON.stringify({
|
|
246
|
+
token
|
|
247
|
+
})
|
|
248
|
+
});
|
|
249
|
+
this.platformEventsSubscribe();
|
|
250
|
+
} catch (ex) {
|
|
251
|
+
console.error(`Attempt to update token failed: ${ex}`);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
this.trackLoginSuccess();
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
if (userInfo) {
|
|
258
|
+
this.updateAvatarWithUserInfo(userInfo);
|
|
259
|
+
this._didReceiveUserInfo = true;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
this.isLoading = false;
|
|
263
|
+
};
|
|
264
|
+
|
|
265
|
+
private handleComponentLogin = (event: MouseEvent) => {
|
|
266
|
+
if (window.SFIDWidget) {
|
|
267
|
+
// If the SFIDWidget is around, defer to it for login. This will ensure that the SSO
|
|
268
|
+
// session is used if possible.
|
|
269
|
+
event.preventDefault();
|
|
270
|
+
window.SFIDWidget.login();
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
this.trackLoginClick();
|
|
274
|
+
};
|
|
275
|
+
|
|
276
|
+
private trackLoginClick = () => {
|
|
277
|
+
track(document, "custEv_userLogin", {
|
|
278
|
+
clickText: "trailblazer.me",
|
|
279
|
+
clickUrl: this.loginUrl,
|
|
280
|
+
itemTitle: "TBID Login Link",
|
|
281
|
+
elementType: "link"
|
|
282
|
+
});
|
|
283
|
+
};
|
|
284
|
+
|
|
285
|
+
private trackLogoutClick = () => {
|
|
286
|
+
track(document, "custEv_userLogout", {
|
|
287
|
+
clickText: "logout",
|
|
288
|
+
clickUrl: TBID_API_LOGOUT_URL,
|
|
289
|
+
itemTitle: "TBID Logout Link",
|
|
290
|
+
elementType: "link"
|
|
291
|
+
});
|
|
292
|
+
};
|
|
293
|
+
|
|
294
|
+
private trackLoginSuccess = () => {
|
|
295
|
+
track(document, "custEv_userLogin", {
|
|
296
|
+
authenticationMethod: "tbid",
|
|
297
|
+
id: {
|
|
298
|
+
tb: Object.entries(this.userInfo)
|
|
299
|
+
.filter(([key]) => trackableUserInfo.has(key))
|
|
300
|
+
.reduce(
|
|
301
|
+
(infoToTrack, [key, value]) => ({
|
|
302
|
+
...infoToTrack,
|
|
303
|
+
[key]: value
|
|
304
|
+
}),
|
|
305
|
+
{} as Record<string, string>
|
|
306
|
+
)
|
|
307
|
+
}
|
|
308
|
+
});
|
|
309
|
+
};
|
|
310
|
+
|
|
311
|
+
private updateAvatarWithUserInfo = (userInfo: any) => {
|
|
312
|
+
if (!userInfo) {
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
const nextUserInfo: UserInfo = {};
|
|
317
|
+
nextUserInfo.avatarImgSrc = userInfo.photos?.thumbnail;
|
|
318
|
+
nextUserInfo.firstName = userInfo.given_name;
|
|
319
|
+
nextUserInfo.lastName = userInfo.family_name;
|
|
320
|
+
nextUserInfo.username = userInfo.preferred_username;
|
|
321
|
+
nextUserInfo.id = userInfo.user_id;
|
|
322
|
+
nextUserInfo.orgId = userInfo.organization_id;
|
|
323
|
+
nextUserInfo.fullName =
|
|
324
|
+
nextUserInfo.firstName && nextUserInfo.lastName
|
|
325
|
+
? `${nextUserInfo.firstName} ${nextUserInfo.lastName}`
|
|
326
|
+
: nextUserInfo.firstName || nextUserInfo.lastName || "";
|
|
327
|
+
|
|
328
|
+
if (userInfo.custom_attributes) {
|
|
329
|
+
// ProfilePictureUrl updates earlier than photos.thumbnail, so we prefer it if it's available here.
|
|
330
|
+
nextUserInfo.avatarImgSrc =
|
|
331
|
+
userInfo.custom_attributes.ProfilePictureUrl ||
|
|
332
|
+
nextUserInfo.avatarImgSrc;
|
|
333
|
+
nextUserInfo.company = userInfo.custom_attributes.CompanyName;
|
|
334
|
+
nextUserInfo.relationship =
|
|
335
|
+
userInfo.custom_attributes.RelationshipToSalesforce;
|
|
336
|
+
nextUserInfo.role = userInfo.custom_attributes.Role;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// A little extra safety here just to make sure we aren't receiving/displaying any unsafe values...
|
|
340
|
+
const htmlSafeNextUserInfo: UserInfo = Object.fromEntries(
|
|
341
|
+
Object.entries(nextUserInfo).map(([key, val]) => [
|
|
342
|
+
key,
|
|
343
|
+
escapeHtml(val)
|
|
344
|
+
])
|
|
345
|
+
);
|
|
346
|
+
|
|
347
|
+
this.userInfo = defaults(htmlSafeNextUserInfo, this.userInfo);
|
|
348
|
+
};
|
|
349
|
+
|
|
350
|
+
private requestUserInfo = async () => {
|
|
351
|
+
this.isLoading = true;
|
|
352
|
+
|
|
353
|
+
try {
|
|
354
|
+
const userInfoRes = await fetch(TBID_API_USERINFO_URL);
|
|
355
|
+
|
|
356
|
+
if (userInfoRes.ok) {
|
|
357
|
+
if (!this.eventSource) {
|
|
358
|
+
this.setupEventSource();
|
|
359
|
+
}
|
|
360
|
+
const userInfo = await userInfoRes.json();
|
|
361
|
+
this.updateAvatarWithUserInfo(userInfo);
|
|
362
|
+
this._didReceiveUserInfo = true;
|
|
363
|
+
}
|
|
364
|
+
} catch (ex) {
|
|
365
|
+
// Something not directly related to auth went wrong. Unsure what to do here.
|
|
366
|
+
console.error(`Could not request user info: ${ex}`);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
this.isLoading = false;
|
|
370
|
+
};
|
|
371
|
+
}
|
|
@@ -49,7 +49,6 @@
|
|
|
49
49
|
.button.variant_inline dx-icon,
|
|
50
50
|
.button.variant_inline-inherit dx-icon {
|
|
51
51
|
display: inline-block;
|
|
52
|
-
transform: translate(0, -2px);
|
|
53
52
|
}
|
|
54
53
|
|
|
55
54
|
.button > span {
|
|
@@ -61,11 +60,6 @@
|
|
|
61
60
|
font-family: var(--dx-g-font-display);
|
|
62
61
|
}
|
|
63
62
|
|
|
64
|
-
.button.font-display:not(.variant_inline):not(.variant_inline-inherit) > span {
|
|
65
|
-
/* offset accounts for display font's ghost padding */
|
|
66
|
-
margin-top: 5px;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
63
|
.button.font-sans {
|
|
70
64
|
font-family: var(--dx-g-font-sans);
|
|
71
65
|
}
|
|
@@ -19,6 +19,9 @@
|
|
|
19
19
|
onstatechange={handleStateChange}
|
|
20
20
|
></dx-header-search>
|
|
21
21
|
</div>
|
|
22
|
+
<div if:true={showTbidLogin} class="header-tbid-login">
|
|
23
|
+
<dx-avatar-button></dx-avatar-button>
|
|
24
|
+
</div>
|
|
22
25
|
<div if:true={showSignup} class="header-login-signup">
|
|
23
26
|
<dx-button
|
|
24
27
|
aria-label="Sign Up For Salesforce Developer Edition"
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import { HeaderBase } from "dxBaseElements/headerBase";
|
|
2
2
|
|
|
3
3
|
export default class Header extends HeaderBase {
|
|
4
|
+
private get showTbidLogin(): boolean {
|
|
5
|
+
return this.showSignup;
|
|
6
|
+
}
|
|
7
|
+
|
|
4
8
|
private get showSignup(): boolean {
|
|
5
9
|
return this.signupLink
|
|
6
10
|
? (this.mobile && !this.isSearchOpen) || !this.mobile
|
package/src/modules/dx/hr/hr.css
CHANGED
|
@@ -3,6 +3,6 @@ import { LightningElement, api } from "lwc";
|
|
|
3
3
|
export default class Logo extends LightningElement {
|
|
4
4
|
@api href: string = "/";
|
|
5
5
|
@api imgSrc: string = "/assets/svg/salesforce-cloud.svg";
|
|
6
|
-
@api imgAlt: string = "Salesforce
|
|
6
|
+
@api imgAlt: string = "Salesforce logo";
|
|
7
7
|
@api label!: string;
|
|
8
8
|
}
|
|
@@ -32,14 +32,6 @@
|
|
|
32
32
|
letter-spacing: 0.5px;
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
-
.badge span {
|
|
36
|
-
transform: translateY(2px);
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
.badge.size-small span {
|
|
40
|
-
transform: translateY(1px);
|
|
41
|
-
}
|
|
42
|
-
|
|
43
35
|
@media screen and (max-width: 768px) {
|
|
44
36
|
.badge:not(.size-small) {
|
|
45
37
|
height: var(--dx-g-spacing-lg);
|
|
@@ -27,8 +27,7 @@
|
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
.dx-pagination-pages button {
|
|
30
|
-
padding: var(--dx-g-spacing-
|
|
31
|
-
var(--dx-g-spacing-2xs) var(--dx-g-spacing-md);
|
|
30
|
+
padding: var(--dx-g-spacing-xs) var(--dx-g-spacing-md);
|
|
32
31
|
border-radius: var(--dx-g-spacing-lg);
|
|
33
32
|
font-family: var(--dx-g-font-display);
|
|
34
33
|
font-size: var(--dx-g-text-sm);
|
|
@@ -63,3 +63,18 @@
|
|
|
63
63
|
.popover-small {
|
|
64
64
|
width: 136px;
|
|
65
65
|
}
|
|
66
|
+
|
|
67
|
+
.popover-arrow {
|
|
68
|
+
position: absolute;
|
|
69
|
+
background: white;
|
|
70
|
+
width: 8px;
|
|
71
|
+
height: 8px;
|
|
72
|
+
transform: rotate(45deg);
|
|
73
|
+
border: var(--popover-border);
|
|
74
|
+
border-bottom: transparent;
|
|
75
|
+
border-right: transparent;
|
|
76
|
+
box-shadow: var(--dx-g-box-shadow-lg);
|
|
77
|
+
transition: opacity 0.2s linear, transform 0.2s linear;
|
|
78
|
+
transition-delay: 0.02s;
|
|
79
|
+
z-index: var(--dx-c-popover-z-index, var(--dx-g-z-index-max));
|
|
80
|
+
}
|
|
@@ -5,7 +5,7 @@ import {
|
|
|
5
5
|
PopperPlacement
|
|
6
6
|
} from "typings/custom";
|
|
7
7
|
|
|
8
|
-
import { computePosition, flip, size, shift } from "@floating-ui/dom";
|
|
8
|
+
import { computePosition, flip, size, shift, arrow } from "@floating-ui/dom";
|
|
9
9
|
import cx from "classnames";
|
|
10
10
|
import debounce from "debounce";
|
|
11
11
|
import { isPrerender } from "dxUtils/seo";
|
|
@@ -31,6 +31,7 @@ export default class Popover extends LightningElement {
|
|
|
31
31
|
@api openOnHover?: boolean = false; // dropdown opens/closes with hover
|
|
32
32
|
@api width?: string | null = null;
|
|
33
33
|
@api fullWidth?: boolean | null;
|
|
34
|
+
@api showArrow?: boolean | null;
|
|
34
35
|
|
|
35
36
|
@api
|
|
36
37
|
get open() {
|
|
@@ -103,9 +104,11 @@ export default class Popover extends LightningElement {
|
|
|
103
104
|
private _open: boolean = false;
|
|
104
105
|
private _rendered: boolean = isPrerender();
|
|
105
106
|
private _role: string | null = null;
|
|
107
|
+
private arrow: HTMLElement | null = null;
|
|
106
108
|
private control: (HTMLElement & { disabled?: boolean }) | null = null;
|
|
107
109
|
private focusedValue: string | null = null;
|
|
108
110
|
private popover: HTMLElement | null = null;
|
|
111
|
+
private popoverContent: HTMLElement | null = null;
|
|
109
112
|
|
|
110
113
|
// LIFECYCLE METHODS
|
|
111
114
|
|
|
@@ -120,6 +123,12 @@ export default class Popover extends LightningElement {
|
|
|
120
123
|
if (!this.popover) {
|
|
121
124
|
this.popover = this.template.querySelector(".popover-container");
|
|
122
125
|
}
|
|
126
|
+
if (!this.popoverContent) {
|
|
127
|
+
this.popoverContent = this.template.querySelector('.popover');
|
|
128
|
+
}
|
|
129
|
+
if (!this.arrow && this.showArrow) {
|
|
130
|
+
this.arrow = this.template.querySelector(".popover-arrow");
|
|
131
|
+
}
|
|
123
132
|
}
|
|
124
133
|
|
|
125
134
|
// GETTERS
|
|
@@ -232,23 +241,29 @@ export default class Popover extends LightningElement {
|
|
|
232
241
|
this.openPopover();
|
|
233
242
|
};
|
|
234
243
|
|
|
235
|
-
private onSlotChange(e:
|
|
236
|
-
const slot = e.target;
|
|
244
|
+
private onSlotChange(e: Event) {
|
|
245
|
+
const slot = e.target as LightningSlotElement;
|
|
237
246
|
const elements = slot.assignedElements();
|
|
238
247
|
const slotted = elements.length === 0 ? null : elements[0];
|
|
239
248
|
// allows dropdown/select to compose popover
|
|
240
|
-
const
|
|
241
|
-
slotted
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
control
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
249
|
+
const slotElement = (
|
|
250
|
+
slotted?.tagName === "SLOT" ? slotted.firstChild : slotted
|
|
251
|
+
) as HTMLElement | null;
|
|
252
|
+
|
|
253
|
+
const isNoWorkToDo =
|
|
254
|
+
slot.name !== "control" ||
|
|
255
|
+
!slotElement ||
|
|
256
|
+
(this.control && slotElement.isSameNode(this.control));
|
|
257
|
+
|
|
258
|
+
if (isNoWorkToDo) {
|
|
259
|
+
return;
|
|
251
260
|
}
|
|
261
|
+
|
|
262
|
+
slotElement.setAttribute("aria-haspopup", "true");
|
|
263
|
+
slotElement.style.cursor = this.openOnHover ? "default" : "cursor";
|
|
264
|
+
this.control = slotElement;
|
|
265
|
+
this.addControlListeners();
|
|
266
|
+
this.setPosition();
|
|
252
267
|
}
|
|
253
268
|
|
|
254
269
|
private onKeyDown(e: KeyboardEvent): void {
|
|
@@ -277,29 +292,60 @@ export default class Popover extends LightningElement {
|
|
|
277
292
|
if (this.popover && this.control) {
|
|
278
293
|
await Promise.resolve();
|
|
279
294
|
const popoverEl = this.popover;
|
|
295
|
+
const middleware = [flip(), shift({ padding: this.pagePadding })];
|
|
296
|
+
|
|
297
|
+
if (this.fullWidth) {
|
|
298
|
+
middleware.unshift(
|
|
299
|
+
size({
|
|
300
|
+
apply({ rects }) {
|
|
301
|
+
Object.assign(popoverEl.style, {
|
|
302
|
+
width: `${rects.reference.width}px`
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
})
|
|
306
|
+
);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
if (this.arrow) {
|
|
310
|
+
middleware.push(arrow({ element: this.arrow, padding: 24 }));
|
|
311
|
+
}
|
|
280
312
|
|
|
281
313
|
computePosition(this.control, popoverEl, {
|
|
282
314
|
placement: this.placement,
|
|
283
|
-
middleware
|
|
284
|
-
|
|
285
|
-
? [
|
|
286
|
-
size({
|
|
287
|
-
apply({ rects }) {
|
|
288
|
-
Object.assign(popoverEl.style, {
|
|
289
|
-
width: `${rects.reference.width}px`
|
|
290
|
-
});
|
|
291
|
-
}
|
|
292
|
-
})
|
|
293
|
-
]
|
|
294
|
-
: []),
|
|
295
|
-
flip(),
|
|
296
|
-
shift({ padding: this.pagePadding })
|
|
297
|
-
]
|
|
298
|
-
}).then(({ x, y }) => {
|
|
315
|
+
middleware
|
|
316
|
+
}).then(({ x, y, placement, middlewareData }) => {
|
|
299
317
|
Object.assign(popoverEl.style, {
|
|
300
318
|
left: `${x}px`,
|
|
301
319
|
top: `${y}px`
|
|
302
320
|
});
|
|
321
|
+
|
|
322
|
+
if (this.arrow && middlewareData.arrow) {
|
|
323
|
+
const { x: arrowX, y: arrowY } = middlewareData.arrow;
|
|
324
|
+
const popoverPlacementSide = placement.split("-")[0] as
|
|
325
|
+
| "top"
|
|
326
|
+
| "bottom"
|
|
327
|
+
| "left"
|
|
328
|
+
| "right";
|
|
329
|
+
const staticSide = {
|
|
330
|
+
top: "bottom",
|
|
331
|
+
right: "left",
|
|
332
|
+
bottom: "top",
|
|
333
|
+
left: "right"
|
|
334
|
+
}[popoverPlacementSide];
|
|
335
|
+
const arrowStyles = {
|
|
336
|
+
left: arrowX != null ? `${arrowX}px` : "",
|
|
337
|
+
top: arrowY != null ? `${arrowY}px` : "",
|
|
338
|
+
right: "",
|
|
339
|
+
bottom: "",
|
|
340
|
+
[staticSide]: "-4px"
|
|
341
|
+
};
|
|
342
|
+
|
|
343
|
+
if (this.offset && this.popoverContent) {
|
|
344
|
+
arrowStyles.marginTop = getComputedStyle(this.popoverContent).marginTop;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
Object.assign(this.arrow.style, arrowStyles);
|
|
348
|
+
}
|
|
303
349
|
});
|
|
304
350
|
}
|
|
305
351
|
};
|