@ncds/ui-admin 1.8.24 → 1.8.25-alpha.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.
Files changed (86) hide show
  1. package/dist/cjs/src/components/feedback-and-status/badge/Badge.js +1 -1
  2. package/dist/cjs/src/components/forms-and-input/combo-box/ComboBox.js +109 -28
  3. package/dist/cjs/src/components/forms-and-input/combo-box/__tests__/ComboBox.portal.test.js +581 -0
  4. package/dist/cjs/src/components/navigation/bread-crumb/BreadCrumb.js +14 -6
  5. package/dist/cjs/src/components/navigation/bread-crumb/__tests__/BreadCrumb.test.js +207 -0
  6. package/dist/cjs/src/generated/scoped-css.js +1 -1
  7. package/dist/cjs/src/hooks/__tests__/useFloatingPosition.ssr.test.js +29 -0
  8. package/dist/cjs/src/hooks/__tests__/useFloatingPosition.test.js +322 -0
  9. package/dist/cjs/src/hooks/dropdown/useDropdown.js +2 -1
  10. package/dist/cjs/src/hooks/useFloatingPosition.js +14 -53
  11. package/dist/cjs/src/utils/dropdown/__tests__/dropdownUtils.test.js +316 -0
  12. package/dist/cjs/src/utils/dropdown/dropdownUtils.js +8 -12
  13. package/dist/cjs/src/utils/portal-position/__tests__/portalPosition.test.js +227 -0
  14. package/dist/cjs/src/utils/portal-position/__tests__/portalPosition.tracking.test.js +397 -0
  15. package/dist/cjs/src/utils/portal-position/index.js +16 -0
  16. package/dist/cjs/src/utils/portal-position/portalPosition.js +162 -0
  17. package/dist/esm/src/components/feedback-and-status/badge/Badge.js +1 -1
  18. package/dist/esm/src/components/forms-and-input/combo-box/ComboBox.js +109 -28
  19. package/dist/esm/src/components/forms-and-input/combo-box/__tests__/ComboBox.portal.test.js +578 -0
  20. package/dist/esm/src/components/navigation/bread-crumb/BreadCrumb.js +14 -6
  21. package/dist/esm/src/components/navigation/bread-crumb/__tests__/BreadCrumb.test.js +204 -0
  22. package/dist/esm/src/generated/scoped-css.js +1 -1
  23. package/dist/esm/src/hooks/__tests__/useFloatingPosition.ssr.test.js +26 -0
  24. package/dist/esm/src/hooks/__tests__/useFloatingPosition.test.js +319 -0
  25. package/dist/esm/src/hooks/dropdown/useDropdown.js +2 -1
  26. package/dist/esm/src/hooks/useFloatingPosition.js +15 -54
  27. package/dist/esm/src/utils/dropdown/__tests__/dropdownUtils.test.js +314 -0
  28. package/dist/esm/src/utils/dropdown/dropdownUtils.js +8 -12
  29. package/dist/esm/src/utils/portal-position/__tests__/portalPosition.test.js +225 -0
  30. package/dist/esm/src/utils/portal-position/__tests__/portalPosition.tracking.test.js +394 -0
  31. package/dist/esm/src/utils/portal-position/index.js +1 -0
  32. package/dist/esm/src/utils/portal-position/portalPosition.js +150 -0
  33. package/dist/temp/assets/scripts/notification/const/sizes.d.ts +1 -1
  34. package/dist/temp/src/components/action/button-group/ButtonGroup.d.ts +1 -1
  35. package/dist/temp/src/components/feedback-and-status/badge/Badge.d.ts +1 -1
  36. package/dist/temp/src/components/feedback-and-status/badge/Badge.js +1 -1
  37. package/dist/temp/src/components/forms-and-input/combo-box/ComboBox.d.ts +6 -0
  38. package/dist/temp/src/components/forms-and-input/combo-box/ComboBox.js +54 -4
  39. package/dist/temp/src/components/forms-and-input/combo-box/__tests__/ComboBox.portal.test.d.ts +1 -0
  40. package/dist/temp/src/components/forms-and-input/combo-box/__tests__/ComboBox.portal.test.js +418 -0
  41. package/dist/temp/src/components/navigation/bread-crumb/BreadCrumb.d.ts +3 -1
  42. package/dist/temp/src/components/navigation/bread-crumb/BreadCrumb.js +9 -5
  43. package/dist/temp/src/components/navigation/bread-crumb/__tests__/BreadCrumb.test.d.ts +1 -0
  44. package/dist/temp/src/components/navigation/bread-crumb/__tests__/BreadCrumb.test.js +161 -0
  45. package/dist/temp/src/generated/scoped-css.js +1 -1
  46. package/dist/temp/src/hooks/__tests__/useFloatingPosition.ssr.test.d.ts +1 -0
  47. package/dist/temp/src/hooks/__tests__/useFloatingPosition.ssr.test.js +26 -0
  48. package/dist/temp/src/hooks/__tests__/useFloatingPosition.test.d.ts +1 -0
  49. package/dist/temp/src/hooks/__tests__/useFloatingPosition.test.js +205 -0
  50. package/dist/temp/src/hooks/dropdown/useDropdown.d.ts +3 -1
  51. package/dist/temp/src/hooks/dropdown/useDropdown.js +2 -2
  52. package/dist/temp/src/hooks/dropdown/useDropdownPosition.d.ts +3 -3
  53. package/dist/temp/src/hooks/useFloatingPosition.d.ts +6 -4
  54. package/dist/temp/src/hooks/useFloatingPosition.js +14 -47
  55. package/dist/temp/src/types/dropdown/dropdown.d.ts +2 -1
  56. package/dist/temp/src/utils/dropdown/__tests__/dropdownUtils.test.d.ts +1 -0
  57. package/dist/temp/src/utils/dropdown/__tests__/dropdownUtils.test.js +174 -0
  58. package/dist/temp/src/utils/dropdown/dropdownUtils.js +8 -9
  59. package/dist/temp/src/utils/portal-position/__tests__/portalPosition.test.d.ts +1 -0
  60. package/dist/temp/src/utils/portal-position/__tests__/portalPosition.test.js +181 -0
  61. package/dist/temp/src/utils/portal-position/__tests__/portalPosition.tracking.test.d.ts +1 -0
  62. package/dist/temp/src/utils/portal-position/__tests__/portalPosition.tracking.test.js +301 -0
  63. package/dist/temp/src/utils/portal-position/index.d.ts +1 -0
  64. package/dist/temp/src/utils/portal-position/index.js +1 -0
  65. package/dist/temp/src/utils/portal-position/portalPosition.d.ts +67 -0
  66. package/dist/temp/src/utils/portal-position/portalPosition.js +88 -0
  67. package/dist/types/assets/scripts/notification/const/sizes.d.ts +1 -1
  68. package/dist/types/src/components/action/button-group/ButtonGroup.d.ts +1 -1
  69. package/dist/types/src/components/feedback-and-status/badge/Badge.d.ts +1 -1
  70. package/dist/types/src/components/forms-and-input/combo-box/ComboBox.d.ts +6 -0
  71. package/dist/types/src/components/forms-and-input/combo-box/__tests__/ComboBox.portal.test.d.ts +1 -0
  72. package/dist/types/src/components/navigation/bread-crumb/BreadCrumb.d.ts +3 -1
  73. package/dist/types/src/components/navigation/bread-crumb/__tests__/BreadCrumb.test.d.ts +1 -0
  74. package/dist/types/src/hooks/__tests__/useFloatingPosition.ssr.test.d.ts +1 -0
  75. package/dist/types/src/hooks/__tests__/useFloatingPosition.test.d.ts +1 -0
  76. package/dist/types/src/hooks/dropdown/useDropdown.d.ts +3 -1
  77. package/dist/types/src/hooks/dropdown/useDropdownPosition.d.ts +3 -3
  78. package/dist/types/src/hooks/useFloatingPosition.d.ts +6 -4
  79. package/dist/types/src/types/dropdown/dropdown.d.ts +2 -1
  80. package/dist/types/src/utils/dropdown/__tests__/dropdownUtils.test.d.ts +1 -0
  81. package/dist/types/src/utils/portal-position/__tests__/portalPosition.test.d.ts +1 -0
  82. package/dist/types/src/utils/portal-position/__tests__/portalPosition.tracking.test.d.ts +1 -0
  83. package/dist/types/src/utils/portal-position/index.d.ts +1 -0
  84. package/dist/types/src/utils/portal-position/portalPosition.d.ts +67 -0
  85. package/dist/ui-admin/assets/styles/style.css +26 -16
  86. package/package.json +2 -2
