@iyulab/components 0.1.5 → 0.1.7

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.
Files changed (82) hide show
  1. package/CHANGELOG.md +18 -18
  2. package/LICENSE +20 -20
  3. package/README.md +33 -33
  4. package/dist/assets/icons/arrow-clockwise.svg.js +1 -1
  5. package/dist/assets/icons/arrow-down.svg.js +1 -1
  6. package/dist/assets/icons/arrow-up.svg.js +1 -1
  7. package/dist/assets/icons/ban.svg.js +1 -1
  8. package/dist/assets/icons/bell-fill.svg.js +1 -1
  9. package/dist/assets/icons/check-circle-fill.svg.js +1 -1
  10. package/dist/assets/icons/check-lg.svg.js +1 -1
  11. package/dist/assets/icons/chevron-down.svg.js +1 -1
  12. package/dist/assets/icons/chevron-left.svg.js +1 -1
  13. package/dist/assets/icons/chevron-right.svg.js +1 -1
  14. package/dist/assets/icons/chevron-up.svg.js +1 -1
  15. package/dist/assets/icons/copy.svg.js +1 -1
  16. package/dist/assets/icons/dash-lg.svg.js +1 -1
  17. package/dist/assets/icons/exclamation-circle-fill.svg.js +1 -1
  18. package/dist/assets/icons/exclamation-triangle-fill.svg.js +1 -1
  19. package/dist/assets/icons/eye-slash.svg.js +1 -1
  20. package/dist/assets/icons/eye.svg.js +1 -1
  21. package/dist/assets/icons/info-circle-fill.svg.js +1 -1
  22. package/dist/assets/icons/layout-sidebar.svg.js +1 -1
  23. package/dist/assets/icons/lightbulb-fill.svg.js +1 -1
  24. package/dist/assets/icons/lightbulb-off.svg.js +1 -1
  25. package/dist/assets/icons/lightbulb.svg.js +1 -1
  26. package/dist/assets/icons/mic-fill.svg.js +1 -1
  27. package/dist/assets/icons/mic-mute-fill.svg.js +1 -1
  28. package/dist/assets/icons/paperclip.svg.js +1 -1
  29. package/dist/assets/icons/pencil-square.svg.js +1 -1
  30. package/dist/assets/icons/plus-lg.svg.js +1 -1
  31. package/dist/assets/icons/search.svg.js +1 -1
  32. package/dist/assets/icons/speedometer.svg.js +1 -1
  33. package/dist/assets/icons/x-lg.svg.js +1 -1
  34. package/dist/assets/styles/dark.css +129 -129
  35. package/dist/assets/styles/dark.css.js +1 -1
  36. package/dist/assets/styles/light.css +129 -129
  37. package/dist/assets/styles/light.css.js +1 -1
  38. package/dist/components/FloatingElement.d.ts +84 -0
  39. package/dist/components/FloatingElement.js +166 -0
  40. package/dist/components/FloatingElement.styles.d.ts +1 -0
  41. package/dist/components/FloatingElement.styles.js +26 -0
  42. package/dist/components/context-menu/ContextMenu.d.ts +20 -39
  43. package/dist/components/context-menu/ContextMenu.js +92 -137
  44. package/dist/components/context-menu/ContextMenu.styles.js +3 -28
  45. package/dist/components/context-menu/index.d.ts +1 -1
  46. package/dist/components/dialog/Dialog.d.ts +14 -7
  47. package/dist/components/dialog/Dialog.js +88 -27
  48. package/dist/components/dialog/Dialog.styles.js +60 -11
  49. package/dist/components/divider/Divider.d.ts +5 -5
  50. package/dist/components/divider/Divider.js +17 -17
  51. package/dist/components/divider/Divider.styles.js +4 -6
  52. package/dist/components/dropdown-menu/DropdownMenu.d.ts +27 -0
  53. package/dist/components/dropdown-menu/DropdownMenu.js +135 -0
  54. package/dist/components/dropdown-menu/DropdownMenu.styles.d.ts +1 -0
  55. package/dist/components/dropdown-menu/DropdownMenu.styles.js +15 -0
  56. package/dist/components/dropdown-menu/index.d.ts +7 -0
  57. package/dist/components/dropdown-menu/index.js +5 -0
  58. package/dist/components/icon/Icon.js +1 -0
  59. package/dist/components/index.d.ts +2 -0
  60. package/dist/components/input/Input.js +1 -1
  61. package/dist/components/input/Input.styles.js +5 -5
  62. package/dist/components/menu/Menu.d.ts +4 -15
  63. package/dist/components/menu/Menu.js +43 -28
  64. package/dist/components/menu/Menu.styles.js +7 -19
  65. package/dist/components/menu-item/MenuItem.d.ts +21 -12
  66. package/dist/components/menu-item/MenuItem.js +184 -15
  67. package/dist/components/menu-item/MenuItem.styles.js +6 -0
  68. package/dist/components/menu-item/index.js +5 -0
  69. package/dist/components/split-panel/SplitPanel.d.ts +10 -2
  70. package/dist/components/split-panel/SplitPanel.js +41 -22
  71. package/dist/components/split-panel/SplitPanel.styles.js +1 -16
  72. package/dist/components/tooltip/Tooltip.d.ts +9 -29
  73. package/dist/components/tooltip/Tooltip.js +27 -149
  74. package/dist/components/tooltip/Tooltip.styles.js +8 -22
  75. package/dist/index.js +4 -0
  76. package/dist/integrations/react/UDropdownMenu.d.ts +8 -0
  77. package/dist/integrations/react/UDropdownMenu.js +10 -0
  78. package/dist/integrations/react/index.d.ts +1 -0
  79. package/dist/integrations/react/index.js +1 -0
  80. package/dist/internals/node-helpers.d.ts +6 -1
  81. package/dist/internals/node-helpers.js +5 -6
  82. package/package.json +59 -59
