@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.
@@ -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
  }
@@ -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
@@ -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);
@@ -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
  };
@@ -38,10 +38,6 @@ a {
38
38
  margin: 0 var(--horizontal-spacing);
39
39
  }
40
40
 
41
- .nav-list-item > span {
42
- margin-top: var(--dx-g-spacing-xs);
43
- }
44
-
45
41
  button.nav-list-item_default > dx-icon {
46
42
  margin-left: var(--dx-g-spacing-sm);
47
43
  }