@@ -0,0 +1,397 @@
1
+ "use strict";
2
+
3
+ var _vitest = require("vitest");
4
+ var _ = require("..");
5
+ // @vitest-environment jsdom
6
+
7
+ const installResizeObserverStub = () => {
8
+ const instances = [];
9
+ class ResizeObserverStub {
10
+ constructor(_callback) {
11
+ this.observe = _vitest.vi.fn();
12
+ this.unobserve = _vitest.vi.fn();
13
+ this.disconnect = _vitest.vi.fn();
14
+ instances.push(this);
15
+ }
16
+ }
17
+ _vitest.vi.stubGlobal('ResizeObserver', ResizeObserverStub);
18
+ return {
19
+ instances
20
+ };
21
+ };
22
+ (0, _vitest.afterEach)(() => {
23
+ _vitest.vi.unstubAllGlobals();
24
+ _vitest.vi.restoreAllMocks();
25
+ });
26
+ const makeAnchorRect = function () {
27
+ let overrides = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
28
+ return {
29
+ top: 100,
30
+ bottom: 140,
31
+ left: 50,
32
+ right: 250,
33
+ width: 200,
34
+ ...overrides
35
+ };
36
+ };
37
+ const PANEL_HEIGHT = 300;
38
+ const PANEL_WIDTH = 180;
39
+ const DEFAULT_OFFSET = 4;
40
+ const EXPECTED_Z_INDEX = 1500;
41
+ const ON_CHANGE_CALLS = {
42
+ initial: 1,
43
+ afterScroll: 2,
44
+ afterResize: 3
45
+ };
46
+ const toDomRect = rect => DOMRect.fromRect({
47
+ x: rect.left,
48
+ y: rect.top,
49
+ width: rect.width,
50
+ height: rect.bottom - rect.top
51
+ });
52
+ const createAnchor = function () {
53
+ let rect = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : makeAnchorRect();
54
+ const element = document.createElement('button');
55
+ document.body.appendChild(element);
56
+ const boundingRect = _vitest.vi.spyOn(element, 'getBoundingClientRect').mockReturnValue(toDomRect(rect));
57
+ const moveTo = next => boundingRect.mockReturnValue(toDomRect(next));
58
+ return {
59
+ element,
60
+ moveTo
61
+ };
62
+ };
63
+ const createPanel = function () {
64
+ let {
65
+ height,
66
+ width
67
+ } = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {
68
+ height: PANEL_HEIGHT,
69
+ width: PANEL_WIDTH
70
+ };
71
+ const element = document.createElement('div');
72
+ document.body.appendChild(element);
73
+ Object.defineProperty(element, 'offsetHeight', {
74
+ value: height,
75
+ configurable: true
76
+ });
77
+ Object.defineProperty(element, 'offsetWidth', {
78
+ value: width,
79
+ configurable: true
80
+ });
81
+ return element;
82
+ };
83
+ const dispatchWindow = type => window.dispatchEvent(new Event(type));
84
+ (0, _vitest.describe)('observeAnchorGeometry — 구독 (E-6)', () => {
85
+ (0, _vitest.it)('구독 직후 onChange 를 1회 호출하고 window scroll·resize 마다 다시 호출한다', () => {
86
+ installResizeObserverStub();
87
+ const anchor = createAnchor();
88
+ const onChange = _vitest.vi.fn();
89
+ const cleanup = (0, _.observeAnchorGeometry)({
90
+ getAnchor: () => anchor.element,
91
+ getPanel: () => null,
92
+ onChange
93
+ });
94
+ (0, _vitest.expect)(onChange).toHaveBeenCalledTimes(ON_CHANGE_CALLS.initial);
95
+ dispatchWindow('scroll');
96
+ (0, _vitest.expect)(onChange).toHaveBeenCalledTimes(ON_CHANGE_CALLS.afterScroll);
97
+ dispatchWindow('resize');
98
+ (0, _vitest.expect)(onChange).toHaveBeenCalledTimes(ON_CHANGE_CALLS.afterResize);
99
+ cleanup();
100
+ });
101
+ (0, _vitest.it)('중첩 컨테이너의 scroll(비버블링) 도 capture 로 포착한다', () => {
102
+ installResizeObserverStub();
103
+ const anchor = createAnchor();
104
+ const container = document.createElement('div');
105
+ document.body.appendChild(container);
106
+ const onChange = _vitest.vi.fn();
107
+ const cleanup = (0, _.observeAnchorGeometry)({
108
+ getAnchor: () => anchor.element,
109
+ getPanel: () => null,
110
+ onChange
111
+ });
112
+ container.dispatchEvent(new Event('scroll', {
113
+ bubbles: false
114
+ }));
115
+ (0, _vitest.expect)(onChange).toHaveBeenCalledTimes(ON_CHANGE_CALLS.afterScroll);
116
+ cleanup();
117
+ });
118
+ (0, _vitest.it)('anchor 와 panel 각각에 ResizeObserver 를 붙인다', () => {
119
+ const {
120
+ instances
121
+ } = installResizeObserverStub();
122
+ const anchor = createAnchor();
123
+ const panel = createPanel();
124
+ const cleanup = (0, _.observeAnchorGeometry)({
125
+ getAnchor: () => anchor.element,
126
+ getPanel: () => panel,
127
+ onChange: _vitest.vi.fn()
128
+ });
129
+ (0, _vitest.expect)(instances).toHaveLength(2);
130
+ (0, _vitest.expect)(instances[0]?.observe).toHaveBeenCalledWith(anchor.element);
131
+ (0, _vitest.expect)(instances[1]?.observe).toHaveBeenCalledWith(panel);
132
+ cleanup();
133
+ });
134
+ (0, _vitest.it)('구독 시점에 panel 이 없으면 anchor 옵저버만 만든다', () => {
135
+ const {
136
+ instances
137
+ } = installResizeObserverStub();
138
+ const anchor = createAnchor();
139
+ const cleanup = (0, _.observeAnchorGeometry)({
140
+ getAnchor: () => anchor.element,
141
+ getPanel: () => null,
142
+ onChange: _vitest.vi.fn()
143
+ });
144
+ (0, _vitest.expect)(instances).toHaveLength(1);
145
+ cleanup();
146
+ });
147
+ (0, _vitest.it)('anchor 가 null 이면 구독하지 않고 no-op cleanup 을 반환한다', () => {
148
+ const {
149
+ instances
150
+ } = installResizeObserverStub();
151
+ const addEventListener = _vitest.vi.spyOn(window, 'addEventListener');
152
+ const onChange = _vitest.vi.fn();
153
+ const cleanup = (0, _.observeAnchorGeometry)({
154
+ getAnchor: () => null,
155
+ getPanel: () => null,
156
+ onChange
157
+ });
158
+ (0, _vitest.expect)(onChange).not.toHaveBeenCalled();
159
+ (0, _vitest.expect)(addEventListener).not.toHaveBeenCalled();
160
+ (0, _vitest.expect)(instances).toHaveLength(0);
161
+ (0, _vitest.expect)(() => cleanup()).not.toThrow();
162
+ });
163
+ });
164
+ (0, _vitest.describe)('observeAnchorGeometry — cleanup (E-7)', () => {
165
+ (0, _vitest.it)('cleanup 뒤에는 이벤트가 발생해도 onChange 가 늘지 않고 옵저버가 disconnect 된다', () => {
166
+ const {
167
+ instances
168
+ } = installResizeObserverStub();
169
+ const anchor = createAnchor();
170
+ const panel = createPanel();
171
+ const onChange = _vitest.vi.fn();
172
+ const cleanup = (0, _.observeAnchorGeometry)({
173
+ getAnchor: () => anchor.element,
174
+ getPanel: () => panel,
175
+ onChange
176
+ });
177
+ cleanup();
178
+ dispatchWindow('scroll');
179
+ dispatchWindow('resize');
180
+ (0, _vitest.expect)(onChange).toHaveBeenCalledTimes(ON_CHANGE_CALLS.initial);
181
+ for (const instance of instances) {
182
+ (0, _vitest.expect)(instance.disconnect).toHaveBeenCalledTimes(1);
183
+ }
184
+ });
185
+ });
186
+ (0, _vitest.describe)('observeAnchorGeometry — ResizeObserver 미정의 환경 (E-8)', () => {
187
+ (0, _vitest.it)('ResizeObserver 가 없어도 예외 없이 window 이벤트만 구독한다', () => {
188
+ (0, _vitest.expect)('ResizeObserver' in globalThis).toBe(false);
189
+ const anchor = createAnchor();
190
+ const onChange = _vitest.vi.fn();
191
+ let cleanup = () => undefined;
192
+ (0, _vitest.expect)(() => {
193
+ cleanup = (0, _.observeAnchorGeometry)({
194
+ getAnchor: () => anchor.element,
195
+ getPanel: () => null,
196
+ onChange
197
+ });
198
+ }).not.toThrow();
199
+ dispatchWindow('scroll');
200
+ (0, _vitest.expect)(onChange).toHaveBeenCalledTimes(ON_CHANGE_CALLS.afterScroll);
201
+ cleanup();
202
+ });
203
+ });
204
+ (0, _vitest.describe)('trackPortalPosition — 측정·조립 합성', () => {
205
+ (0, _vitest.it)('구독 직후 anchor·panel 실측값으로 조립한 style 을 onUpdate 에 동기 전달한다', () => {
206
+ installResizeObserverStub();
207
+ const rect = makeAnchorRect();
208
+ const anchor = createAnchor(rect);
209
+ const panel = createPanel();
210
+ const onUpdate = _vitest.vi.fn();
211
+ const cleanup = (0, _.trackPortalPosition)({
212
+ getAnchor: () => anchor.element,
213
+ getPanel: () => panel,
214
+ direction: 'down',
215
+ align: 'right',
216
+ onUpdate
217
+ });
218
+ (0, _vitest.expect)(onUpdate).toHaveBeenCalledTimes(1);
219
+ (0, _vitest.expect)(onUpdate).toHaveBeenLastCalledWith((0, _.buildPortalPanelStyle)({
220
+ anchorRect: rect,
221
+ panelHeight: PANEL_HEIGHT,
222
+ panelWidth: PANEL_WIDTH,
223
+ direction: 'down',
224
+ align: 'right'
225
+ }));
226
+ cleanup();
227
+ });
228
+ (0, _vitest.it)('panel 이 아직 없으면 panelHeight 0 · panelWidth 는 anchor 너비로 조립한다', () => {
229
+ installResizeObserverStub();
230
+ const rect = makeAnchorRect();
231
+ const anchor = createAnchor(rect);
232
+ const onUpdate = _vitest.vi.fn();
233
+ const cleanup = (0, _.trackPortalPosition)({
234
+ getAnchor: () => anchor.element,
235
+ getPanel: () => null,
236
+ direction: 'up',
237
+ align: 'right',
238
+ onUpdate
239
+ });
240
+ (0, _vitest.expect)(onUpdate).toHaveBeenLastCalledWith({
241
+ position: 'fixed',
242
+ top: rect.top - DEFAULT_OFFSET,
243
+ left: rect.right - rect.width,
244
+ zIndex: EXPECTED_Z_INDEX
245
+ });
246
+ cleanup();
247
+ });
248
+ (0, _vitest.it)('matchAnchorWidth=true 이면 minWidth 를 anchor 너비로 낸다', () => {
249
+ installResizeObserverStub();
250
+ const rect = makeAnchorRect();
251
+ const anchor = createAnchor(rect);
252
+ const panel = createPanel();
253
+ const onUpdate = _vitest.vi.fn();
254
+ const cleanup = (0, _.trackPortalPosition)({
255
+ getAnchor: () => anchor.element,
256
+ getPanel: () => panel,
257
+ direction: 'down',
258
+ matchAnchorWidth: true,
259
+ onUpdate
260
+ });
261
+ (0, _vitest.expect)(onUpdate.mock.lastCall?.[0].minWidth).toBe(rect.width);
262
+ cleanup();
263
+ });
264
+ (0, _vitest.it)('scroll 이 발생하면 anchor 를 다시 측정해 갱신된 style 을 전달한다', () => {
265
+ installResizeObserverStub();
266
+ const anchor = createAnchor(makeAnchorRect());
267
+ const panel = createPanel();
268
+ const movedRect = makeAnchorRect({
269
+ top: 60,
270
+ bottom: 100
271
+ });
272
+ const onUpdate = _vitest.vi.fn();
273
+ const cleanup = (0, _.trackPortalPosition)({
274
+ getAnchor: () => anchor.element,
275
+ getPanel: () => panel,
276
+ direction: 'down',
277
+ onUpdate
278
+ });
279
+ anchor.moveTo(movedRect);
280
+ dispatchWindow('scroll');
281
+ (0, _vitest.expect)(onUpdate).toHaveBeenCalledTimes(2);
282
+ (0, _vitest.expect)(onUpdate.mock.lastCall?.[0].top).toBe(movedRect.bottom + DEFAULT_OFFSET);
283
+ cleanup();
284
+ });
285
+ (0, _vitest.it)('getPanel 이 나중에 요소를 반환하면 다음 갱신에 panelHeight 가 반영된다', () => {
286
+ installResizeObserverStub();
287
+ const rect = makeAnchorRect();
288
+ const anchor = createAnchor(rect);
289
+ const panel = createPanel();
290
+ const mounted = {
291
+ current: null
292
+ };
293
+ const onUpdate = _vitest.vi.fn();
294
+ const cleanup = (0, _.trackPortalPosition)({
295
+ getAnchor: () => anchor.element,
296
+ getPanel: () => mounted.current,
297
+ direction: 'up',
298
+ onUpdate
299
+ });
300
+ (0, _vitest.expect)(onUpdate.mock.lastCall?.[0].top).toBe(rect.top - DEFAULT_OFFSET);
301
+ mounted.current = panel;
302
+ dispatchWindow('scroll');
303
+ (0, _vitest.expect)(onUpdate.mock.lastCall?.[0].top).toBe(rect.top - PANEL_HEIGHT - DEFAULT_OFFSET);
304
+ cleanup();
305
+ });
306
+ (0, _vitest.it)('anchor 가 null 이면 onUpdate 를 호출하지 않고 리스너도 붙이지 않는다', () => {
307
+ installResizeObserverStub();
308
+ const addEventListener = _vitest.vi.spyOn(window, 'addEventListener');
309
+ const onUpdate = _vitest.vi.fn();
310
+ const cleanup = (0, _.trackPortalPosition)({
311
+ getAnchor: () => null,
312
+ getPanel: () => null,
313
+ direction: 'down',
314
+ onUpdate
315
+ });
316
+ (0, _vitest.expect)(onUpdate).not.toHaveBeenCalled();
317
+ (0, _vitest.expect)(addEventListener).not.toHaveBeenCalled();
318
+ (0, _vitest.expect)(() => cleanup()).not.toThrow();
319
+ });
320
+ (0, _vitest.it)('cleanup 뒤에는 scroll 이 발생해도 onUpdate 가 늘지 않는다', () => {
321
+ installResizeObserverStub();
322
+ const anchor = createAnchor();
323
+ const onUpdate = _vitest.vi.fn();
324
+ const cleanup = (0, _.trackPortalPosition)({
325
+ getAnchor: () => anchor.element,
326
+ getPanel: () => null,
327
+ direction: 'down',
328
+ onUpdate
329
+ });
330
+ cleanup();
331
+ dispatchWindow('scroll');
332
+ (0, _vitest.expect)(onUpdate).toHaveBeenCalledTimes(1);
333
+ });
334
+ });
335
+ (0, _vitest.describe)('trackPortalPosition — direction 게터 (추가형 API)', () => {
336
+ (0, _vitest.it)('direction 에 함수를 넘기면 갱신마다 그 반환값으로 조립한다', () => {
337
+ installResizeObserverStub();
338
+ const anchor = createAnchor();
339
+ const panel = createPanel();
340
+ const onUpdate = _vitest.vi.fn();
341
+ let direction = 'down';
342
+ (0, _.trackPortalPosition)({
343
+ getAnchor: () => anchor.element,
344
+ getPanel: () => panel,
345
+ direction: () => direction,
346
+ onUpdate
347
+ });
348
+ direction = 'up';
349
+ dispatchWindow('scroll');
350
+ (0, _vitest.expect)(onUpdate.mock.calls[0][0].top).toBe(makeAnchorRect().bottom + DEFAULT_OFFSET);
351
+ (0, _vitest.expect)(onUpdate.mock.calls[1][0].top).toBe(makeAnchorRect().top - PANEL_HEIGHT - DEFAULT_OFFSET);
352
+ });
353
+ });
354
+ (0, _vitest.describe)('applyPortalPanelStyle — vanilla 어댑터 보조', () => {
355
+ (0, _vitest.it)('position·top·left·zIndex 를 px 단위로 element.style 에 적용한다', () => {
356
+ const element = document.createElement('div');
357
+ (0, _.applyPortalPanelStyle)(element, {
358
+ position: 'fixed',
359
+ top: 144,
360
+ left: 50,
361
+ zIndex: EXPECTED_Z_INDEX
362
+ });
363
+ (0, _vitest.expect)(element.style.position).toBe('fixed');
364
+ (0, _vitest.expect)(element.style.top).toBe('144px');
365
+ (0, _vitest.expect)(element.style.left).toBe('50px');
366
+ (0, _vitest.expect)(element.style.zIndex).toBe('1500');
367
+ (0, _vitest.expect)(element.style.minWidth).toBe('');
368
+ });
369
+ (0, _vitest.it)('minWidth 가 있으면 px 단위로 함께 적용한다', () => {
370
+ const element = document.createElement('div');
371
+ (0, _.applyPortalPanelStyle)(element, {
372
+ position: 'fixed',
373
+ top: 144,
374
+ left: 50,
375
+ zIndex: EXPECTED_Z_INDEX,
376
+ minWidth: 200
377
+ });
378
+ (0, _vitest.expect)(element.style.minWidth).toBe('200px');
379
+ });
380
+ (0, _vitest.it)('이전 호출에서 minWidth 가 설정된 요소에 minWidth 없는 스타일을 적용하면 minWidth 를 제거한다', () => {
381
+ const element = document.createElement('div');
382
+ (0, _.applyPortalPanelStyle)(element, {
383
+ position: 'fixed',
384
+ top: 0,
385
+ left: 0,
386
+ zIndex: EXPECTED_Z_INDEX,
387
+ minWidth: 200
388
+ });
389
+ (0, _.applyPortalPanelStyle)(element, {
390
+ position: 'fixed',
391
+ top: 10,
392
+ left: 0,
393
+ zIndex: EXPECTED_Z_INDEX
394
+ });
395
+ (0, _vitest.expect)(element.style.minWidth).toBe('');
396
+ });
397
+ });
@@ -0,0 +1,16 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ var _portalPosition = require("./portalPosition");
7
+ Object.keys(_portalPosition).forEach(function (key) {
8
+ if (key === "default" || key === "__esModule") return;
9
+ if (key in exports && exports[key] === _portalPosition[key]) return;
10
+ Object.defineProperty(exports, key, {
11
+ enumerable: true,
12
+ get: function () {
13
+ return _portalPosition[key];
14
+ }
15
+ });
16
+ });
@@ -0,0 +1,162 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.trackPortalPosition = exports.resolveDropdownDirection = exports.observeAnchorGeometry = exports.computePortalPosition = exports.buildPortalPanelStyle = exports.applyPortalPanelStyle = exports.PORTAL_PANEL_Z_INDEX = void 0;
7
+ const DEFAULT_OFFSET = 4;
8
+ const DEFAULT_GAP = 20;
9
+ const noop = () => undefined;
10
+ const observeElementResize = (target, onChange) => {
11
+ if (target === null || typeof ResizeObserver === 'undefined') {
12
+ return null;
13
+ }
14
+ const observer = new ResizeObserver(onChange);
15
+ observer.observe(target);
16
+ return observer;
17
+ };
18
+ const measurePortalPanelStyle = _ref => {
19
+ let {
20
+ getAnchor,
21
+ getPanel,
22
+ direction,
23
+ align,
24
+ offset,
25
+ matchAnchorWidth
26
+ } = _ref;
27
+ const anchor = getAnchor();
28
+ if (anchor === null) {
29
+ return null;
30
+ }
31
+ const panel = getPanel();
32
+ return buildPortalPanelStyle({
33
+ anchorRect: anchor.getBoundingClientRect(),
34
+ panelHeight: panel?.offsetHeight ?? 0,
35
+ // matchAnchorWidth 이면 패널 너비를 쓰지 않으므로 offsetWidth 를 읽지 않는다 — 레이아웃 강제 산출 1회 회피
36
+ panelWidth: matchAnchorWidth ? null : panel?.offsetWidth ?? null,
37
+ direction: typeof direction === 'function' ? direction() : direction,
38
+ align,
39
+ offset,
40
+ matchAnchorWidth
41
+ });
42
+ };
43
+ const PORTAL_PANEL_Z_INDEX = exports.PORTAL_PANEL_Z_INDEX = 1500;
44
+ const computePortalPosition = _ref2 => {
45
+ let {
46
+ anchorRect,
47
+ panelHeight,
48
+ panelWidth,
49
+ direction,
50
+ align = 'left',
51
+ offset = DEFAULT_OFFSET
52
+ } = _ref2;
53
+ return {
54
+ top: direction === 'up' ? anchorRect.top - panelHeight - offset : anchorRect.bottom + offset,
55
+ left: align === 'right' ? anchorRect.right - panelWidth : anchorRect.left
56
+ };
57
+ };
58
+ exports.computePortalPosition = computePortalPosition;
59
+ const resolveDropdownDirection = _ref3 => {
60
+ let {
61
+ anchorRect,
62
+ panelHeight,
63
+ viewportHeight,
64
+ gap = DEFAULT_GAP
65
+ } = _ref3;
66
+ return viewportHeight - anchorRect.bottom < panelHeight + gap ? 'up' : 'down';
67
+ };
68
+ exports.resolveDropdownDirection = resolveDropdownDirection;
69
+ const buildPortalPanelStyle = _ref4 => {
70
+ let {
71
+ anchorRect,
72
+ panelHeight,
73
+ panelWidth,
74
+ direction,
75
+ align,
76
+ offset,
77
+ matchAnchorWidth = false
78
+ } = _ref4;
79
+ const effectivePanelWidth = matchAnchorWidth || panelWidth === null ? anchorRect.width : panelWidth;
80
+ const {
81
+ top,
82
+ left
83
+ } = computePortalPosition({
84
+ anchorRect,
85
+ panelHeight,
86
+ panelWidth: effectivePanelWidth,
87
+ direction,
88
+ align,
89
+ offset
90
+ });
91
+ const base = {
92
+ position: 'fixed',
93
+ top,
94
+ left,
95
+ zIndex: PORTAL_PANEL_Z_INDEX
96
+ };
97
+ // width 가 아니라 min-width 인 이유: 패널은 max-content 로 자연 확장하되 앵커보다 좁아지지만 않게 한다.
98
+ // 옵션이 짧으면 앵커 너비, 길면 자연 확장 — Portal 이전 absolute 모드와 동일한 UX.
99
+ return matchAnchorWidth ? {
100
+ ...base,
101
+ minWidth: anchorRect.width
102
+ } : base;
103
+ };
104
+ exports.buildPortalPanelStyle = buildPortalPanelStyle;
105
+ const observeAnchorGeometry = _ref5 => {
106
+ let {
107
+ getAnchor,
108
+ getPanel,
109
+ onChange
110
+ } = _ref5;
111
+ const anchor = getAnchor();
112
+ if (anchor === null) {
113
+ return noop;
114
+ }
115
+ onChange();
116
+ window.addEventListener('scroll', onChange, {
117
+ passive: true,
118
+ capture: true
119
+ });
120
+ window.addEventListener('resize', onChange);
121
+ const observers = [observeElementResize(anchor, onChange), observeElementResize(getPanel(), onChange)];
122
+ return () => {
123
+ window.removeEventListener('scroll', onChange, {
124
+ capture: true
125
+ });
126
+ window.removeEventListener('resize', onChange);
127
+ for (const observer of observers) {
128
+ observer?.disconnect();
129
+ }
130
+ };
131
+ };
132
+ exports.observeAnchorGeometry = observeAnchorGeometry;
133
+ const trackPortalPosition = _ref6 => {
134
+ let {
135
+ onUpdate,
136
+ ...measureOptions
137
+ } = _ref6;
138
+ return observeAnchorGeometry({
139
+ getAnchor: measureOptions.getAnchor,
140
+ getPanel: measureOptions.getPanel,
141
+ onChange: () => {
142
+ const style = measurePortalPanelStyle(measureOptions);
143
+ if (style !== null) {
144
+ onUpdate(style);
145
+ }
146
+ }
147
+ });
148
+ };
149
+ // TODO(#497): vanilla 어댑터 도입 시 소비
150
+ exports.trackPortalPosition = trackPortalPosition;
151
+ const applyPortalPanelStyle = (element, style) => {
152
+ element.style.position = style.position;
153
+ element.style.top = `${style.top}px`;
154
+ element.style.left = `${style.left}px`;
155
+ element.style.zIndex = String(style.zIndex);
156
+ if (style.minWidth === undefined) {
157
+ element.style.removeProperty('min-width');
158
+ return;
159
+ }
160
+ element.style.minWidth = `${style.minWidth}px`;
161
+ };
162
+ exports.applyPortalPanelStyle = applyPortalPanelStyle;
@@ -9,7 +9,7 @@ const BADGE_ICON_SIZE = 12;
9
9
  /** new-badge 박스 안에 들어가는 'N' 아이콘 크기. 박스는 sm 16px / md 20px 이며 아이콘은 그보다 작다(패딩 포함). */
10
10
  const NEW_BADGE_ICON_SIZE = {
11
11
  sm: 12,
12
- md: 16
12
+ md: 14
13
13
  };
14
14
  const Badge = _ref => {
15
15
  let {