Thomas G. Lopes commited on
Commit
2dd779d
·
1 Parent(s): ba88125

fix auto scroll behaviour

Browse files
src/lib/components/inference-playground/conversation.svelte CHANGED
@@ -1,11 +1,11 @@
1
  <script lang="ts">
2
- import { run } from "svelte/legacy";
3
-
4
  import type { Conversation } from "$lib/types.js";
5
 
6
  import IconPlus from "~icons/carbon/add";
7
  import CodeSnippets from "./code-snippets.svelte";
8
  import Message from "./message.svelte";
 
 
9
 
10
  interface Props {
11
  conversation: Conversation;
@@ -15,35 +15,20 @@
15
  }
16
 
17
  let { conversation = $bindable(), loading, viewCode, compareActive }: Props = $props();
18
-
19
- let shouldScrollToBottom = $state(true);
20
- let isProgrammaticScroll = $state(true);
21
- let conversationLength = $state(conversation.messages.length);
22
-
23
  let messageContainer: HTMLDivElement | null = $state(null);
24
-
25
- function scrollToBottom() {
26
- if (messageContainer) {
27
- isProgrammaticScroll = true;
28
- messageContainer.scrollTop = messageContainer.scrollHeight;
29
- }
30
- }
31
-
32
- run(() => {
33
- if (conversation.messages.at(-1)) {
34
- if (shouldScrollToBottom) {
35
- scrollToBottom();
36
- }
37
- }
38
  });
39
 
40
- run(() => {
41
- if (conversation.messages.length !== conversationLength) {
42
- // enable automatic scrolling when new message was added
43
- conversationLength = conversation.messages.length;
44
- shouldScrollToBottom = true;
 
45
  }
46
- });
47
 
48
  function addMessage() {
49
  const msgs = conversation.messages.slice();
@@ -68,13 +53,7 @@
68
  : 'max-h-[calc(100dvh-5.8rem-2.5rem-75px)] md:max-h-[calc(100dvh-5.8rem)]'}"
69
  class:animate-pulse={loading && !conversation.streaming}
70
  bind:this={messageContainer}
71
- onscroll={() => {
72
- // disable automatic scrolling is user initiates scroll
73
- if (!isProgrammaticScroll) {
74
- shouldScrollToBottom = false;
75
- }
76
- isProgrammaticScroll = false;
77
- }}
78
  >
