ropav 0.11.0 → 0.12.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.
Files changed (59) hide show
  1. package/dist/components/autocomplete/autocomplete-clear-button.js +1 -1
  2. package/dist/components/autocomplete/autocomplete-filter.d.ts +1 -1
  3. package/dist/components/autocomplete/autocomplete-root.js +3 -0
  4. package/dist/components/checkbox/checkbox-root.js +4 -1
  5. package/dist/components/radio/radio-root.js +4 -1
  6. package/dist/components/scroll-shadow/scroll-shadow-root.js +2 -10
  7. package/dist/components/scroll-shadow/use-scroll-shadow.d.ts +10 -0
  8. package/dist/components/scroll-shadow/use-scroll-shadow.js +34 -2
  9. package/dist/components/select/index.d.ts +3 -2
  10. package/dist/components/select/index.js +2 -1
  11. package/dist/components/select/select-clear-button.d.ts +21 -0
  12. package/dist/components/select/select-clear-button.js +66 -0
  13. package/dist/components/select/select-root.d.ts +2 -1
  14. package/dist/components/select/select-root.js +15 -1
  15. package/dist/components/select/select.context.d.ts +11 -0
  16. package/dist/components/select/select.types.d.ts +3 -0
  17. package/dist/components/switch/switch-root.js +4 -1
  18. package/dist/components/table/table-row.js +3 -2
  19. package/dist/components/table/use-grid-keyboard.js +2 -3
  20. package/dist/components/table/use-table-collection.d.ts +0 -11
  21. package/dist/components/table/use-table-collection.js +1 -17
  22. package/dist/components/tabs/tabs-root.js +5 -1
  23. package/dist/components/tabs/tabs.types.d.ts +6 -0
  24. package/dist/components/textarea/textarea-root.js +20 -0
  25. package/dist/components/textarea/textarea.types.d.ts +21 -0
  26. package/dist/components/toast/index.d.ts +1 -1
  27. package/dist/components/toast/index.js +3 -3
  28. package/dist/components/toast/toast-default-content.js +26 -4
  29. package/dist/components/toast/toast-indicator.js +8 -2
  30. package/dist/components/toast/toast-provider.js +53 -5
  31. package/dist/components/toast/toast-queue.d.ts +60 -5
  32. package/dist/components/toast/toast-queue.js +85 -27
  33. package/dist/components/toast/toast-root.js +67 -14
  34. package/dist/components/toast/toast.constants.d.ts +17 -1
  35. package/dist/components/toast/toast.constants.js +18 -2
  36. package/dist/components/toast/toast.context.d.ts +6 -0
  37. package/dist/components/toast/toast.types.d.ts +19 -0
  38. package/dist/composables/use-list-keyboard.d.ts +1 -1
  39. package/dist/composables/use-list-keyboard.js +4 -5
  40. package/dist/composables/use-measured-height.d.ts +18 -10
  41. package/dist/composables/use-measured-height.js +53 -15
  42. package/dist/composables/use-overlay-position.js +7 -4
  43. package/dist/composables/use-select-state.d.ts +8 -0
  44. package/dist/composables/use-select-state.js +5 -0
  45. package/dist/composables/use-select.d.ts +7 -0
  46. package/dist/composables/use-select.js +6 -0
  47. package/dist/composables/use-textarea-autosize.d.ts +55 -0
  48. package/dist/composables/use-textarea-autosize.js +189 -0
  49. package/dist/composables/use-toast-region.d.ts +18 -4
  50. package/dist/composables/use-toast-region.js +128 -6
  51. package/dist/composables/use-toast.d.ts +1 -1
  52. package/dist/composables/use-toast.js +4 -3
  53. package/dist/composables/use-typeahead.js +3 -0
  54. package/dist/index.js +5 -4
  55. package/dist/ropav.min.css +1 -1
  56. package/dist/utils/focus.d.ts +13 -0
  57. package/dist/utils/focus.js +18 -1
  58. package/dist/version.js +1 -1
  59. package/package.json +3 -3
