@363045841yyt/klinechart 0.8.4 → 0.8.6

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 (69) hide show
  1. package/README.md +6 -1
  2. package/dist/components/BaseModal.vue.d.ts +54 -0
  3. package/dist/components/BaseModal.vue.d.ts.map +1 -0
  4. package/dist/components/BatchStockDialog.vue.d.ts +13 -0
  5. package/dist/components/BatchStockDialog.vue.d.ts.map +1 -0
  6. package/dist/components/ChartSettingsDialog.vue.d.ts.map +1 -1
  7. package/dist/components/ColorPresetPanel.vue.d.ts +4 -1
  8. package/dist/components/ColorPresetPanel.vue.d.ts.map +1 -1
  9. package/dist/components/CompareSymbolSelector.vue.d.ts.map +1 -1
  10. package/dist/components/DrawingStyleToolbar.vue.d.ts.map +1 -1
  11. package/dist/components/Dropdown.vue.d.ts.map +1 -1
  12. package/dist/components/ExportProgressDialog.vue.d.ts +15 -0
  13. package/dist/components/ExportProgressDialog.vue.d.ts.map +1 -0
  14. package/dist/components/IndicatorParams.vue.d.ts.map +1 -1
  15. package/dist/components/IndicatorSelector.vue.d.ts.map +1 -1
  16. package/dist/components/KLineChart.vue.d.ts +5 -9
  17. package/dist/components/KLineChart.vue.d.ts.map +1 -1
  18. package/dist/components/LeftToolbar.vue.d.ts.map +1 -1
  19. package/dist/components/RangeSelectionExport.vue.d.ts +23 -0
  20. package/dist/components/RangeSelectionExport.vue.d.ts.map +1 -0
  21. package/dist/components/SymbolSelector.vue.d.ts.map +1 -1
  22. package/dist/components/TopToolbar.vue.d.ts.map +1 -1
  23. package/dist/components/common/CanvasToolbar.vue.d.ts +14 -0
  24. package/dist/components/common/CanvasToolbar.vue.d.ts.map +1 -0
  25. package/dist/components/common/CanvasToolbarStack.vue.d.ts +14 -0
  26. package/dist/components/common/CanvasToolbarStack.vue.d.ts.map +1 -0
  27. package/dist/composables/chart/useChartTheme.d.ts +329 -0
  28. package/dist/composables/chart/useChartTheme.d.ts.map +1 -0
  29. package/dist/composables/chart/useDrawingManager.d.ts +86 -0
  30. package/dist/composables/chart/useDrawingManager.d.ts.map +1 -0
  31. package/dist/composables/chart/useIndicatorManager.d.ts +38 -0
  32. package/dist/composables/chart/useIndicatorManager.d.ts.map +1 -0
  33. package/dist/composables/chart/useRangeSelection.d.ts +66 -0
  34. package/dist/composables/chart/useRangeSelection.d.ts.map +1 -0
  35. package/dist/composables/useTeleportedPopup.d.ts +8 -0
  36. package/dist/composables/useTeleportedPopup.d.ts.map +1 -0
  37. package/dist/index.cjs +9 -2
  38. package/dist/index.css +1 -1
  39. package/dist/index.js +2149 -1409
  40. package/dist/tools/calcRangeOverlayPixel.d.ts +15 -0
  41. package/dist/tools/calcRangeOverlayPixel.d.ts.map +1 -0
  42. package/dist/tools/getKLineIndexByTimestamp.d.ts +4 -0
  43. package/dist/tools/getKLineIndexByTimestamp.d.ts.map +1 -0
  44. package/dist/web-component.d.ts.map +1 -1
  45. package/package.json +1 -1
  46. package/src/components/BaseModal.vue +292 -0
  47. package/src/components/BatchStockDialog.vue +128 -0
  48. package/src/components/ChartSettingsDialog.vue +248 -405
  49. package/src/components/ColorPresetPanel.vue +58 -106
  50. package/src/components/CompareSymbolSelector.vue +37 -10
  51. package/src/components/DrawingStyleToolbar.vue +33 -72
  52. package/src/components/Dropdown.vue +42 -19
  53. package/src/components/ExportProgressDialog.vue +118 -0
  54. package/src/components/IndicatorParams.vue +194 -321
  55. package/src/components/IndicatorSelector.vue +188 -405
  56. package/src/components/KLineChart.vue +228 -403
  57. package/src/components/LeftToolbar.vue +3 -2
  58. package/src/components/RangeSelectionExport.vue +117 -0
  59. package/src/components/SymbolSelector.vue +37 -10
  60. package/src/components/TopToolbar.vue +55 -2
  61. package/src/components/common/CanvasToolbar.vue +70 -0
  62. package/src/components/common/CanvasToolbarStack.vue +32 -0
  63. package/src/composables/chart/useChartTheme.ts +86 -0
  64. package/src/composables/chart/useDrawingManager.ts +67 -0
  65. package/src/composables/chart/useIndicatorManager.ts +307 -0
  66. package/src/composables/chart/useRangeSelection.ts +424 -0
  67. package/src/composables/useTeleportedPopup.ts +46 -0
  68. package/src/tools/calcRangeOverlayPixel.ts +28 -0
  69. package/src/tools/getKLineIndexByTimestamp.ts +40 -0
