@salesforcedevs/dx-components 1.2.2 → 1.2.6-avatar-button-1

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.
@@ -0,0 +1,366 @@
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
+ // TODO: move to environment variable
8
+ const TBID_BASE_URL = "https://dev1-trailblazer-identity.cs192.force.com";
9
+ const TBID_SETTINGS_URL = `${TBID_BASE_URL}/settings`;
10
+ const TBID_PROFILE_URL = `${TBID_BASE_URL}/id`;
11
+ const TBID_IFRAME_URL = `${TBID_BASE_URL}/secur/logout.jsp`;
12
+ // TODO: move to environment variable
13
+ const TBID_API_BASE_URL = "https://development.developer.salesforce.com/tbid";
14
+ const TBID_API_LOGOUT_URL = `${TBID_API_BASE_URL}/logout`;
15
+ const TBID_API_USERINFO_URL = `${TBID_API_BASE_URL}/userinfo`;
16
+ const TBID_API_LOGIN_URL = `${TBID_API_BASE_URL}/dologin`;
17
+ const TBID_API_TOKEN_URL = `${TBID_API_BASE_URL}/token`;
18
+ const TBID_API_PLATFORM_EVENTS_URL = `${TBID_API_BASE_URL}/platform-events`;
19
+
20
+ declare global {
21
+ interface Window {
22
+ SFIDWidget?: {
23
+ logout(): void;
24
+ login(): void;
25
+ openid_response: any;
26
+ disabled: boolean;
27
+ };
28
+ }
29
+ }
30
+
31
+ export interface UserInfo {
32
+ avatarImgSrc?: string;
33
+ company?: string;
34
+ firstName?: string;
35
+ id?: string;
36
+ lastName?: string;
37
+ orgId?: string;
38
+ relationship?: string;
39
+ role?: string;
40
+ username?: string;
41
+ fullName?: string;
42
+ }
43
+
44
+ interface EventSourceEvent extends Event {
45
+ id: string;
46
+ retry?: number;
47
+ data: string;
48
+ event?: string;
49
+ }
50
+
51
+ const enum PlatformEventsType {
52
+ UserDataChange = "UserDataChange",
53
+ UserPhotoChange = "UserPhotoChange",
54
+ UserMerge = "UserMerge"
55
+ }
56
+
57
+ const trackableUserInfo = new Set([
58
+ "company",
59
+ "id",
60
+ "orgId",
61
+ "relationship",
62
+ "role"
63
+ ]);
64
+
65
+ export default class AvatarButton extends LightningElement {
66
+ @api size: "small" | "medium" | "large" = "medium";
67
+
68
+ private userInfo: UserInfo = {};
69
+ private isLoading = false;
70
+ private settingsUrl = TBID_SETTINGS_URL;
71
+ private profileUrl = TBID_PROFILE_URL;
72
+ private eventSource?: EventSource;
73
+ private _hasRendered = false;
74
+ private _didReceiveUserInfo = false;
75
+
76
+ private get loginUrl() {
77
+ return `${TBID_API_LOGIN_URL}?startURL=${encodeURIComponent(
78
+ window.location.href
79
+ )}`;
80
+ }
81
+
82
+ private get isLoggedIn() {
83
+ return this._didReceiveUserInfo;
84
+ }
85
+
86
+ connectedCallback() {
87
+ const isLoginSuccessRedirect =
88
+ Cookies.get("tbidLoginSuccess") === "true";
89
+
90
+ if (isLoginSuccessRedirect) {
91
+ this.isLoading = true;
92
+ Cookies.remove("tbidLoginSuccess"); // cleanup
93
+ }
94
+
95
+ window.addEventListener("tbid-login", this.handleSsoLogin);
96
+ window.addEventListener("tbid-logout", this.handleSsoLogout);
97
+
98
+ const isWidgetDisabled = window.SFIDWidget?.disabled;
99
+ const isWidgetLoggedIn = window.SFIDWidget?.openid_response;
100
+
101
+ // If the component loads and (1) we are logging in and (2) the embedded login widget script
102
+ // is in the DOM but (3) the widget has either not loaded or not completed login yet, we
103
+ // defer the userInfo request in order to allow the SFIDWidget the time to trigger it (via
104
+ // it's own login handler) after SSO login. If any of (1) - (3) are false, then we request
105
+ // user info immediately. The check prevents duplicate requests.
106
+ if (
107
+ this.isLoading &&
108
+ (!window.SFIDWidget || (!isWidgetDisabled && !isWidgetLoggedIn)) &&
109
+ document.querySelector('script[src*="authProviderEmbeddedLogin"]')
110
+ ) {
111
+ setTimeout(() => {
112
+ if (!window.SFIDWidget?.openid_response) {
113
+ // SFIDWidget not responding in time; handle login ourselves.
114
+ this.trackLoginSuccess();
115
+ this.requestUserInfo();
116
+ }
117
+ }, 1000);
118
+ } else {
119
+ if (this.isLoading) {
120
+ // This is a successful login; track it.
121
+ this.trackLoginSuccess();
122
+ }
123
+ this.requestUserInfo();
124
+ }
125
+ }
126
+
127
+ disconnectedCallback() {
128
+ window.removeEventListener("tbid-login", this.handleSsoLogin);
129
+ window.removeEventListener("tbid-logout", this.handleSsoLogout);
130
+ this.teardownEventSource();
131
+ }
132
+
133
+ private handleUserDataChange = ({ data }: EventSourceEvent) => {
134
+ let parsedEventData: any;
135
+
136
+ try {
137
+ parsedEventData = JSON.parse(data);
138
+ } catch (ex) {
139
+ console.error(
140
+ `Unparseable ${PlatformEventsType.UserDataChange} data received`
141
+ );
142
+ return;
143
+ }
144
+
145
+ this.updateAvatarWithUserInfo(parsedEventData);
146
+ };
147
+
148
+ private setupEventSource = () => {
149
+ // subscribe to platform events
150
+ this.eventSource = new EventSource(TBID_API_PLATFORM_EVENTS_URL);
151
+ this.eventSource.addEventListener(
152
+ PlatformEventsType.UserDataChange,
153
+ // eslint-disable-next-line no-undef
154
+ this.handleUserDataChange as EventListener
155
+ );
156
+ this.eventSource.addEventListener(
157
+ PlatformEventsType.UserMerge,
158
+ this.handleSsoLogout
159
+ );
160
+ };
161
+
162
+ private teardownEventSource = () => {
163
+ if (!this.eventSource) {
164
+ return;
165
+ }
166
+
167
+ this.eventSource.removeEventListener(
168
+ PlatformEventsType.UserDataChange,
169
+ // eslint-disable-next-line no-undef
170
+ this.handleUserDataChange as EventListener
171
+ );
172
+ this.eventSource.removeEventListener(
173
+ PlatformEventsType.UserMerge,
174
+ this.handleSsoLogout
175
+ );
176
+ this.eventSource.close();
177
+ this.eventSource = undefined;
178
+ };
179
+
180
+ private platformEventsSubscribe = () => {
181
+ this.teardownEventSource();
182
+ this.setupEventSource();
183
+ };
184
+
185
+ // This handles logout from within this component, rather than from SSO via the SFIDWidget.
186
+ private handleLogout = (isSsoLogout: boolean) => {
187
+ this._didReceiveUserInfo = false;
188
+ this.isLoading = false;
189
+ this.updateAvatarWithUserInfo({}); // clear old info
190
+ this.teardownEventSource();
191
+
192
+ if (!isSsoLogout && window.SFIDWidget?.openid_response) {
193
+ // If the SFIDWidget is around and has an access token, and if logout was not *already*
194
+ // triggered by SSO logout, defer to the SFIDWidget. This will ensure that the SSO
195
+ // session is logged out as well.
196
+ window.SFIDWidget.logout();
197
+ } else {
198
+ // Always clear the session token; if not SSO logout, this will also revoke the token with
199
+ // TBID; if SSO logout, that step is already taken care of
200
+ fetch(`${TBID_API_LOGOUT_URL}?isSsoLogout=${isSsoLogout}`, {
201
+ method: 'DELETE',
202
+ credentials: 'same-origin'
203
+ }); // no need to await this
204
+
205
+ if (!isSsoLogout) {
206
+ // Dropping an iframe is required to fully get TBID to destroy the session; this is
207
+ // a TBID issue and they have requested that we do this for now.
208
+ const ifr = document.createElement("iframe");
209
+ ifr.setAttribute("src", TBID_IFRAME_URL);
210
+ document.body.appendChild(ifr);
211
+ }
212
+ }
213
+ };
214
+
215
+ // This will only be called for "seamless SSO" login by the embedded login widget (SFIDWidget) on the website, if it exists.
216
+ private handleSsoLogout = this.handleLogout.bind(this, true);
217
+
218
+ private handleComponentLogout = () => {
219
+ this.trackLogoutClick();
220
+ this.handleLogout(false);
221
+ };
222
+
223
+ // This will only be called for "seamless SSO" login by the embedded login widget (SFIDWidget) on the website, if it exists.
224
+ private handleSsoLogin = async (event: Event) => {
225
+ const { userInfo } = (event as CustomEvent).detail;
226
+ const token = userInfo?.access_token;
227
+
228
+ // `token` should always be defined if an SSO login occurs, but we check for extra safety
229
+ if (token) {
230
+ this.isLoading = true;
231
+ // If an SSO login occurred and we have an SSO access token, we want to start using
232
+ // it rather than some other non-linked access token, for the "seamless SSO"
233
+ // experience
234
+ try {
235
+ await fetch(TBID_API_TOKEN_URL, {
236
+ method: "POST",
237
+ headers: {
238
+ "Content-Type": "application/json"
239
+ },
240
+ body: JSON.stringify({
241
+ token
242
+ })
243
+ });
244
+ this.platformEventsSubscribe();
245
+ } catch (ex) {
246
+ console.error(`Attempt to update token failed: ${ex}`);
247
+ }
248
+
249
+ this.trackLoginSuccess();
250
+ }
251
+
252
+ if (userInfo) {
253
+ this.updateAvatarWithUserInfo(userInfo);
254
+ this._didReceiveUserInfo = true;
255
+ }
256
+
257
+ this.isLoading = false;
258
+ };
259
+
260
+ private handleComponentLogin = (event: MouseEvent) => {
261
+ if (window.SFIDWidget) {
262
+ // If the SFIDWidget is around, defer to it for login. This will ensure that the SSO
263
+ // session is used if possible.
264
+ event.preventDefault();
265
+ window.SFIDWidget.login();
266
+ }
267
+
268
+ this.trackLoginClick();
269
+ };
270
+
271
+ private trackLoginClick = () => {
272
+ track(document, "custEv_userLogin", {
273
+ clickText: "trailblazer.me",
274
+ clickUrl: this.loginUrl,
275
+ itemTitle: "TBID Login Link",
276
+ elementType: "link"
277
+ });
278
+ };
279
+
280
+ private trackLogoutClick = () => {
281
+ track(document, "custEv_userLogout", {
282
+ clickText: "logout",
283
+ clickUrl: TBID_API_LOGOUT_URL,
284
+ itemTitle: "TBID Logout Link",
285
+ elementType: "link"
286
+ });
287
+ };
288
+
289
+ private trackLoginSuccess = () => {
290
+ track(document, "custEv_userLogin", {
291
+ authenticationMethod: "tbid",
292
+ id: {
293
+ tb: Object.entries(this.userInfo)
294
+ .filter(([key]) => trackableUserInfo.has(key))
295
+ .reduce(
296
+ (infoToTrack, [key, value]) => ({
297
+ ...infoToTrack,
298
+ [key]: value
299
+ }),
300
+ {} as Record<string, string>
301
+ )
302
+ }
303
+ });
304
+ };
305
+
306
+ private updateAvatarWithUserInfo = (userInfo: any) => {
307
+ if (!userInfo) {
308
+ return;
309
+ }
310
+
311
+ const nextUserInfo: UserInfo = {};
312
+ nextUserInfo.avatarImgSrc = userInfo.photos?.thumbnail;
313
+ nextUserInfo.firstName = userInfo.given_name;
314
+ nextUserInfo.lastName = userInfo.family_name;
315
+ nextUserInfo.username = userInfo.preferred_username;
316
+ nextUserInfo.id = userInfo.user_id;
317
+ nextUserInfo.orgId = userInfo.organization_id;
318
+ nextUserInfo.fullName =
319
+ nextUserInfo.firstName && nextUserInfo.lastName
320
+ ? `${nextUserInfo.firstName} ${nextUserInfo.lastName}`
321
+ : nextUserInfo.firstName || nextUserInfo.lastName || "";
322
+
323
+ if (userInfo.custom_attributes) {
324
+ // ProfilePictureUrl updates earlier than photos.thumbnail, so we prefer it if it's available here.
325
+ nextUserInfo.avatarImgSrc =
326
+ userInfo.custom_attributes.ProfilePictureUrl ||
327
+ nextUserInfo.avatarImgSrc;
328
+ nextUserInfo.company = userInfo.custom_attributes.CompanyName;
329
+ nextUserInfo.relationship =
330
+ userInfo.custom_attributes.RelationshipToSalesforce;
331
+ nextUserInfo.role = userInfo.custom_attributes.Role;
332
+ }
333
+ // TODO: Consider displaying initials if no photo. Is there always a photo even if just a default one?
334
+
335
+ const htmlSafeNextUserInfo: UserInfo = Object.fromEntries(
336
+ Object.entries(nextUserInfo).map(([key, val]) => [
337
+ key,
338
+ escapeHtml(encodeURIComponent(val))
339
+ ])
340
+ );
341
+
342
+ this.userInfo = defaults(htmlSafeNextUserInfo, this.userInfo);
343
+ };
344
+
345
+ private requestUserInfo = async () => {
346
+ this.isLoading = true;
347
+
348
+ try {
349
+ const userInfoRes = await fetch(TBID_API_USERINFO_URL);
350
+
351
+ if (userInfoRes.ok) {
352
+ if (!this.eventSource) {
353
+ this.setupEventSource();
354
+ }
355
+ const userInfo = await userInfoRes.json();
356
+ this.updateAvatarWithUserInfo(userInfo);
357
+ this._didReceiveUserInfo = true;
358
+ }
359
+ } catch (ex) {
360
+ // TODO: Something not directly related to auth went wrong. Unsure what to do here.
361
+ console.error(`Could not request user info: ${ex}`);
362
+ }
363
+
364
+ this.isLoading = false;
365
+ };
366
+ }
@@ -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
  }