@@ -0,0 +1,84 @@
1
+ import { CSSResultGroup, PropertyValues } from 'lit';
2
+ import { Placement, VirtualElement } from '@floating-ui/dom';
3
+ import { BaseElement } from './BaseElement.js';
4
+ /**
5
+ * FloatingElement는 팝오버, 툴팁 등 화면에 떠 있는 엘리먼트를 구현하기 위한 기본 클래스입니다.
6
+ * 이 클래스를 상속하여 커스텀 팝오버 컴포넌트를 만들 수 있습니다.
7
+ *
8
+ * 1. for, anchor 속성을 통해 연결될 대상 엘리먼트를 지정할 수 있습니다.
9
+ * 2. visible 속성을 통해 엘리먼트의 표시 여부를 제어할 수 있습니다.
10
+ * 3. strategy, placement, distance 속성을 통해 표시 위치 결정 방식을 설정할 수 있습니다.
11
+ * 4. show() 및 hide() 메서드를 통해 프로그래밍적으로 표시하거나 숨길 수 있습니다.
12
+ */
13
+ export declare class FloatingElement extends BaseElement {
14
+ /** 기본 스타일을 정의합니다. */
15
+ static styles: CSSResultGroup;
16
+ /** 종속된 컴포넌트를 정의합니다. */
17
+ static dependencies: Record<string, typeof BaseElement>;
18
+ private rafId;
19
+ private cleanup;
20
+ /**
21
+ * 연결될 대상 엘리먼트 입니다. 지정하지 않으면 부모 엘리먼트를 사용합니다.
22
+ */
23
+ anchor?: HTMLElement;
24
+ /**
25
+ * 현재 엘리먼트가 보여지는지 여부입니다.
26
+ */
27
+ visible: boolean;
28
+ /**
29
+ * 현재 엘리먼트의 위치 결정 전략입니다.
30
+ *
31
+ * @remarks
32
+ * - `absolute`: 엘리먼트가 문서 흐름에 따라 배치됩니다.
33
+ * - `fixed`: 엘리먼트가 뷰포트에 고정되어 스크롤과 무관하게 위치합니다.
34
+ */
35
+ strategy: 'absolute' | 'fixed';
36
+ /**
37
+ * 앵커 엘리먼트를 찾기 위한 CSS 선택자입니다. querySelector를 사용하여 DOM에서 대상을 검색합니다.
38
+ *
39
+ * @example
40
+ * - `id`로 찾기: for="#target-id"
41
+ * - `class`로 찾기: for=".target-class"
42
+ * - `태그명`으로 찾기: for="target-tag-name"
43
+ * - `속성`으로 찾기: for="[data-role='target']"
44
+ */
45
+ for?: string;
46
+ /**
47
+ * 대상 엘리먼트로부터의 배치 위치입니다.
48
+ * 지정하지 않으면 `floating-ui`의 `autoPlacement`가 적용됩니다.
49
+ */
50
+ placement?: Placement;
51
+ /**
52
+ * 대상 엘리먼트로부터의 거리 (px) 입니다.
53
+ *
54
+ * @default 0
55
+ */
56
+ distance: number;
57
+ connectedCallback(): void;
58
+ disconnectedCallback(): void;
59
+ protected willUpdate(changedProperties: PropertyValues): void;
60
+ /**
61
+ * 현재 엘리먼트를 표시합니다.
62
+ *
63
+ * @param anchor - 위치 계산에 사용할 앵커 엘리먼트입니다. 지정하지 않으면 this.anchor 가 사용됩니다.
64
+ */
65
+ show(anchor?: Element | VirtualElement): Promise<void>;
66
+ /**
67
+ * 현재 엘리먼트를 숨깁니다.
68
+ */
69
+ hide(): Promise<void>;
70
+ /**
71
+ * `floating-ui` 를 이용하여 위치를 계산하고 style 을 적용합니다.
72
+ *
73
+ * @param target - 위치 계산에 사용할 앵커 엘리먼트입니다.
74
+ */
75
+ protected reposition(target: Element | VirtualElement): Promise<void>;
76
+ /**
77
+ * 자동 위치 업데이트를 설정하거나 해제합니다.
78
+ *
79
+ * @param enable - 자동 위치 업데이트 활성화 여부
80
+ */
81
+ protected autoPosition(enable: boolean): void;
82
+ /** 애니메이션 프레임을 사용하여 표시 상태를 스케줄링합니다. */
83
+ private scheduleVisible;
84
+ }
@@ -0,0 +1,166 @@
1
+ import { state, property } from 'lit/decorators.js';
2
+ import { offset, shift, flip, autoPlacement, computePosition, autoUpdate } from '@floating-ui/dom';
3
+ import { getParentElement, querySelectorFrom } from '../internals/node-helpers.js';
4
+ import { BaseElement } from './BaseElement.js';
5
+ import { styles } from './FloatingElement.styles.js';
6
+
7
+ var __defProp = Object.defineProperty;
8
+ var __decorateClass = (decorators, target, key, kind) => {
9
+ var result = void 0 ;
10
+ for (var i = decorators.length - 1, decorator; i >= 0; i--)
11
+ if (decorator = decorators[i])
12
+ result = (decorator(target, key, result) ) || result;
13
+ if (result) __defProp(target, key, result);
14
+ return result;
15
+ };
16
+ class FloatingElement extends BaseElement {
17
+ constructor() {
18
+ super(...arguments);
19
+ // 애니메이션 프레임 아이디
20
+ this.rafId = null;
21
+ // 자동 위치 업데이트 정리 함수
22
+ this.cleanup = null;
23
+ this.visible = false;
24
+ this.strategy = "absolute";
25
+ this.distance = 0;
26
+ }
27
+ static {
28
+ /** 기본 스타일을 정의합니다. */
29
+ this.styles = [super.styles, styles];
30
+ }
31
+ static {
32
+ /** 종속된 컴포넌트를 정의합니다. */
33
+ this.dependencies = {};
34
+ }
35
+ connectedCallback() {
36
+ super.connectedCallback();
37
+ this.anchor ||= getParentElement(this);
38
+ }
39
+ disconnectedCallback() {
40
+ if (this.rafId !== null) cancelAnimationFrame(this.rafId);
41
+ this.autoPosition(false);
42
+ super.disconnectedCallback();
43
+ }
44
+ willUpdate(changedProperties) {
45
+ super.willUpdate(changedProperties);
46
+ if (changedProperties.has("for")) {
47
+ const founded = this.for ? querySelectorFrom(this, this.for) : void 0;
48
+ this.anchor = founded || void 0;
49
+ }
50
+ }
51
+ /**
52
+ * 현재 엘리먼트를 표시합니다.
53
+ *
54
+ * @param anchor - 위치 계산에 사용할 앵커 엘리먼트입니다. 지정하지 않으면 this.anchor 가 사용됩니다.
55
+ */
56
+ async show(anchor) {
57
+ anchor ||= this.anchor;
58
+ if (!anchor) return;
59
+ await this.updateComplete;
60
+ await this.reposition(anchor);
61
+ this.autoPosition(true);
62
+ this.scheduleVisible(true);
63
+ }
64
+ /**
65
+ * 현재 엘리먼트를 숨깁니다.
66
+ */
67
+ async hide() {
68
+ await this.updateComplete;
69
+ this.autoPosition(false);
70
+ this.scheduleVisible(false);
71
+ }
72
+ /**
73
+ * `floating-ui` 를 이용하여 위치를 계산하고 style 을 적용합니다.
74
+ *
75
+ * @param target - 위치 계산에 사용할 앵커 엘리먼트입니다.
76
+ */
77
+ async reposition(target) {
78
+ const middleware = [
79
+ offset({ mainAxis: this.distance }),
80
+ shift(),
81
+ flip()
82
+ ];
83
+ if (!this.placement) {
84
+ middleware.push(autoPlacement());
85
+ }
86
+ const position = await computePosition(target, this, {
87
+ strategy: this.strategy,
88
+ placement: this.placement,
89
+ middleware
90
+ });
91
+ Object.assign(this.style, {
92
+ left: `${position.x}px`,
93
+ top: `${position.y}px`,
94
+ // placement 에 따라 transform-origin 값을 결정합니다.
95
+ transformOrigin: (() => {
96
+ switch (position.placement) {
97
+ case "top":
98
+ case "top-start":
99
+ case "top-end":
100
+ return "center bottom";
101
+ case "bottom":
102
+ case "bottom-start":
103
+ case "bottom-end":
104
+ return "center top";
105
+ case "left":
106
+ case "left-start":
107
+ case "left-end":
108
+ return "right center";
109
+ case "right":
110
+ case "right-start":
111
+ case "right-end":
112
+ return "left center";
113
+ default:
114
+ return "center";
115
+ }
116
+ })()
117
+ });
118
+ }
119
+ /**
120
+ * 자동 위치 업데이트를 설정하거나 해제합니다.
121
+ *
122
+ * @param enable - 자동 위치 업데이트 활성화 여부
123
+ */
124
+ autoPosition(enable) {
125
+ if (this.cleanup) {
126
+ this.cleanup();
127
+ this.cleanup = null;
128
+ }
129
+ if (enable && this.anchor) {
130
+ this.cleanup = autoUpdate(this.anchor, this, () => {
131
+ this.reposition(this.anchor);
132
+ });
133
+ }
134
+ }
135
+ /** 애니메이션 프레임을 사용하여 표시 상태를 스케줄링합니다. */
136
+ scheduleVisible(visible) {
137
+ if (this.rafId !== null) {
138
+ cancelAnimationFrame(this.rafId);
139
+ this.rafId = null;
140
+ }
141
+ this.rafId = requestAnimationFrame(() => {
142
+ this.visible = visible;
143
+ this.rafId = null;
144
+ });
145
+ }
146
+ }
147
+ __decorateClass([
148
+ state()
149
+ ], FloatingElement.prototype, "anchor");
150
+ __decorateClass([
151
+ property({ type: Boolean, reflect: true })
152
+ ], FloatingElement.prototype, "visible");
153
+ __decorateClass([
154
+ property({ type: String, reflect: true })
155
+ ], FloatingElement.prototype, "strategy");
156
+ __decorateClass([
157
+ property({ type: String })
158
+ ], FloatingElement.prototype, "for");
159
+ __decorateClass([
160
+ property({ type: String })
161
+ ], FloatingElement.prototype, "placement");
162
+ __decorateClass([
163
+ property({ type: Number })
164
+ ], FloatingElement.prototype, "distance");
165
+
166
+ export { FloatingElement };
@@ -0,0 +1 @@
1
+ export declare const styles: import('lit').CSSResult;
@@ -0,0 +1,26 @@
1
+ import { css } from 'lit';
2
+
3
+ const styles = css`
4
+ :host {
5
+ position: absolute;
6
+ z-index: 1000;
7
+ top: 0;
8
+ left: 0;
9
+ width: max-content;
10
+
11
+ opacity: 0;
12
+ pointer-events: none;
13
+ }
14
+ :host([strategy="absolute"]) {
15
+ position: absolute;
16
+ }
17
+ :host([strategy="fixed"]) {
18
+ position: fixed;
19
+ }
20
+ :host([visible]) {
21
+ opacity: 0.8;
22
+ pointer-events: auto;
23
+ }
24
+ `;
25
+
26
+ export { styles };
@@ -1,48 +1,29 @@
1
1
  import { PropertyValues } from 'lit';