@@ -1,27 +1,24 @@
1
1
  <template>
2
- <div>
3
- <div class="color-preset-tools">
4
- <div class="theme-tabs" role="tablist" aria-label="颜色主题">
5
- <button
6
- v-for="option in themeOptions"
7
- :key="option.value"
8
- type="button"
9
- class="theme-tab"
10
- :class="{ active: editingTheme === option.value }"
11
- @click="editingTheme = option.value"
12
- >
13
- {{ option.label }}
14
- </button>
15
- </div>
16
- <button type="button" class="color-reset-btn" @click="resetCurrentThemeColors">
17
- 重置颜色
2
+ <div class="color-preset-container">
3
+ <div class="theme-tabs" role="tablist" aria-label="颜色主题">
4
+ <button
5
+ v-for="option in themeOptions"
6
+ :key="option.value"
7
+ type="button"
8
+ class="theme-tab"
9
+ :class="{ active: editingTheme === option.value }"
10
+ @click="editingTheme = option.value"
11
+ >
12
+ {{ option.label }}
18
13
  </button>
19
14
  </div>
15
+
16
+ <!-- 颜色分组列表 -->
20
17
  <template v-for="group in colorPresetGroups" :key="group.group">
21
18
  <div class="color-group-label">{{ group.label }}</div>
22
19
  <div class="color-grid">
23
20
  <label v-for="item in group.items" :key="item.key" class="color-item">
24
- <span>{{ item.label }}</span>
21
+ <span class="color-item-text">{{ item.label }}</span>
25
22
  <input
26
23
  type="color"
27
24
  class="color-input"
@@ -105,30 +102,27 @@ function resetCurrentThemeColors(): void {
105
102
  delete nextColorSettings[editingTheme.value]
106
103
  emit('update:colorPresetSettings', nextColorSettings)
107
104
  }
105
+
106
+ defineExpose({ resetCurrentThemeColors })
108
107
  </script>
109
108
 
110
109
  <style scoped>