79
  {#if !viewCode}
80
  {#each conversation.messages as _msg, idx}
 
1
  <script lang="ts">
 
 
2
  import type { Conversation } from "$lib/types.js";
3
 
4
  import IconPlus from "~icons/carbon/add";
5
  import CodeSnippets from "./code-snippets.svelte";
6
  import Message from "./message.svelte";
7
+ import { ScrollState } from "$lib/spells/scroll-state.svelte";
8
+ import { watch } from "runed";
9
 
10
  interface Props {
11
  conversation: Conversation;
 
15
  }
16
 
17
  let { conversation = $bindable(), loading, viewCode, compareActive }: Props = $props();
 
 
 
 
 
18
  let messageContainer: HTMLDivElement | null = $state(null);
19
+ const scrollState = new ScrollState({
20
+ element: () => messageContainer,
21
+ offset: { bottom: 100 },
 
 
 
 
 
 
 
 
 
 
 
22
  });
23
 
24
+ watch(
25
+ () => conversation.messages.at(-1)?.content,
26
+ () => {
27
+ const shouldScroll = scrollState.arrived.bottom && !scrollState.isScrolling;
28
+ if (!shouldScroll) return;
29
+ scrollState.scrollToBottom();
30
  }
31
+ );
32
 
33
  function addMessage() {
34
  const msgs = conversation.messages.slice();
 
53
  : 'max-h-[calc(100dvh-5.8rem-2.5rem-75px)] md:max-h-[calc(100dvh-5.8rem)]'}"
54
  class:animate-pulse={loading && !conversation.streaming}
55
  bind:this={messageContainer}
56
+ id="test-this"
 
 
 
 
 
 
57
  >
58
  {#if !viewCode}
59
  {#each conversation.messages as _msg, idx}
src/lib/components/inference-playground/message.svelte CHANGED
@@ -14,7 +14,6 @@
14
 
15
  let element = $state<HTMLTextAreaElement>();
16
  new TextareaAutosize({
17
- styleProp: "minHeight",
18
  element: () => element,
19
  input: () => content,
20
  });
@@ -34,7 +33,7 @@
34
  {autofocus}
35
  bind:value={content}
36
  placeholder="Enter {role} message"
37
- class="resize-none overflow-hidden rounded-sm bg-transparent px-2 py-2.5 ring-gray-100 outline-none group-hover/message:ring-3 hover:resize-y hover:bg-white focus:resize-y focus:bg-white focus:ring-3 @2xl:px-3 dark:ring-gray-600 dark:hover:bg-gray-900 dark:focus:bg-gray-900"
38
  rows="1"
39
  tabindex="2"
40
  ></textarea>
 
14
 
15
  let element = $state<HTMLTextAreaElement>();
16
  new TextareaAutosize({
 
17
  element: () => element,
18
  input: () => content,
19
  });
 
33
  {autofocus}
34
  bind:value={content}
35
  placeholder="Enter {role} message"
36
+ class="resize-none overflow-hidden rounded-sm bg-transparent px-2 py-2.5 ring-gray-100 outline-none group-hover/message:ring-3 hover:bg-white focus:bg-white focus:ring-3 @2xl:px-3 dark:ring-gray-600 dark:hover:bg-gray-900 dark:focus:bg-gray-900"
37
  rows="1"
38
  tabindex="2"
39
  ></textarea>
src/lib/spells/scroll-state.svelte.ts ADDED
@@ -0,0 +1,299 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import type { MaybeGetter } from "$lib/types.js";
2
+ import { AnimationFrames, useDebounce, useEventListener } from "runed";
3
+ import { onMount } from "svelte";
4
+ import { extract } from "./extract.svelte.js";
5
+ import { noop } from "$lib/utils/noop.js";
6
+
7
+ export interface ScrollStateOptions {
8
+ /**
9
+ * The target element.
10
+ */
11
+ element: MaybeGetter<HTMLElement | Window | Document | null | undefined>;
12
+
13
+ // /**
14
+ // * Throttle time for scroll event, it’s disabled by default.
15
+ // *
16
+ // * @default 0
17
+ // */
18
+ // throttle?: MaybeGetter<number | undefined>;
19
+
20
+ /**
21
+ * The check time when scrolling ends.
22
+ * This configuration will be setting to (throttle + idle) when the `throttle` is configured.
23
+ *
24
+ * @default 200
25
+ */
26
+ idle?: MaybeGetter<number | undefined>;
27
+
28
+ /**
29
+ * Offset arrived states by x pixels
30
+ *
31
+ */
32
+ offset?: MaybeGetter<
33
+ | {
34
+ left?: number;
35
+ right?: number;
36
+ top?: number;
37
+ bottom?: number;
38
+ }
39
+ | undefined
40
+ >;
41
+
42
+ /**
43
+ * Trigger it when scrolling.
44
+ *
45
+ */
46
+ onScroll?: (e: Event) => void;
47
+
48
+ /**
49
+ * Trigger it when scrolling ends.
50
+ *
51
+ */
52
+ onStop?: (e: Event) => void;
53
+
54
+ /**
55
+ * Listener options for scroll event.
56
+ *
57
+ * @default {capture: false, passive: true}
58
+ */
59
+ eventListenerOptions?: AddEventListenerOptions;
60
+
61
+ /**
62
+ * Optionally specify a scroll behavior of `auto` (default, not smooth scrolling) or
63
+ * `smooth` (for smooth scrolling) which takes effect when changing the `x` or `y` refs.
64
+ *
65
+ * @default 'auto'
66
+ */
67
+ behavior?: MaybeGetter<ScrollBehavior | undefined>;
68
+
69
+ /**
70
+ * On error callback
71
+ *
72
+ * Default log error to `console.error`
73
+ */
74
+ onError?: (error: unknown) => void;
75
+ }
76
+
77
+ /**
78
+ * We have to check if the scroll amount is close enough to some threshold in order to
79
+ * more accurately calculate arrivedState. This is because scrollTop/scrollLeft are non-rounded
80
+ * numbers, while scrollHeight/scrollWidth and clientHeight/clientWidth are rounded.
81
+ * https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollHeight#determine_if_an_element_has_been_totally_scrolled
82
+ */
83
+ const ARRIVED_STATE_THRESHOLD_PIXELS = 1;
84
+
85
+ /**
86
+ * Reactive scroll.
87
+ *
88
+ * @see https://vueuse.org/useScroll for the inspiration behind this utility.
89
+ * @param element
90
+ * @param options
91
+ */
92
+ export class ScrollState {
93
+ #options!: ScrollStateOptions;
94
+ element = $derived(extract(this.#options.element));
95
+ // throttle = $derived(extract(this.#options.throttle, 0));
96
+ idle = $derived(extract(this.#options.idle, 200));
97
+ offset = $derived(
98
+ extract(this.#options.offset, {
99
+ left: 0,
100
+ right: 0,
101
+ top: 0,
102
+ bottom: 0,
103
+ })
104
+ );
105
+ onScroll = $derived(this.#options.onScroll ?? noop);
106
+ onStop = $derived(this.#options.onStop ?? noop);
107
+ eventListenerOptions = $derived(
108
+ this.#options.eventListenerOptions ?? {
109
+ capture: false,
110
+ passive: true,
111
+ }
112
+ );
113
+ behavior = $derived(extract(this.#options.behavior, "auto"));
114
+ onError = $derived(
115
+ this.#options.onError ??
116
+ ((e: unknown) => {
117
+ console.error(e);
118
+ })
119
+ );
120
+
121
+ /** State */
122
+ internalX = $state(0);
123
+ internalY = $state(0);
124
+
125
+ // Use a get/set pair for x and y because we want to write the value to the refs
126
+ // during a `scrollTo()` without firing additional `scrollTo()`s in the process.
127
+ #x = $derived(this.internalX);
128
+ get x() {
129
+ return this.#x;
130
+ }
131
+ set x(v) {
132
+ this.scrollTo(v, undefined);
133
+ }
134
+
135
+ #y = $derived(this.internalY);
136
+ get y() {
137
+ return this.#y;
138
+ }
139
+ set y(v) {
140
+ this.scrollTo(undefined, v);
141
+ }
142
+
143
+ isScrolling = $state(false);
144
+ arrived = $state({
145
+ left: true,
146
+ right: false,
147
+ top: true,
148
+ bottom: false,
149
+ });
150
+ directions = $state({
151
+ left: false,
152
+ right: false,
153
+ top: false,
154
+ bottom: false,
155
+ });
156
+
157
+ constructor(options: ScrollStateOptions) {
158
+ this.#options = options;
159
+
160
+ useEventListener(
161
+ () => this.element,
162
+ "scroll",
163
+ // throttle ? useThrottleFn(onScrollHandler, throttle, true, false) : onScrollHandler,
164
+ this.onScrollHandler,
165
+ this.eventListenerOptions
166
+ );
167
+
168
+ useEventListener(
169
+ () => this.element,
170
+ "scrollend",
171
+ e => this.onScrollEnd(e),
172
+ this.eventListenerOptions
173
+ );
174
+
175
+ onMount(() => {
176
+ this.setArrivedState();
177
+ });
178
+
179
+ // useResizeObserver(
180
+ // () => (isHtmlElement(this.element) ? this.element : null),
181
+ // () => {
182
+ // setTimeout(() => {
183
+ // this.setArrivedState();
184
+ // }, 100);
185
+ // }
186
+ // );
187
+
188
+ // overkill?
189
+ new AnimationFrames(() => this.setArrivedState());
190
+ }
191
+
192
+ setArrivedState = () => {
193
+ if (!window || !this.element) return;
194
+
195
+ const el: Element = ((this.element as Window)?.document?.documentElement ||
196
+ (this.element as Document)?.documentElement ||
197
+ (this.element as HTMLElement | SVGElement)) as Element;
198
+
199
+ const { display, flexDirection, direction } = getComputedStyle(el);
200
+ const directionMultipler = direction === "rtl" ? -1 : 1;
201
+
202
+ const scrollLeft = el.scrollLeft;
203
+ this.directions.left = scrollLeft < this.internalX;
204
+ this.directions.right = scrollLeft > this.internalX;
205
+
206
+ const left = scrollLeft * directionMultipler <= (this.offset.left || 0);
207
+ const right =
208
+ scrollLeft * directionMultipler + el.clientWidth >=
209
+ el.scrollWidth - (this.offset.right || 0) - ARRIVED_STATE_THRESHOLD_PIXELS;
210
+
211
+ if (display === "flex" && flexDirection === "row-reverse") {
212
+ this.arrived.left = right;
213
+ this.arrived.right = left;
214
+ } else {
215
+ this.arrived.left = left;
216
+ this.arrived.right = right;
217
+ }
218
+
219
+ this.internalX = scrollLeft;
220
+
221
+ let scrollTop = el.scrollTop;
222
+
223
+ // patch for mobile compatible
224
+ if (this.element === window.document && !scrollTop) scrollTop = window.document.body.scrollTop;
225
+
226
+ this.directions.top = scrollTop < this.internalY;
227
+ this.directions.bottom = scrollTop > this.internalY;
228
+ const top = scrollTop <= (this.offset.top || 0);
229
+ const bottom =
230
+ scrollTop + el.clientHeight >= el.scrollHeight - (this.offset.bottom || 0) - ARRIVED_STATE_THRESHOLD_PIXELS;
231
+
232
+ /**
233
+ * reverse columns and rows behave exactly the other way around,
234
+ * bottom is treated as top and top is treated as the negative version of bottom
235
+ */
236
+ if (display === "flex" && flexDirection === "column-reverse") {
237
+ this.arrived.top = bottom;
238
+ this.arrived.bottom = top;
239
+ } else {
240
+ this.arrived.top = top;
241
+ this.arrived.bottom = bottom;
242
+ }
243
+
244
+ this.internalY = scrollTop;
245
+ };
246
+
247
+ onScrollHandler = (e: Event) => {
248
+ if (!window) return;
249
+
250
+ this.setArrivedState();
251
+
252
+ this.isScrolling = true;
253
+ this.onScrollEndDebounced(e);
254
+ this.onScroll(e);
255
+ };
256
+
257
+ scrollTo(x: number | undefined, y: number | undefined) {
258
+ if (!window) return;
259
+
260
+ (this.element instanceof Document ? window.document.body : this.element)?.scrollTo({
261
+ top: y ?? this.y,
262
+ left: x ?? this.x,
263
+ behavior: this.behavior,
264
+ });
265
+ const scrollContainer =
266
+ (this.element as Window)?.document?.documentElement ||
267
+ (this.element as Document)?.documentElement ||
268
+ (this.element as Element);
269
+ if (x != null) this.internalX = scrollContainer.scrollLeft;
270
+ if (y != null) this.internalY = scrollContainer.scrollTop;
271
+ }
272
+
273
+ scrollToTop() {
274
+ this.scrollTo(undefined, 0);
275
+ }
276
+
277
+ scrollToBottom() {
278
+ if (!window) return;
279
+
280
+ const scrollContainer =
281
+ (this.element as Window)?.document?.documentElement ||
282
+ (this.element as Document)?.documentElement ||
283
+ (this.element as Element);
284
+ this.scrollTo(undefined, scrollContainer.scrollHeight);
285
+ }
286
+
287
+ onScrollEnd = (e: Event) => {
288
+ // dedupe if support native scrollend event
289
+ if (!this.isScrolling) return;
290
+
291
+ this.isScrolling = false;
292
+ this.directions.left = false;
293
+ this.directions.right = false;
294
+ this.directions.top = false;
295
+ this.directions.bottom = false;
296
+ this.onStop(e);
297
+ };
298
+ onScrollEndDebounced = useDebounce(this.onScrollEnd, () => this.idle);
299
+ }
src/lib/spells/textarea-autosize.svelte.ts CHANGED
@@ -1,7 +1,7 @@
1
  import type { Getter } from "melt";
2
  import { extract } from "./extract.svelte.js";
3
  import { useResizeObserver, watch } from "runed";
4
- import { tick } from "svelte";
5
 
6
  export interface TextareaAutosizeOptions {
7
  /** Textarea element to autosize. */
@@ -15,26 +15,37 @@ export interface TextareaAutosizeOptions {
15
  * @default `height`
16
  **/
17
  styleProp?: "height" | "minHeight";
 
 
 
 
 
18
  }
19
 
20
  export class TextareaAutosize {
21
  #options: TextareaAutosizeOptions;
 
 
 
22
  element = $derived.by(() => extract(this.#options.element));
23
  input = $derived.by(() => extract(this.#options.input));
24
  styleProp = $derived.by(() => extract(this.#options.styleProp, "height"));
25
-
26
- textareaScrollHeight = $state(1);
27
  textareaOldWidth = $state(0);
28
 
29
  constructor(options: TextareaAutosizeOptions) {
30
  this.#options = options;
31
 
 
 
 
32
  watch([() => this.input, () => this.element], () => {
33
  tick().then(() => this.triggerResize());
34
  });
35
 
36
  watch(
37
- () => this.textareaScrollHeight,
38
  () => options?.onResize?.()
39
  );
40
 
@@ -45,23 +56,106 @@ export class TextareaAutosize {
45
  const { contentRect } = entry;
46
  if (this.textareaOldWidth === contentRect.width) return;
47
 
48
- requestAnimationFrame(() => {
49
- this.textareaOldWidth = contentRect.width;
50
- this.triggerResize();
51
- });
52
  }
53
  );
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
54
  }
55
 
56
  triggerResize = () => {
57
- if (!this.element) return;
 
 
 
 
58
 
59
- let height = "";
 
60
 
61
- this.element.style[this.styleProp] = "1px";
62
- this.textareaScrollHeight = this.element?.scrollHeight;
63
- height = `${this.textareaScrollHeight}px`;
 
 
 
 
 
64
 
65
- this.element.style[this.styleProp] = height;
 
 
 
 
66
  };
67
  }
 
1
  import type { Getter } from "melt";
2
  import { extract } from "./extract.svelte.js";
3
  import { useResizeObserver, watch } from "runed";
4
+ import { onDestroy, tick } from "svelte";
5
 
6
  export interface TextareaAutosizeOptions {
7
  /** Textarea element to autosize. */
 
15
  * @default `height`
16
  **/
17
  styleProp?: "height" | "minHeight";
18
+ /**
19
+ * Maximum height of the textarea before enabling scrolling.
20
+ * @default `undefined` (no maximum)
21
+ */
22
+ maxHeight?: number;
23
  }
24
 
25
  export class TextareaAutosize {
26
  #options: TextareaAutosizeOptions;
27
+ #resizeTimeout: number | null = null;
28
+ #hiddenTextarea: HTMLTextAreaElement | null = null;
29
+
30
  element = $derived.by(() => extract(this.#options.element));
31
  input = $derived.by(() => extract(this.#options.input));
32
  styleProp = $derived.by(() => extract(this.#options.styleProp, "height"));
33
+ maxHeight = $derived.by(() => extract(this.#options.maxHeight, undefined));
34
+ textareaHeight = $state(0);
35
  textareaOldWidth = $state(0);
36
 
37
  constructor(options: TextareaAutosizeOptions) {
38
  this.#options = options;
39
 
40
+ // Create hidden textarea for measurements
41
+ this.#createHiddenTextarea();
42
+
43
  watch([() => this.input, () => this.element], () => {
44
  tick().then(() => this.triggerResize());
45
  });
46
 
47
  watch(
48
+ () => this.textareaHeight,
49
  () => options?.onResize?.()
50
  );
51
 
 
56
  const { contentRect } = entry;
57
  if (this.textareaOldWidth === contentRect.width) return;
58
 
59
+ this.textareaOldWidth = contentRect.width;
60
+ this.triggerResize();
 
 
61
  }
62
  );
63
+
64
+ onDestroy(() => {
65
+ // Clean up
66
+ if (this.#hiddenTextarea) {
67
+ this.#hiddenTextarea.remove();
68
+ this.#hiddenTextarea = null;
69
+ }
70
+
71
+ if (this.#resizeTimeout) {
72
+ window.cancelAnimationFrame(this.#resizeTimeout);
73
+ this.#resizeTimeout = null;
74
+ }
75
+ });
76
+ }
77
+
78
+ #createHiddenTextarea() {
79
+ // Create a hidden textarea that will be used for measurements
80
+ // This avoids layout shifts caused by manipulating the actual textarea
81
+ if (typeof window === "undefined") return;
82
+
83
+ this.#hiddenTextarea = document.createElement("textarea");
84
+ const style = this.#hiddenTextarea.style;
85
+
86
+ // Make it invisible but keep same text layout properties
87
+ style.visibility = "hidden";
88
+ style.position = "absolute";
89
+ style.overflow = "hidden";
90
+ style.height = "0";
91
+ style.top = "0";
92
+ style.left = "-9999px";
93
+
94
+ document.body.appendChild(this.#hiddenTextarea);
95
+ }
96
+
97
+ #copyStyles() {
98
+ if (!this.element || !this.#hiddenTextarea) return;
99
+
100
+ const computed = window.getComputedStyle(this.element);
101
+
102
+ // Copy all the styles that affect text layout
103
+ const stylesToCopy = [
104
+ "box-sizing",
105
+ "width",
106
+ "padding-top",
107
+ "padding-right",
108
+ "padding-bottom",
109
+ "padding-left",
110
+ "border-top-width",
111
+ "border-right-width",
112
+ "border-bottom-width",
113
+ "border-left-width",
114
+ "font-family",
115
+ "font-size",
116
+ "font-weight",
117
+ "font-style",
118
+ "letter-spacing",
119
+ "text-indent",
120
+ "text-transform",
121
+ "line-height",
122
+ "word-spacing",
123
+ "word-wrap",
124
+ "word-break",
125
+ "white-space",
126
+ ];
127
+
128
+ stylesToCopy.forEach(style => {
129
+ this.#hiddenTextarea!.style.setProperty(style, computed.getPropertyValue(style));
130
+ });
131
+
132
+ // Ensure the width matches exactly
133
+ this.#hiddenTextarea.style.width = `${this.element.clientWidth}px`;
134
  }
135
 
136
  triggerResize = () => {
137
+ if (!this.element || !this.#hiddenTextarea) return;
138
+
139
+ // Copy current styles and content to hidden textarea
140
+ this.#copyStyles();
141
+ this.#hiddenTextarea.value = this.input || "";
142
 
143
+ // Measure the hidden textarea
144
+ const scrollHeight = this.#hiddenTextarea.scrollHeight;
145
 
146
+ // Apply the height, respecting maxHeight if set
147
+ let newHeight = scrollHeight;
148
+ if (this.maxHeight && newHeight > this.maxHeight) {
149
+ newHeight = this.maxHeight;
150
+ this.element.style.overflowY = "auto";
151
+ } else {
152
+ this.element.style.overflowY = "hidden";
153
+ }
154
 
155
+ // Only update if height actually changed
156
+ if (this.textareaHeight !== newHeight) {
157
+ this.textareaHeight = newHeight;
158
+ this.element.style[this.styleProp] = `${newHeight}px`;
159
+ }
160
  };
161
  }