2
- import { Placement } from '@floating-ui/dom';
3
2
  import { BaseElement } from '../BaseElement.js';
4
- /**
5
- * ContextMenu 컴포넌트는 마우스 오른쪽 버튼 클릭으로 표시되는 컨텍스트 메뉴입니다.
6
- * MenuItem 컴포넌트를 자식으로 포함하여 메뉴를 구성합니다.
7
- */
8
- export declare class ContextMenu extends BaseElement {
3
+ import { FloatingElement } from '../FloatingElement.js';
4
+ export declare class ContextMenu extends FloatingElement {
9
5
  static styles: import('lit').CSSResultGroup[];
10
6
  static dependencies: Record<string, typeof BaseElement>;
11
- /** 마우스 클릭 위치 */
12
- private mouseX;
13
- private mouseY;
14
- /** 컨텍스트 메뉴가 연결될 대상 엘리먼트입니다. 지정하지 않으면 부모 엘리먼트가 대상이 됩니다. */
15
- trigger?: HTMLElement;
16
- /** 현재 메뉴가 열려있는지 여부입니다. */
17
- open: boolean;
18
- /** 'fixed' 위치에서 포지션을 계산할지 여부를 설정합니다. 기본값은 true입니다. */
19
- hoist: boolean;
20
- /** 메뉴가 나타날 위치를 설정합니다. */
21
- placement?: Placement;
22
- /** 메뉴의 위치에서 마우스 포인터까지의 거리를 픽셀단위로 설정합니다. 기본값은 2입니다. */
23
- distance: number;
7
+ preventNative: boolean;
8
+ closeOnSelect: boolean;
9
+ closeOnCheck: boolean;
10
+ private get menu();
24
11
  connectedCallback(): void;
25
12
  disconnectedCallback(): void;
26
- protected updated(changedProperties: PropertyValues): void;
13
+ protected updated(changed: PropertyValues): void;
27
14
  render(): import('lit-html').TemplateResult<1>;
28
- /** 컨텍스트 메뉴를 표시합니다. */
29
- show: (x?: number, y?: number) => Promise<void>;
30
- /** 컨텍스트 메뉴를 숨깁니다. */
15
+ /** 컨텍스트 메뉴는 Virtual anchor를 쓰므로 autoUpdate는 꺼버림 */
16
+ protected autoPosition(_enable: boolean): void;
17
+ private attachAnchor;
18
+ private detachAnchor;
19
+ private onContextMenu;
31
20
  hide: () => Promise<void>;
32
- /**
33
- * 메뉴가 나타날 위치에 따라 transform-origin 값을 반환합니다.
34
- */
35
- private getTransformOrigin;
36
- /** 대상 엘리먼트에 컨텍스트 메뉴 이벤트를 바인딩 합니다. */
37
- private attachTrigger;
38
- /** 대상 엘리먼트에 바인딩된 컨텍스트 메뉴 이벤트를 제거합니다. */
39
- private detachTrigger;
40
- /** 컨텍스트 메뉴 이벤트를 처리합니다. */
41
- private handleContextMenu;
42
- /** 문서 클릭 이벤트를 처리합니다. */
43
- private handleDocumentClick;
44
- /** 문서 컨텍스트 메뉴 이벤트를 처리합니다. */
45
- private handleDocumentContextMenu;
46
- /** 슬롯 변경 이벤트를 처리합니다. */
47
- private handleSlotChange;
21
+ private onDocPointerDown;
22
+ private onWindowBlur;
23
+ private onWindowScrollOrResize;
24
+ private attachDismiss;
25
+ private detachDismiss;
26
+ private onSelect;
27
+ private onCheck;
28
+ private onRequestClose;
48
29
  }
@@ -1,8 +1,7 @@
1
1
  import { html } from 'lit';
2
2
  import { property } from 'lit/decorators.js';
3
- import { offset, shift, flip, autoPlacement, computePosition } from '@floating-ui/dom';
4
- import { getParentElement } from '../../internals/node-helpers.js';
5
- import { BaseElement } from '../BaseElement.js';
3
+ import { FloatingElement } from '../FloatingElement.js';
4
+ import { Menu } from '../menu/Menu.js';
6
5
  import { MenuItem } from '../menu-item/MenuItem.js';
7
6
  import { styles } from './ContextMenu.styles.js';
8
7
 
@@ -15,91 +14,58 @@ var __decorateClass = (decorators, target, key, kind) => {
15
14
  if (result) __defProp(target, key, result);
16
15
  return result;
17
16
  };
18
- class ContextMenu extends BaseElement {
17
+ class ContextMenu extends FloatingElement {
19
18
  constructor() {
20
19
  super(...arguments);
21
- /** 마우스 클릭 위치 */
22
- this.mouseX = 0;
23
- this.mouseY = 0;
24
- this.open = false;
25
- this.hoist = true;
26
- this.distance = 2;
27
- /** 컨텍스트 메뉴를 표시합니다. */
28
- this.show = async (x, y) => {
29
- if (x !== void 0) this.mouseX = x;
30
- if (y !== void 0) this.mouseY = y;
31
- await this.updateComplete;
32
- const virtualElement = {
20
+ this.preventNative = true;
21
+ this.closeOnSelect = true;
22
+ this.closeOnCheck = false;
23
+ this.onContextMenu = async (e) => {
24
+ if (this.preventNative) e.preventDefault();
25
+ this.menu && (this.menu.open = true);
26
+ const virtual = {
33
27
  getBoundingClientRect: () => ({
28
+ x: e.clientX,
29
+ y: e.clientY,
30
+ top: e.clientY,
31
+ left: e.clientX,
32
+ right: e.clientX,
33
+ bottom: e.clientY,
34
34
  width: 0,
35
35
  height: 0,
36
- x: this.mouseX,
37
- y: this.mouseY,
38
- top: this.mouseY,
39
- left: this.mouseX,
40
- right: this.mouseX,
41
- bottom: this.mouseY
36
+ toJSON() {
37
+ return {};
38
+ }
42
39
  })
43
40
  };
44
- const middleware = [
45
- offset({ mainAxis: this.distance }),
46
- shift({ padding: 8 }),
47
- flip()
48
- ];
49
- if (!this.placement) {
50
- middleware.push(autoPlacement({ allowedPlacements: ["bottom-start", "bottom-end", "top-start", "top-end"] }));
51
- }
52
- const position = await computePosition(virtualElement, this, {
53
- placement: this.placement || "bottom-start",
54
- middleware,
55
- strategy: this.hoist ? "fixed" : "absolute"
56
- });
57
- Object.assign(this.style, {
58
- left: `${position.x}px`,
59
- top: `${position.y}px`,
60
- transformOrigin: this.getTransformOrigin(position.placement)
61
- });
62
- await this.updateComplete;
63
- requestAnimationFrame(() => {
64
- this.open = true;
65
- this.emit("u-show");
66
- });
41
+ await super.show(virtual);
42
+ await this.menu?.focusFirstItem?.();
67
43
  };
68
- /** 컨텍스트 메뉴를 숨깁니다. */
69
44
  this.hide = async () => {
70
- await this.updateComplete;
71
- requestAnimationFrame(() => {
72
- this.open = false;
73
- this.emit("u-hide");
74
- });
45
+ await super.hide();
46
+ this.menu && (this.menu.open = false);
47
+ this.querySelectorAll("u-menu-item").forEach((i) => i.closeSubmenu?.());
75
48
  };
76
- /** 컨텍스트 메뉴 이벤트를 처리합니다. */
77
- this.handleContextMenu = (e) => {
78
- e.preventDefault();
79
- e.stopPropagation();
80
- this.show(e.clientX, e.clientY);
49
+ this.onDocPointerDown = (e) => {
50
+ if (!this.visible) return;
51
+ const path = e.composedPath();
52
+ if (path.includes(this)) return;
53
+ this.hide();
81
54
  };
82
- /** 문서 클릭 이벤트를 처리합니다. */
83
- this.handleDocumentClick = (e) => {
84
- if (this.open && !this.contains(e.target)) {
85
- this.hide();
86
- }
55
+ this.onWindowBlur = () => {
56
+ if (this.visible) this.hide();
87
57
  };
88
- /** 문서 컨텍스트 메뉴 이벤트를 처리합니다. */
89
- this.handleDocumentContextMenu = (e) => {
90
- if (this.open && !this.trigger?.contains(e.target)) {
91
- this.hide();
92
- }
58
+ this.onWindowScrollOrResize = () => {
59
+ if (this.visible) this.hide();
93
60
  };
94
- /** 슬롯 변경 이벤트를 처리합니다. */
95
- this.handleSlotChange = () => {
96
- const slot = this.shadowRoot?.querySelector("slot");
97
- const items = slot?.assignedElements({ flatten: true });
98
- items?.forEach((item) => {
99
- item.addEventListener("u-select", () => {
100
- this.hide();
101
- });
102
- });
61
+ this.onSelect = async () => {
62
+ if (this.closeOnSelect) await this.hide();
63
+ };
64
+ this.onCheck = async () => {
65
+ if (this.closeOnCheck) await this.hide();
66
+ };
67
+ this.onRequestClose = async () => {
68
+ await this.hide();
103
69
  };
104
70
  }
105
71
  static {
@@ -107,87 +73,76 @@ class ContextMenu extends BaseElement {
107
73
  }
108
74
  static {
109
75
  this.dependencies = {
76
+ "u-menu": Menu,
110
77
  "u-menu-item": MenuItem
111
78
  };
112
79
  }
80
+ get menu() {
81
+ return this.querySelector("u-menu");
82
+ }
113
83
  connectedCallback() {
114
84
  super.connectedCallback();
115
- this.trigger ||= getParentElement(this);
116
- this.setAttribute("role", "menu");
85
+ this.strategy = "fixed";
86
+ this.placement ||= "bottom-start";
87
+ this.distance ||= 4;
88
+ this.addEventListener("u-select", this.onSelect);
89
+ this.addEventListener("u-check", this.onCheck);
90
+ this.addEventListener("u-request-close", this.onRequestClose);
117
91
  }
118
92
  disconnectedCallback() {
119
- this.detachTrigger(this.trigger);
93
+ this.detachAnchor(this.anchor);
94
+ this.detachDismiss();
95
+ this.removeEventListener("u-select", this.onSelect);
96
+ this.removeEventListener("u-check", this.onCheck);
97
+ this.removeEventListener("u-request-close", this.onRequestClose);
120
98
  super.disconnectedCallback();
121
99
  }
122
- updated(changedProperties) {
123
- super.updated(changedProperties);
124
- if (changedProperties.has("trigger") && this.trigger) {
125
- const oldTrigger = changedProperties.get("trigger");
126
- this.detachTrigger(oldTrigger);
127
- this.attachTrigger(this.trigger);
100
+ updated(changed) {
101
+ super.updated(changed);
102
+ if (changed.has("anchor")) {
103
+ this.detachAnchor(changed.get("anchor"));
104
+ this.attachAnchor(this.anchor);
105
+ }
106
+ if (changed.has("visible")) {
107
+ if (this.visible) this.attachDismiss();
108
+ else this.detachDismiss();
128
109
  }
129
110
  }
130
111
  render() {
131
- return html`
132
- <div class="container">
133
- <slot @slotchange=${this.handleSlotChange}></slot>
134
- </div>
135
- `;
112
+ return html`<slot></slot>`;
136
113
  }
137
- /**
138
- * 메뉴가 나타날 위치에 따라 transform-origin 값을 반환합니다.
139
- */
140
- getTransformOrigin(placement) {
141
- switch (placement) {
142
- case "top":
143
- case "top-start":
144
- case "top-end":
145
- return "center bottom";
146
- case "bottom":
147
- case "bottom-start":
148
- case "bottom-end":
149
- return "center top";
150
- case "left":
151
- case "left-start":
152
- case "left-end":
153
- return "right center";
154
- case "right":
155
- case "right-start":
156
- case "right-end":
157
- return "left center";
158
- default:
159
- return "top left";
160
- }
114
+ /** 컨텍스트 메뉴는 Virtual anchor를 쓰므로 autoUpdate는 꺼버림 */
115
+ autoPosition(_enable) {
161
116
  }
162
- /** 대상 엘리먼트에 컨텍스트 메뉴 이벤트를 바인딩 합니다. */
163
- attachTrigger(trigger) {
164
- if (!trigger) return;
165
- trigger.addEventListener("contextmenu", this.handleContextMenu);
166
- document.addEventListener("click", this.handleDocumentClick);
167
- document.addEventListener("contextmenu", this.handleDocumentContextMenu);
117
+ attachAnchor(target) {
118
+ if (!target) return;
119
+ target.addEventListener("contextmenu", this.onContextMenu);
168
120
  }
169
- /** 대상 엘리먼트에 바인딩된 컨텍스트 메뉴 이벤트를 제거합니다. */
170
- detachTrigger(trigger) {
171
- if (!trigger) return;
172
- trigger.removeEventListener("contextmenu", this.handleContextMenu);
173
- document.removeEventListener("click", this.handleDocumentClick);
174
- document.removeEventListener("contextmenu", this.handleDocumentContextMenu);
121
+ detachAnchor(target) {
122
+ if (!target) return;
123
+ target.removeEventListener("contextmenu", this.onContextMenu);
124
+ }
125
+ attachDismiss() {
126
+ document.addEventListener("pointerdown", this.onDocPointerDown, true);
127
+ window.addEventListener("blur", this.onWindowBlur);
128
+ window.addEventListener("scroll", this.onWindowScrollOrResize, true);
129
+ window.addEventListener("resize", this.onWindowScrollOrResize);
130
+ }
131
+ detachDismiss() {
132
+ document.removeEventListener("pointerdown", this.onDocPointerDown, true);
133
+ window.removeEventListener("blur", this.onWindowBlur);
134
+ window.removeEventListener("scroll", this.onWindowScrollOrResize, true);
135
+ window.removeEventListener("resize", this.onWindowScrollOrResize);
175
136
  }
176
137
  }
177
138
  __decorateClass([
178
- property({ attribute: false })
179
- ], ContextMenu.prototype, "trigger");
180
- __decorateClass([
181
- property({ type: Boolean, reflect: true })
182
- ], ContextMenu.prototype, "open");
183
- __decorateClass([
184
- property({ type: Boolean, reflect: true })
185
- ], ContextMenu.prototype, "hoist");
139
+ property({ type: Boolean })
140
+ ], ContextMenu.prototype, "preventNative");
186
141
  __decorateClass([
187
- property({ type: String })
188
- ], ContextMenu.prototype, "placement");
142
+ property({ type: Boolean })
143
+ ], ContextMenu.prototype, "closeOnSelect");
189
144
  __decorateClass([
190
- property({ type: Number })
191
- ], ContextMenu.prototype, "distance");
145
+ property({ type: Boolean })
146
+ ], ContextMenu.prototype, "closeOnCheck");
192
147
 
193
148
  export { ContextMenu };
@@ -2,38 +2,13 @@ import { css } from 'lit';
2
2
 
3
3
  const styles = css`
4
4
  :host {
5
- position: absolute;
6
- z-index: 1000;
7
- top: 0;
8
- left: 0;
9
- display: block;
10
- width: max-content;
11
- min-width: 180px;
12
-
13
5
  opacity: 0;
14
- pointer-events: none;
15
- transform: scale(0.95);
16
- transition: opacity 0.2s ease, transform 0.2s ease;
6
+ transform: scale(0.98);
7
+ transition: opacity 0.12s ease, transform 0.12s ease;
17
8
  }
18
- :host([open]) {
9
+ :host([visible]) {
19
10
  opacity: 1;
20
11
  transform: scale(1);
21
- pointer-events: auto;
22
- }
23
- :host([hoist]) {
24
- position: fixed;
25
- }
26
-
27
- .container {
28
- display: flex;
29
- flex-direction: column;
30
- gap: 2px;
31
- padding: 4px;
32
- border: 1px solid var(--u-border-color, #ddd);
33
- border-radius: 8px;
34
- background-color: var(--u-bg-color, #fff);
35
- box-shadow: 0 4px 16px rgba(0, 0, 0, 0.2);
36
- overflow: hidden;
37
12
  }
38
13
  `;
39
14