@veltra/utils 1.1.0 → 1.1.2
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/dist/dom/expand-transition.d.ts +27 -0
- package/dist/dom/expand-transition.js +149 -0
- package/dist/dom/expand-transition.js.map +1 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +3 -2
- package/package.json +1 -1
- package/src/dom/__test__/expand-transition.test.ts +98 -0
- package/src/dom/expand-transition.ts +183 -0
- package/src/index.ts +2 -0
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
//#region src/dom/expand-transition.d.ts
|
|
2
|
+
interface ExpandTransitionOptions {
|
|
3
|
+
transition?: string;
|
|
4
|
+
enterTransition?: string;
|
|
5
|
+
leaveTransition?: string;
|
|
6
|
+
opacity?: boolean;
|
|
7
|
+
}
|
|
8
|
+
declare class ExpandTransition {
|
|
9
|
+
private options;
|
|
10
|
+
private cleanupMap;
|
|
11
|
+
constructor(options?: ExpandTransitionOptions);
|
|
12
|
+
cancel(el: HTMLElement): void;
|
|
13
|
+
enter(el: HTMLElement): void;
|
|
14
|
+
afterEnter(el: HTMLElement): void;
|
|
15
|
+
beforeLeave(el: HTMLElement): void;
|
|
16
|
+
leave(el: HTMLElement): void;
|
|
17
|
+
afterLeave(el: HTMLElement): void;
|
|
18
|
+
expand(el: HTMLElement): void;
|
|
19
|
+
collapse(el: HTMLElement): void;
|
|
20
|
+
setExpanded(el: HTMLElement, expanded: boolean): void;
|
|
21
|
+
private animate;
|
|
22
|
+
private resetTransitionStyles;
|
|
23
|
+
private resetTemporaryStyles;
|
|
24
|
+
}
|
|
25
|
+
//#endregion
|
|
26
|
+
export { ExpandTransition, ExpandTransitionOptions };
|
|
27
|
+
//# sourceMappingURL=expand-transition.d.ts.map
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { removeStyles, setStyles } from "./style.js";
|
|
2
|
+
//#region src/dom/expand-transition.ts
|
|
3
|
+
function readVerticalPadding(el) {
|
|
4
|
+
const { paddingTop, paddingBottom } = getComputedStyle(el);
|
|
5
|
+
return {
|
|
6
|
+
paddingTop,
|
|
7
|
+
paddingBottom
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
function getTransition(options, type) {
|
|
11
|
+
return (type === "enter" ? options.enterTransition : options.leaveTransition) ?? options.transition ?? "height 0.25s cubic-bezier(0.4, 0, 0.2, 1)";
|
|
12
|
+
}
|
|
13
|
+
var ExpandTransition = class {
|
|
14
|
+
cleanupMap = /* @__PURE__ */ new Map();
|
|
15
|
+
constructor(options = {}) {
|
|
16
|
+
this.options = options;
|
|
17
|
+
}
|
|
18
|
+
cancel(el) {
|
|
19
|
+
this.cleanupMap.get(el)?.();
|
|
20
|
+
this.cleanupMap.delete(el);
|
|
21
|
+
}
|
|
22
|
+
enter(el) {
|
|
23
|
+
const height = el.scrollHeight;
|
|
24
|
+
const { paddingTop, paddingBottom } = readVerticalPadding(el);
|
|
25
|
+
setStyles(el, {
|
|
26
|
+
boxSizing: "border-box",
|
|
27
|
+
height: 0,
|
|
28
|
+
paddingTop: 0,
|
|
29
|
+
paddingBottom: 0,
|
|
30
|
+
overflow: "hidden",
|
|
31
|
+
transition: getTransition(this.options, "enter"),
|
|
32
|
+
willChange: this.options.opacity ? "height, opacity" : "height"
|
|
33
|
+
});
|
|
34
|
+
if (this.options.opacity) el.style.opacity = "0";
|
|
35
|
+
el.offsetHeight;
|
|
36
|
+
setStyles(el, {
|
|
37
|
+
height: `${height}px`,
|
|
38
|
+
paddingTop,
|
|
39
|
+
paddingBottom
|
|
40
|
+
});
|
|
41
|
+
if (this.options.opacity) el.style.opacity = "1";
|
|
42
|
+
}
|
|
43
|
+
afterEnter(el) {
|
|
44
|
+
this.resetTransitionStyles(el);
|
|
45
|
+
}
|
|
46
|
+
beforeLeave(el) {
|
|
47
|
+
const { paddingTop, paddingBottom } = readVerticalPadding(el);
|
|
48
|
+
setStyles(el, {
|
|
49
|
+
boxSizing: "border-box",
|
|
50
|
+
height: `${el.scrollHeight}px`,
|
|
51
|
+
paddingTop,
|
|
52
|
+
paddingBottom,
|
|
53
|
+
overflow: "hidden",
|
|
54
|
+
transition: getTransition(this.options, "leave"),
|
|
55
|
+
willChange: this.options.opacity ? "height, opacity" : "height"
|
|
56
|
+
});
|
|
57
|
+
if (this.options.opacity) el.style.opacity = "1";
|
|
58
|
+
}
|
|
59
|
+
leave(el) {
|
|
60
|
+
el.offsetHeight;
|
|
61
|
+
setStyles(el, {
|
|
62
|
+
height: 0,
|
|
63
|
+
paddingTop: 0,
|
|
64
|
+
paddingBottom: 0
|
|
65
|
+
});
|
|
66
|
+
if (this.options.opacity) el.style.opacity = "0";
|
|
67
|
+
}
|
|
68
|
+
afterLeave(el) {
|
|
69
|
+
this.resetTransitionStyles(el);
|
|
70
|
+
}
|
|
71
|
+
expand(el) {
|
|
72
|
+
this.animate(el, true);
|
|
73
|
+
}
|
|
74
|
+
collapse(el) {
|
|
75
|
+
this.animate(el, false);
|
|
76
|
+
}
|
|
77
|
+
setExpanded(el, expanded) {
|
|
78
|
+
this.cancel(el);
|
|
79
|
+
this.resetTemporaryStyles(el);
|
|
80
|
+
el.style.overflow = "hidden";
|
|
81
|
+
el.style.height = expanded ? "auto" : "0px";
|
|
82
|
+
}
|
|
83
|
+
animate(el, expanded) {
|
|
84
|
+
this.cancel(el);
|
|
85
|
+
const startHeight = el.offsetHeight;
|
|
86
|
+
const endHeight = expanded ? el.scrollHeight : 0;
|
|
87
|
+
const { paddingTop, paddingBottom } = readVerticalPadding(el);
|
|
88
|
+
if (startHeight === endHeight) {
|
|
89
|
+
this.setExpanded(el, expanded);
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
setStyles(el, {
|
|
93
|
+
boxSizing: "border-box",
|
|
94
|
+
height: `${startHeight}px`,
|
|
95
|
+
paddingTop,
|
|
96
|
+
paddingBottom,
|
|
97
|
+
overflow: "hidden",
|
|
98
|
+
transition: getTransition(this.options, expanded ? "enter" : "leave"),
|
|
99
|
+
willChange: "height"
|
|
100
|
+
});
|
|
101
|
+
el.offsetHeight;
|
|
102
|
+
setStyles(el, {
|
|
103
|
+
height: `${endHeight}px`,
|
|
104
|
+
paddingTop: expanded ? paddingTop : 0,
|
|
105
|
+
paddingBottom: expanded ? paddingBottom : 0
|
|
106
|
+
});
|
|
107
|
+
const cleanup = () => {
|
|
108
|
+
el.removeEventListener("transitionend", onEnd);
|
|
109
|
+
el.removeEventListener("transitioncancel", onEnd);
|
|
110
|
+
this.cleanupMap.delete(el);
|
|
111
|
+
};
|
|
112
|
+
const onEnd = (e) => {
|
|
113
|
+
if (e.target !== el || e.propertyName !== "height") return;
|
|
114
|
+
cleanup();
|
|
115
|
+
this.resetTemporaryStyles(el);
|
|
116
|
+
el.style.overflow = "hidden";
|
|
117
|
+
el.style.height = expanded ? "auto" : "0px";
|
|
118
|
+
};
|
|
119
|
+
el.addEventListener("transitionend", onEnd);
|
|
120
|
+
el.addEventListener("transitioncancel", onEnd);
|
|
121
|
+
this.cleanupMap.set(el, cleanup);
|
|
122
|
+
}
|
|
123
|
+
resetTransitionStyles(el) {
|
|
124
|
+
removeStyles(el, [
|
|
125
|
+
"box-sizing",
|
|
126
|
+
"height",
|
|
127
|
+
"padding-top",
|
|
128
|
+
"padding-bottom",
|
|
129
|
+
"overflow",
|
|
130
|
+
"transition",
|
|
131
|
+
"opacity",
|
|
132
|
+
"will-change"
|
|
133
|
+
]);
|
|
134
|
+
}
|
|
135
|
+
resetTemporaryStyles(el) {
|
|
136
|
+
removeStyles(el, [
|
|
137
|
+
"box-sizing",
|
|
138
|
+
"padding-top",
|
|
139
|
+
"padding-bottom",
|
|
140
|
+
"transition",
|
|
141
|
+
"opacity",
|
|
142
|
+
"will-change"
|
|
143
|
+
]);
|
|
144
|
+
}
|
|
145
|
+
};
|
|
146
|
+
//#endregion
|
|
147
|
+
export { ExpandTransition };
|
|
148
|
+
|
|
149
|
+
//# sourceMappingURL=expand-transition.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"expand-transition.js","names":[],"sources":["../../src/dom/expand-transition.ts"],"sourcesContent":["import { removeStyles, setStyles } from './style'\n\nexport interface ExpandTransitionOptions {\n transition?: string\n enterTransition?: string\n leaveTransition?: string\n opacity?: boolean\n}\n\nfunction readVerticalPadding(el: HTMLElement) {\n const { paddingTop, paddingBottom } = getComputedStyle(el)\n return { paddingTop, paddingBottom }\n}\n\nfunction getTransition(options: ExpandTransitionOptions, type: 'enter' | 'leave') {\n return (\n (type === 'enter' ? options.enterTransition : options.leaveTransition) ??\n options.transition ??\n 'height 0.25s cubic-bezier(0.4, 0, 0.2, 1)'\n )\n}\n\nexport class ExpandTransition {\n private cleanupMap = new Map<HTMLElement, () => void>()\n\n constructor(private options: ExpandTransitionOptions = {}) {}\n\n cancel(el: HTMLElement) {\n this.cleanupMap.get(el)?.()\n this.cleanupMap.delete(el)\n }\n\n enter(el: HTMLElement) {\n const height = el.scrollHeight\n const { paddingTop, paddingBottom } = readVerticalPadding(el)\n\n setStyles(el, {\n boxSizing: 'border-box',\n height: 0,\n paddingTop: 0,\n paddingBottom: 0,\n overflow: 'hidden',\n transition: getTransition(this.options, 'enter'),\n willChange: this.options.opacity ? 'height, opacity' : 'height'\n })\n\n if (this.options.opacity) {\n el.style.opacity = '0'\n }\n\n void el.offsetHeight\n\n setStyles(el, { height: `${height}px`, paddingTop, paddingBottom })\n\n if (this.options.opacity) {\n el.style.opacity = '1'\n }\n }\n\n afterEnter(el: HTMLElement) {\n this.resetTransitionStyles(el)\n }\n\n beforeLeave(el: HTMLElement) {\n const { paddingTop, paddingBottom } = readVerticalPadding(el)\n\n setStyles(el, {\n boxSizing: 'border-box',\n height: `${el.scrollHeight}px`,\n paddingTop,\n paddingBottom,\n overflow: 'hidden',\n transition: getTransition(this.options, 'leave'),\n willChange: this.options.opacity ? 'height, opacity' : 'height'\n })\n\n if (this.options.opacity) {\n el.style.opacity = '1'\n }\n }\n\n leave(el: HTMLElement) {\n void el.offsetHeight\n\n setStyles(el, { height: 0, paddingTop: 0, paddingBottom: 0 })\n\n if (this.options.opacity) {\n el.style.opacity = '0'\n }\n }\n\n afterLeave(el: HTMLElement) {\n this.resetTransitionStyles(el)\n }\n\n expand(el: HTMLElement) {\n this.animate(el, true)\n }\n\n collapse(el: HTMLElement) {\n this.animate(el, false)\n }\n\n setExpanded(el: HTMLElement, expanded: boolean) {\n this.cancel(el)\n this.resetTemporaryStyles(el)\n el.style.overflow = 'hidden'\n el.style.height = expanded ? 'auto' : '0px'\n }\n\n private animate(el: HTMLElement, expanded: boolean) {\n this.cancel(el)\n\n const startHeight = el.offsetHeight\n const endHeight = expanded ? el.scrollHeight : 0\n const { paddingTop, paddingBottom } = readVerticalPadding(el)\n\n if (startHeight === endHeight) {\n this.setExpanded(el, expanded)\n return\n }\n\n setStyles(el, {\n boxSizing: 'border-box',\n height: `${startHeight}px`,\n paddingTop,\n paddingBottom,\n overflow: 'hidden',\n transition: getTransition(this.options, expanded ? 'enter' : 'leave'),\n willChange: 'height'\n })\n\n void el.offsetHeight\n\n setStyles(el, {\n height: `${endHeight}px`,\n paddingTop: expanded ? paddingTop : 0,\n paddingBottom: expanded ? paddingBottom : 0\n })\n\n const cleanup = () => {\n el.removeEventListener('transitionend', onEnd)\n el.removeEventListener('transitioncancel', onEnd)\n this.cleanupMap.delete(el)\n }\n\n const onEnd = (e: TransitionEvent) => {\n if (e.target !== el || e.propertyName !== 'height') return\n cleanup()\n this.resetTemporaryStyles(el)\n el.style.overflow = 'hidden'\n el.style.height = expanded ? 'auto' : '0px'\n }\n\n el.addEventListener('transitionend', onEnd)\n el.addEventListener('transitioncancel', onEnd)\n this.cleanupMap.set(el, cleanup)\n }\n\n private resetTransitionStyles(el: HTMLElement) {\n removeStyles(el, [\n 'box-sizing',\n 'height',\n 'padding-top',\n 'padding-bottom',\n 'overflow',\n 'transition',\n 'opacity',\n 'will-change'\n ])\n }\n\n private resetTemporaryStyles(el: HTMLElement) {\n removeStyles(el, [\n 'box-sizing',\n 'padding-top',\n 'padding-bottom',\n 'transition',\n 'opacity',\n 'will-change'\n ])\n }\n}\n"],"mappings":"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"}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { BEM, BEMFactory, makeBEM } from "./helper/make-bem.js";
|
|
2
2
|
import { CLS_PREFIX, FORM_EMPTY_CONTENT, NAME_SPACE } from "./shared/constants.js";
|
|
3
3
|
import { addClass, bem, removeClass } from "./dom/class-name.js";
|
|
4
|
+
import { ExpandTransition, ExpandTransitionOptions } from "./dom/expand-transition.js";
|
|
4
5
|
import { getHighlightChunks } from "./dom/highlight.js";
|
|
5
6
|
import { getNearestScrollParent, getScrollParents, scrollIntoContainerView } from "./dom/position.js";
|
|
6
7
|
import { removeStyles, setStyles, withUnit } from "./dom/style.js";
|
|
@@ -16,4 +17,4 @@ import { middleProxy } from "./reactive/proxy.js";
|
|
|
16
17
|
import { DeconstructValue, DefineEvent, Index, Null, RenderReturn, Undef } from "./types/helper.js";
|
|
17
18
|
import { BreakpointName, ColorType, ComponentProps, ComponentSize, FormComponentProps, PropsWithServerQuery } from "./types/component-common.js";
|
|
18
19
|
import { FormContextInjectProps } from "./types/form-context.js";
|
|
19
|
-
export { AnimeConfig, BEM, BEMFactory, BreakpointName, CLS_PREFIX, ColorType, ComponentProps, ComponentSize, Data, DeconstructValue, DefineEvent, FORM_EMPTY_CONTENT, FormComponentProps, FormContextInjectProps, Index, NAME_SPACE, Null, PresetRule, PropsWithServerQuery, RenderReturn, Tween, TweenConfig, Undef, ValidateRule, Validator, ValidatorConfig, addClass, bem, createIncrease, createToggle, extractNormalVNodes, getHighlightChunks, getNearestScrollParent, getScrollParents, isComment, isFragment, isTemplate, isTextNode, makeBEM, middleProxy, nextFrame, removeClass, removeStyles, scrollIntoContainerView, setStyles, shallowComputed, withUnit, zIndex };
|
|
20
|
+
export { AnimeConfig, BEM, BEMFactory, BreakpointName, CLS_PREFIX, ColorType, ComponentProps, ComponentSize, Data, DeconstructValue, DefineEvent, ExpandTransition, ExpandTransitionOptions, FORM_EMPTY_CONTENT, FormComponentProps, FormContextInjectProps, Index, NAME_SPACE, Null, PresetRule, PropsWithServerQuery, RenderReturn, Tween, TweenConfig, Undef, ValidateRule, Validator, ValidatorConfig, addClass, bem, createIncrease, createToggle, extractNormalVNodes, getHighlightChunks, getNearestScrollParent, getScrollParents, isComment, isFragment, isTemplate, isTextNode, makeBEM, middleProxy, nextFrame, removeClass, removeStyles, scrollIntoContainerView, setStyles, shallowComputed, withUnit, zIndex };
|
package/dist/index.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { makeBEM } from "./helper/make-bem.js";
|
|
2
2
|
import { CLS_PREFIX, FORM_EMPTY_CONTENT, NAME_SPACE } from "./shared/constants.js";
|
|
3
3
|
import { addClass, bem, removeClass } from "./dom/class-name.js";
|
|
4
|
+
import { removeStyles, setStyles, withUnit } from "./dom/style.js";
|
|
5
|
+
import { ExpandTransition } from "./dom/expand-transition.js";
|
|
4
6
|
import { getHighlightChunks } from "./dom/highlight.js";
|
|
5
7
|
import { getNearestScrollParent, getScrollParents, scrollIntoContainerView } from "./dom/position.js";
|
|
6
|
-
import { removeStyles, setStyles, withUnit } from "./dom/style.js";
|
|
7
8
|
import { createIncrease } from "./helper/create-increase.js";
|
|
8
9
|
import { zIndex } from "./dom/z-index.js";
|
|
9
10
|
import { Validator } from "./form/validate.js";
|
|
@@ -12,4 +13,4 @@ import { Tween } from "./helper/tween.js";
|
|
|
12
13
|
import { nextFrame } from "./helper/frame.js";
|
|
13
14
|
import { extractNormalVNodes, isComment, isFragment, isTemplate, isTextNode, shallowComputed } from "./helper/vue.js";
|
|
14
15
|
import { middleProxy } from "./reactive/proxy.js";
|
|
15
|
-
export { CLS_PREFIX, FORM_EMPTY_CONTENT, NAME_SPACE, Tween, Validator, addClass, bem, createIncrease, createToggle, extractNormalVNodes, getHighlightChunks, getNearestScrollParent, getScrollParents, isComment, isFragment, isTemplate, isTextNode, makeBEM, middleProxy, nextFrame, removeClass, removeStyles, scrollIntoContainerView, setStyles, shallowComputed, withUnit, zIndex };
|
|
16
|
+
export { CLS_PREFIX, ExpandTransition, FORM_EMPTY_CONTENT, NAME_SPACE, Tween, Validator, addClass, bem, createIncrease, createToggle, extractNormalVNodes, getHighlightChunks, getNearestScrollParent, getScrollParents, isComment, isFragment, isTemplate, isTextNode, makeBEM, middleProxy, nextFrame, removeClass, removeStyles, scrollIntoContainerView, setStyles, shallowComputed, withUnit, zIndex };
|
package/package.json
CHANGED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
// @vitest-environment happy-dom
|
|
2
|
+
import { ExpandTransition } from '../expand-transition'
|
|
3
|
+
|
|
4
|
+
function createPanel() {
|
|
5
|
+
const el = document.createElement('div')
|
|
6
|
+
|
|
7
|
+
el.style.paddingTop = '2px'
|
|
8
|
+
el.style.paddingBottom = '2px'
|
|
9
|
+
document.body.appendChild(el)
|
|
10
|
+
|
|
11
|
+
Object.defineProperties(el, {
|
|
12
|
+
offsetHeight: { configurable: true, value: 16 },
|
|
13
|
+
scrollHeight: { configurable: true, value: 44 }
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
return el
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function fireHeightTransitionEnd(el: HTMLElement) {
|
|
20
|
+
const event = new Event('transitionend') as TransitionEvent
|
|
21
|
+
Object.defineProperty(event, 'propertyName', { configurable: true, value: 'height' })
|
|
22
|
+
el.dispatchEvent(event)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
describe('ExpandTransition', () => {
|
|
26
|
+
it('drives Vue transition hooks with a measured border-box height', () => {
|
|
27
|
+
const el = createPanel()
|
|
28
|
+
const transition = new ExpandTransition({
|
|
29
|
+
enterTransition: 'height 0.25s ease, padding-top 0.25s ease, padding-bottom 0.25s ease',
|
|
30
|
+
leaveTransition: 'height 0.2s ease, padding-top 0.2s ease, padding-bottom 0.2s ease',
|
|
31
|
+
opacity: true
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
try {
|
|
35
|
+
transition.enter(el)
|
|
36
|
+
|
|
37
|
+
expect(el.style.height).toBe('44px')
|
|
38
|
+
expect(el.style.paddingTop).toBe('2px')
|
|
39
|
+
expect(el.style.paddingBottom).toBe('2px')
|
|
40
|
+
expect(el.style.boxSizing).toBe('border-box')
|
|
41
|
+
expect(el.style.opacity).toBe('1')
|
|
42
|
+
|
|
43
|
+
transition.beforeLeave(el)
|
|
44
|
+
transition.leave(el)
|
|
45
|
+
|
|
46
|
+
expect(el.style.height).toBe('0px')
|
|
47
|
+
expect(el.style.paddingTop).toBe('0px')
|
|
48
|
+
expect(el.style.paddingBottom).toBe('0px')
|
|
49
|
+
expect(el.style.boxSizing).toBe('border-box')
|
|
50
|
+
expect(el.style.opacity).toBe('0')
|
|
51
|
+
} finally {
|
|
52
|
+
el.remove()
|
|
53
|
+
}
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
it('keeps expanded imperative panels at auto height after transition end', () => {
|
|
57
|
+
const el = createPanel()
|
|
58
|
+
const transition = new ExpandTransition({ transition: 'height 0.25s ease' })
|
|
59
|
+
|
|
60
|
+
try {
|
|
61
|
+
transition.expand(el)
|
|
62
|
+
|
|
63
|
+
expect(el.style.height).toBe('44px')
|
|
64
|
+
expect(el.style.overflow).toBe('hidden')
|
|
65
|
+
|
|
66
|
+
fireHeightTransitionEnd(el)
|
|
67
|
+
|
|
68
|
+
expect(el.style.height).toBe('auto')
|
|
69
|
+
expect(el.style.transition).toBe('')
|
|
70
|
+
expect(el.style.willChange).toBe('')
|
|
71
|
+
} finally {
|
|
72
|
+
el.remove()
|
|
73
|
+
transition.cancel(el)
|
|
74
|
+
}
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
it('keeps collapsed imperative panels at zero height after transition end', () => {
|
|
78
|
+
const el = createPanel()
|
|
79
|
+
const transition = new ExpandTransition({ transition: 'height 0.25s ease' })
|
|
80
|
+
|
|
81
|
+
try {
|
|
82
|
+
transition.collapse(el)
|
|
83
|
+
|
|
84
|
+
expect(el.style.height).toBe('0px')
|
|
85
|
+
expect(el.style.paddingTop).toBe('0px')
|
|
86
|
+
expect(el.style.paddingBottom).toBe('0px')
|
|
87
|
+
|
|
88
|
+
fireHeightTransitionEnd(el)
|
|
89
|
+
|
|
90
|
+
expect(el.style.height).toBe('0px')
|
|
91
|
+
expect(el.style.transition).toBe('')
|
|
92
|
+
expect(el.style.willChange).toBe('')
|
|
93
|
+
} finally {
|
|
94
|
+
el.remove()
|
|
95
|
+
transition.cancel(el)
|
|
96
|
+
}
|
|
97
|
+
})
|
|
98
|
+
})
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import { removeStyles, setStyles } from './style'
|
|
2
|
+
|
|
3
|
+
export interface ExpandTransitionOptions {
|
|
4
|
+
transition?: string
|
|
5
|
+
enterTransition?: string
|
|
6
|
+
leaveTransition?: string
|
|
7
|
+
opacity?: boolean
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function readVerticalPadding(el: HTMLElement) {
|
|
11
|
+
const { paddingTop, paddingBottom } = getComputedStyle(el)
|
|
12
|
+
return { paddingTop, paddingBottom }
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function getTransition(options: ExpandTransitionOptions, type: 'enter' | 'leave') {
|
|
16
|
+
return (
|
|
17
|
+
(type === 'enter' ? options.enterTransition : options.leaveTransition) ??
|
|
18
|
+
options.transition ??
|
|
19
|
+
'height 0.25s cubic-bezier(0.4, 0, 0.2, 1)'
|
|
20
|
+
)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export class ExpandTransition {
|
|
24
|
+
private cleanupMap = new Map<HTMLElement, () => void>()
|
|
25
|
+
|
|
26
|
+
constructor(private options: ExpandTransitionOptions = {}) {}
|
|
27
|
+
|
|
28
|
+
cancel(el: HTMLElement) {
|
|
29
|
+
this.cleanupMap.get(el)?.()
|
|
30
|
+
this.cleanupMap.delete(el)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
enter(el: HTMLElement) {
|
|
34
|
+
const height = el.scrollHeight
|
|
35
|
+
const { paddingTop, paddingBottom } = readVerticalPadding(el)
|
|
36
|
+
|
|
37
|
+
setStyles(el, {
|
|
38
|
+
boxSizing: 'border-box',
|
|
39
|
+
height: 0,
|
|
40
|
+
paddingTop: 0,
|
|
41
|
+
paddingBottom: 0,
|
|
42
|
+
overflow: 'hidden',
|
|
43
|
+
transition: getTransition(this.options, 'enter'),
|
|
44
|
+
willChange: this.options.opacity ? 'height, opacity' : 'height'
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
if (this.options.opacity) {
|
|
48
|
+
el.style.opacity = '0'
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
void el.offsetHeight
|
|
52
|
+
|
|
53
|
+
setStyles(el, { height: `${height}px`, paddingTop, paddingBottom })
|
|
54
|
+
|
|
55
|
+
if (this.options.opacity) {
|
|
56
|
+
el.style.opacity = '1'
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
afterEnter(el: HTMLElement) {
|
|
61
|
+
this.resetTransitionStyles(el)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
beforeLeave(el: HTMLElement) {
|
|
65
|
+
const { paddingTop, paddingBottom } = readVerticalPadding(el)
|
|
66
|
+
|
|
67
|
+
setStyles(el, {
|
|
68
|
+
boxSizing: 'border-box',
|
|
69
|
+
height: `${el.scrollHeight}px`,
|
|
70
|
+
paddingTop,
|
|
71
|
+
paddingBottom,
|
|
72
|
+
overflow: 'hidden',
|
|
73
|
+
transition: getTransition(this.options, 'leave'),
|
|
74
|
+
willChange: this.options.opacity ? 'height, opacity' : 'height'
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
if (this.options.opacity) {
|
|
78
|
+
el.style.opacity = '1'
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
leave(el: HTMLElement) {
|
|
83
|
+
void el.offsetHeight
|
|
84
|
+
|
|
85
|
+
setStyles(el, { height: 0, paddingTop: 0, paddingBottom: 0 })
|
|
86
|
+
|
|
87
|
+
if (this.options.opacity) {
|
|
88
|
+
el.style.opacity = '0'
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
afterLeave(el: HTMLElement) {
|
|
93
|
+
this.resetTransitionStyles(el)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
expand(el: HTMLElement) {
|
|
97
|
+
this.animate(el, true)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
collapse(el: HTMLElement) {
|
|
101
|
+
this.animate(el, false)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
setExpanded(el: HTMLElement, expanded: boolean) {
|
|
105
|
+
this.cancel(el)
|
|
106
|
+
this.resetTemporaryStyles(el)
|
|
107
|
+
el.style.overflow = 'hidden'
|
|
108
|
+
el.style.height = expanded ? 'auto' : '0px'
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
private animate(el: HTMLElement, expanded: boolean) {
|
|
112
|
+
this.cancel(el)
|
|
113
|
+
|
|
114
|
+
const startHeight = el.offsetHeight
|
|
115
|
+
const endHeight = expanded ? el.scrollHeight : 0
|
|
116
|
+
const { paddingTop, paddingBottom } = readVerticalPadding(el)
|
|
117
|
+
|
|
118
|
+
if (startHeight === endHeight) {
|
|
119
|
+
this.setExpanded(el, expanded)
|
|
120
|
+
return
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
setStyles(el, {
|
|
124
|
+
boxSizing: 'border-box',
|
|
125
|
+
height: `${startHeight}px`,
|
|
126
|
+
paddingTop,
|
|
127
|
+
paddingBottom,
|
|
128
|
+
overflow: 'hidden',
|
|
129
|
+
transition: getTransition(this.options, expanded ? 'enter' : 'leave'),
|
|
130
|
+
willChange: 'height'
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
void el.offsetHeight
|
|
134
|
+
|
|
135
|
+
setStyles(el, {
|
|
136
|
+
height: `${endHeight}px`,
|
|
137
|
+
paddingTop: expanded ? paddingTop : 0,
|
|
138
|
+
paddingBottom: expanded ? paddingBottom : 0
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
const cleanup = () => {
|
|
142
|
+
el.removeEventListener('transitionend', onEnd)
|
|
143
|
+
el.removeEventListener('transitioncancel', onEnd)
|
|
144
|
+
this.cleanupMap.delete(el)
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const onEnd = (e: TransitionEvent) => {
|
|
148
|
+
if (e.target !== el || e.propertyName !== 'height') return
|
|
149
|
+
cleanup()
|
|
150
|
+
this.resetTemporaryStyles(el)
|
|
151
|
+
el.style.overflow = 'hidden'
|
|
152
|
+
el.style.height = expanded ? 'auto' : '0px'
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
el.addEventListener('transitionend', onEnd)
|
|
156
|
+
el.addEventListener('transitioncancel', onEnd)
|
|
157
|
+
this.cleanupMap.set(el, cleanup)
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
private resetTransitionStyles(el: HTMLElement) {
|
|
161
|
+
removeStyles(el, [
|
|
162
|
+
'box-sizing',
|
|
163
|
+
'height',
|
|
164
|
+
'padding-top',
|
|
165
|
+
'padding-bottom',
|
|
166
|
+
'overflow',
|
|
167
|
+
'transition',
|
|
168
|
+
'opacity',
|
|
169
|
+
'will-change'
|
|
170
|
+
])
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
private resetTemporaryStyles(el: HTMLElement) {
|
|
174
|
+
removeStyles(el, [
|
|
175
|
+
'box-sizing',
|
|
176
|
+
'padding-top',
|
|
177
|
+
'padding-bottom',
|
|
178
|
+
'transition',
|
|
179
|
+
'opacity',
|
|
180
|
+
'will-change'
|
|
181
|
+
])
|
|
182
|
+
}
|
|
183
|
+
}
|