@@ -20,5 +20,5 @@
20
20
  .icon {
21
21
  color: #3ba755;
22
22
  padding-right: var(--dx-g-spacing-md);
23
- padding-top: var(--dx-g-spacing-2xs);
23
+ padding-top: var(--dx-g-spacing-sm);
24
24
  }
@@ -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
@@ -13,7 +13,7 @@ h2 {
13
13
  font-family: var(--dx-g-font-display);
14
14
  font-size: var(--dx-g-text-sm);
15
15
  color: var(--dx-g-blue-vibrant-20);
16
- text-transform: uppercase;
16
+ text-transform: capitalize;
17
17
  letter-spacing: 0;
18
18
  line-height: 18px;
19
19
  }
@@ -22,7 +22,6 @@ p::before,
22
22
  h2::before,
23
23
  p::after,
24
24
  h2::after {
25
- transform: translateY(-2px); /* offset for display font ghost padding */
26
25
  content: "";
27
26
  flex: 1 1;
28
27
  border-bottom: 2px solid var(--dx-g-gray-95);
@@ -42,7 +41,7 @@ h2::after {
42
41
  dx-button {
43
42
  display: flex;
44
43
  width: 100%;
45
- text-transform: uppercase;
44
+ text-transform: capitalize;
46
45
  padding: 0 var(--dx-c-hr-padding-horizontal);
47
46
  }
48
47
 
@@ -17,7 +17,6 @@ img {
17
17
  }
18
18
 
19
19
  span {
20
- margin-top: 3px;
21
20
  white-space: nowrap;
22
21
  }
23
22
 
@@ -23,7 +23,6 @@ a:hover {
23
23
  }
24
24
 
25
25
  .logo-link > span {
26
- margin-top: 3px;
27
26
  color: var(--dx-c-logo-label-color);
28
27
  }
29
28
 
@@ -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 log";
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-sm) var(--dx-g-spacing-md)
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
+ }
@@ -1,6 +1,7 @@
1
1
  <template>
2
2
  <slot name="control" onslotchange={onSlotChange}></slot>
3
3
  <div class={containerClassName}>
4
+ <div class="popover-arrow" if:true={showArrow}></div>
4
5
  <div
5
6
  class={className}
6
7
  style={style}
@@ -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: LightningSlotElement) {
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 control =
241
- slotted.tagName === "SLOT" ? slotted.firstChild : slotted;
242
- const onboardControl =
243
- (!this.control || (control && !control.isSameNode(this.control))) &&
244
- control;
245
- if (onboardControl) {
246
- control.setAttribute("aria-haspopup", "true");
247
- control.style.cursor = this.openOnHover ? "default" : "cursor";
248
- this.control = control;
249
- this.addControlListeners();
250
- this.setPosition();
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
- ...(this.fullWidth
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
  };