@svgrid/grid 2.6.23 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +88 -88
- package/README.md +199 -199
- package/dist/FlexRender.svelte +96 -96
- package/dist/GridFooter.svelte +181 -181
- package/dist/SvAutoComplete.svelte +169 -169
- package/dist/SvAvatar.svelte +75 -75
- package/dist/SvCalendar.svelte +503 -503
- package/dist/SvCarousel.svelte +141 -141
- package/dist/SvCheckBox.svelte +102 -102
- package/dist/SvCircularProgress.svelte +109 -109
- package/dist/SvColorInput.svelte +181 -181
- package/dist/SvComboBox.svelte +279 -279
- package/dist/SvContextMenu.svelte +116 -116
- package/dist/SvCountryInput.svelte +163 -163
- package/dist/SvDrawer.svelte +254 -254
- package/dist/SvDropDownList.svelte +378 -378
- package/dist/SvDurationInput.svelte +126 -126
- package/dist/SvField.svelte +293 -293
- package/dist/SvForm.svelte +437 -437
- package/dist/SvGridCellEditor.svelte +744 -744
- package/dist/SvGridChart.svelte +1716 -1716
- package/dist/SvGridChartPanel.svelte +485 -485
- package/dist/SvGridChartView.svelte +70 -70
- package/dist/SvGridDropdown.svelte +728 -728
- package/dist/SvGridSelect.svelte +270 -270
- package/dist/SvGroupCell.svelte +116 -116
- package/dist/SvListBox.svelte +334 -334
- package/dist/SvMaskedInput.svelte +122 -122
- package/dist/SvMenu.svelte +124 -124
- package/dist/SvMenuList.svelte +146 -146
- package/dist/SvMultiSelect.svelte +293 -293
- package/dist/SvNumberInput.svelte +172 -172
- package/dist/SvOtpInput.svelte +158 -158
- package/dist/SvPasswordInput.svelte +151 -151
- package/dist/SvPhoneInput.svelte +133 -133
- package/dist/SvPopover.svelte +197 -197
- package/dist/SvProgress.svelte +116 -116
- package/dist/SvRadioGroup.svelte +107 -107
- package/dist/SvRating.svelte +112 -112
- package/dist/SvResult.svelte +73 -73
- package/dist/SvRichText.svelte +211 -211
- package/dist/SvRowGroupPanel.svelte +170 -170
- package/dist/SvScrollArea.svelte +61 -61
- package/dist/SvSlider.svelte +203 -203
- package/dist/SvSwitchButton.svelte +108 -108
- package/dist/SvTagsInput.svelte +115 -115
- package/dist/SvTextInput.svelte +147 -147
- package/dist/SvTimePicker.svelte +245 -245
- package/dist/SvToaster.svelte +159 -159
- package/dist/SvToggleButton.svelte +85 -85
- package/dist/SvTooltip.svelte +161 -161
- package/dist/SvTour.svelte +208 -208
- package/dist/SvTree.svelte +444 -444
- package/dist/SvTreeSelect.svelte +239 -239
- package/dist/cdn/{GridMenus-BzWXQjv3.js → GridMenus-DMihUtma.js} +3 -3
- package/dist/cdn/{GridMenus-xr_rHx8F.js → GridMenus-nmNDj1a3.js} +3 -3
- package/dist/cdn/{SvDateRangeInput-DMLKmGEc.js → SvDateRangeInput-Bh0A0JkF.js} +1 -1
- package/dist/cdn/{SvDateRangeInput-CaOuMs8O.js → SvDateRangeInput-DflbiP7N.js} +1 -1
- package/dist/cdn/{SvDateTimePicker-sonaH0oh.js → SvDateTimePicker-BWwpfB_o.js} +1 -1
- package/dist/cdn/{SvDateTimePicker-CCbDZNZB.js → SvDateTimePicker-Bivn8dAP.js} +1 -1
- package/dist/cdn/{SvGridCellEditor-BAPSC7EL.js → SvGridCellEditor-AXL8cHGO.js} +1 -1
- package/dist/cdn/{SvGridCellEditor-BwzmUWtL.js → SvGridCellEditor-Ba7rY3eu.js} +1 -1
- package/dist/cdn/{SvGridChart-BwVos976.js → SvGridChart-Bs2GIR2Q.js} +1 -1
- package/dist/cdn/{SvGridChart-BEJmNNx9.js → SvGridChart-CwhFz7GV.js} +1 -1
- package/dist/cdn/{SvGridChartPanel-BOknOkrP.js → SvGridChartPanel-1WXuStzM.js} +1 -1
- package/dist/cdn/{SvGridChartPanel-D2PjII4O.js → SvGridChartPanel-e5_nqNP9.js} +1 -1
- package/dist/cdn/{SvGridChartView-00LPUHL1.js → SvGridChartView-SmPW10dI.js} +1 -1
- package/dist/cdn/{SvGridChartView-BhUEJ5ki.js → SvGridChartView-eSPuJE6g.js} +1 -1
- package/dist/cdn/{SvGridDropdown-D0VdjeR8.js → SvGridDropdown-B1ZcLpgs.js} +1 -1
- package/dist/cdn/{SvGridDropdown-D13MtJ2j.js → SvGridDropdown-CtdbHcIS.js} +1 -1
- package/dist/cdn/{date-format-BnnHlqGw.js → date-format-CcP1tafP.js} +2 -2
- package/dist/cdn/{date-format-BNii4zeD.js → date-format-DZT7T1wf.js} +2 -2
- package/dist/cdn/{row-drag-touch-Ddaa-QeB.js → row-drag-touch-CycRR65n.js} +12 -10
- package/dist/cdn/{src-skQN5eqh.js → src-B_YS5AOc.js} +45 -45
- package/dist/cdn/{src-DGo7IHug.js → src-DYXXpuSk.js} +44 -44
- package/dist/cdn/svgrid.js +7 -7
- package/dist/cdn/svgrid.svelte-external.js +7 -7
- package/dist/chart-export.js +8 -8
- package/dist/row-drag-touch.d.ts +5 -1
- package/dist/row-drag-touch.js +15 -4
- package/dist/row-drag.js +7 -3
- package/package.json +11 -11
- package/src/FlexRender.svelte +96 -96
- package/src/GridFooter.svelte +181 -181
- package/src/SvAutoComplete.svelte +169 -169
- package/src/SvAvatar.svelte +75 -75
- package/src/SvCalendar.svelte +503 -503
- package/src/SvCalendar.test.ts +226 -226
- package/src/SvCarousel.svelte +141 -141
- package/src/SvCheckBox.svelte +102 -102
- package/src/SvCircularProgress.svelte +109 -109
- package/src/SvColorInput.svelte +181 -181
- package/src/SvComboBox.svelte +279 -279
- package/src/SvContextMenu.svelte +116 -116
- package/src/SvCountryInput.svelte +163 -163
- package/src/SvDrawer.svelte +254 -254
- package/src/SvDropDownList.svelte +378 -378
- package/src/SvDurationInput.svelte +126 -126
- package/src/SvField.svelte +293 -293
- package/src/SvForm.svelte +437 -437
- package/src/SvForm.test.ts +411 -411
- package/src/SvGrid.types.ts +2092 -2092
- package/src/SvGridCellEditor.svelte +744 -744
- package/src/SvGridChart.svelte +1716 -1716
- package/src/SvGridChartPanel.svelte +485 -485
- package/src/SvGridChartView.svelte +70 -70
- package/src/SvGridDropdown.svelte +728 -728
- package/src/SvGridSelect.svelte +270 -270
- package/src/SvGroupCell.svelte +116 -116
- package/src/SvListBox.svelte +334 -334
- package/src/SvMaskedInput.svelte +122 -122
- package/src/SvMenu.svelte +124 -124
- package/src/SvMenu.test.ts +97 -97
- package/src/SvMenuList.svelte +146 -146
- package/src/SvMultiSelect.svelte +293 -293
- package/src/SvNumberInput.svelte +172 -172
- package/src/SvOtpInput.svelte +158 -158
- package/src/SvPasswordInput.svelte +151 -151
- package/src/SvPhoneInput.svelte +133 -133
- package/src/SvPopover.svelte +197 -197
- package/src/SvProgress.svelte +116 -116
- package/src/SvRadioGroup.svelte +107 -107
- package/src/SvRating.svelte +112 -112
- package/src/SvResult.svelte +73 -73
- package/src/SvRichText.svelte +211 -211
- package/src/SvRowGroupPanel.svelte +170 -170
- package/src/SvScrollArea.svelte +61 -61
- package/src/SvSlider.svelte +203 -203
- package/src/SvSwitchButton.svelte +108 -108
- package/src/SvTagsInput.svelte +115 -115
- package/src/SvTextInput.svelte +147 -147
- package/src/SvTimePicker.svelte +245 -245
- package/src/SvToaster.svelte +159 -159
- package/src/SvToaster.test.ts +95 -95
- package/src/SvToggleButton.svelte +85 -85
- package/src/SvTooltip.svelte +161 -161
- package/src/SvTour.svelte +208 -208
- package/src/SvTree.svelte +444 -444
- package/src/SvTreeSelect.svelte +239 -239
- package/src/a11y/dismissable.test.ts +119 -119
- package/src/a11y/dismissable.ts +114 -114
- package/src/a11y.contract.test.ts +49 -49
- package/src/a11y.test.ts +59 -59
- package/src/a11y.ts +61 -61
- package/src/ai.test.ts +502 -502
- package/src/ai.ts +1419 -1419
- package/src/build-api.coverage.test.ts +633 -633
- package/src/build-api.ts +846 -846
- package/src/builtin-editors.grid.test.ts +83 -83
- package/src/cell-formatting.ts +171 -171
- package/src/cell-render.test.ts +513 -513
- package/src/cell-render.ts +496 -496
- package/src/cell-values.ts +148 -148
- package/src/chart-export.ts +202 -202
- package/src/chart-view.svelte.ts +36 -36
- package/src/chart.ts +2321 -2321
- package/src/collaboration.test.ts +104 -104
- package/src/collaboration.ts +167 -167
- package/src/column-groups.ts +78 -78
- package/src/core.performance.test.ts +30 -30
- package/src/core.ts +1865 -1865
- package/src/createAutocomplete.svelte.ts +132 -132
- package/src/createCombobox.svelte.ts +191 -191
- package/src/createCountryInput.svelte.ts +157 -157
- package/src/createDropdownList.svelte.ts +168 -168
- package/src/createForm.svelte.ts +386 -386
- package/src/createGrid.svelte.ts +42 -42
- package/src/createGrid.test.ts +10 -10
- package/src/createGridState.svelte.ts +17 -17
- package/src/createListbox.svelte.ts +250 -250
- package/src/createMenu.svelte.ts +224 -224
- package/src/createPopoverSelect.svelte.ts +213 -213
- package/src/createSlider.svelte.ts +191 -191
- package/src/createTooltip.svelte.ts +144 -144
- package/src/createTree.svelte.ts +322 -322
- package/src/datetime/date-core.ts +206 -206
- package/src/datetime/date-restrict.ts +61 -61
- package/src/datetime/timezone.ts +135 -135
- package/src/dock-manager-model.ts +596 -596
- package/src/dock-model.ts +374 -374
- package/src/editing.test.ts +974 -974
- package/src/editing.ts +609 -609
- package/src/editor-contract.ts +171 -171
- package/src/editor-registry.grid.test.ts +144 -144
- package/src/editor-registry.ts +122 -122
- package/src/export-data-api.test.ts +126 -126
- package/src/export-format.test.ts +107 -107
- package/src/export-format.ts +601 -601
- package/src/filter-operators.ts +160 -160
- package/src/filtering/excel-filters.ts +325 -325
- package/src/flex-render.ts +3 -3
- package/src/form-field.ts +127 -127
- package/src/group-display.test.ts +167 -167
- package/src/group-display.ts +200 -200
- package/src/headless.ts +87 -87
- package/src/js-scroller.svelte.ts +173 -173
- package/src/keyboard-handlers.ts +270 -270
- package/src/keyboard.test.ts +59 -59
- package/src/keyboard.ts +97 -97
- package/src/list-nav.test.ts +49 -49
- package/src/list-nav.ts +29 -29
- package/src/list-option.test.ts +56 -56
- package/src/list-option.ts +179 -179
- package/src/menus.ts +597 -597
- package/src/merge-objects.ts +48 -48
- package/src/overlays.test.ts +90 -90
- package/src/positioning.ts +268 -268
- package/src/render-component.ts +28 -28
- package/src/row-drag-touch.ts +20 -5
- package/src/row-drag.test.ts +401 -353
- package/src/row-drag.ts +419 -415
- package/src/row-resize.test.ts +524 -524
- package/src/row-resize.ts +228 -228
- package/src/scheduler-ical.ts +181 -181
- package/src/scheduler-model.test.ts +562 -562
- package/src/scheduler-model.ts +873 -873
- package/src/selection.test.ts +885 -885
- package/src/server-data-source.test.ts +383 -383
- package/src/server-data-source.ts +469 -469
- package/src/sparkline.test.ts +68 -68
- package/src/sparkline.ts +169 -169
- package/src/spreadsheet.test.ts +488 -488
- package/src/spreadsheet.ts +312 -312
- package/src/static-functions.ts +11 -11
- package/src/subscribe.ts +38 -38
- package/src/summaries.ts +113 -113
- package/src/svgrid-wrapper.types.ts +563 -563
- package/src/svgrid.async-editor-options.test.ts +273 -273
- package/src/svgrid.auto-row-height.test.ts +204 -204
- package/src/svgrid.behavior.test.ts +910 -910
- package/src/svgrid.charting.test.ts +534 -534
- package/src/svgrid.comments-autocomplete.test.ts +127 -127
- package/src/svgrid.context-menu.test.ts +147 -147
- package/src/svgrid.features.test.ts +157 -157
- package/src/svgrid.filter-depth.test.ts +163 -163
- package/src/svgrid.filter-menu-listbox.svelte.test.ts +377 -377
- package/src/svgrid.filter-menu-scroll.test.ts +112 -112
- package/src/svgrid.grand-total.test.ts +188 -188
- package/src/svgrid.group-display-mode.test.ts +171 -171
- package/src/svgrid.group-footers.test.ts +121 -121
- package/src/svgrid.group-pagination.test.ts +153 -153
- package/src/svgrid.new-features.wrapper.test.ts +251 -251
- package/src/svgrid.tree-data.test.ts +186 -186
- package/src/svgrid.wrapper.test.ts +63 -63
- package/src/svgriddropdown.async-panel.svelte.test.ts +195 -195
- package/src/test-setup.ts +62 -62
- package/src/themes/index.ts +288 -288
- package/src/toast-store.svelte.ts +250 -250
- package/src/toast-store.test.ts +147 -147
- package/src/tree-row-model.test.ts +168 -168
- package/src/ui-buttons.test.ts +144 -144
- package/src/ui-inputs.test.ts +118 -118
- package/src/ui-localization.test.ts +113 -113
- package/src/ui-range.test.ts +70 -70
- package/src/ui-selection.test.ts +155 -155
- package/src/ui-tier1.test.ts +142 -142
- package/src/virtual.test.ts +88 -88
- package/src/virtualization/column-virtualizer.test.ts +27 -27
- package/src/virtualization/column-virtualizer.ts +30 -30
- package/src/virtualization/svelte-virtualizer.svelte.ts +26 -26
- package/src/virtualization/types.ts +30 -30
- package/src/virtualization/virtualizer.test.ts +47 -47
- package/src/virtualization/virtualizer.ts +322 -322
package/src/positioning.ts
CHANGED
|
@@ -1,268 +1,268 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* positioning - a small, dependency-free anchor-positioning engine for the
|
|
3
|
-
* overlay layer (popover, tooltip, menu, dropdowns). It replaces the vertical-
|
|
4
|
-
* flip-only `anchoredRect` for components that need the full placement matrix.
|
|
5
|
-
*
|
|
6
|
-
* Two halves:
|
|
7
|
-
*
|
|
8
|
-
* 1. `computePosition(reference, floating, options)` - PURE geometry. Given the
|
|
9
|
-
* reference rect and the floating size (plus a viewport), it returns the
|
|
10
|
-
* floating `x`/`y`, the resolved `placement` (after flip), the arrow offset,
|
|
11
|
-
* and the available `maxWidth`/`maxHeight` on the chosen side. No DOM, so it
|
|
12
|
-
* is fully unit-tested by feeding rects and asserting placement.
|
|
13
|
-
*
|
|
14
|
-
* 2. `autoUpdate(referenceEl, floatingEl, update)` - the browser half. Re-runs
|
|
15
|
-
* `update` whenever the reference/floating resize, an ancestor scrolls, or the
|
|
16
|
-
* window resizes (via `ResizeObserver` + scroll/resize listeners). SSR-safe
|
|
17
|
-
* (a no-op without `window`).
|
|
18
|
-
*
|
|
19
|
-
* The middleware (flip / shift / size / arrow) is implemented inline - no
|
|
20
|
-
* Floating-UI dependency - matching the kit's zero-runtime-dep posture.
|
|
21
|
-
*/
|
|
22
|
-
|
|
23
|
-
/** A physical side the floating element is placed on, relative to the reference. */
|
|
24
|
-
export type Side = 'top' | 'bottom' | 'left' | 'right'
|
|
25
|
-
|
|
26
|
-
/** Cross-axis alignment. `center` is the bare side (e.g. `'bottom'`). */
|
|
27
|
-
export type Align = 'start' | 'center' | 'end'
|
|
28
|
-
|
|
29
|
-
/** `'bottom'` = bottom-center; `'bottom-start'` / `'bottom-end'` align the edges. */
|
|
30
|
-
export type Placement = Side | `${Side}-start` | `${Side}-end`
|
|
31
|
-
|
|
32
|
-
/** A viewport-relative rectangle (CSS `getBoundingClientRect` shape suffices). */
|
|
33
|
-
export type Rect = { x: number; y: number; width: number; height: number }
|
|
34
|
-
|
|
35
|
-
/** The available viewport (defaults to `window` in the browser). */
|
|
36
|
-
export type Viewport = { width: number; height: number }
|
|
37
|
-
|
|
38
|
-
/** Where to put a floating element: its preferred side, offsets, and collision behaviour. */
|
|
39
|
-
export type ComputePositionOptions = {
|
|
40
|
-
/** Preferred placement. Default `'bottom-start'`. */
|
|
41
|
-
placement?: Placement
|
|
42
|
-
/** Main-axis gap between reference and floating. Default 6. */
|
|
43
|
-
offset?: number
|
|
44
|
-
/** Viewport edge kept clear on every side. Default 8. */
|
|
45
|
-
padding?: number
|
|
46
|
-
/** Flip to the opposite side when the preferred side overflows. Default true. */
|
|
47
|
-
flip?: boolean
|
|
48
|
-
/** Shift along the cross axis to stay in view (without flipping). Default true. */
|
|
49
|
-
shift?: boolean
|
|
50
|
-
/** Compute `maxWidth`/`maxHeight` for the chosen side. Default true. */
|
|
51
|
-
size?: boolean
|
|
52
|
-
/** When set, compute the arrow offset for an arrow element of this size (px). */
|
|
53
|
-
arrow?: { size: number }
|
|
54
|
-
/** Extra placements to try (after the automatic opposite) before giving up. */
|
|
55
|
-
fallbackPlacements?: Placement[]
|
|
56
|
-
/** Viewport to fit within. Defaults to the window; pure callers pass it. */
|
|
57
|
-
viewport?: Viewport
|
|
58
|
-
/** Floor for the available main-axis size returned by `size`. Default 0. */
|
|
59
|
-
minMainAxis?: number
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
/** The resolved position, including the side actually used after collision handling. */
|
|
63
|
-
export type ComputePositionResult = {
|
|
64
|
-
/** Floating left, in viewport (fixed-position) coordinates. */
|
|
65
|
-
x: number
|
|
66
|
-
/** Floating top, in viewport (fixed-position) coordinates. */
|
|
67
|
-
y: number
|
|
68
|
-
/** The resolved placement after flip. */
|
|
69
|
-
placement: Placement
|
|
70
|
-
/** The resolved side after flip. */
|
|
71
|
-
side: Side
|
|
72
|
-
/** The resolved cross-axis alignment. */
|
|
73
|
-
align: Align
|
|
74
|
-
/** Space available for the floating on the chosen side's main axis (+cross). */
|
|
75
|
-
maxWidth: number
|
|
76
|
-
maxHeight: number
|
|
77
|
-
/**
|
|
78
|
-
* Arrow offset within the floating box: `x` for top/bottom placements, `y` for
|
|
79
|
-
* left/right. Only present when `options.arrow` is set. Clamped so the arrow
|
|
80
|
-
* stays inside the floating (minus padding).
|
|
81
|
-
*/
|
|
82
|
-
arrow?: { x?: number; y?: number }
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
const SIDES: Record<Side, Side> = { top: 'bottom', bottom: 'top', left: 'right', right: 'left' }
|
|
86
|
-
|
|
87
|
-
/** Split a placement into its side + alignment (`center` when no suffix). */
|
|
88
|
-
export function parsePlacement(p: Placement): { side: Side; align: Align } {
|
|
89
|
-
const [side, align] = p.split('-') as [Side, 'start' | 'end' | undefined]
|
|
90
|
-
return { side, align: align ?? 'center' }
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
function opposite(p: Placement): Placement {
|
|
94
|
-
const { side, align } = parsePlacement(p)
|
|
95
|
-
return (align === 'center' ? SIDES[side] : `${SIDES[side]}-${align}`) as Placement
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
function clamp(v: number, min: number, max: number): number {
|
|
99
|
-
if (max < min) return min
|
|
100
|
-
return v < min ? min : v > max ? max : v
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
/** Floating x/y for a given side+align (before flip/shift). */
|
|
104
|
-
function coordsFor(side: Side, align: Align, r: Rect, f: Viewport, offset: number): { x: number; y: number } {
|
|
105
|
-
if (side === 'top' || side === 'bottom') {
|
|
106
|
-
const y = side === 'top' ? r.y - f.height - offset : r.y + r.height + offset
|
|
107
|
-
const x =
|
|
108
|
-
align === 'start' ? r.x : align === 'end' ? r.x + r.width - f.width : r.x + r.width / 2 - f.width / 2
|
|
109
|
-
return { x, y }
|
|
110
|
-
}
|
|
111
|
-
const x = side === 'left' ? r.x - f.width - offset : r.x + r.width + offset
|
|
112
|
-
const y =
|
|
113
|
-
align === 'start' ? r.y : align === 'end' ? r.y + r.height - f.height : r.y + r.height / 2 - f.height / 2
|
|
114
|
-
return { x, y }
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
/** How far the floating exceeds the viewport on the placement's MAIN axis. >0 = overflow. */
|
|
118
|
-
function mainAxisOverflow(side: Side, x: number, y: number, f: Viewport, v: Viewport, pad: number): number {
|
|
119
|
-
switch (side) {
|
|
120
|
-
case 'top':
|
|
121
|
-
return pad - y
|
|
122
|
-
case 'bottom':
|
|
123
|
-
return y + f.height - (v.height - pad)
|
|
124
|
-
case 'left':
|
|
125
|
-
return pad - x
|
|
126
|
-
default:
|
|
127
|
-
return x + f.width - (v.width - pad)
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
function resolveViewport(v?: Viewport): Viewport {
|
|
132
|
-
if (v) return v
|
|
133
|
-
if (typeof window !== 'undefined') return { width: window.innerWidth, height: window.innerHeight }
|
|
134
|
-
return { width: 0, height: 0 }
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
/**
|
|
138
|
-
* Compute the floating position anchored to `reference`, applying flip (opposite
|
|
139
|
-
* side when the preferred side overflows), shift (cross-axis clamp to stay in
|
|
140
|
-
* view), size (available max width/height on the chosen side) and an optional
|
|
141
|
-
* arrow offset. Pure - pass an explicit `viewport` in tests.
|
|
142
|
-
*/
|
|
143
|
-
export function computePosition(
|
|
144
|
-
reference: Rect,
|
|
145
|
-
floating: Viewport,
|
|
146
|
-
options: ComputePositionOptions = {},
|
|
147
|
-
): ComputePositionResult {
|
|
148
|
-
const offset = options.offset ?? 6
|
|
149
|
-
const pad = options.padding ?? 8
|
|
150
|
-
const doFlip = options.flip !== false
|
|
151
|
-
const doShift = options.shift !== false
|
|
152
|
-
const v = resolveViewport(options.viewport)
|
|
153
|
-
const preferred = options.placement ?? 'bottom-start'
|
|
154
|
-
|
|
155
|
-
// ---- flip: pick the placement whose main-axis overflow is smallest ----
|
|
156
|
-
const candidates: Placement[] = [preferred]
|
|
157
|
-
if (doFlip) {
|
|
158
|
-
candidates.push(opposite(preferred))
|
|
159
|
-
for (const fb of options.fallbackPlacements ?? []) candidates.push(fb)
|
|
160
|
-
}
|
|
161
|
-
let chosen = preferred
|
|
162
|
-
let best = Infinity
|
|
163
|
-
for (const cand of candidates) {
|
|
164
|
-
const { side, align } = parsePlacement(cand)
|
|
165
|
-
const { x, y } = coordsFor(side, align, reference, floating, offset)
|
|
166
|
-
const overflow = mainAxisOverflow(side, x, y, floating, v, pad)
|
|
167
|
-
if (overflow <= 0) {
|
|
168
|
-
chosen = cand
|
|
169
|
-
best = overflow
|
|
170
|
-
break
|
|
171
|
-
}
|
|
172
|
-
if (overflow < best) {
|
|
173
|
-
best = overflow
|
|
174
|
-
chosen = cand
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
const { side, align } = parsePlacement(chosen)
|
|
179
|
-
let { x, y } = coordsFor(side, align, reference, floating, offset)
|
|
180
|
-
|
|
181
|
-
// ---- shift: clamp along the CROSS axis so the floating stays in view ----
|
|
182
|
-
if (doShift) {
|
|
183
|
-
if (side === 'top' || side === 'bottom') {
|
|
184
|
-
x = clamp(x, pad, v.width - pad - floating.width)
|
|
185
|
-
} else {
|
|
186
|
-
y = clamp(y, pad, v.height - pad - floating.height)
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
// ---- size: available room on the chosen side ----
|
|
191
|
-
const minMain = options.minMainAxis ?? 0
|
|
192
|
-
let maxWidth: number
|
|
193
|
-
let maxHeight: number
|
|
194
|
-
if (side === 'top' || side === 'bottom') {
|
|
195
|
-
const avail = side === 'top' ? reference.y - offset - pad : v.height - pad - (reference.y + reference.height + offset)
|
|
196
|
-
maxHeight = Math.max(minMain, Math.floor(avail))
|
|
197
|
-
maxWidth = Math.max(0, v.width - 2 * pad)
|
|
198
|
-
} else {
|
|
199
|
-
const avail = side === 'left' ? reference.x - offset - pad : v.width - pad - (reference.x + reference.width + offset)
|
|
200
|
-
maxWidth = Math.max(minMain, Math.floor(avail))
|
|
201
|
-
maxHeight = Math.max(0, v.height - 2 * pad)
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
const result: ComputePositionResult = { x, y, placement: chosen, side, align, maxWidth, maxHeight }
|
|
205
|
-
|
|
206
|
-
// ---- arrow: point at the reference center, clamped inside the floating ----
|
|
207
|
-
if (options.arrow) {
|
|
208
|
-
const a = options.arrow.size
|
|
209
|
-
if (side === 'top' || side === 'bottom') {
|
|
210
|
-
const center = reference.x + reference.width / 2
|
|
211
|
-
const ax = clamp(center - x - a / 2, pad, floating.width - a - pad)
|
|
212
|
-
result.arrow = { x: ax }
|
|
213
|
-
} else {
|
|
214
|
-
const center = reference.y + reference.height / 2
|
|
215
|
-
const ay = clamp(center - y - a / 2, pad, floating.height - a - pad)
|
|
216
|
-
result.arrow = { y: ay }
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
return result
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
/** Collect scrollable ancestors so `autoUpdate` can listen to their scroll. */
|
|
224
|
-
function scrollParents(node: Element): Element[] {
|
|
225
|
-
const out: Element[] = []
|
|
226
|
-
let el: Element | null = node.parentElement
|
|
227
|
-
while (el) {
|
|
228
|
-
const style = getComputedStyle(el)
|
|
229
|
-
if (/(auto|scroll|overlay|hidden)/.test(style.overflow + style.overflowY + style.overflowX)) out.push(el)
|
|
230
|
-
el = el.parentElement
|
|
231
|
-
}
|
|
232
|
-
return out
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
/**
|
|
236
|
-
* Re-run `update` whenever the anchored position could change: the reference or
|
|
237
|
-
* floating element resizes, an ancestor scrolls, or the window scrolls/resizes.
|
|
238
|
-
* Returns a cleanup function. SSR-safe (a no-op without `window`).
|
|
239
|
-
*
|
|
240
|
-
* ```ts
|
|
241
|
-
* const stop = autoUpdate(triggerEl, panelEl, () => reposition())
|
|
242
|
-
* // ...later
|
|
243
|
-
* stop()
|
|
244
|
-
* ```
|
|
245
|
-
*/
|
|
246
|
-
export function autoUpdate(reference: Element, floating: HTMLElement, update: () => void): () => void {
|
|
247
|
-
if (typeof window === 'undefined') return () => {}
|
|
248
|
-
const ancestors = scrollParents(reference)
|
|
249
|
-
for (const a of ancestors) a.addEventListener('scroll', update, { passive: true })
|
|
250
|
-
window.addEventListener('scroll', update, { passive: true })
|
|
251
|
-
window.addEventListener('resize', update)
|
|
252
|
-
|
|
253
|
-
let ro: ResizeObserver | undefined
|
|
254
|
-
if (typeof ResizeObserver === 'function') {
|
|
255
|
-
ro = new ResizeObserver(update)
|
|
256
|
-
ro.observe(reference)
|
|
257
|
-
ro.observe(floating)
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
update()
|
|
261
|
-
|
|
262
|
-
return () => {
|
|
263
|
-
for (const a of ancestors) a.removeEventListener('scroll', update)
|
|
264
|
-
window.removeEventListener('scroll', update)
|
|
265
|
-
window.removeEventListener('resize', update)
|
|
266
|
-
ro?.disconnect()
|
|
267
|
-
}
|
|
268
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* positioning - a small, dependency-free anchor-positioning engine for the
|
|
3
|
+
* overlay layer (popover, tooltip, menu, dropdowns). It replaces the vertical-
|
|
4
|
+
* flip-only `anchoredRect` for components that need the full placement matrix.
|
|
5
|
+
*
|
|
6
|
+
* Two halves:
|
|
7
|
+
*
|
|
8
|
+
* 1. `computePosition(reference, floating, options)` - PURE geometry. Given the
|
|
9
|
+
* reference rect and the floating size (plus a viewport), it returns the
|
|
10
|
+
* floating `x`/`y`, the resolved `placement` (after flip), the arrow offset,
|
|
11
|
+
* and the available `maxWidth`/`maxHeight` on the chosen side. No DOM, so it
|
|
12
|
+
* is fully unit-tested by feeding rects and asserting placement.
|
|
13
|
+
*
|
|
14
|
+
* 2. `autoUpdate(referenceEl, floatingEl, update)` - the browser half. Re-runs
|
|
15
|
+
* `update` whenever the reference/floating resize, an ancestor scrolls, or the
|
|
16
|
+
* window resizes (via `ResizeObserver` + scroll/resize listeners). SSR-safe
|
|
17
|
+
* (a no-op without `window`).
|
|
18
|
+
*
|
|
19
|
+
* The middleware (flip / shift / size / arrow) is implemented inline - no
|
|
20
|
+
* Floating-UI dependency - matching the kit's zero-runtime-dep posture.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
/** A physical side the floating element is placed on, relative to the reference. */
|
|
24
|
+
export type Side = 'top' | 'bottom' | 'left' | 'right'
|
|
25
|
+
|
|
26
|
+
/** Cross-axis alignment. `center` is the bare side (e.g. `'bottom'`). */
|
|
27
|
+
export type Align = 'start' | 'center' | 'end'
|
|
28
|
+
|
|
29
|
+
/** `'bottom'` = bottom-center; `'bottom-start'` / `'bottom-end'` align the edges. */
|
|
30
|
+
export type Placement = Side | `${Side}-start` | `${Side}-end`
|
|
31
|
+
|
|
32
|
+
/** A viewport-relative rectangle (CSS `getBoundingClientRect` shape suffices). */
|
|
33
|
+
export type Rect = { x: number; y: number; width: number; height: number }
|
|
34
|
+
|
|
35
|
+
/** The available viewport (defaults to `window` in the browser). */
|
|
36
|
+
export type Viewport = { width: number; height: number }
|
|
37
|
+
|
|
38
|
+
/** Where to put a floating element: its preferred side, offsets, and collision behaviour. */
|
|
39
|
+
export type ComputePositionOptions = {
|
|
40
|
+
/** Preferred placement. Default `'bottom-start'`. */
|
|
41
|
+
placement?: Placement
|
|
42
|
+
/** Main-axis gap between reference and floating. Default 6. */
|
|
43
|
+
offset?: number
|
|
44
|
+
/** Viewport edge kept clear on every side. Default 8. */
|
|
45
|
+
padding?: number
|
|
46
|
+
/** Flip to the opposite side when the preferred side overflows. Default true. */
|
|
47
|
+
flip?: boolean
|
|
48
|
+
/** Shift along the cross axis to stay in view (without flipping). Default true. */
|
|
49
|
+
shift?: boolean
|
|
50
|
+
/** Compute `maxWidth`/`maxHeight` for the chosen side. Default true. */
|
|
51
|
+
size?: boolean
|
|
52
|
+
/** When set, compute the arrow offset for an arrow element of this size (px). */
|
|
53
|
+
arrow?: { size: number }
|
|
54
|
+
/** Extra placements to try (after the automatic opposite) before giving up. */
|
|
55
|
+
fallbackPlacements?: Placement[]
|
|
56
|
+
/** Viewport to fit within. Defaults to the window; pure callers pass it. */
|
|
57
|
+
viewport?: Viewport
|
|
58
|
+
/** Floor for the available main-axis size returned by `size`. Default 0. */
|
|
59
|
+
minMainAxis?: number
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** The resolved position, including the side actually used after collision handling. */
|
|
63
|
+
export type ComputePositionResult = {
|
|
64
|
+
/** Floating left, in viewport (fixed-position) coordinates. */
|
|
65
|
+
x: number
|
|
66
|
+
/** Floating top, in viewport (fixed-position) coordinates. */
|
|
67
|
+
y: number
|
|
68
|
+
/** The resolved placement after flip. */
|
|
69
|
+
placement: Placement
|
|
70
|
+
/** The resolved side after flip. */
|
|
71
|
+
side: Side
|
|
72
|
+
/** The resolved cross-axis alignment. */
|
|
73
|
+
align: Align
|
|
74
|
+
/** Space available for the floating on the chosen side's main axis (+cross). */
|
|
75
|
+
maxWidth: number
|
|
76
|
+
maxHeight: number
|
|
77
|
+
/**
|
|
78
|
+
* Arrow offset within the floating box: `x` for top/bottom placements, `y` for
|
|
79
|
+
* left/right. Only present when `options.arrow` is set. Clamped so the arrow
|
|
80
|
+
* stays inside the floating (minus padding).
|
|
81
|
+
*/
|
|
82
|
+
arrow?: { x?: number; y?: number }
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const SIDES: Record<Side, Side> = { top: 'bottom', bottom: 'top', left: 'right', right: 'left' }
|
|
86
|
+
|
|
87
|
+
/** Split a placement into its side + alignment (`center` when no suffix). */
|
|
88
|
+
export function parsePlacement(p: Placement): { side: Side; align: Align } {
|
|
89
|
+
const [side, align] = p.split('-') as [Side, 'start' | 'end' | undefined]
|
|
90
|
+
return { side, align: align ?? 'center' }
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function opposite(p: Placement): Placement {
|
|
94
|
+
const { side, align } = parsePlacement(p)
|
|
95
|
+
return (align === 'center' ? SIDES[side] : `${SIDES[side]}-${align}`) as Placement
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function clamp(v: number, min: number, max: number): number {
|
|
99
|
+
if (max < min) return min
|
|
100
|
+
return v < min ? min : v > max ? max : v
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** Floating x/y for a given side+align (before flip/shift). */
|
|
104
|
+
function coordsFor(side: Side, align: Align, r: Rect, f: Viewport, offset: number): { x: number; y: number } {
|
|
105
|
+
if (side === 'top' || side === 'bottom') {
|
|
106
|
+
const y = side === 'top' ? r.y - f.height - offset : r.y + r.height + offset
|
|
107
|
+
const x =
|
|
108
|
+
align === 'start' ? r.x : align === 'end' ? r.x + r.width - f.width : r.x + r.width / 2 - f.width / 2
|
|
109
|
+
return { x, y }
|
|
110
|
+
}
|
|
111
|
+
const x = side === 'left' ? r.x - f.width - offset : r.x + r.width + offset
|
|
112
|
+
const y =
|
|
113
|
+
align === 'start' ? r.y : align === 'end' ? r.y + r.height - f.height : r.y + r.height / 2 - f.height / 2
|
|
114
|
+
return { x, y }
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** How far the floating exceeds the viewport on the placement's MAIN axis. >0 = overflow. */
|
|
118
|
+
function mainAxisOverflow(side: Side, x: number, y: number, f: Viewport, v: Viewport, pad: number): number {
|
|
119
|
+
switch (side) {
|
|
120
|
+
case 'top':
|
|
121
|
+
return pad - y
|
|
122
|
+
case 'bottom':
|
|
123
|
+
return y + f.height - (v.height - pad)
|
|
124
|
+
case 'left':
|
|
125
|
+
return pad - x
|
|
126
|
+
default:
|
|
127
|
+
return x + f.width - (v.width - pad)
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function resolveViewport(v?: Viewport): Viewport {
|
|
132
|
+
if (v) return v
|
|
133
|
+
if (typeof window !== 'undefined') return { width: window.innerWidth, height: window.innerHeight }
|
|
134
|
+
return { width: 0, height: 0 }
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Compute the floating position anchored to `reference`, applying flip (opposite
|
|
139
|
+
* side when the preferred side overflows), shift (cross-axis clamp to stay in
|
|
140
|
+
* view), size (available max width/height on the chosen side) and an optional
|
|
141
|
+
* arrow offset. Pure - pass an explicit `viewport` in tests.
|
|
142
|
+
*/
|
|
143
|
+
export function computePosition(
|
|
144
|
+
reference: Rect,
|
|
145
|
+
floating: Viewport,
|
|
146
|
+
options: ComputePositionOptions = {},
|
|
147
|
+
): ComputePositionResult {
|
|
148
|
+
const offset = options.offset ?? 6
|
|
149
|
+
const pad = options.padding ?? 8
|
|
150
|
+
const doFlip = options.flip !== false
|
|
151
|
+
const doShift = options.shift !== false
|
|
152
|
+
const v = resolveViewport(options.viewport)
|
|
153
|
+
const preferred = options.placement ?? 'bottom-start'
|
|
154
|
+
|
|
155
|
+
// ---- flip: pick the placement whose main-axis overflow is smallest ----
|
|
156
|
+
const candidates: Placement[] = [preferred]
|
|
157
|
+
if (doFlip) {
|
|
158
|
+
candidates.push(opposite(preferred))
|
|
159
|
+
for (const fb of options.fallbackPlacements ?? []) candidates.push(fb)
|
|
160
|
+
}
|
|
161
|
+
let chosen = preferred
|
|
162
|
+
let best = Infinity
|
|
163
|
+
for (const cand of candidates) {
|
|
164
|
+
const { side, align } = parsePlacement(cand)
|
|
165
|
+
const { x, y } = coordsFor(side, align, reference, floating, offset)
|
|
166
|
+
const overflow = mainAxisOverflow(side, x, y, floating, v, pad)
|
|
167
|
+
if (overflow <= 0) {
|
|
168
|
+
chosen = cand
|
|
169
|
+
best = overflow
|
|
170
|
+
break
|
|
171
|
+
}
|
|
172
|
+
if (overflow < best) {
|
|
173
|
+
best = overflow
|
|
174
|
+
chosen = cand
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const { side, align } = parsePlacement(chosen)
|
|
179
|
+
let { x, y } = coordsFor(side, align, reference, floating, offset)
|
|
180
|
+
|
|
181
|
+
// ---- shift: clamp along the CROSS axis so the floating stays in view ----
|
|
182
|
+
if (doShift) {
|
|
183
|
+
if (side === 'top' || side === 'bottom') {
|
|
184
|
+
x = clamp(x, pad, v.width - pad - floating.width)
|
|
185
|
+
} else {
|
|
186
|
+
y = clamp(y, pad, v.height - pad - floating.height)
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// ---- size: available room on the chosen side ----
|
|
191
|
+
const minMain = options.minMainAxis ?? 0
|
|
192
|
+
let maxWidth: number
|
|
193
|
+
let maxHeight: number
|
|
194
|
+
if (side === 'top' || side === 'bottom') {
|
|
195
|
+
const avail = side === 'top' ? reference.y - offset - pad : v.height - pad - (reference.y + reference.height + offset)
|
|
196
|
+
maxHeight = Math.max(minMain, Math.floor(avail))
|
|
197
|
+
maxWidth = Math.max(0, v.width - 2 * pad)
|
|
198
|
+
} else {
|
|
199
|
+
const avail = side === 'left' ? reference.x - offset - pad : v.width - pad - (reference.x + reference.width + offset)
|
|
200
|
+
maxWidth = Math.max(minMain, Math.floor(avail))
|
|
201
|
+
maxHeight = Math.max(0, v.height - 2 * pad)
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const result: ComputePositionResult = { x, y, placement: chosen, side, align, maxWidth, maxHeight }
|
|
205
|
+
|
|
206
|
+
// ---- arrow: point at the reference center, clamped inside the floating ----
|
|
207
|
+
if (options.arrow) {
|
|
208
|
+
const a = options.arrow.size
|
|
209
|
+
if (side === 'top' || side === 'bottom') {
|
|
210
|
+
const center = reference.x + reference.width / 2
|
|
211
|
+
const ax = clamp(center - x - a / 2, pad, floating.width - a - pad)
|
|
212
|
+
result.arrow = { x: ax }
|
|
213
|
+
} else {
|
|
214
|
+
const center = reference.y + reference.height / 2
|
|
215
|
+
const ay = clamp(center - y - a / 2, pad, floating.height - a - pad)
|
|
216
|
+
result.arrow = { y: ay }
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
return result
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/** Collect scrollable ancestors so `autoUpdate` can listen to their scroll. */
|
|
224
|
+
function scrollParents(node: Element): Element[] {
|
|
225
|
+
const out: Element[] = []
|
|
226
|
+
let el: Element | null = node.parentElement
|
|
227
|
+
while (el) {
|
|
228
|
+
const style = getComputedStyle(el)
|
|
229
|
+
if (/(auto|scroll|overlay|hidden)/.test(style.overflow + style.overflowY + style.overflowX)) out.push(el)
|
|
230
|
+
el = el.parentElement
|
|
231
|
+
}
|
|
232
|
+
return out
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Re-run `update` whenever the anchored position could change: the reference or
|
|
237
|
+
* floating element resizes, an ancestor scrolls, or the window scrolls/resizes.
|
|
238
|
+
* Returns a cleanup function. SSR-safe (a no-op without `window`).
|
|
239
|
+
*
|
|
240
|
+
* ```ts
|
|
241
|
+
* const stop = autoUpdate(triggerEl, panelEl, () => reposition())
|
|
242
|
+
* // ...later
|
|
243
|
+
* stop()
|
|
244
|
+
* ```
|
|
245
|
+
*/
|
|
246
|
+
export function autoUpdate(reference: Element, floating: HTMLElement, update: () => void): () => void {
|
|
247
|
+
if (typeof window === 'undefined') return () => {}
|
|
248
|
+
const ancestors = scrollParents(reference)
|
|
249
|
+
for (const a of ancestors) a.addEventListener('scroll', update, { passive: true })
|
|
250
|
+
window.addEventListener('scroll', update, { passive: true })
|
|
251
|
+
window.addEventListener('resize', update)
|
|
252
|
+
|
|
253
|
+
let ro: ResizeObserver | undefined
|
|
254
|
+
if (typeof ResizeObserver === 'function') {
|
|
255
|
+
ro = new ResizeObserver(update)
|
|
256
|
+
ro.observe(reference)
|
|
257
|
+
ro.observe(floating)
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
update()
|
|
261
|
+
|
|
262
|
+
return () => {
|
|
263
|
+
for (const a of ancestors) a.removeEventListener('scroll', update)
|
|
264
|
+
window.removeEventListener('scroll', update)
|
|
265
|
+
window.removeEventListener('resize', update)
|
|
266
|
+
ro?.disconnect()
|
|
267
|
+
}
|
|
268
|
+
}
|
package/src/render-component.ts
CHANGED
|
@@ -1,28 +1,28 @@
|
|
|
1
|
-
import type { Component, ComponentProps, Snippet } from 'svelte'
|
|
2
|
-
|
|
3
|
-
export class RenderComponentConfig<TComponent extends Component> {
|
|
4
|
-
constructor(
|
|
5
|
-
public component: TComponent,
|
|
6
|
-
public props?: ComponentProps<TComponent> | Record<string, never>,
|
|
7
|
-
) {}
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
export class RenderSnippetConfig<TProps> {
|
|
11
|
-
constructor(
|
|
12
|
-
public snippet: Snippet<[TProps]>,
|
|
13
|
-
public params?: TProps,
|
|
14
|
-
) {}
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export const renderComponent = <
|
|
18
|
-
TComponent extends Component<any>,
|
|
19
|
-
TProps extends ComponentProps<TComponent>,
|
|
20
|
-
>(
|
|
21
|
-
component: TComponent,
|
|
22
|
-
props?: TProps,
|
|
23
|
-
) => new RenderComponentConfig(component, props)
|
|
24
|
-
|
|
25
|
-
export const renderSnippet = <TProps>(
|
|
26
|
-
snippet: Snippet<[TProps]>,
|
|
27
|
-
params?: TProps,
|
|
28
|
-
) => new RenderSnippetConfig(snippet, params)
|
|
1
|
+
import type { Component, ComponentProps, Snippet } from 'svelte'
|
|
2
|
+
|
|
3
|
+
export class RenderComponentConfig<TComponent extends Component> {
|
|
4
|
+
constructor(
|
|
5
|
+
public component: TComponent,
|
|
6
|
+
public props?: ComponentProps<TComponent> | Record<string, never>,
|
|
7
|
+
) {}
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export class RenderSnippetConfig<TProps> {
|
|
11
|
+
constructor(
|
|
12
|
+
public snippet: Snippet<[TProps]>,
|
|
13
|
+
public params?: TProps,
|
|
14
|
+
) {}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export const renderComponent = <
|
|
18
|
+
TComponent extends Component<any>,
|
|
19
|
+
TProps extends ComponentProps<TComponent>,
|
|
20
|
+
>(
|
|
21
|
+
component: TComponent,
|
|
22
|
+
props?: TProps,
|
|
23
|
+
) => new RenderComponentConfig(component, props)
|
|
24
|
+
|
|
25
|
+
export const renderSnippet = <TProps>(
|
|
26
|
+
snippet: Snippet<[TProps]>,
|
|
27
|
+
params?: TProps,
|
|
28
|
+
) => new RenderSnippetConfig(snippet, params)
|
package/src/row-drag-touch.ts
CHANGED
|
@@ -8,8 +8,10 @@
|
|
|
8
8
|
// `onRowDragEnd` payload cannot drift between mouse and touch.
|
|
9
9
|
//
|
|
10
10
|
// Loaded with `import()` on the first touch that lands on a draggable row, so
|
|
11
|
-
// desktop users never download it. The 350 ms long press below
|
|
12
|
-
//
|
|
11
|
+
// desktop users never download it. The 350 ms long press below absorbs the
|
|
12
|
+
// fetch rather than following it: `start` takes the pointerdown timestamp and
|
|
13
|
+
// subtracts the time already spent waiting, so a fetch that finishes inside the
|
|
14
|
+
// press costs the user nothing.
|
|
13
15
|
//
|
|
14
16
|
// A touch that starts on a row must still be able to SCROLL the grid, so the
|
|
15
17
|
// drag only begins after a long press; moving before that threshold hands the
|
|
@@ -50,7 +52,11 @@ type TouchState = {
|
|
|
50
52
|
|
|
51
53
|
/** What {@link createTouchDrag} returns. */
|
|
52
54
|
export type TouchDragHandle = {
|
|
53
|
-
|
|
55
|
+
/**
|
|
56
|
+
* @param pressStartedAt `performance.now()` at the pointerdown that began
|
|
57
|
+
* this press. Defaults to now. See the long-press comment in `start`.
|
|
58
|
+
*/
|
|
59
|
+
start: (e: PointerEvent, rowIndex: number, pressStartedAt?: number) => void;
|
|
54
60
|
destroy: () => void;
|
|
55
61
|
};
|
|
56
62
|
|
|
@@ -168,7 +174,7 @@ export function createTouchDrag(deps: TouchDragDeps): TouchDragHandle {
|
|
|
168
174
|
|
|
169
175
|
return {
|
|
170
176
|
/** Begin a press that may become a drag. Caller has already checked touch. */
|
|
171
|
-
start(e: PointerEvent, rowIndex: number) {
|
|
177
|
+
start(e: PointerEvent, rowIndex: number, pressStartedAt?: number) {
|
|
172
178
|
if (!deps.managed() || touch) return;
|
|
173
179
|
const row = deps.originalAt(rowIndex);
|
|
174
180
|
if (row == null) return;
|
|
@@ -188,6 +194,15 @@ export function createTouchDrag(deps: TouchDragDeps): TouchDragHandle {
|
|
|
188
194
|
window.addEventListener("pointerup", onPointerUp);
|
|
189
195
|
window.addEventListener("pointercancel", onPointerCancel);
|
|
190
196
|
window.addEventListener("touchmove", blockTouchScroll, { passive: false });
|
|
197
|
+
// The long press is measured from when the finger LANDED, not from when
|
|
198
|
+
// this module finished loading. `row-drag.ts` fetches it with import() on
|
|
199
|
+
// the first touch, and that fetch is not free - measured at ~190 ms cold.
|
|
200
|
+
// Starting the clock here would make the real cost `fetch + 350 ms`, so a
|
|
201
|
+
// user on a slow connection would have to hold noticeably longer for
|
|
202
|
+
// their first drag than for every one after it. Subtracting the elapsed
|
|
203
|
+
// time keeps it at 350 ms from the finger's point of view, which is what
|
|
204
|
+
// the comment at the top of this file has always claimed.
|
|
205
|
+
const waited = pressStartedAt == null ? 0 : Math.max(0, performance.now() - pressStartedAt);
|
|
191
206
|
touch.timer = setTimeout(() => {
|
|
192
207
|
if (!touch) return;
|
|
193
208
|
touch.started = true;
|
|
@@ -202,7 +217,7 @@ export function createTouchDrag(deps: TouchDragDeps): TouchDragHandle {
|
|
|
202
217
|
ctx.rowDragActive = true;
|
|
203
218
|
ctx.rowDropIndex = rowIndex;
|
|
204
219
|
ctx.rowDropSide = "before";
|
|
205
|
-
}, LONG_PRESS_MS);
|
|
220
|
+
}, Math.max(0, LONG_PRESS_MS - waited));
|
|
206
221
|
},
|
|
207
222
|
destroy() {
|
|
208
223
|
end(false);
|