@ds-mo/ui 1.6.0 → 1.6.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/dist/.build-stamp +1 -1
- package/dist/components/ds-bar-nav.js +1 -1
- package/dist/components/ds-bar-nav.js.map +1 -1
- package/dist/components/ds-menu.js +1 -1
- package/dist/components/ds-panel-nav.js +1 -1
- package/dist/components/ds-panel-nav.js.map +1 -1
- package/dist/components/ds-select.js +1 -1
- package/dist/components/ds-select.js.map +1 -1
- package/dist/components/p-C4VSrrAB.js +2 -0
- package/dist/components/p-C4VSrrAB.js.map +1 -0
- package/dist/types/components/BarNav/bar-nav-tabs-menu-utils.d.ts +1 -1
- package/dist/types/components/Menu/Menu.d.ts +9 -16
- package/dist/types/components/Menu/menu-position.d.ts +20 -0
- package/dist/types/components/Menu/menu-types.d.ts +16 -0
- package/dist/types/components/PanelNav/PanelNav.d.ts +3 -3
- package/dist/types/components/PanelNav/index.d.ts +2 -1
- package/dist/types/components/PanelNav/panel-nav-types.d.ts +6 -0
- package/dist/types/components.d.ts +9 -7
- package/package.json +8 -2
- package/src/angular/proxies.ts +5 -3
- package/src/react/ds-panel-nav.ts +2 -2
- package/src/wc/components/BarNav/BarNav.tsx +1 -1
- package/src/wc/components/BarNav/bar-nav-tabs-menu-utils.ts +1 -1
- package/src/wc/components/Menu/Menu.tsx +78 -67
- package/src/wc/components/Menu/menu-position.ts +78 -0
- package/src/wc/components/Menu/menu-types.ts +17 -0
- package/src/wc/components/PanelNav/PanelNav.tsx +14 -6
- package/src/wc/components/PanelNav/index.ts +7 -1
- package/src/wc/components/PanelNav/panel-nav-types.ts +8 -0
- package/src/wc/components/Select/Select.tsx +1 -1
- package/src/wc/components.d.ts +9 -7
- package/src/wc/utils/index.ts +4 -0
- package/src/wc/utils/resolve-css-length-px.ts +64 -0
- package/src/wc/utils/resolve-css-time-ms.ts +46 -0
- package/src/wc/utils/token-defaults.ts +48 -0
- package/dist/components/p-C4WbCXBj.js +0 -2
- package/dist/components/p-C4WbCXBj.js.map +0 -1
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export type MenuSide = 'top' | 'right' | 'bottom' | 'left';
|
|
2
|
+
export type MenuAlign = 'start' | 'center' | 'end';
|
|
3
|
+
export interface MenuPositionInput {
|
|
4
|
+
anchorRect: Pick<DOMRectReadOnly, 'top' | 'left' | 'right' | 'bottom' | 'width' | 'height'>;
|
|
5
|
+
popupWidth: number;
|
|
6
|
+
popupHeight: number;
|
|
7
|
+
side: MenuSide;
|
|
8
|
+
align: MenuAlign;
|
|
9
|
+
sideOffsetPx: number;
|
|
10
|
+
alignOffsetPx: number;
|
|
11
|
+
viewportPadPx: number;
|
|
12
|
+
viewportWidth: number;
|
|
13
|
+
viewportHeight: number;
|
|
14
|
+
}
|
|
15
|
+
/** Pure layout math for ds-menu — anchor rect + placement props → viewport-fixed x/y. */
|
|
16
|
+
export declare function computeMenuPosition(input: MenuPositionInput): {
|
|
17
|
+
x: number;
|
|
18
|
+
y: number;
|
|
19
|
+
};
|
|
20
|
+
//# sourceMappingURL=menu-position.d.ts.map
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export type { MenuAlign, MenuSide } from './menu-position';
|
|
2
|
+
export interface MenuItemData {
|
|
3
|
+
label: string;
|
|
4
|
+
value?: string;
|
|
5
|
+
subtext?: string;
|
|
6
|
+
isSelected?: boolean;
|
|
7
|
+
isInactive?: boolean;
|
|
8
|
+
isDestructive?: boolean;
|
|
9
|
+
showToggle?: boolean;
|
|
10
|
+
toggleValue?: boolean;
|
|
11
|
+
}
|
|
12
|
+
export interface MenuSection {
|
|
13
|
+
header?: string;
|
|
14
|
+
items: MenuItemData[];
|
|
15
|
+
}
|
|
16
|
+
//# sourceMappingURL=menu-types.d.ts.map
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { EventEmitter } from '../../stencil-public-runtime';
|
|
2
2
|
import type { NavChromeStyle } from '../../nav/nav-chrome';
|
|
3
|
-
import type
|
|
3
|
+
import { type PanelNavGroup, type PanelNavRouterMode, type PanelNavUserActionDetail } from './panel-nav-types';
|
|
4
4
|
export declare class PanelNav {
|
|
5
5
|
/** Chrome style: `navigation` = navigation tokens, `default` = standard app tokens.
|
|
6
6
|
* Property: `navStyle`. HTML attribute: `nav-style`. */
|
|
@@ -40,8 +40,8 @@ export declare class PanelNav {
|
|
|
40
40
|
dsNavToggle: EventEmitter<boolean>;
|
|
41
41
|
/** Emitted when the footer left button (gear / dashboard) is clicked. */
|
|
42
42
|
dsNavFooterAction: EventEmitter<void>;
|
|
43
|
-
/** Emitted when the footer user button is clicked. */
|
|
44
|
-
dsNavUserAction: EventEmitter<
|
|
43
|
+
/** Emitted when the footer user button is clicked. Detail includes the anchor for `ds-menu`. */
|
|
44
|
+
dsNavUserAction: EventEmitter<PanelNavUserActionDetail>;
|
|
45
45
|
el: HTMLElement;
|
|
46
46
|
/** Mirrors `style` but is only updated inside the VT callback so the
|
|
47
47
|
* View Transition captures a clean before/after snapshot. */
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
export type { PanelNavRouterMode, PanelNavItem, PanelNavGroup } from './panel-nav-types';
|
|
1
|
+
export type { PanelNavRouterMode, PanelNavItem, PanelNavGroup, PanelNavUserActionDetail, } from './panel-nav-types';
|
|
2
|
+
export { PANEL_NAV_USER_MENU_ANCHOR_ID } from './panel-nav-types';
|
|
2
3
|
export type { NavChromeStyle } from '../../nav/nav-chrome';
|
|
3
4
|
export { NAV_STYLE_HINT_ATTR, setNavStyleHint, clearNavStyleHint, resolvePanelNavStyle, shouldResyncPanelNavStyle, ensurePanelNavVtStyle, deriveActiveIdFromUrl, parsePanelNavGroups, } from './panel-nav-utils';
|
|
4
5
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -17,4 +17,10 @@ export interface PanelNavGroup {
|
|
|
17
17
|
label?: string;
|
|
18
18
|
items: PanelNavItem[];
|
|
19
19
|
}
|
|
20
|
+
/** Stable `id` on the footer user button — use with `ds-menu` `anchor-id`. */
|
|
21
|
+
export declare const PANEL_NAV_USER_MENU_ANCHOR_ID = "ds-panel-nav-user-menu-anchor";
|
|
22
|
+
/** Detail for `dsNavUserAction` — anchor element for an external `ds-menu`. */
|
|
23
|
+
export interface PanelNavUserActionDetail {
|
|
24
|
+
anchor: HTMLElement;
|
|
25
|
+
}
|
|
20
26
|
//# sourceMappingURL=panel-nav-types.d.ts.map
|
|
@@ -22,9 +22,10 @@ import { FadeSide, FadeSize, FadeSurface } from "./components/Fade/Fade";
|
|
|
22
22
|
import { HeaderBackground } from "./components/Header/Header";
|
|
23
23
|
import { IconColor, IconSize } from "./components/Icon/Icon";
|
|
24
24
|
import { InputType } from "./components/Input/Input";
|
|
25
|
-
import {
|
|
25
|
+
import { MenuItemData, MenuSection } from "./components/Menu/menu-types";
|
|
26
|
+
import { MenuAlign, MenuSide } from "./components/Menu/menu-position";
|
|
26
27
|
import { ModalWidth } from "./components/Modal/Modal";
|
|
27
|
-
import { PanelNavGroup, PanelNavRouterMode } from "./components/PanelNav/panel-nav-types";
|
|
28
|
+
import { PanelNavGroup, PanelNavRouterMode, PanelNavUserActionDetail } from "./components/PanelNav/panel-nav-types";
|
|
28
29
|
import { RadioOption } from "./components/RadioGroup/RadioGroup";
|
|
29
30
|
import { ScrollbarVariant } from "./components/Scrollbar/Scrollbar";
|
|
30
31
|
import { SelectOption } from "./components/Select/Select";
|
|
@@ -57,9 +58,10 @@ export { FadeSide, FadeSize, FadeSurface } from "./components/Fade/Fade";
|
|
|
57
58
|
export { HeaderBackground } from "./components/Header/Header";
|
|
58
59
|
export { IconColor, IconSize } from "./components/Icon/Icon";
|
|
59
60
|
export { InputType } from "./components/Input/Input";
|
|
60
|
-
export {
|
|
61
|
+
export { MenuItemData, MenuSection } from "./components/Menu/menu-types";
|
|
62
|
+
export { MenuAlign, MenuSide } from "./components/Menu/menu-position";
|
|
61
63
|
export { ModalWidth } from "./components/Modal/Modal";
|
|
62
|
-
export { PanelNavGroup, PanelNavRouterMode } from "./components/PanelNav/panel-nav-types";
|
|
64
|
+
export { PanelNavGroup, PanelNavRouterMode, PanelNavUserActionDetail } from "./components/PanelNav/panel-nav-types";
|
|
63
65
|
export { RadioOption } from "./components/RadioGroup/RadioGroup";
|
|
64
66
|
export { ScrollbarVariant } from "./components/Scrollbar/Scrollbar";
|
|
65
67
|
export { SelectOption } from "./components/Select/Select";
|
|
@@ -1442,7 +1444,7 @@ declare global {
|
|
|
1442
1444
|
"dsNavSelect": string;
|
|
1443
1445
|
"dsNavToggle": boolean;
|
|
1444
1446
|
"dsNavFooterAction": void;
|
|
1445
|
-
"dsNavUserAction":
|
|
1447
|
+
"dsNavUserAction": PanelNavUserActionDetail;
|
|
1446
1448
|
}
|
|
1447
1449
|
interface HTMLDsPanelNavElement extends Components.DsPanelNav, HTMLStencilElement {
|
|
1448
1450
|
addEventListener<K extends keyof HTMLDsPanelNavElementEventMap>(type: K, listener: (this: HTMLDsPanelNavElement, ev: DsPanelNavCustomEvent<HTMLDsPanelNavElementEventMap[K]>) => any, options?: boolean | AddEventListenerOptions): void;
|
|
@@ -2330,9 +2332,9 @@ declare namespace LocalJSX {
|
|
|
2330
2332
|
*/
|
|
2331
2333
|
"onDsNavToggle"?: (event: DsPanelNavCustomEvent<boolean>) => void;
|
|
2332
2334
|
/**
|
|
2333
|
-
* Emitted when the footer user button is clicked.
|
|
2335
|
+
* Emitted when the footer user button is clicked. Detail includes the anchor for `ds-menu`.
|
|
2334
2336
|
*/
|
|
2335
|
-
"onDsNavUserAction"?: (event: DsPanelNavCustomEvent<
|
|
2337
|
+
"onDsNavUserAction"?: (event: DsPanelNavCustomEvent<PanelNavUserActionDetail>) => void;
|
|
2336
2338
|
/**
|
|
2337
2339
|
* How items with `href` render: - `anchor` (default): native `<a href>` — works with routers that intercept anchors. - `event`: always `<button>`; host handles navigation via `dsNavSelect`.
|
|
2338
2340
|
* @default 'anchor'
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ds-mo/ui",
|
|
3
|
-
"version": "1.6.
|
|
3
|
+
"version": "1.6.2",
|
|
4
4
|
"description": "CompoMo — composable web components styled with TokoMo design tokens",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"web-components",
|
|
@@ -45,6 +45,10 @@
|
|
|
45
45
|
"types": "./src/wc/nav/index.ts",
|
|
46
46
|
"import": "./src/wc/nav/index.ts"
|
|
47
47
|
},
|
|
48
|
+
"./utils": {
|
|
49
|
+
"types": "./src/wc/utils/index.ts",
|
|
50
|
+
"import": "./src/wc/utils/index.ts"
|
|
51
|
+
},
|
|
48
52
|
"./dist/components/*": "./dist/components/*"
|
|
49
53
|
},
|
|
50
54
|
"files": [
|
|
@@ -55,6 +59,7 @@
|
|
|
55
59
|
"!src/wc/components/**/*.stories.ts",
|
|
56
60
|
"!src/wc/components/**/*.stories.tsx",
|
|
57
61
|
"src/wc/nav",
|
|
62
|
+
"src/wc/utils",
|
|
58
63
|
"src/angular",
|
|
59
64
|
"src/react"
|
|
60
65
|
],
|
|
@@ -63,7 +68,8 @@
|
|
|
63
68
|
"pretypecheck": "node scripts/generate-icon-catalog.mjs",
|
|
64
69
|
"pretest": "node scripts/generate-icon-catalog.mjs",
|
|
65
70
|
"build": "stencil build && node scripts/patch-index-types.mjs && node scripts/verify-icons-externalized.mjs && node scripts/write-build-stamp.mjs",
|
|
66
|
-
"
|
|
71
|
+
"verify:pack": "node scripts/verify-npm-pack.mjs && node scripts/verify-nav-import.mjs",
|
|
72
|
+
"test": "node --import tsx/esm --test tests/panel-nav-utils.test.ts tests/shell-view-transition.test.ts tests/shell-gradient.test.ts tests/resolve-css-length-px.test.ts tests/resolve-css-time-ms.test.ts tests/overlay-positioning.test.ts tests/menu-position.test.ts tests/bar-nav-utils.test.ts tests/bar-nav-tabs-menu-utils.test.ts tests/bar-nav-dom-utils.test.ts tests/icon-catalog.test.ts",
|
|
67
73
|
"test:e2e": "npm run build && playwright test",
|
|
68
74
|
"test:e2e:install": "playwright install chromium",
|
|
69
75
|
"build:docs": "stencil build --docs",
|
package/src/angular/proxies.ts
CHANGED
|
@@ -658,7 +658,7 @@ export class DsPanelNav {
|
|
|
658
658
|
@Output() dsNavSelect = new EventEmitter<CustomEvent<string>>();
|
|
659
659
|
@Output() dsNavToggle = new EventEmitter<CustomEvent<boolean>>();
|
|
660
660
|
@Output() dsNavFooterAction = new EventEmitter<CustomEvent<void>>();
|
|
661
|
-
@Output() dsNavUserAction = new EventEmitter<CustomEvent<
|
|
661
|
+
@Output() dsNavUserAction = new EventEmitter<CustomEvent<IDsPanelNavPanelNavUserActionDetail>>();
|
|
662
662
|
constructor(c: ChangeDetectorRef, r: ElementRef, protected z: NgZone) {
|
|
663
663
|
c.detach();
|
|
664
664
|
this.el = r.nativeElement;
|
|
@@ -666,6 +666,8 @@ export class DsPanelNav {
|
|
|
666
666
|
}
|
|
667
667
|
|
|
668
668
|
|
|
669
|
+
import type { PanelNavUserActionDetail as IDsPanelNavPanelNavUserActionDetail } from '@ds-mo/ui';
|
|
670
|
+
|
|
669
671
|
export declare interface DsPanelNav extends Components.DsPanelNav {
|
|
670
672
|
/**
|
|
671
673
|
* Emitted when a nav item is clicked. Detail = the item's `id`.
|
|
@@ -680,9 +682,9 @@ export declare interface DsPanelNav extends Components.DsPanelNav {
|
|
|
680
682
|
*/
|
|
681
683
|
dsNavFooterAction: EventEmitter<CustomEvent<void>>;
|
|
682
684
|
/**
|
|
683
|
-
* Emitted when the footer user button is clicked.
|
|
685
|
+
* Emitted when the footer user button is clicked. Detail includes the anchor for `ds-menu`.
|
|
684
686
|
*/
|
|
685
|
-
dsNavUserAction: EventEmitter<CustomEvent<
|
|
687
|
+
dsNavUserAction: EventEmitter<CustomEvent<IDsPanelNavPanelNavUserActionDetail>>;
|
|
686
688
|
}
|
|
687
689
|
|
|
688
690
|
|
|
@@ -11,7 +11,7 @@ import type { EventName, StencilReactComponent } from '@stencil/react-output-tar
|
|
|
11
11
|
import { createComponent } from '@stencil/react-output-target/runtime';
|
|
12
12
|
import React from 'react';
|
|
13
13
|
|
|
14
|
-
import { type DsPanelNavCustomEvent } from "@ds-mo/ui";
|
|
14
|
+
import { type DsPanelNavCustomEvent, type PanelNavUserActionDetail } from "@ds-mo/ui";
|
|
15
15
|
import type { Components } from "@ds-mo/ui/dist/components";
|
|
16
16
|
import { DsPanelNav as DsPanelNavElement, defineCustomElement as defineDsPanelNav } from "@ds-mo/ui/dist/components/ds-panel-nav.js";
|
|
17
17
|
|
|
@@ -19,7 +19,7 @@ export type DsPanelNavEvents = {
|
|
|
19
19
|
onDsNavSelect: EventName<DsPanelNavCustomEvent<string>>,
|
|
20
20
|
onDsNavToggle: EventName<DsPanelNavCustomEvent<boolean>>,
|
|
21
21
|
onDsNavFooterAction: EventName<DsPanelNavCustomEvent<void>>,
|
|
22
|
-
onDsNavUserAction: EventName<DsPanelNavCustomEvent<
|
|
22
|
+
onDsNavUserAction: EventName<DsPanelNavCustomEvent<PanelNavUserActionDetail>>
|
|
23
23
|
};
|
|
24
24
|
|
|
25
25
|
export const DsPanelNav: StencilReactComponent<DsPanelNavElement, DsPanelNavEvents, Components.DsPanelNav> = /*@__PURE__*/ createComponent<DsPanelNavElement, DsPanelNavEvents, Components.DsPanelNav>({
|
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
h,
|
|
9
9
|
Host,
|
|
10
10
|
} from '@stencil/core';
|
|
11
|
-
import type { MenuItemData } from '../Menu/
|
|
11
|
+
import type { MenuItemData } from '../Menu/menu-types';
|
|
12
12
|
import type { BarNavActionBackground } from '../BarNavAction/BarNavAction';
|
|
13
13
|
import type { NavChromeStyle } from '../../nav/nav-chrome';
|
|
14
14
|
import { SHELL_BAR_NAV_VT_NAME } from '../../nav/shell-view-transition';
|
|
@@ -1,24 +1,10 @@
|
|
|
1
1
|
import { Component, Prop, State, Event, EventEmitter, Element, Watch, Listen, h, Host } from '@stencil/core';
|
|
2
2
|
import { resolveCssLengthPx, resolveCssTimeMs, TOKEN_DEFAULTS } from '../../utils';
|
|
3
|
+
import { computeMenuPosition, type MenuAlign, type MenuSide } from './menu-position';
|
|
4
|
+
import type { MenuItemData, MenuSection } from './menu-types';
|
|
3
5
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
export interface MenuItemData {
|
|
8
|
-
label: string;
|
|
9
|
-
value?: string;
|
|
10
|
-
subtext?: string;
|
|
11
|
-
isSelected?: boolean;
|
|
12
|
-
isInactive?: boolean;
|
|
13
|
-
isDestructive?: boolean;
|
|
14
|
-
showToggle?: boolean;
|
|
15
|
-
toggleValue?: boolean;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export interface MenuSection {
|
|
19
|
-
header?: string;
|
|
20
|
-
items: MenuItemData[];
|
|
21
|
-
}
|
|
6
|
+
/** rAF retries while the popup mounts or the anchor resolves. */
|
|
7
|
+
const POSITION_RETRY_BUDGET = 8;
|
|
22
8
|
|
|
23
9
|
@Component({
|
|
24
10
|
tag: 'ds-menu',
|
|
@@ -57,12 +43,15 @@ export class Menu {
|
|
|
57
43
|
private scrollResizeHandler: (() => void) | null = null;
|
|
58
44
|
private closeTimer: ReturnType<typeof setTimeout> | null = null;
|
|
59
45
|
private itemEls: HTMLElement[] = [];
|
|
46
|
+
private positionRetryRaf: number | null = null;
|
|
47
|
+
private listenersReady = false;
|
|
60
48
|
|
|
61
49
|
componentDidLoad() {
|
|
62
50
|
if (this.open) this.onOpenChange(true);
|
|
63
51
|
}
|
|
64
52
|
|
|
65
53
|
disconnectedCallback() {
|
|
54
|
+
this.cancelPositionRetry();
|
|
66
55
|
this.teardownListeners();
|
|
67
56
|
}
|
|
68
57
|
|
|
@@ -72,15 +61,19 @@ export class Menu {
|
|
|
72
61
|
this.shouldRender = true;
|
|
73
62
|
this.closing = false;
|
|
74
63
|
this.positionReady = false;
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
this.
|
|
78
|
-
|
|
79
|
-
|
|
64
|
+
this.listenersReady = false;
|
|
65
|
+
this.schedulePositionUpdate(() => {
|
|
66
|
+
if (!this.listenersReady) {
|
|
67
|
+
this.listenersReady = true;
|
|
68
|
+
this.focusInitialItem();
|
|
69
|
+
this.setupListeners();
|
|
70
|
+
}
|
|
80
71
|
});
|
|
81
72
|
} else if (this.shouldRender) {
|
|
73
|
+
this.cancelPositionRetry();
|
|
82
74
|
this.closing = true;
|
|
83
75
|
this.teardownListeners();
|
|
76
|
+
this.listenersReady = false;
|
|
84
77
|
this.closeTimer = setTimeout(() => {
|
|
85
78
|
this.shouldRender = false;
|
|
86
79
|
this.closing = false;
|
|
@@ -89,12 +82,18 @@ export class Menu {
|
|
|
89
82
|
}
|
|
90
83
|
}
|
|
91
84
|
|
|
85
|
+
@Watch('anchor')
|
|
86
|
+
@Watch('anchorId')
|
|
87
|
+
onAnchorChange() {
|
|
88
|
+
if (this.open) this.schedulePositionUpdate();
|
|
89
|
+
}
|
|
90
|
+
|
|
92
91
|
@Watch('side')
|
|
93
92
|
@Watch('align')
|
|
94
93
|
@Watch('sideOffset')
|
|
95
94
|
@Watch('alignOffset')
|
|
96
95
|
onPositionPropsChange() {
|
|
97
|
-
if (this.open) this.
|
|
96
|
+
if (this.open) this.schedulePositionUpdate();
|
|
98
97
|
}
|
|
99
98
|
|
|
100
99
|
private get viewportPadPx(): number {
|
|
@@ -137,51 +136,61 @@ export class Menu {
|
|
|
137
136
|
return this.activeSections.flatMap(s => s.items);
|
|
138
137
|
}
|
|
139
138
|
|
|
140
|
-
private
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
if (!popup) return;
|
|
145
|
-
|
|
146
|
-
const a = anchorEl.getBoundingClientRect();
|
|
147
|
-
const pw = popup.offsetWidth || this.popupFallbackWidthPx;
|
|
148
|
-
const ph = popup.offsetHeight || this.popupFallbackHeightPx;
|
|
149
|
-
const sideOffset = this.sideOffsetPx;
|
|
150
|
-
const alignOffset = this.alignOffsetPx;
|
|
151
|
-
const vpPad = this.viewportPadPx;
|
|
152
|
-
let x = 0, y = 0;
|
|
153
|
-
|
|
154
|
-
switch (this.side) {
|
|
155
|
-
case 'top':
|
|
156
|
-
y = a.top - ph - sideOffset;
|
|
157
|
-
x = this.align === 'start' ? a.left + alignOffset
|
|
158
|
-
: this.align === 'end' ? a.right - pw + alignOffset
|
|
159
|
-
: a.left + a.width / 2 - pw / 2 + alignOffset;
|
|
160
|
-
break;
|
|
161
|
-
case 'bottom':
|
|
162
|
-
y = a.bottom + sideOffset;
|
|
163
|
-
x = this.align === 'start' ? a.left + alignOffset
|
|
164
|
-
: this.align === 'end' ? a.right - pw + alignOffset
|
|
165
|
-
: a.left + a.width / 2 - pw / 2 + alignOffset;
|
|
166
|
-
break;
|
|
167
|
-
case 'left':
|
|
168
|
-
x = a.left - pw - sideOffset;
|
|
169
|
-
y = this.align === 'start' ? a.top + alignOffset
|
|
170
|
-
: this.align === 'end' ? a.bottom - ph + alignOffset
|
|
171
|
-
: a.top + a.height / 2 - ph / 2 + alignOffset;
|
|
172
|
-
break;
|
|
173
|
-
case 'right':
|
|
174
|
-
x = a.right + sideOffset;
|
|
175
|
-
y = this.align === 'start' ? a.top + alignOffset
|
|
176
|
-
: this.align === 'end' ? a.bottom - ph + alignOffset
|
|
177
|
-
: a.top + a.height / 2 - ph / 2 + alignOffset;
|
|
178
|
-
break;
|
|
139
|
+
private cancelPositionRetry() {
|
|
140
|
+
if (this.positionRetryRaf !== null) {
|
|
141
|
+
cancelAnimationFrame(this.positionRetryRaf);
|
|
142
|
+
this.positionRetryRaf = null;
|
|
179
143
|
}
|
|
144
|
+
}
|
|
180
145
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
146
|
+
/** Retry until anchor + popup exist — do not reveal at 0,0 on a failed first pass. */
|
|
147
|
+
private schedulePositionUpdate(onReady?: () => void) {
|
|
148
|
+
if (!this.open) return;
|
|
149
|
+
|
|
150
|
+
this.cancelPositionRetry();
|
|
151
|
+
this.positionReady = false;
|
|
152
|
+
|
|
153
|
+
let remaining = POSITION_RETRY_BUDGET;
|
|
154
|
+
|
|
155
|
+
const attempt = () => {
|
|
156
|
+
this.positionRetryRaf = null;
|
|
157
|
+
if (!this.open) return;
|
|
158
|
+
|
|
159
|
+
if (this.calculatePosition()) {
|
|
160
|
+
this.positionReady = true;
|
|
161
|
+
onReady?.();
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (remaining > 0) {
|
|
166
|
+
remaining -= 1;
|
|
167
|
+
this.positionRetryRaf = requestAnimationFrame(attempt);
|
|
168
|
+
}
|
|
184
169
|
};
|
|
170
|
+
|
|
171
|
+
this.positionRetryRaf = requestAnimationFrame(attempt);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** @returns `true` when anchor and popup were found and `pos` was updated. */
|
|
175
|
+
private calculatePosition(): boolean {
|
|
176
|
+
const anchorEl = this.resolvedAnchor;
|
|
177
|
+
if (!anchorEl) return false;
|
|
178
|
+
const popup = this.el.querySelector('.menu-popup') as HTMLElement | null;
|
|
179
|
+
if (!popup) return false;
|
|
180
|
+
|
|
181
|
+
this.pos = computeMenuPosition({
|
|
182
|
+
anchorRect: anchorEl.getBoundingClientRect(),
|
|
183
|
+
popupWidth: popup.offsetWidth || this.popupFallbackWidthPx,
|
|
184
|
+
popupHeight: popup.offsetHeight || this.popupFallbackHeightPx,
|
|
185
|
+
side: this.side,
|
|
186
|
+
align: this.align,
|
|
187
|
+
sideOffsetPx: this.sideOffsetPx,
|
|
188
|
+
alignOffsetPx: this.alignOffsetPx,
|
|
189
|
+
viewportPadPx: this.viewportPadPx,
|
|
190
|
+
viewportWidth: window.innerWidth,
|
|
191
|
+
viewportHeight: window.innerHeight,
|
|
192
|
+
});
|
|
193
|
+
return true;
|
|
185
194
|
}
|
|
186
195
|
|
|
187
196
|
/** Focus the selected item when present, otherwise the first enabled item. */
|
|
@@ -208,7 +217,9 @@ export class Menu {
|
|
|
208
217
|
this.close();
|
|
209
218
|
};
|
|
210
219
|
|
|
211
|
-
this.scrollResizeHandler = () =>
|
|
220
|
+
this.scrollResizeHandler = () => {
|
|
221
|
+
if (this.open) this.calculatePosition();
|
|
222
|
+
};
|
|
212
223
|
|
|
213
224
|
document.addEventListener('mousedown', this.clickOutsideHandler, true);
|
|
214
225
|
window.addEventListener('scroll', this.scrollResizeHandler, true);
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
export type MenuSide = 'top' | 'right' | 'bottom' | 'left';
|
|
2
|
+
export type MenuAlign = 'start' | 'center' | 'end';
|
|
3
|
+
|
|
4
|
+
export interface MenuPositionInput {
|
|
5
|
+
anchorRect: Pick<DOMRectReadOnly, 'top' | 'left' | 'right' | 'bottom' | 'width' | 'height'>;
|
|
6
|
+
popupWidth: number;
|
|
7
|
+
popupHeight: number;
|
|
8
|
+
side: MenuSide;
|
|
9
|
+
align: MenuAlign;
|
|
10
|
+
sideOffsetPx: number;
|
|
11
|
+
alignOffsetPx: number;
|
|
12
|
+
viewportPadPx: number;
|
|
13
|
+
viewportWidth: number;
|
|
14
|
+
viewportHeight: number;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Pure layout math for ds-menu — anchor rect + placement props → viewport-fixed x/y. */
|
|
18
|
+
export function computeMenuPosition(input: MenuPositionInput): { x: number; y: number } {
|
|
19
|
+
const {
|
|
20
|
+
anchorRect: a,
|
|
21
|
+
popupWidth: pw,
|
|
22
|
+
popupHeight: ph,
|
|
23
|
+
side,
|
|
24
|
+
align,
|
|
25
|
+
sideOffsetPx,
|
|
26
|
+
alignOffsetPx,
|
|
27
|
+
viewportPadPx: vpPad,
|
|
28
|
+
viewportWidth,
|
|
29
|
+
viewportHeight,
|
|
30
|
+
} = input;
|
|
31
|
+
|
|
32
|
+
let x = 0;
|
|
33
|
+
let y = 0;
|
|
34
|
+
|
|
35
|
+
switch (side) {
|
|
36
|
+
case 'top':
|
|
37
|
+
y = a.top - ph - sideOffsetPx;
|
|
38
|
+
x =
|
|
39
|
+
align === 'start'
|
|
40
|
+
? a.left + alignOffsetPx
|
|
41
|
+
: align === 'end'
|
|
42
|
+
? a.right - pw + alignOffsetPx
|
|
43
|
+
: a.left + a.width / 2 - pw / 2 + alignOffsetPx;
|
|
44
|
+
break;
|
|
45
|
+
case 'bottom':
|
|
46
|
+
y = a.bottom + sideOffsetPx;
|
|
47
|
+
x =
|
|
48
|
+
align === 'start'
|
|
49
|
+
? a.left + alignOffsetPx
|
|
50
|
+
: align === 'end'
|
|
51
|
+
? a.right - pw + alignOffsetPx
|
|
52
|
+
: a.left + a.width / 2 - pw / 2 + alignOffsetPx;
|
|
53
|
+
break;
|
|
54
|
+
case 'left':
|
|
55
|
+
x = a.left - pw - sideOffsetPx;
|
|
56
|
+
y =
|
|
57
|
+
align === 'start'
|
|
58
|
+
? a.top + alignOffsetPx
|
|
59
|
+
: align === 'end'
|
|
60
|
+
? a.bottom - ph + alignOffsetPx
|
|
61
|
+
: a.top + a.height / 2 - ph / 2 + alignOffsetPx;
|
|
62
|
+
break;
|
|
63
|
+
case 'right':
|
|
64
|
+
x = a.right + sideOffsetPx;
|
|
65
|
+
y =
|
|
66
|
+
align === 'start'
|
|
67
|
+
? a.top + alignOffsetPx
|
|
68
|
+
: align === 'end'
|
|
69
|
+
? a.bottom - ph + alignOffsetPx
|
|
70
|
+
: a.top + a.height / 2 - ph / 2 + alignOffsetPx;
|
|
71
|
+
break;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return {
|
|
75
|
+
x: Math.min(Math.max(x, vpPad), viewportWidth - pw - vpPad),
|
|
76
|
+
y: Math.min(Math.max(y, vpPad), viewportHeight - ph - vpPad),
|
|
77
|
+
};
|
|
78
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export type { MenuAlign, MenuSide } from './menu-position';
|
|
2
|
+
|
|
3
|
+
export interface MenuItemData {
|
|
4
|
+
label: string;
|
|
5
|
+
value?: string;
|
|
6
|
+
subtext?: string;
|
|
7
|
+
isSelected?: boolean;
|
|
8
|
+
isInactive?: boolean;
|
|
9
|
+
isDestructive?: boolean;
|
|
10
|
+
showToggle?: boolean;
|
|
11
|
+
toggleValue?: boolean;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface MenuSection {
|
|
15
|
+
header?: string;
|
|
16
|
+
items: MenuItemData[];
|
|
17
|
+
}
|
|
@@ -14,7 +14,13 @@ import {
|
|
|
14
14
|
shouldResyncPanelNavGroups,
|
|
15
15
|
shouldResyncPanelNavStyle,
|
|
16
16
|
} from './panel-nav-utils';
|
|
17
|
-
import
|
|
17
|
+
import {
|
|
18
|
+
PANEL_NAV_USER_MENU_ANCHOR_ID,
|
|
19
|
+
type PanelNavGroup,
|
|
20
|
+
type PanelNavItem,
|
|
21
|
+
type PanelNavRouterMode,
|
|
22
|
+
type PanelNavUserActionDetail,
|
|
23
|
+
} from './panel-nav-types';
|
|
18
24
|
|
|
19
25
|
// Module-level WeakMaps
|
|
20
26
|
interface ViewTransition {
|
|
@@ -86,8 +92,8 @@ export class PanelNav {
|
|
|
86
92
|
/** Emitted when the footer left button (gear / dashboard) is clicked. */
|
|
87
93
|
@Event() dsNavFooterAction!: EventEmitter<void>;
|
|
88
94
|
|
|
89
|
-
/** Emitted when the footer user button is clicked. */
|
|
90
|
-
@Event() dsNavUserAction!: EventEmitter<
|
|
95
|
+
/** Emitted when the footer user button is clicked. Detail includes the anchor for `ds-menu`. */
|
|
96
|
+
@Event() dsNavUserAction!: EventEmitter<PanelNavUserActionDetail>;
|
|
91
97
|
|
|
92
98
|
@Element() el!: HTMLElement;
|
|
93
99
|
|
|
@@ -379,8 +385,9 @@ export class PanelNav {
|
|
|
379
385
|
this.dsNavFooterAction.emit();
|
|
380
386
|
}
|
|
381
387
|
|
|
382
|
-
private handleUserAction() {
|
|
383
|
-
|
|
388
|
+
private handleUserAction(e: MouseEvent) {
|
|
389
|
+
const anchor = e.currentTarget as HTMLElement;
|
|
390
|
+
this.dsNavUserAction.emit({ anchor });
|
|
384
391
|
}
|
|
385
392
|
|
|
386
393
|
private clearEdgeOverlayTimer() {
|
|
@@ -587,9 +594,10 @@ export class PanelNav {
|
|
|
587
594
|
{/* Right user — label fades like nav items; right icon cross-fades chevron ↔ circle+initial */}
|
|
588
595
|
<button
|
|
589
596
|
type="button"
|
|
597
|
+
id={PANEL_NAV_USER_MENU_ANCHOR_ID}
|
|
590
598
|
class="panel-nav__item panel-nav__footer-user"
|
|
591
599
|
aria-label={collapsed ? `User: ${userName}` : `User menu for ${userName}`}
|
|
592
|
-
onClick={() => this.handleUserAction()}
|
|
600
|
+
onClick={(e) => this.handleUserAction(e)}
|
|
593
601
|
>
|
|
594
602
|
<span class="panel-nav__item-label panel-nav__footer-user-label">
|
|
595
603
|
<span class="panel-nav__item-label-text">
|
|
@@ -1,4 +1,10 @@
|
|
|
1
|
-
export type {
|
|
1
|
+
export type {
|
|
2
|
+
PanelNavRouterMode,
|
|
3
|
+
PanelNavItem,
|
|
4
|
+
PanelNavGroup,
|
|
5
|
+
PanelNavUserActionDetail,
|
|
6
|
+
} from './panel-nav-types';
|
|
7
|
+
export { PANEL_NAV_USER_MENU_ANCHOR_ID } from './panel-nav-types';
|
|
2
8
|
export type { NavChromeStyle } from '../../nav/nav-chrome';
|
|
3
9
|
export {
|
|
4
10
|
NAV_STYLE_HINT_ATTR,
|
|
@@ -20,3 +20,11 @@ export interface PanelNavGroup {
|
|
|
20
20
|
label?: string;
|
|
21
21
|
items: PanelNavItem[];
|
|
22
22
|
}
|
|
23
|
+
|
|
24
|
+
/** Stable `id` on the footer user button — use with `ds-menu` `anchor-id`. */
|
|
25
|
+
export const PANEL_NAV_USER_MENU_ANCHOR_ID = 'ds-panel-nav-user-menu-anchor';
|
|
26
|
+
|
|
27
|
+
/** Detail for `dsNavUserAction` — anchor element for an external `ds-menu`. */
|
|
28
|
+
export interface PanelNavUserActionDetail {
|
|
29
|
+
anchor: HTMLElement;
|
|
30
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Component, Prop, State, Event, EventEmitter, Element, Watch, Listen, h, Host } from '@stencil/core';
|
|
2
|
-
import type { MenuItemData } from '../Menu/
|
|
2
|
+
import type { MenuItemData } from '../Menu/menu-types';
|
|
3
3
|
|
|
4
4
|
export interface SelectOption {
|
|
5
5
|
label: string;
|