@@ -12,5 +12,21 @@ export declare const DEFAULT_MAX_VISIBLE_TOAST = 3;
12
12
  export declare const DEFAULT_SCALE_FACTOR = 0.05;
13
13
  /** Pixels wide, written out as `--toast-width` for the stylesheet to read. */
14
14
  export declare const DEFAULT_TOAST_WIDTH = 460;
15
- /** Milliseconds a toast lives for. Matches sonner's default. */
15
+ /** Milliseconds a toast lives for. */
16
16
  export declare const DEFAULT_TOAST_TIMEOUT = 4000;
17
+ /**
18
+ * Milliseconds after which a closed toast is dropped whether or not it reported its exit.
19
+ *
20
+ * Not the length of the exit, which the toast waits out against its own animations. This is the
21
+ * ceiling for a toast that can never report one — a slot the caller filled with something other
22
+ * than a `ToastRoot` — so a list of them cannot grow without end.
23
+ */
24
+ export declare const EXIT_FALLBACK = 2000;
25
+ /**
26
+ * The key combination that moves focus to the toast region.
27
+ *
28
+ * An entry naming one of the `KeyboardEvent` modifier booleans matches that modifier; anything
29
+ * else matches `event.code`, so what counts is the key in that position rather than the character
30
+ * the layout produces there. An empty list turns the shortcut off.
31
+ */
32
+ export declare const DEFAULT_HOTKEY: readonly string[];
@@ -13,7 +13,23 @@ var DEFAULT_MAX_VISIBLE_TOAST = 3;
13
13
  var DEFAULT_SCALE_FACTOR = .05;
14
14
  /** Pixels wide, written out as `--toast-width` for the stylesheet to read. */
15
15
  var DEFAULT_TOAST_WIDTH = 460;
16
- /** Milliseconds a toast lives for. Matches sonner's default. */
16
+ /** Milliseconds a toast lives for. */
17
17
  var DEFAULT_TOAST_TIMEOUT = 4e3;
18
+ /**
19
+ * Milliseconds after which a closed toast is dropped whether or not it reported its exit.
20
+ *
21
+ * Not the length of the exit, which the toast waits out against its own animations. This is the
22
+ * ceiling for a toast that can never report one — a slot the caller filled with something other
23
+ * than a `ToastRoot` — so a list of them cannot grow without end.
24
+ */
25
+ var EXIT_FALLBACK = 2e3;
26
+ /**
27
+ * The key combination that moves focus to the toast region.
28
+ *
29
+ * An entry naming one of the `KeyboardEvent` modifier booleans matches that modifier; anything
30
+ * else matches `event.code`, so what counts is the key in that position rather than the character
31
+ * the layout produces there. An empty list turns the shortcut off.
32
+ */
33
+ var DEFAULT_HOTKEY = ["altKey", "KeyT"];
18
34
  //#endregion