111
- /* ── 工具栏 ── */
112
- .color-preset-tools {
113
- display: grid;
114
- grid-template-columns: 1fr auto;
115
- align-items: center;
116
- gap: 8px;
117
- margin-bottom: 4px;
110
+ .color-preset-container {
111
+ padding: 4px 0;
118
112
  }
119
113
 
120
- /* ── 主题切换 ── */
121
114
  .theme-tabs {
122
- display: grid;
123
- grid-template-columns: 1fr 1fr;
124
- gap: 3px;
125
- padding: 3px;
115
+ display: flex;
116
+ gap: 4px;
117
+ padding: 4px;
118
+ margin-bottom: 12px;
126
119
  border: 1px solid var(--klc-color-border-button);
127
120
  border-radius: 8px;
128
121
  background: var(--klc-color-grid-minor);
129
122
  }
130
123
 
131
124
  .theme-tab {
125
+ flex: 1;
132
126
  height: 28px;
133
127
  border: none;
134
128
  border-radius: 6px;
@@ -137,149 +131,107 @@ function resetCurrentThemeColors(): void {
137
131
  font-size: 12px;
138
132
  font-weight: 500;
139
133
  cursor: pointer;
140
- transition:
141
- background 0.18s ease,
142
- color 0.18s ease,
143
- box-shadow 0.18s ease;
134
+ transition: all 0.18s ease;
135
+ white-space: nowrap;
144
136
  }
145
137
 
146
138
  .theme-tab:not(.active):hover {
147
139
  color: var(--klc-color-foreground);
148
- background: color-mix(in srgb, var(--klc-color-tag-bg-white) 60%, transparent);
140
+ background: color-mix(in srgb, var(--klc-color-background) 60%, transparent);
149
141
  }
150
142
 
151
143
  .theme-tab.active {
152
- background: var(--klc-color-tag-bg-white);
153
144
  color: var(--klc-color-foreground);
154
145
  font-weight: 600;
155
- box-shadow: 0 1px 4px rgba(0, 0, 0, 0.1);
156
146
  }
157
147
 
158
- /* ── 重置按钮 ── */
159
- .color-reset-btn {
160
- height: 36px;
161
- padding: 0 14px;
162
- border: 1px solid var(--klc-color-axis-line);
163
- border-radius: 8px;
164
- background: var(--klc-color-tag-bg-white);
148
+ /* ── 分组标签 ── */
149
+ .color-group-label {
150
+ margin: 18px 0 6px;
165
151
  color: var(--klc-color-axis-text);
166
152
  font-size: 12px;
167
153
  font-weight: 500;
168
- white-space: nowrap;
169
- cursor: pointer;
170
- transition:
171
- background 0.18s ease,
172
- border-color 0.18s ease,
173
- color 0.18s ease,
174
- box-shadow 0.18s ease;
175
- }
176
-
177
- .color-reset-btn:hover {
178
- border-color: var(--klc-color-axis-text);
179
- background: var(--klc-color-background);
180
- color: var(--klc-color-foreground);
181
- box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);
182
- }
183
-
184
- .color-reset-btn:active {
185
- background: var(--klc-color-tag-bg-hover);
186
- box-shadow: none;
154
+ letter-spacing: 0.3px;
155
+ line-height: 1;
187
156
  }
188
157
 
189
- /* ── 分组标签 ── */
190
- .color-group-label {
191
- margin: 6px 0 6px;
192
- color: var(--klc-color-axis-text);
193
- font-size: 12px;
194
- font-weight: 600;
195
- line-height: 1.3;
158
+ .color-group-label:first-of-type {
159
+ margin-top: 0;
196
160
  }
197
161
 
198
162
  /* ── 颜色网格 ── */
199
163
  .color-grid {
200
164
  display: grid;
201
165
  grid-template-columns: repeat(2, minmax(0, 1fr));
202
- gap: 6px;
166
+ gap: 4px;
203
167
  }
204
168
 
205
- /* ── 颜色条目 ── */
169
+ /* ── 颜色条目 (扁平化样式) ── */
206
170
  .color-item {
207
171
  display: flex;
208
172
  align-items: center;
209
173
  justify-content: space-between;
210
174
  gap: 8px;
211
- min-height: 36px;
212
- padding: 6px 10px;
213
- border: 1px solid var(--klc-color-grid-major);
214
- border-radius: 8px;
215
- background: var(--klc-color-background);
175
+ min-height: 40px;
176
+ padding: 8px 12px;
177
+ border-radius: 6px;
178
+ background: transparent;
216
179
  color: var(--klc-color-foreground);
217
- font-size: 12px;
218
- line-height: 1.3;
180
+ font-size: 13px;
219
181
  cursor: pointer;
220
- transition:
221
- border-color 0.18s ease,
222
- background 0.18s ease,
223
- box-shadow 0.18s ease;
182
+ transition: background 0.15s ease;
224
183
  }
225
184
 
226
185
  .color-item:hover {
227
- border-color: var(--klc-color-axis-line);
228
- background: var(--klc-color-tag-bg-hover);
229
- box-shadow: 0 1px 4px color-mix(in srgb, var(--klc-color-foreground) 6%, transparent);
186
+ background: var(--klc-color-grid-minor);
230
187
  }
231
188
 
232
- .color-item span {
189
+ .color-item-text {
233
190
  min-width: 0;
234
191
  overflow: hidden;
235
192
  text-overflow: ellipsis;
236
193
  white-space: nowrap;
237
194
  user-select: none;
195
+ line-height: 1.4;
238
196
  }
239
197
 
240
- /* ── 颜色输入 ── */
198
+ /* ── 颜色输入 (无边框圆角矩形) ── */
241
199
  .color-input {
242
200
  flex: 0 0 auto;
243
201
  width: 26px;
244
202
  height: 26px;
245
203
  padding: 0;
246
- border: 1px solid var(--klc-color-axis-line);
204
+ border: 1px solid var(--klc-color-border-button);
247
205
  border-radius: 6px;
248
206
  background: transparent;
249
207
  cursor: pointer;
250
- transition:
251
- border-color 0.18s ease,
252
- box-shadow 0.18s ease;
208
+ transition: transform 0.15s ease;
209
+ overflow: hidden;
253
210
  }
254
211
 
255
212
  .color-input:hover {
256
- border-color: var(--klc-color-axis-text);
257
- box-shadow: 0 0 0 2px color-mix(in srgb, var(--klc-color-foreground) 6%, transparent);
258
- }
259
-
260
- .color-input:focus-visible {
261
- outline: none;
262
- border-color: var(--klc-color-axis-text);
263
- box-shadow: 0 0 0 2px color-mix(in srgb, var(--klc-color-foreground) 10%, transparent);
213
+ transform: scale(1.1);
264
214
  }
265
215
 
266
216
  .color-input::-webkit-color-swatch-wrapper {
267
- padding: 2px;
217
+ padding: 0;
268
218
  }
269
219
 
270
220
  .color-input::-webkit-color-swatch {
271
221
  border: none;
272
- border-radius: 4px;
222
+ border-radius: 6px;
223
+ }
224
+
225
+ .color-input::-moz-color-swatch {
226
+ border: none;
227
+ border-radius: 6px;
273
228
  }
274
229
 
275
230
  /* ── 响应式 ── */
276
231
  @media (max-width: 480px) {
277
232
  .color-preset-tools {
278
- grid-template-columns: 1fr;
279
- }
280
-
281
- .color-reset-btn {
282
- width: 100%;
233
+ flex-direction: column;
234
+ align-items: stretch;
283
235
  }
284
236
 
285
237
  .color-grid {
@@ -14,8 +14,16 @@
14
14
  <span v-if="comparisonLoading" class="compare-chip__spinner" />
15
15
  <span v-if="selected.length > 0" class="compare-chip__badge">{{ selected.length }}</span>
16
16
  </button>
17
- <Transition name="symbol-popover">
18
- <div v-if="showPopup" class="compare-popover" role="dialog" aria-label="比较商品">
17
+ <Teleport :to="teleportTarget">
18
+ <Transition name="symbol-popover">
19
+ <div
20
+ v-if="showPopup"
21
+ ref="popupRef"
22
+ class="compare-popover"
23
+ :style="popupStyle"
24
+ role="dialog"
25
+ aria-label="比较商品"
26
+ >
19
27
  <div class="compare-search">
20
28
  <span class="compare-search__icon" aria-hidden="true">
21
29
  <svg width="14" height="14" viewBox="0 0 16 16" fill="none">
@@ -134,13 +142,16 @@
134
142
  </button>
135
143
  </div>
136
144
  </div>
137
- </Transition>
145
+ </Transition>
146
+ </Teleport>
138
147
  </div>
139
148
  </template>
140
149
 
141
150
  <script setup lang="ts">
142
- import { ref, computed, nextTick, onMounted, onBeforeUnmount } from 'vue'
151
+ import { ref, computed, watch, nextTick, onMounted, onBeforeUnmount } from 'vue'
143
152
  import type { SymbolItem } from './SymbolSelector.vue'
153
+ import { useTeleportedPopup } from '../composables/useTeleportedPopup'
154
+ import { useFullscreenTeleportTarget } from '../composables/useFullscreenTeleportTarget'
144
155
 
145
156
  const props = withDefaults(defineProps<{
146
157
  symbols: SymbolItem[]
@@ -160,6 +171,15 @@ const showPopup = ref(false)
160
171
  const searchQuery = ref('')
161
172
  const searchInputRef = ref<HTMLInputElement | null>(null)
162
173
  const rootRef = ref<HTMLElement | null>(null)
174
+ const popupRef = ref<HTMLElement | null>(null)
175
+
176
+ const teleportTarget = useFullscreenTeleportTarget()
177
+
178
+ const { popupStyle, startPositionSync, stopPositionSync } = useTeleportedPopup(
179
+ rootRef,
180
+ popupRef,
181
+ 8,
182
+ )
163
183
 
164
184
  const selectedSet = computed(() => new Set(props.selected ?? []))
165
185
 
@@ -202,13 +222,23 @@ function togglePopup() {
202
222
  }
203
223
  }
204
224
 
225
+ watch(showPopup, (val) => {
226
+ if (val) {
227
+ startPositionSync()
228
+ } else {
229
+ stopPositionSync()
230
+ }
231
+ })
232
+
205
233
  function clearSearch() {
206
234
  searchQuery.value = ''
207
235
  searchInputRef.value?.focus()
208
236
  }
209
237
 
210
238
  function onDocumentClick(e: MouseEvent) {
211
- if (rootRef.value && !rootRef.value.contains(e.target as Node)) {
239
+ const root = rootRef.value
240
+ const popup = popupRef.value
241
+ if (root && !root.contains(e.target as Node) && !popup?.contains(e.target as Node)) {
212
242
  showPopup.value = false
213
243
  searchQuery.value = ''
214
244
  }
@@ -302,15 +332,12 @@ onBeforeUnmount(() => document.removeEventListener('mousedown', onDocumentClick)
302
332
  }
303
333
 
304
334
  .compare-popover {
305
- position: absolute;
306
- top: calc(100% + 8px);
307
- left: 0;
308
- z-index: 20;
335
+ z-index: 110;
309
336
  width: min(360px, calc(100vw - 24px));
310
337
  padding: 14px;
311
338
  border: 1px solid var(--klc-color-border-button);
312
339
  border-radius: 3px;
313
- background: var(--klc-color-tag-bg-white);
340
+ background: var(--klc-color-background);
314
341
  color: var(--klc-color-foreground);
315
342
 
316
343
  box-sizing: border-box;
@@ -1,11 +1,6 @@
1
1
  <template>
2
- <div
3
- class="drawing-style-toolbar"
4
- @pointerdown.stop
5
- @pointermove.stop
6
- @pointerup.stop
7
- >
8
- <div class="toolbar-item color-item" title="颜色">
2
+ <CanvasToolbar>
3
+ <div class="color-item" title="颜色">
9
4
  <span class="color-swatch" :style="{ background: drawing.style.stroke ?? '#2962ff' }"></span>
10
5
  <input
11
6
  type="color"
@@ -31,20 +26,35 @@
31
26
  @update:model-value="onLineStyleChange($event as 'solid' | 'dashed' | 'dotted')"
32
27
  />
33
28
 
34
- <button type="button" class="toolbar-btn delete-btn" title="删除" @click="$emit('delete')">
35
- <svg class="delete-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
29
+ <button
30
+ type="button"
31
+ class="toolbar-btn toolbar-btn--delete"
32
+ title="删除"
33
+ @click="$emit('delete')"
34
+ >
35
+ <svg
36
+ class="delete-icon"
37
+ viewBox="0 0 24 24"
38
+ fill="none"
39
+ stroke="currentColor"
40
+ stroke-width="2"
41
+ stroke-linecap="round"
42
+ stroke-linejoin="round"
43
+ aria-hidden="true"
44
+ >
36
45
  <path d="M3 6h18" />
37
46
  <path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6" />
38
47
  <path d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2" />
39
48
  </svg>
40
49
  </button>
41
- </div>
50
+ </CanvasToolbar>
42
51
  </template>
43
52
 
44
53
  <script setup lang="ts">
45
54
  import { onMounted, onUnmounted } from 'vue'
46
55
  import type { DrawingObject, DrawingStyle } from '@363045841yyt/klinechart-core/plugin'
47
56
  import Dropdown from './Dropdown.vue'
57
+ import CanvasToolbar from './common/CanvasToolbar.vue'
48
58
 
49
59
  const widthOptions = [
50
60
  { label: '1px', value: '1' },
@@ -92,46 +102,29 @@ function onLineStyleChange(style: 'solid' | 'dashed' | 'dotted') {
92
102
  </script>
93
103
 
94
104
  <style scoped>
95
- .drawing-style-toolbar {
96
- position: absolute;
97
- left: 50%;
98
- top: 8px;
99
- transform: translateX(-50%);
100
- display: flex;
101
- align-items: center;
102
- gap: 6px;
103
- padding: 4px 8px;
104
- height: 32px;
105
- background: color-mix(in srgb, var(--klc-color-tag-bg-white) 88%, transparent);
106
- backdrop-filter: blur(8px);
107
- -webkit-backdrop-filter: blur(8px);
108
- border: 1px solid var(--klc-color-border-button);
109
- border-radius: 6px;
110
- box-shadow: 0 1px 3px rgba(0, 0, 0, 0.06);
111
- z-index: 100;
112
- user-select: none;
113
- pointer-events: auto;
114
- }
115
-
116
- .toolbar-item {
105
+ .color-item {
106
+ position: relative;
117
107
  display: inline-flex;
118
108
  align-items: center;
119
109
  justify-content: center;
110
+ width: 26px;
111
+ height: 26px;
112
+ border-radius: 4px;
113
+ cursor: pointer;
114
+ transition: background 0.15s ease;
120
115
  }
121
116
 
122
- .color-item {
123
- position: relative;
124
- width: 24px;
125
- height: 24px;
117
+ .color-item:hover {
118
+ background: var(--klc-color-grid-minor);
126
119
  }
127
120
 
128
121
  .color-swatch {
129
122
  display: block;
130
- width: 100%;
131
- height: 100%;
132
- border: 1px solid var(--klc-color-axis-line);
123
+ width: 16px;
124
+ height: 16px;
125
+ border: 1px solid rgba(0, 0, 0, 0.15);
133
126
  border-radius: 4px;
134
- cursor: pointer;
127
+ pointer-events: none;
135
128
  }
136
129
 
137
130
  .color-input {
@@ -142,36 +135,4 @@ function onLineStyleChange(style: 'solid' | 'dashed' | 'dotted') {
142
135
  width: 100%;
143
136
  height: 100%;
144
137
  }
145
-
146
- .toolbar-btn {
147
- display: inline-flex;
148
- align-items: center;
149
- justify-content: center;
150
- width: 24px;
151
- height: 24px;
152
- padding: 0;
153
- border: 1px solid transparent;
154
- border-radius: 4px;
155
- background: transparent;
156
- color: var(--klc-color-axis-text);
157
- cursor: pointer;
158
- transition: border-color 0.15s ease, background 0.15s ease, color 0.15s ease;
159
- }
160
-
161
- .toolbar-btn:hover {
162
- border-color: var(--klc-color-axis-line);
163
- background: var(--klc-color-grid-minor);
164
- color: var(--klc-color-foreground);
165
- }
166
-
167
- .delete-btn:hover {
168
- color: #dc2626;
169
- border-color: #fca5a5;
170
- background: #fef2f2;
171
- }
172
-
173
- .delete-icon {
174
- width: 14px;
175
- height: 14px;
176
- }
177
138
  </style>
@@ -19,20 +19,29 @@
19
19
  <span class="dropdown__chevron" aria-hidden="true"></span>
20
20
  </button>
21
21
 
22
- <div v-if="isOpen" class="dropdown__menu" :style="menuStyle" role="listbox" tabindex="-1">
23
- <button
24
- v-for="option in options"
25
- :key="option.value"
26
- type="button"
27
- class="dropdown__option"
28
- :class="{ 'is-selected': option.value === selectedValue }"
29
- role="option"
30
- :aria-selected="option.value === selectedValue"
31
- @click="selectOption(option.value)"
22
+ <Teleport :to="teleportTarget">
23
+ <div
24
+ v-if="isOpen"
25
+ ref="menuRef"
26
+ class="dropdown__menu"
27
+ :style="menuStyle"
28
+ role="listbox"
29
+ tabindex="-1"
32
30
  >
33
- {{ option.label }}
34
- </button>
35
- </div>
31
+ <button
32
+ v-for="option in options"
33
+ :key="option.value"
34
+ type="button"
35
+ class="dropdown__option"
36
+ :class="{ 'is-selected': option.value === selectedValue }"
37
+ role="option"
38
+ :aria-selected="option.value === selectedValue"
39
+ @click="selectOption(option.value)"
40
+ >
41
+ {{ option.label }}
42
+ </button>
43
+ </div>
44
+ </Teleport>
36
45
  </div>
37
46
  </template>
38
47
 
@@ -44,6 +53,8 @@ let dropdownIdSeed = 0
44
53
 
45
54
  <script setup lang="ts">
46
55
  import { computed, onBeforeUnmount, ref } from 'vue'
56
+ import { useTeleportedPopup } from '../composables/useTeleportedPopup'
57
+ import { useFullscreenTeleportTarget } from '../composables/useFullscreenTeleportTarget'
47
58
 
48
59
  export interface DropdownOption<T extends string = string> {
49
60
  label: string
@@ -71,10 +82,19 @@ const emit = defineEmits<{
71
82
 
72
83
  const rootRef = ref<HTMLElement | null>(null)
73
84
  const triggerRef = ref<HTMLElement | null>(null)
85
+ const menuRef = ref<HTMLElement | null>(null)
74
86
  const isOpen = ref(false)
75
87
  const menuWidth = ref(0)
76
88
  const dropdownId = ++dropdownIdSeed
77
89
 
90
+ const teleportTarget = useFullscreenTeleportTarget()
91
+
92
+ const { popupStyle, startPositionSync, stopPositionSync } = useTeleportedPopup(
93
+ triggerRef,
94
+ menuRef,
95
+ 4,
96
+ )
97
+
78
98
  const triggerStyle = computed(() => {
79
99
  if (props.minWidth) return { minWidth: props.minWidth }
80
100
  return {}
@@ -83,7 +103,11 @@ const triggerStyle = computed(() => {
83
103
  const menuStyle = computed(() => {
84
104
  if (!isOpen.value) return undefined
85
105
  const w = menuWidth.value || (props.minWidth ? parseInt(props.minWidth) : 0)
86
- return { width: w ? `${w}px` : undefined }
106
+ return {
107
+ width: w ? `${w}px` : undefined,
108
+ zIndex: 1010,
109
+ ...popupStyle.value,
110
+ }
87
111
  })
88
112
 
89
113
  const selectedValue = computed(() => {
@@ -107,6 +131,7 @@ function open() {
107
131
  activeDropdownClose = close
108
132
  menuWidth.value = triggerRef.value?.offsetWidth ?? 0
109
133
  isOpen.value = true
134
+ startPositionSync()
110
135
  document.addEventListener('pointerdown', handleDocumentPointerDown)
111
136
  }
112
137
 
@@ -117,6 +142,7 @@ function close() {
117
142
  activeDropdownId = 0
118
143
  activeDropdownClose = null
119
144
  }
145
+ stopPositionSync()
120
146
  document.removeEventListener('pointerdown', handleDocumentPointerDown)
121
147
  }
122
148
 
@@ -135,7 +161,8 @@ function selectOption(value: string) {
135
161
 
136
162
  function handleDocumentPointerDown(event: PointerEvent) {
137
163
  const root = rootRef.value
138
- if (root && !root.contains(event.target as Node | null)) {
164
+ const menu = menuRef.value
165
+ if (root && !root.contains(event.target as Node | null) && !menu?.contains(event.target as Node | null)) {
139
166
  close()
140
167
  }
141
168
  }
@@ -219,10 +246,6 @@ onBeforeUnmount(close)
219
246
  }
220
247
 
221
248
  .dropdown__menu {
222
- position: absolute;
223
- z-index: 30;
224
- top: calc(100% + 4px);
225
- left: 0;
226
249
  padding: 4px;
227
250
  border: 1px solid var(--klc-color-border-button);
228
251
  border-radius: 4px;