ropav 0.11.0 → 0.11.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,5 +1,5 @@
1
1
  import type { AutocompleteFilterProps, AutocompleteFilterSlotProps } from "./autocomplete.types.js";
2
- declare const __VLS_export: <T>(__VLS_props: NonNullable<Awaited<typeof __VLS_setup>>['props'], __VLS_ctx?: __VLS_PrettifyLocal<Pick<NonNullable<Awaited<typeof __VLS_setup>>, 'attrs' | 'emit' | 'slots'>>, __VLS_exposed?: NonNullable<Awaited<typeof __VLS_setup>>['expose'], __VLS_setup?: Promise<{
2
+ declare const __VLS_export: <T = any>(__VLS_props: NonNullable<Awaited<typeof __VLS_setup>>['props'], __VLS_ctx?: __VLS_PrettifyLocal<Pick<NonNullable<Awaited<typeof __VLS_setup>>, 'attrs' | 'emit' | 'slots'>>, __VLS_exposed?: NonNullable<Awaited<typeof __VLS_setup>>['expose'], __VLS_setup?: Promise<{
3
3
  props: import('vue').PublicProps & __VLS_PrettifyLocal<AutocompleteFilterProps<T> & {
4
4
  onInputChange?: ((value: string) => any) | undefined;
5
5
  "onUpdate:inputValue"?: ((value: string) => any) | undefined;
@@ -3,6 +3,7 @@ import { useInteractionStates } from "../../composables/use-interaction-states.j
3
3
  import { setFormValue } from "../../utils/form-value.js";
4
4
  import { useTextFieldControlContext } from "../../composables/use-text-field.js";
5
5
  import { useTextFieldContext } from "../textfield/textfield.context.js";
6
+ import { useTextareaAutosize } from "../../composables/use-textarea-autosize.js";
6
7
  import { computed, createTemplateRefSetter, defineVaporComponent, on, renderEffect, setDynamicProps, shallowRef, template, unref, watch } from "vue";
7
8
  import { textAreaVariants } from "@ropav/styles";
8
9
  //#region src/components/textarea/textarea-root.vue?vue&type=script&setup=true&vapor=true&lang.ts
@@ -18,6 +19,13 @@ var textarea_root_vue_vue_type_script_setup_true_vapor_true_lang_default = /*@__
18
19
  type: Boolean,
19
20
  default: void 0
20
21
  },
22
+ autosize: {
23
+ type: Boolean,
24
+ default: void 0
25
+ },
26
+ minRows: { default: void 0 },
27
+ maxRows: { default: void 0 },
28
+ resize: { default: void 0 },
21
29
  placeholder: {}
22
30
  },
23
31
  emits: ["change", "update:value"],
@@ -33,9 +41,11 @@ var textarea_root_vue_vue_type_script_setup_true_vapor_true_lang_default = /*@__
33
41
  };
34
42
  const resolvedVariant = computed(() => props.variant ?? textField?.variant.value);
35
43
  const resolvedSize = computed(() => props.size ?? textField?.size.value);
44
+ const resolvedResize = computed(() => props.autosize ? "none" : props.resize);
36
45
  const styles = computed(() => textAreaVariants({
37
46
  class: props.class,
38
47
  fullWidth: props.fullWidth,
48
+ resize: resolvedResize.value,
39
49
  size: resolvedSize.value,
40
50
  variant: resolvedVariant.value
41
51
  }));
@@ -43,15 +53,24 @@ var textarea_root_vue_vue_type_script_setup_true_vapor_true_lang_default = /*@__
43
53
  const merged = { ...control?.attrs.value };
44
54
  if (props.placeholder !== void 0) merged["placeholder"] = props.placeholder;
45
55
  if (props.value !== void 0) merged["value"] = props.value;
56
+ if (props.autosize && props.minRows !== void 0) merged["rows"] = props.minRows;
46
57
  return merged;
47
58
  });
48
59
  const interaction = useInteractionStates({ isDisabled: () => control?.isDisabled.value });
49
60
  const inputCount = shallowRef(0);
61
+ const { sync: syncAutosize } = useTextareaAutosize({
62
+ content: () => control?.attrs.value["value"],
63
+ element,
64
+ enabled: () => props.autosize,
65
+ maxRows: () => props.maxRows,
66
+ minRows: () => props.minRows
67
+ });
50
68
  const onInput = (event) => {
51
69
  control?.handlers.onInput(event);
52
70
  const next = event.currentTarget.value;
53
71
  emit("change", next);
54
72
  emit("update:value", next);
73
+ syncAutosize({ fromInput: true });
55
74
  if (props.value !== void 0) inputCount.value++;
56
75
  };
57
76
  /**
@@ -66,6 +85,7 @@ var textarea_root_vue_vue_type_script_setup_true_vapor_true_lang_default = /*@__
66
85
  ], ([el, pinned]) => {
67
86
  if (pinned === void 0) return;
68
87
  setFormValue(el, pinned);
88
+ syncAutosize();
69
89
  }, {
70
90
  flush: "post",
71
91
  immediate: true
@@ -16,6 +16,27 @@ export interface TextAreaRootProps {
16
16
  size?: TextAreaVariants["size"];
17
17
  /** Whether the control stretches to fill its container. */
18
18
  fullWidth?: boolean;
19
+ /**
20
+ * Whether the control grows with its content. `minRows` and `maxRows` only apply when this
21
+ * is set. Native resize is forced off while it is, so the drag handle cannot fight the
22
+ * measured height. @default false
23
+ */
24
+ autosize?: boolean;
25
+ /**
26
+ * Minimum visible rows while `autosize` is set. Ignored otherwise. Written through to the
27
+ * native `rows` attribute so the first paint is already that tall.
28
+ */
29
+ minRows?: number;
30
+ /**
31
+ * Maximum visible rows while `autosize` is set. Ignored otherwise. Absent, the control grows
32
+ * without a cap.
33
+ */
34
+ maxRows?: number;
35
+ /**
36
+ * Whether the pointer can drag the edge. `none` by default; `vertical` or `both` opt in.
37
+ * Ignored while `autosize` is set. @default "none"
38
+ */
39
+ resize?: TextAreaVariants["resize"];
19
40
  /**
20
41
  * Placeholder shown while the control is empty. Declared so it can also be set here rather
21
42
  * than only on the field; every other native attribute arrives by attribute fallthrough.
@@ -0,0 +1,55 @@
1
+ import type { MaybeRefOrGetter } from "vue";
2
+ export interface UseTextareaAutosizeOptions {
3
+ element: MaybeRefOrGetter<HTMLTextAreaElement | null | undefined>;
4
+ /** When false, inline height and overflow are cleared and the native `rows` height is used. */
5
+ enabled: MaybeRefOrGetter<boolean | undefined>;
6
+ /** Floor, in rows. Ignored when `enabled` is false. */
7
+ minRows: MaybeRefOrGetter<number | undefined>;
8
+ /** Ceiling, in rows. Absent, the control grows without a cap. Ignored when `enabled` is false. */
9
+ maxRows: MaybeRefOrGetter<number | undefined>;
10
+ /**
11
+ * Text held somewhere the element cannot be read from. Reading `element.value` is not
12
+ * reactive, so a caller that does not already remeasure when its own text moves passes
13
+ * the value it holds here.
14
+ */
15
+ content?: MaybeRefOrGetter<unknown>;
16
+ }
17
+ export interface UseTextareaAutosizeSyncOptions {
18
+ /**
19
+ * From `input` only. A caret at the end then keeps the last line's padding in view.
20
+ * Layout remeasures omit this so a scroll the user made is not stolen.
21
+ */
22
+ fromInput?: boolean;
23
+ }
24
+ export interface UseTextareaAutosizeReturn {
25
+ /** Measure now. Needed from `input`, where the DOM already holds the next text. */
26
+ sync: (options?: UseTextareaAutosizeSyncOptions) => void;
27
+ }
28
+ /**
29
+ * Keep a textarea as tall as its content, optionally between `minRows` and `maxRows`.
30
+ *
31
+ * The caller has to invoke {@link UseTextareaAutosizeReturn.sync} from `input`: reading
32
+ * `element.value` inside a getter is not a reactive dependency, and waiting for a post-flush
33
+ * watch would size against the previous stroke. `minRows`, `maxRows` and {@link
34
+ * UseTextareaAutosizeOptions.content} go through a post-flush watch instead.
35
+ *
36
+ * Observers attach only while autosize is on. Native `resize` writes inline `width` and
37
+ * `height` as the pointer drags; an observer that stayed attached with autosize off would
38
+ * `clear` that height on every width change and fight the handle.
39
+ *
40
+ * Writing `style.height` is itself a size change, so the observer has to discriminate width
41
+ * or it would loop. That makes the seed load-bearing: it is read from the same content box
42
+ * `contentRect` reports, because a seed off by the border makes the first delivery look like
43
+ * a width change, and skipping that first delivery instead loses a real one — `sync` after
44
+ * `observe` can make a scrollbar appear, and the callback reporting the narrower width is
45
+ * the only notice of it.
46
+ *
47
+ * An inline height also hides height-only metric changes from the observer — a webfont that
48
+ * lands after first paint, or `.rp-textarea`'s `@media (width >= 40rem)` type/padding switch
49
+ * on a fixed-width control. Those go through `document.fonts` and `window` `resize`. Used
50
+ * metrics that do not move the box (`--rp-leading`, Firefox text-only zoom) never notify at
51
+ * all, which is why every field settles with `overflow-y: auto` — a stale height then
52
+ * scrolls rather than clips. A leftover clip on a field no cap clamped is the observer
53
+ * backstop for same-width box changes such as padding, which do notify.
54
+ */
55
+ export declare const useTextareaAutosize: (options: UseTextareaAutosizeOptions) => UseTextareaAutosizeReturn;
@@ -0,0 +1,189 @@
1
+ import { onScopeDispose, toValue, watch } from "vue";
2
+ //#region src/composables/use-textarea-autosize.ts
3
+ var px = (value) => {
4
+ const parsed = Number.parseFloat(value);
5
+ return Number.isFinite(parsed) ? parsed : 0;
6
+ };
7
+ /**
8
+ * A used line-height, not the keyword.
9
+ *
10
+ * `normal` is a computed value the platform leaves unresolved, and a row count in `normal`s is
11
+ * not a length. Font-size times 1.2 is the usual stand-in, and is what a missing font-size falls
12
+ * back through as well.
13
+ */
14
+ var lineHeightOf = (style) => {
15
+ const raw = style.lineHeight;
16
+ const parsed = Number.parseFloat(raw);
17
+ if (raw !== "normal" && Number.isFinite(parsed)) return parsed;
18
+ const fontSize = Number.parseFloat(style.fontSize);
19
+ return Number.isFinite(fontSize) ? fontSize * 1.2 : 16;
20
+ };
21
+ var positive = (value) => value !== void 0 && value > 0 ? value : void 0;
22
+ var clear = (element) => {
23
+ element.style.height = "";
24
+ element.style.overflowY = "";
25
+ element.style.scrollPaddingBottom = "";
26
+ };
27
+ /**
28
+ * Content-box width, the same box ResizeObserver reports as `contentRect`.
29
+ *
30
+ * `clientWidth` is the padding box minus the scrollbar, so subtracting inline
31
+ * padding lands on the content box without mixing in the border the way
32
+ * `getBoundingClientRect` does.
33
+ */
34
+ var contentWidthOf = (element) => {
35
+ const style = getComputedStyle(element);
36
+ return element.clientWidth - px(style.paddingLeft) - px(style.paddingRight);
37
+ };
38
+ /**
39
+ * Size a textarea to its content, clamped to a row range.
40
+ *
41
+ * Measured on the live element rather than a hidden clone of it: a clone is a second node in
42
+ * the document to keep in step, and the live element already has the font, the padding and
43
+ * the wrapping width. Height is set to `auto` for the read so a previous inline height cannot
44
+ * pin `scrollHeight` to itself, then written back as a pixel height.
45
+ *
46
+ * `scrollHeight` includes padding and not the border. The reset is `border-box`, so the height
47
+ * that is written has to put the border back or the last line is clipped by that much.
48
+ *
49
+ * The bottom-pin is input-only. Typing at the end has to bring `padding-bottom` into view;
50
+ * a width, font or row-range remeasure must not yank a scroll the user just made.
51
+ *
52
+ * @returns Whether `maxRows` clamped the height, which is the one case where a field left
53
+ * scrolling its own content is doing what it was asked to.
54
+ */
55
+ var measure = (element, minRows, maxRows, fromInput) => {
56
+ const style = getComputedStyle(element);
57
+ const paddingY = px(style.paddingTop) + px(style.paddingBottom);
58
+ const borderY = px(style.borderTopWidth) + px(style.borderBottomWidth);
59
+ const lineHeight = lineHeightOf(style);
60
+ const borderBox = style.boxSizing === "border-box";
61
+ const extras = borderBox ? paddingY + borderY : 0;
62
+ const min = positive(minRows);
63
+ const max = positive(maxRows);
64
+ const paddingBottom = px(style.paddingBottom);
65
+ const scrollTop = element.scrollTop;
66
+ const atEnd = element.selectionStart === element.value.length && element.selectionEnd === element.value.length;
67
+ if (min !== void 0) element.rows = min;
68
+ element.style.height = "auto";
69
+ element.style.overflowY = "hidden";
70
+ let height = element.scrollHeight;
71
+ if (borderBox) height += borderY;
72
+ else height -= paddingY;
73
+ if (min !== void 0) height = Math.max(height, min * lineHeight + extras);
74
+ let capped = false;
75
+ if (max !== void 0) {
76
+ const cap = max * lineHeight + extras;
77
+ if (height > cap) {
78
+ height = cap;
79
+ capped = true;
80
+ }
81
+ }
82
+ element.style.height = `${height}px`;
83
+ element.style.overflowY = "auto";
84
+ element.style.scrollPaddingBottom = `${paddingBottom}px`;
85
+ if (fromInput && capped && atEnd) element.scrollTop = element.scrollHeight;
86
+ else element.scrollTop = scrollTop;
87
+ return capped;
88
+ };
89
+ /**
90
+ * Keep a textarea as tall as its content, optionally between `minRows` and `maxRows`.
91
+ *
92
+ * The caller has to invoke {@link UseTextareaAutosizeReturn.sync} from `input`: reading
93
+ * `element.value` inside a getter is not a reactive dependency, and waiting for a post-flush
94
+ * watch would size against the previous stroke. `minRows`, `maxRows` and {@link
95
+ * UseTextareaAutosizeOptions.content} go through a post-flush watch instead.
96
+ *
97
+ * Observers attach only while autosize is on. Native `resize` writes inline `width` and
98
+ * `height` as the pointer drags; an observer that stayed attached with autosize off would
99
+ * `clear` that height on every width change and fight the handle.
100
+ *
101
+ * Writing `style.height` is itself a size change, so the observer has to discriminate width
102
+ * or it would loop. That makes the seed load-bearing: it is read from the same content box
103
+ * `contentRect` reports, because a seed off by the border makes the first delivery look like
104
+ * a width change, and skipping that first delivery instead loses a real one — `sync` after
105
+ * `observe` can make a scrollbar appear, and the callback reporting the narrower width is
106
+ * the only notice of it.
107
+ *
108
+ * An inline height also hides height-only metric changes from the observer — a webfont that
109
+ * lands after first paint, or `.rp-textarea`'s `@media (width >= 40rem)` type/padding switch
110
+ * on a fixed-width control. Those go through `document.fonts` and `window` `resize`. Used
111
+ * metrics that do not move the box (`--rp-leading`, Firefox text-only zoom) never notify at
112
+ * all, which is why every field settles with `overflow-y: auto` — a stale height then
113
+ * scrolls rather than clips. A leftover clip on a field no cap clamped is the observer
114
+ * backstop for same-width box changes such as padding, which do notify.
115
+ */
116
+ var useTextareaAutosize = (options) => {
117
+ let observer;
118
+ let lastWidth = NaN;
119
+ /** So turning autosize off does not wipe a height the native resize handle wrote. */
120
+ let applied = false;
121
+ /** Whether `maxRows` clamped the last measure, which is when a clip is meant to be there. */
122
+ let capped = false;
123
+ const sync = (syncOptions) => {
124
+ const element = toValue(options.element) ?? null;
125
+ const enabled = Boolean(toValue(options.enabled));
126
+ if (!element) return;
127
+ if (!enabled) {
128
+ if (applied) {
129
+ clear(element);
130
+ applied = false;
131
+ }
132
+ return;
133
+ }
134
+ capped = measure(element, toValue(options.minRows), toValue(options.maxRows), Boolean(syncOptions?.fromInput));
135
+ applied = true;
136
+ };
137
+ const onMetrics = () => {
138
+ sync();
139
+ };
140
+ const metrics = (bind) => {
141
+ if (typeof document === "undefined") return;
142
+ window[bind]("resize", onMetrics);
143
+ document.fonts?.[bind]("loadingdone", onMetrics);
144
+ };
145
+ const detach = () => {
146
+ observer?.disconnect();
147
+ observer = void 0;
148
+ metrics("removeEventListener");
149
+ };
150
+ const observe = (element) => {
151
+ detach();
152
+ if (typeof ResizeObserver !== "undefined") {
153
+ lastWidth = contentWidthOf(element);
154
+ observer = new ResizeObserver((entries) => {
155
+ const width = entries[0]?.contentRect.width;
156
+ if (width === void 0) return;
157
+ const widthChanged = width !== lastWidth;
158
+ lastWidth = width;
159
+ if (widthChanged) {
160
+ sync();
161
+ return;
162
+ }
163
+ if (!capped && element.scrollHeight > element.clientHeight) sync();
164
+ });
165
+ observer.observe(element);
166
+ }
167
+ metrics("addEventListener");
168
+ };
169
+ watch([() => toValue(options.element) ?? null, () => Boolean(toValue(options.enabled))], ([element, enabled]) => {
170
+ detach();
171
+ if (!element) return;
172
+ if (enabled) observe(element);
173
+ sync();
174
+ }, {
175
+ flush: "post",
176
+ immediate: true
177
+ });
178
+ watch([
179
+ () => toValue(options.minRows),
180
+ () => toValue(options.maxRows),
181
+ () => toValue(options.content)
182
+ ], () => {
183
+ sync();
184
+ }, { flush: "post" });
185
+ onScopeDispose(detach, true);
186
+ return { sync };
187
+ };
188
+ //#endregion
189
+ export { useTextareaAutosize };