19
- export { DEFAULT_GAP, DEFAULT_MAX_VISIBLE_TOAST, DEFAULT_SCALE_FACTOR, DEFAULT_TOAST_TIMEOUT, DEFAULT_TOAST_WIDTH };
35
+ export { DEFAULT_GAP, DEFAULT_HOTKEY, DEFAULT_MAX_VISIBLE_TOAST, DEFAULT_SCALE_FACTOR, DEFAULT_TOAST_TIMEOUT, DEFAULT_TOAST_WIDTH, EXIT_FALLBACK };
@@ -12,10 +12,16 @@ import type { ComputedRef } from "vue";
12
12
  export interface ToastRegionContext {
13
13
  /** Closes one toast by key. */
14
14
  close: (key: string) => void;
15
+ /** Keys still mounted for their exit animation, which have left the queue already. */
16
+ exitingKeys: ComputedRef<ReadonlySet<string>>;
15
17
  gap: ComputedRef<number>;
16
18
  /** Every mounted toast's measured height, so a stacked toast can be clipped to the front one. */
17
19
  heightsByKey: ComputedRef<Record<string, number>>;
20
+ /** Whether the stack is opened out, so every toast shows at its own height. */
21
+ isExpanded: ComputedRef<boolean>;
18
22
  maxVisibleToasts: ComputedRef<number>;
23
+ /** Reported by a toast once its exit animation has finished, so it can be unmounted. */
24
+ onExitFinished: (key: string) => void;
19
25
  onToastHeightChange: (key: string, height: number) => void;
20
26
  onToastHeightRemove: (key: string) => void;
21
27
  placement: ComputedRef<ToastVariants["placement"]>;
@@ -82,6 +82,19 @@ export interface ToastProviderProps {
82
82
  class?: string;
83
83
  /** Pixels between stacked toasts. @default 12 */
84
84
  gap?: number;
85
+ /**
86
+ * Key combination that moves focus to the region, as `KeyboardEvent` modifier names plus an
87
+ * `event.code`. Modifiers left out have to be up, so `Alt`+`T` does not answer `Ctrl`+`Alt`+`T`.
88
+ * Pass an empty list to turn it off.
89
+ *
90
+ * @default ["altKey", "KeyT"]
91
+ */
92
+ hotkey?: readonly string[];
93
+ /**
94
+ * Holds the stack open regardless of pointer or focus. Deliberately does not pause the clocks:
95
+ * expansion asked for by the page is not someone reading the toasts.
96
+ */
97
+ isExpanded?: boolean;
85
98
  /**
86
99
  * How many toasts are drawn at once. Visual only — the rest are faded out, not dropped.
87
100
  *
@@ -118,6 +131,12 @@ export interface ToastContentProps {
118
131
  }
119
132
  export interface ToastIndicatorProps {
120
133
  class?: string;
134
+ /**
135
+ * Whether this indicator is replacing one the toast was already showing, which the stylesheet
136
+ * animates. The first indicator a toast renders is not a replacement and must not be marked as
137
+ * one, or every toast would arrive with its icon animating.
138
+ */
139
+ isSwapped?: boolean;
121
140
  /** Picks the default icon. Falls back to the toast's own variant. */
122
141
  variant?: ToastVariants["variant"];
123
142
  }
@@ -50,7 +50,7 @@ export interface UseListKeyboardOptions {
50
50
  disallowSelectAll?: MaybeRefOrGetter<boolean | undefined>;
51
51
  /** @default "clearSelection" */
52
52
  escapeKeyBehavior?: MaybeRefOrGetter<"clearSelection" | "none" | undefined>;
53
- /** Called when an item is activated rather than selected. */
53
+ /** Called when an item is activated: Enter, or a press where there is no selection to make. */
54
54
  onAction?: (key: CollectionKey) => void;
55
55
  /**
56
56
  * Whether focus over the collection is nominal rather than real.
@@ -278,14 +278,13 @@ var useListKeyboard = (options) => {
278
278
  return;
279
279
  }
280
280
  case "Enter":
281
- if (focused == null || toValue(options.disallowActivation)) return;
282
- options.onAction?.(focused);
283
- event.preventDefault();
284
- return;
285
281
  case " ":
286
282
  if (focused == null || toValue(options.disallowActivation)) return;
287
283
  if (selection.selectionMode.value === "none") options.onAction?.(focused);
288
- else selection.select(focused, { isShiftPressed: event.shiftKey });
284
+ else {
285
+ selection.select(focused, { isShiftPressed: event.shiftKey });
286
+ if (event.key === "Enter") options.onAction?.(focused);
287
+ }
289
288
  event.preventDefault();
290
289
  return;
291
290
  default: return;
@@ -10,16 +10,24 @@ export interface UseMeasuredHeightReturn {
10
10
  * the content *wants*: a toast that is not frontmost is clipped to the front one's height, so its
11
11
  * own border box has already been overwritten by the very number this feeds.
12
12
  *
13
- * Two narrowings against the usual shape, both measured rather than assumed:
13
+ * That overwriting is also why the height is unset before every reading. The value this produces
14
+ * is what decides the element's own `height`, so reading it back off a sized box would return the
15
+ * previous answer for ever — a toast whose content is replaced in place would keep the height of
16
+ * the content it no longer holds.
14
17
  *
15
- * 1. It measures directly on attach and then lets the observer take over, rather than relying on
16
- * the observer's first notification alone. jsdom *has* a `ResizeObserver` constructor but it
17
- * never notifies anything, so an observer-only reading is permanently absent there — and this
18
- * value drives an inline custom property that decides layout, which is exactly the kind of
19
- * thing a fast suite should be able to assert. In a real browser the two readings agree, and
20
- * the repeat is dropped, so the only difference is that the first one lands a frame earlier.
21
- * 2. React also watches `aria-hidden` with a `MutationObserver`. That attribute never reaches the
22
- * DOM there — `react-aria-components` filters it off the toast — so the observer is dead code,
23
- * and this package renders no `aria-hidden` for it to watch either.
18
+ * Three narrowings against the usual shape, each measured rather than assumed:
19
+ *
20
+ * 1. It measures directly on attach and then lets the observers take over, rather than relying on
21
+ * a first notification alone. jsdom *has* a `ResizeObserver` constructor but it never notifies
22
+ * anything, so an observer-only reading is permanently absent there — and this value drives an
23
+ * inline custom property that decides layout, which is exactly the kind of thing a fast suite
24
+ * should be able to assert. In a real browser the two readings agree, and the repeat is
25
+ * dropped, so the only difference is that the first one lands a frame earlier.
26
+ * 2. Only the inline axis of a resize counts. The block axis is transitioned by the caller, and a
27
+ * reading forces layout twice to unset and restore the height — so reacting to it would
28
+ * re-measure on every frame of that animation and retarget the very transition it is watching.
29
+ * 3. Content is watched separately, because a box whose height is already forced does not resize
30
+ * when what is inside it changes. Attributes are left out of that watch: the caller writes its
31
+ * state onto this element, and none of it changes what the content wants.
24
32
  */
25
33
  export declare const useMeasuredHeight: (elementRef: MaybeRefOrGetter<HTMLElement | null | undefined>) => UseMeasuredHeightReturn;
@@ -7,36 +7,74 @@ import { computed, onScopeDispose, shallowRef, toValue, watch } from "vue";
7
7
  * the content *wants*: a toast that is not frontmost is clipped to the front one's height, so its
8
8
  * own border box has already been overwritten by the very number this feeds.
9
9
  *
10
- * Two narrowings against the usual shape, both measured rather than assumed:
10
+ * That overwriting is also why the height is unset before every reading. The value this produces
11
+ * is what decides the element's own `height`, so reading it back off a sized box would return the
12
+ * previous answer for ever — a toast whose content is replaced in place would keep the height of
13
+ * the content it no longer holds.
11
14
  *
12
- * 1. It measures directly on attach and then lets the observer take over, rather than relying on
13
- * the observer's first notification alone. jsdom *has* a `ResizeObserver` constructor but it
14
- * never notifies anything, so an observer-only reading is permanently absent there — and this
15
- * value drives an inline custom property that decides layout, which is exactly the kind of
16
- * thing a fast suite should be able to assert. In a real browser the two readings agree, and
17
- * the repeat is dropped, so the only difference is that the first one lands a frame earlier.
18
- * 2. React also watches `aria-hidden` with a `MutationObserver`. That attribute never reaches the
19
- * DOM there — `react-aria-components` filters it off the toast — so the observer is dead code,
20
- * and this package renders no `aria-hidden` for it to watch either.
15
+ * Three narrowings against the usual shape, each measured rather than assumed:
16
+ *
17
+ * 1. It measures directly on attach and then lets the observers take over, rather than relying on
18
+ * a first notification alone. jsdom *has* a `ResizeObserver` constructor but it never notifies
19
+ * anything, so an observer-only reading is permanently absent there — and this value drives an
20
+ * inline custom property that decides layout, which is exactly the kind of thing a fast suite
21
+ * should be able to assert. In a real browser the two readings agree, and the repeat is
22
+ * dropped, so the only difference is that the first one lands a frame earlier.
23
+ * 2. Only the inline axis of a resize counts. The block axis is transitioned by the caller, and a
24
+ * reading forces layout twice to unset and restore the height — so reacting to it would
25
+ * re-measure on every frame of that animation and retarget the very transition it is watching.
26
+ * 3. Content is watched separately, because a box whose height is already forced does not resize
27
+ * when what is inside it changes. Attributes are left out of that watch: the caller writes its
28
+ * state onto this element, and none of it changes what the content wants.
21
29
  */
22
30
  var useMeasuredHeight = (elementRef) => {
23
31
  const height = shallowRef(void 0);
24
- let observer;
32
+ let resizeObserver;
33
+ let contentObserver;
34
+ let frame;
25
35
  const detach = () => {
26
- observer?.disconnect();
27
- observer = void 0;
36
+ resizeObserver?.disconnect();
37
+ resizeObserver = void 0;
38
+ contentObserver?.disconnect();
39
+ contentObserver = void 0;
40
+ if (frame !== void 0 && typeof cancelAnimationFrame === "function") cancelAnimationFrame(frame);
41
+ frame = void 0;
28
42
  };
29
43
  watch(() => toValue(elementRef) ?? null, (element) => {
30
44
  detach();
31
45
  if (!element) return;
32
46
  const measure = () => {
47
+ const forced = element.style.height;
48
+ element.style.height = "auto";
33
49
  const next = element.scrollHeight;
50
+ element.style.height = forced;
34
51
  if (height.value !== next) height.value = next;
35
52
  };
53
+ const scheduleMeasure = () => {
54
+ if (frame !== void 0 || typeof requestAnimationFrame !== "function") return;
55
+ frame = requestAnimationFrame(() => {
56
+ frame = void 0;
57
+ measure();
58
+ });
59
+ };
36
60
  measure();
61
+ if (typeof MutationObserver === "function") {
62
+ contentObserver = new MutationObserver(scheduleMeasure);
63
+ contentObserver.observe(element, {
64
+ characterData: true,
65
+ childList: true,
66
+ subtree: true
67
+ });
68
+ }
37
69
  if (typeof ResizeObserver === "undefined") return;
38
- observer = new ResizeObserver(measure);
39
- observer.observe(element);
70
+ let lastWidth = element.getBoundingClientRect().width;
71
+ resizeObserver = new ResizeObserver((entries) => {
72
+ const width = entries[0]?.contentRect.width;
73
+ if (width === void 0 || Math.abs(width - lastWidth) < .5) return;
74
+ lastWidth = width;
75
+ scheduleMeasure();
76
+ });
77
+ resizeObserver.observe(element);
40
78
  }, {
41
79
  flush: "post",
42
80
  immediate: true
@@ -1,8 +1,11 @@
1
1
  import { calculatePosition, getRect, translateRTL } from "../utils/position.js";
2
2
  import { computed, onScopeDispose, shallowRef, toValue, watch } from "vue";
3
3
  //#region src/composables/use-overlay-position.ts
4
- /** Overlays sit above the page; matched to what the React build renders. */
5
- var OVERLAY_Z_INDEX = 1e5;
4
+ /**
5
+ * Overlays sit above the page. The value lives in the stylesheet so the modal-level surfaces,
6
+ * which can only declare theirs in CSS, resolve to the same level as this inline one.
7
+ */
8
+ var OVERLAY_Z_INDEX = "var(--rp-z-index-overlay)";
6
9
  /** Whether an element or one of its descendants holds focus. */
7
10
  var isFocusWithin = (element) => {
8
11
  const active = document.activeElement;
@@ -212,12 +215,12 @@ var useOverlayPosition = (options) => {
212
215
  "max-height": "100vh",
213
216
  position: "fixed",
214
217
  top: "0px",
215
- "z-index": String(OVERLAY_Z_INDEX)
218
+ "z-index": OVERLAY_Z_INDEX
216
219
  };
217
220
  const style = {
218
221
  "max-height": `${resolved.maxHeight}px`,
219
222
  position: "absolute",
220
- "z-index": String(OVERLAY_Z_INDEX)
223
+ "z-index": OVERLAY_Z_INDEX
221
224
  };
222
225
  for (const [key, value] of Object.entries(resolved.position)) style[key] = `${value}px`;
223
226
  style["--trigger-anchor-point"] = `${resolved.triggerAnchorPoint.x}px ${resolved.triggerAnchorPoint.y}px`;
@@ -61,6 +61,14 @@ export interface UseSelectStateReturn<T> extends MenuTriggerState, FormValidatio
61
61
  /** The value a form reset goes back to. */
62
62
  defaultValue: ComputedRef<SelectedValue>;
63
63
  setValue: (value: SelectedValue) => void;
64
+ /**
65
+ * Empties the selection.
66
+ *
67
+ * Not `selection.clearSelection()`: a single select sets `disallowEmptySelection`, which is
68
+ * what stops a press on the chosen option from un-choosing it — and which makes that call a
69
+ * no-op in the one mode most selects are in.
70
+ */
71
+ clearValue: () => void;
64
72
  /** The first chosen key, which is the whole value when single. */
65
73
  selectedKey: ComputedRef<CollectionKey | null>;
66
74
  selectedItems: ComputedRef<SelectedItem<T>[]>;
@@ -99,6 +99,10 @@ var useSelectState = (options) => {
99
99
  return Array.isArray(current) && current.length === 0 ? null : current;
100
100
  }
101
101
  });
102
+ const clearValue = () => {
103
+ setValue(null);
104
+ validation.commitValidation();
105
+ };
102
106
  const selection = useSelectionManager({
103
107
  collection,
104
108
  disabledBehavior: options.disabledBehavior,
@@ -132,6 +136,7 @@ var useSelectState = (options) => {
132
136
  const canOpen = () => collection.size.value !== 0 || Boolean(toValue(options.allowsEmptyCollection));
133
137
  return {
134
138
  ...validation,
139
+ clearValue,
135
140
  close: trigger.close,
136
141
  collection,
137
142
  defaultValue,
@@ -10,6 +10,13 @@ export interface UseSelectOptions {
10
10
  ariaLabelledby?: MaybeRefOrGetter<string | undefined>;
11
11
  ariaDescribedby?: MaybeRefOrGetter<string | undefined>;
12
12
  onFocusChange?: (isFocused: boolean) => void;
13
+ /**
14
+ * Whether a clear button is composed into the select, which is what opens the trigger's
15
+ * Backspace/Delete shortcut. Read at event time, so registering one never costs a render.
16
+ */
17
+ hasClearButton?: () => boolean;
18
+ /** Empties the selection. Called by the shortcut above. */
19
+ onClear?: () => void;
13
20
  }
14
21
  /** Attributes the trigger element renders, beside its own class and `data-slot`. */
15
22
  export type SelectTriggerAttributes = Record<string, string | number | boolean | undefined>;
@@ -83,6 +83,12 @@ var useSelect = (options, state) => {
83
83
  return;
84
84
  }
85
85
  }
86
+ if ((event.key === "Backspace" || event.key === "Delete") && !state.isOpen.value && options.hasClearButton?.() && state.selectedItems.value.length > 0) {
87
+ event.preventDefault();
88
+ state.clearValue();
89
+ options.onClear?.();
90
+ return;
91
+ }
86
92
  typeahead.onKeydown(event);
87
93
  if (event.defaultPrevented) return;
88
94
  trigger.responder.handlers.value.onKeydown?.(event);
@@ -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 };