@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,29 @@
1
+ "use strict";
2
+
3
+ var _react = require("react");
4
+ var _server = require("react-dom/server");
5
+ var _vitest = require("vitest");
6
+ var _useFloatingPosition = require("../useFloatingPosition");
7
+ // @vitest-environment node
8
+
9
+ (0, _vitest.afterEach)(() => {
10
+ _vitest.vi.restoreAllMocks();
11
+ });
12
+ const FloatingProbe = () => {
13
+ (0, _useFloatingPosition.useFloatingPosition)({
14
+ enabled: true,
15
+ isOpen: true,
16
+ triggerRef: /*#__PURE__*/(0, _react.createRef)(),
17
+ floatingRef: /*#__PURE__*/(0, _react.createRef)(),
18
+ direction: 'down'
19
+ });
20
+ return null;
21
+ };
22
+ (0, _vitest.describe)('useFloatingPosition — SSR', () => {
23
+ (0, _vitest.it)('서버 렌더에서 useLayoutEffect 경고를 내지 않는다', () => {
24
+ const consoleError = _vitest.vi.spyOn(console, 'error').mockImplementation(() => undefined);
25
+ (0, _server.renderToString)(/*#__PURE__*/(0, _react.createElement)(FloatingProbe));
26
+ const layoutEffectWarnings = consoleError.mock.calls.filter(args => args.some(arg => typeof arg === 'string' && arg.includes('useLayoutEffect')));
27
+ (0, _vitest.expect)(layoutEffectWarnings).toHaveLength(0);
28
+ });
29
+ });
@@ -0,0 +1,322 @@
1
+ "use strict";
2
+
3
+ var _react = require("react");
4
+ var _client = require("react-dom/client");
5
+ var _testUtils = require("react-dom/test-utils");
6
+ var _vitest = require("vitest");
7
+ var _portalPosition = require("../../utils/portal-position");
8
+ var _useFloatingPosition = require("../useFloatingPosition");
9
+ // @vitest-environment jsdom
10
+
11
+ Object.assign(globalThis, {
12
+ IS_REACT_ACT_ENVIRONMENT: true
13
+ });
14
+ const installResizeObserverStub = () => {
15
+ class ResizeObserverStub {
16
+ constructor() {
17
+ this.observe = _vitest.vi.fn();
18
+ this.unobserve = _vitest.vi.fn();
19
+ this.disconnect = _vitest.vi.fn();
20
+ }
21
+ }
22
+ _vitest.vi.stubGlobal('ResizeObserver', ResizeObserverStub);
23
+ };
24
+ (0, _vitest.afterEach)(() => {
25
+ document.body.innerHTML = '';
26
+ _vitest.vi.unstubAllGlobals();
27
+ _vitest.vi.restoreAllMocks();
28
+ });
29
+ const makeAnchorRect = function () {
30
+ let overrides = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
31
+ return {
32
+ top: 100,
33
+ bottom: 140,
34
+ left: 50,
35
+ right: 250,
36
+ width: 200,
37
+ ...overrides
38
+ };
39
+ };
40
+ const PANEL_HEIGHT = 300;
41
+ const PANEL_WIDTH = 180;
42
+ const DEFAULT_OFFSET = 4;
43
+ const CHANGED_OFFSET = 12;
44
+ const EXPECTED_Z_INDEX = 1500;
45
+ const toDomRect = rect => DOMRect.fromRect({
46
+ x: rect.left,
47
+ y: rect.top,
48
+ width: rect.width,
49
+ height: rect.bottom - rect.top
50
+ });
51
+ const createAnchor = function () {
52
+ let rect = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : makeAnchorRect();
53
+ const element = document.createElement('button');
54
+ document.body.appendChild(element);
55
+ _vitest.vi.spyOn(element, 'getBoundingClientRect').mockReturnValue(toDomRect(rect));
56
+ return element;
57
+ };
58
+ const createPanel = () => {
59
+ const element = document.createElement('div');
60
+ document.body.appendChild(element);
61
+ Object.defineProperty(element, 'offsetHeight', {
62
+ value: PANEL_HEIGHT,
63
+ configurable: true
64
+ });
65
+ Object.defineProperty(element, 'offsetWidth', {
66
+ value: PANEL_WIDTH,
67
+ configurable: true
68
+ });
69
+ return element;
70
+ };
71
+ const makeRef = element => ({
72
+ current: element
73
+ });
74
+ const Probe = _ref => {
75
+ let {
76
+ params,
77
+ record
78
+ } = _ref;
79
+ record((0, _useFloatingPosition.useFloatingPosition)(params));
80
+ return null;
81
+ };
82
+ const renderHook = initialParams => {
83
+ const container = document.createElement('div');
84
+ document.body.appendChild(container);
85
+ const root = (0, _client.createRoot)(container);
86
+ const record = _vitest.vi.fn();
87
+ const rerender = params => {
88
+ (0, _testUtils.act)(() => {
89
+ root.render(/*#__PURE__*/(0, _react.createElement)(Probe, {
90
+ params,
91
+ record
92
+ }));
93
+ });
94
+ };
95
+ rerender(initialParams);
96
+ const unmount = () => {
97
+ (0, _testUtils.act)(() => {
98
+ root.unmount();
99
+ });
100
+ container.remove();
101
+ };
102
+ return {
103
+ rerender,
104
+ unmount,
105
+ latest: () => record.mock.lastCall?.[0]
106
+ };
107
+ };
108
+ const baseParams = function () {
109
+ let overrides = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
110
+ return {
111
+ enabled: true,
112
+ isOpen: true,
113
+ triggerRef: makeRef(null),
114
+ floatingRef: makeRef(null),
115
+ direction: 'down',
116
+ ...overrides
117
+ };
118
+ };
119
+ const dispatchScroll = () => {
120
+ (0, _testUtils.act)(() => {
121
+ window.dispatchEvent(new Event('scroll'));
122
+ });
123
+ };
124
+ (0, _vitest.describe)('useFloatingPosition ↔ trackPortalPosition 동등성 (E-1)', () => {
125
+ _vitest.it.each([{
126
+ direction: 'down',
127
+ align: 'left',
128
+ matchTriggerWidth: false
129
+ }, {
130
+ direction: 'down',
131
+ align: 'right',
132
+ matchTriggerWidth: false
133
+ }, {
134
+ direction: 'up',
135
+ align: 'left',
136
+ matchTriggerWidth: true
137
+ }, {
138
+ direction: 'up',
139
+ align: 'right',
140
+ matchTriggerWidth: true
141
+ }])('direction=$direction · align=$align · matchTriggerWidth=$matchTriggerWidth 에서 훅 style 과 onUpdate 인자가 같다', _ref2 => {
142
+ let {
143
+ direction,
144
+ align,
145
+ matchTriggerWidth
146
+ } = _ref2;
147
+ installResizeObserverStub();
148
+ const anchor = createAnchor();
149
+ const panel = createPanel();
150
+ const onUpdate = _vitest.vi.fn();
151
+ const hook = renderHook(baseParams({
152
+ triggerRef: makeRef(anchor),
153
+ floatingRef: makeRef(panel),
154
+ direction,
155
+ align,
156
+ matchTriggerWidth
157
+ }));
158
+ const cleanup = (0, _portalPosition.trackPortalPosition)({
159
+ getAnchor: () => anchor,
160
+ getPanel: () => panel,
161
+ direction,
162
+ align,
163
+ matchAnchorWidth: matchTriggerWidth,
164
+ onUpdate
165
+ });
166
+ (0, _vitest.expect)(hook.latest()).toEqual(onUpdate.mock.lastCall?.[0]);
167
+ (0, _vitest.expect)(hook.latest()).toMatchObject({
168
+ position: 'fixed',
169
+ zIndex: EXPECTED_Z_INDEX
170
+ });
171
+ cleanup();
172
+ hook.unmount();
173
+ });
174
+ });
175
+ (0, _vitest.describe)('useFloatingPosition — 비활성 (E-2)', () => {
176
+ (0, _vitest.it)('enabled=false 이면 null 을 반환한다', () => {
177
+ installResizeObserverStub();
178
+ const hook = renderHook(baseParams({
179
+ enabled: false,
180
+ triggerRef: makeRef(createAnchor()),
181
+ floatingRef: makeRef(createPanel())
182
+ }));
183
+ (0, _vitest.expect)(hook.latest()).toBeNull();
184
+ hook.unmount();
185
+ });
186
+ (0, _vitest.it)('isOpen=false 이면 null 을 반환한다', () => {
187
+ installResizeObserverStub();
188
+ const hook = renderHook(baseParams({
189
+ isOpen: false,
190
+ triggerRef: makeRef(createAnchor()),
191
+ floatingRef: makeRef(createPanel())
192
+ }));
193
+ (0, _vitest.expect)(hook.latest()).toBeNull();
194
+ hook.unmount();
195
+ });
196
+ (0, _vitest.it)('열린 뒤 닫히면 style 이 null 로 돌아간다', () => {
197
+ installResizeObserverStub();
198
+ const triggerRef = makeRef(createAnchor());
199
+ const hook = renderHook(baseParams({
200
+ triggerRef
201
+ }));
202
+ (0, _vitest.expect)(hook.latest()).not.toBeNull();
203
+ hook.rerender(baseParams({
204
+ triggerRef,
205
+ isOpen: false
206
+ }));
207
+ (0, _vitest.expect)(hook.latest()).toBeNull();
208
+ hook.unmount();
209
+ });
210
+ });
211
+ (0, _vitest.describe)('useFloatingPosition — 트리거 부재 (E-3)', () => {
212
+ (0, _vitest.it)('triggerRef.current 가 null 이면 scroll·resize 리스너를 붙이지 않고 style 은 null 로 남는다', () => {
213
+ installResizeObserverStub();
214
+ const addEventListener = _vitest.vi.spyOn(window, 'addEventListener');
215
+ const hook = renderHook(baseParams({
216
+ triggerRef: makeRef(null)
217
+ }));
218
+ const positionListeners = addEventListener.mock.calls.filter(_ref3 => {
219
+ let [type] = _ref3;
220
+ return type === 'scroll' || type === 'resize';
221
+ });
222
+ (0, _vitest.expect)(positionListeners).toHaveLength(0);
223
+ (0, _vitest.expect)(hook.latest()).toBeNull();
224
+ hook.unmount();
225
+ });
226
+ (0, _vitest.it)('산출된 뒤 트리거가 사라지면 이전 style 을 유지한다', () => {
227
+ installResizeObserverStub();
228
+ const triggerRef = makeRef(createAnchor());
229
+ const hook = renderHook(baseParams({
230
+ triggerRef,
231
+ offset: DEFAULT_OFFSET
232
+ }));
233
+ const previous = hook.latest();
234
+ (0, _vitest.expect)(previous).not.toBeNull();
235
+ triggerRef.current = null;
236
+ hook.rerender(baseParams({
237
+ triggerRef,
238
+ offset: CHANGED_OFFSET
239
+ }));
240
+ (0, _vitest.expect)(hook.latest()).toEqual(previous);
241
+ hook.unmount();
242
+ });
243
+ });
244
+ (0, _vitest.describe)('useFloatingPosition — 패널 지연 마운트 (E-4)', () => {
245
+ (0, _vitest.it)('effect 시점에 패널이 없다가 마운트되면 scroll 시 panelHeight 가 반영된다', () => {
246
+ installResizeObserverStub();
247
+ const rect = makeAnchorRect();
248
+ const floatingRef = makeRef(null);
249
+ const hook = renderHook(baseParams({
250
+ triggerRef: makeRef(createAnchor(rect)),
251
+ floatingRef,
252
+ direction: 'up'
253
+ }));
254
+ (0, _vitest.expect)(hook.latest()).toMatchObject({
255
+ top: rect.top - DEFAULT_OFFSET
256
+ });
257
+ floatingRef.current = createPanel();
258
+ dispatchScroll();
259
+ (0, _vitest.expect)(hook.latest()).toMatchObject({
260
+ top: rect.top - PANEL_HEIGHT - DEFAULT_OFFSET
261
+ });
262
+ hook.unmount();
263
+ });
264
+ });
265
+ (0, _vitest.describe)('useFloatingPosition — 프롭 변경 재구독 (E-5)', () => {
266
+ (0, _vitest.it)('align 이 left→right 로 바뀌면 left 를 다시 계산한다', () => {
267
+ installResizeObserverStub();
268
+ const rect = makeAnchorRect();
269
+ const triggerRef = makeRef(createAnchor(rect));
270
+ const floatingRef = makeRef(createPanel());
271
+ const hook = renderHook(baseParams({
272
+ triggerRef,
273
+ floatingRef,
274
+ align: 'left'
275
+ }));
276
+ (0, _vitest.expect)(hook.latest()).toMatchObject({
277
+ left: rect.left
278
+ });
279
+ hook.rerender(baseParams({
280
+ triggerRef,
281
+ floatingRef,
282
+ align: 'right'
283
+ }));
284
+ (0, _vitest.expect)(hook.latest()).toMatchObject({
285
+ left: rect.right - PANEL_WIDTH
286
+ });
287
+ hook.unmount();
288
+ });
289
+ (0, _vitest.it)('offset 이 바뀌면 top 을 다시 계산한다', () => {
290
+ installResizeObserverStub();
291
+ const rect = makeAnchorRect();
292
+ const triggerRef = makeRef(createAnchor(rect));
293
+ const hook = renderHook(baseParams({
294
+ triggerRef,
295
+ offset: DEFAULT_OFFSET
296
+ }));
297
+ (0, _vitest.expect)(hook.latest()).toMatchObject({
298
+ top: rect.bottom + DEFAULT_OFFSET
299
+ });
300
+ hook.rerender(baseParams({
301
+ triggerRef,
302
+ offset: CHANGED_OFFSET
303
+ }));
304
+ (0, _vitest.expect)(hook.latest()).toMatchObject({
305
+ top: rect.bottom + CHANGED_OFFSET
306
+ });
307
+ hook.unmount();
308
+ });
309
+ (0, _vitest.it)('matchTriggerWidth=true 이면 minWidth 를 트리거 너비로 낸다', () => {
310
+ installResizeObserverStub();
311
+ const rect = makeAnchorRect();
312
+ const hook = renderHook(baseParams({
313
+ triggerRef: makeRef(createAnchor(rect)),
314
+ floatingRef: makeRef(createPanel()),
315
+ matchTriggerWidth: true
316
+ }));
317
+ (0, _vitest.expect)(hook.latest()).toMatchObject({
318
+ minWidth: rect.width
319
+ });
320
+ hook.unmount();
321
+ });
322
+ });
@@ -16,6 +16,7 @@ const useDropdown = config => {
16
16
  const {
17
17
  triggerRef,
18
18
  dropdownRef,
19
+ positionRef,
19
20
  maxHeight,
20
21
  itemCount,
21
22
  optionItems,
@@ -31,7 +32,7 @@ const useDropdown = config => {
31
32
  direction,
32
33
  updatePosition
33
34
  } = (0, _useDropdownPosition.useDropdownPosition)({
34
- triggerRef,
35
+ triggerRef: positionRef ?? triggerRef,
35
36
  maxHeight,
36
37
  itemCount
37
38
  });
@@ -5,7 +5,8 @@ Object.defineProperty(exports, "__esModule", {
5
5
  });
6
6
  exports.useFloatingPosition = void 0;
7
7
  var _react = require("react");
8
- const FLOATING_Z_INDEX = 1500;
8
+ var _portalPosition = require("../utils/portal-position");
9
+ var _useIsomorphicLayoutEffect = require("./useIsomorphicLayoutEffect");
9
10
  const useFloatingPosition = _ref => {
10
11
  let {
11
12
  enabled,
@@ -13,65 +14,25 @@ const useFloatingPosition = _ref => {
13
14
  triggerRef,
14
15
  floatingRef,
15
16
  direction,
16
- offset = 4,
17
- align = 'left',
17
+ offset,
18
+ align,
18
19
  matchTriggerWidth = false
19
20
  } = _ref;
20
21
  const [style, setStyle] = (0, _react.useState)(null);
21
- (0, _react.useLayoutEffect)(() => {
22
+ (0, _useIsomorphicLayoutEffect.useIsomorphicLayoutEffect)(() => {
22
23
  if (!enabled || !isOpen) {
23
24
  setStyle(null);
24
25
  return;
25
26
  }
26
- const trigger = triggerRef.current;
27
- if (!trigger) return;
28
- const calculatePosition = (trigger, floatingHeight, floatingWidth) => {
29
- const top = direction === 'up' ? trigger.top - floatingHeight - offset : trigger.bottom + offset;
30
- const left = align === 'right' ? trigger.right - floatingWidth : trigger.left;
31
- return {
32
- top,
33
- left
34
- };
35
- };
36
- const update = () => {
37
- const t = triggerRef.current;
38
- const f = floatingRef.current;
39
- if (!t) return;
40
- const r = t.getBoundingClientRect();
41
- const floatingHeight = f?.offsetHeight ?? 0;
42
- const floatingWidth = matchTriggerWidth ? r.width : f?.offsetWidth ?? r.width;
43
- const {
44
- top,
45
- left
46
- } = calculatePosition(r, floatingHeight, floatingWidth);
47
- // matchTriggerWidth=true: width는 max-content(콘텐츠 자연 너비), min-width만 trigger 너비로 보장.
48
- // 옵션이 짧으면 trigger 너비, 길면 자연 확장 — 기존 absolute 모드와 동일한 UX.
49
- setStyle({
50
- position: 'fixed',
51
- top,
52
- left,
53
- zIndex: FLOATING_Z_INDEX,
54
- ...(matchTriggerWidth ? {
55
- minWidth: r.width
56
- } : {})
57
- });
58
- };
59
- update();
60
- window.addEventListener('scroll', update, true);
61
- window.addEventListener('resize', update);
62
- const triggerObserver = new ResizeObserver(update);
63
- triggerObserver.observe(trigger);
64
- let floatingObserver;
65
- if (floatingRef.current) {
66
- floatingObserver = new ResizeObserver(update);
67
- floatingObserver.observe(floatingRef.current);
68
- }
69
- return () => {
70
- window.removeEventListener('scroll', update, true);
71
- window.removeEventListener('resize', update);
72
- triggerObserver.disconnect();
73
- floatingObserver?.disconnect();
74
- };
27
+ return (0, _portalPosition.trackPortalPosition)({
28
+ getAnchor: () => triggerRef.current,
29
+ getPanel: () => floatingRef.current,
30
+ direction,
31
+ align,
32
+ offset,
33
+ matchAnchorWidth: matchTriggerWidth,
34
+ onUpdate: setStyle
35
+ });
75
36
  }, [enabled, isOpen, direction, offset, align, matchTriggerWidth, triggerRef, floatingRef]);
76
37
  return style;
77
38
  };