@jsenv/navi 0.29.15 → 0.29.16
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/jsenv_navi.js +238 -15
- package/dist/jsenv_navi.js.map +22 -11
- package/docs/AI_INSTRUCTIONS.md +3 -0
- package/docs/z_index.md +111 -0
- package/package.json +2 -2
package/dist/jsenv_navi.js
CHANGED
|
@@ -6490,6 +6490,10 @@ const POSITION_PROPS = {
|
|
|
6490
6490
|
fixed: applyToCssPropWhenTruthy("position", "fixed", "static"),
|
|
6491
6491
|
sticky: applyToCssPropWhenTruthy("position", "sticky", "static"),
|
|
6492
6492
|
zIndex: PASS_THROUGH,
|
|
6493
|
+
// Keeps the zIndex values used inside this box local to it — see
|
|
6494
|
+
// docs/z_index.md: a z-index that opens no stacking context competes with
|
|
6495
|
+
// the whole page, fixed bars included.
|
|
6496
|
+
isolation: PASS_THROUGH,
|
|
6493
6497
|
order: PASS_THROUGH,
|
|
6494
6498
|
left: (value) => {
|
|
6495
6499
|
return { left: value === true ? 0 : value };
|
|
@@ -8905,6 +8909,20 @@ import.meta.css = [/* css */`
|
|
|
8905
8909
|
between them — and a control flush against the edge of a scrolling area
|
|
8906
8910
|
overflows it (a focus outline is drawn outside the control it belongs to)
|
|
8907
8911
|
and raises a scrollbar of its own. */
|
|
8912
|
+
/* A control sitting right against the edge of what scrolls must keep its
|
|
8913
|
+
loading outline within its own box: the outline is drawn a couple pixels
|
|
8914
|
+
outside the control (see loading_outline.jsx), and that bleed alone is
|
|
8915
|
+
enough to make the area scrollable — a scrollbar appearing and disappearing
|
|
8916
|
+
as things load. Only what the scroller directly contains is against that
|
|
8917
|
+
edge; anything nested deeper has room around it and keeps the outline it
|
|
8918
|
+
asked for, hence the child combinators. Written on the outline itself
|
|
8919
|
+
rather than on the control, because the var inherits: setting it on a
|
|
8920
|
+
container would reach every control below it, edge or not. */
|
|
8921
|
+
[data-scrollable] > .navi_loading_outline_wrapper,
|
|
8922
|
+
[data-scrollable] > * > .navi_loading_outline_wrapper {
|
|
8923
|
+
--loading-outline-min-inset: 0px;
|
|
8924
|
+
}
|
|
8925
|
+
|
|
8908
8926
|
[data-scrollable] {
|
|
8909
8927
|
overflow: var(--x-scrollable-overflow, auto);
|
|
8910
8928
|
|
|
@@ -10057,6 +10075,15 @@ const LoadingOutlineUI = props => {
|
|
|
10057
10075
|
} = props;
|
|
10058
10076
|
const shouldShowSpinner = useDebounceTrue(loading, debounce);
|
|
10059
10077
|
const rectangleRef = useRef(null);
|
|
10078
|
+
|
|
10079
|
+
// Nothing in the DOM until something actually loads: the box below is
|
|
10080
|
+
// absolutely positioned slightly outside the control, which is enough to
|
|
10081
|
+
// make an ancestor scrollable (a 1px scrollbar on a control sitting against
|
|
10082
|
+
// the edge of a scrolling area). A control that never loads must not pay for
|
|
10083
|
+
// a decoration it will never draw.
|
|
10084
|
+
if (!loading) {
|
|
10085
|
+
return children;
|
|
10086
|
+
}
|
|
10060
10087
|
let insetTop = inset + spacingTop + marginTop;
|
|
10061
10088
|
let insetRight = inset + spacingRight + marginRight;
|
|
10062
10089
|
let insetBottom = inset + spacingBottom + marginBottom;
|
|
@@ -10087,7 +10114,7 @@ const LoadingOutlineUI = props => {
|
|
|
10087
10114
|
"--loading-rectangle-bottom": `${insetBottom}px`,
|
|
10088
10115
|
"--loading-rectangle-left": `${insetLeft}px`
|
|
10089
10116
|
},
|
|
10090
|
-
children:
|
|
10117
|
+
children: jsx(LoadingIndicatorFluid, {
|
|
10091
10118
|
visuallyHidden: !shouldShowSpinner,
|
|
10092
10119
|
radius: radius,
|
|
10093
10120
|
color: color,
|
|
@@ -17794,6 +17821,104 @@ const useExecuteAction = (
|
|
|
17794
17821
|
return executeAction;
|
|
17795
17822
|
};
|
|
17796
17823
|
|
|
17824
|
+
/**
|
|
17825
|
+
* A control placed inside a region that expands on click — a `<summary>`, an
|
|
17826
|
+
* accordion header carrying `aria-expanded` — has its click read twice: once by
|
|
17827
|
+
* the control it was aimed at, once by the region around it. The second reading
|
|
17828
|
+
* is never wanted; a menu opened from a collapsed row should not also unfold the
|
|
17829
|
+
* row.
|
|
17830
|
+
*
|
|
17831
|
+
* Cancelling the click is the only way to stop the region: a `<summary>` runs
|
|
17832
|
+
* its default action after the propagation, so `stopPropagation` does not reach
|
|
17833
|
+
* it. And it can only be done once the control has taken the click for itself —
|
|
17834
|
+
* navi refuses an interaction on an already-cancelled event (see
|
|
17835
|
+
* `onRequestInteraction`), so cancelling any earlier silences the control
|
|
17836
|
+
* instead of the region.
|
|
17837
|
+
*
|
|
17838
|
+
* That moment — right after an interaction was allowed — only exists inside
|
|
17839
|
+
* navi, which is why the cancellation lives here rather than in application code.
|
|
17840
|
+
*/
|
|
17841
|
+
|
|
17842
|
+
const CLICK_TO_EXPAND_SELECTOR = "summary, [aria-expanded]";
|
|
17843
|
+
|
|
17844
|
+
/**
|
|
17845
|
+
* Cancels `event` when the control consumed a click that a surrounding
|
|
17846
|
+
* click-to-expand region would otherwise read as "unfold me".
|
|
17847
|
+
*
|
|
17848
|
+
* Does nothing when cancelling the click would also cancel what the control
|
|
17849
|
+
* itself does with it (a link navigating, a checkbox toggling): there, the two
|
|
17850
|
+
* behaviours cannot be separated and the control's own comes first.
|
|
17851
|
+
*/
|
|
17852
|
+
const preventClickToExpand = (element, event) => {
|
|
17853
|
+
if (!event || event.type !== "click") {
|
|
17854
|
+
return;
|
|
17855
|
+
}
|
|
17856
|
+
if (event.defaultPrevented) {
|
|
17857
|
+
return;
|
|
17858
|
+
}
|
|
17859
|
+
if (!clickDefaultActionIsInert(element, event)) {
|
|
17860
|
+
return;
|
|
17861
|
+
}
|
|
17862
|
+
const parentElement = element.parentElement;
|
|
17863
|
+
if (!parentElement) {
|
|
17864
|
+
return;
|
|
17865
|
+
}
|
|
17866
|
+
// From the parent: a control that opens something carries its own
|
|
17867
|
+
// `aria-expanded` and would find itself.
|
|
17868
|
+
const clickToExpandRegion = parentElement.closest(CLICK_TO_EXPAND_SELECTOR);
|
|
17869
|
+
if (!clickToExpandRegion) {
|
|
17870
|
+
return;
|
|
17871
|
+
}
|
|
17872
|
+
event.preventDefault();
|
|
17873
|
+
};
|
|
17874
|
+
|
|
17875
|
+
const clickDefaultActionIsInert = (element, event) => {
|
|
17876
|
+
if (!isInertOnClick(element)) {
|
|
17877
|
+
return false;
|
|
17878
|
+
}
|
|
17879
|
+
// The activation belongs to what was clicked, which can be deeper than the
|
|
17880
|
+
// control host (a button inside it) or above it (a label wrapping it).
|
|
17881
|
+
const { target } = event;
|
|
17882
|
+
if (target && target !== element && target.nodeType === 1) {
|
|
17883
|
+
let ancestor = target;
|
|
17884
|
+
while (ancestor) {
|
|
17885
|
+
if (!isInertOnClick(ancestor)) {
|
|
17886
|
+
return false;
|
|
17887
|
+
}
|
|
17888
|
+
ancestor = ancestor.parentElement;
|
|
17889
|
+
}
|
|
17890
|
+
}
|
|
17891
|
+
return true;
|
|
17892
|
+
};
|
|
17893
|
+
|
|
17894
|
+
const NON_INERT_INPUT_TYPE_SET = new Set([
|
|
17895
|
+
"checkbox",
|
|
17896
|
+
"radio",
|
|
17897
|
+
"submit",
|
|
17898
|
+
"reset",
|
|
17899
|
+
"image",
|
|
17900
|
+
"file",
|
|
17901
|
+
]);
|
|
17902
|
+
|
|
17903
|
+
const isInertOnClick = (element) => {
|
|
17904
|
+
const { tagName } = element;
|
|
17905
|
+
if (tagName === "A" || tagName === "AREA") {
|
|
17906
|
+
return !element.hasAttribute("href");
|
|
17907
|
+
}
|
|
17908
|
+
if (tagName === "LABEL") {
|
|
17909
|
+
// A label forwards the click to its control, whose activation would be
|
|
17910
|
+
// cancelled along with the click.
|
|
17911
|
+
return false;
|
|
17912
|
+
}
|
|
17913
|
+
if (tagName === "INPUT") {
|
|
17914
|
+
return !NON_INERT_INPUT_TYPE_SET.has(element.type);
|
|
17915
|
+
}
|
|
17916
|
+
if (tagName === "BUTTON") {
|
|
17917
|
+
return element.type === "button";
|
|
17918
|
+
}
|
|
17919
|
+
return true;
|
|
17920
|
+
};
|
|
17921
|
+
|
|
17797
17922
|
const BUSY_CONSTRAINT = {
|
|
17798
17923
|
name: "busy",
|
|
17799
17924
|
messageAttribute: "data-busy-message",
|
|
@@ -18162,6 +18287,9 @@ const onRequestInteraction = (
|
|
|
18162
18287
|
debugInteraction(event, `"${name}" allowed`);
|
|
18163
18288
|
allowed?.();
|
|
18164
18289
|
always?.();
|
|
18290
|
+
// The click served this control; it must not serve a second time whatever
|
|
18291
|
+
// unfolds around it (see click_to_expand.js).
|
|
18292
|
+
preventClickToExpand(controlHost, event);
|
|
18165
18293
|
return true;
|
|
18166
18294
|
};
|
|
18167
18295
|
|
|
@@ -25732,8 +25860,8 @@ const css$T = /* css */`
|
|
|
25732
25860
|
* the screen and a centered box ends up both cramped and out of thumb
|
|
25733
25861
|
* reach, while under a mouse the centered box is already the right shape —
|
|
25734
25862
|
* hence a prop that only ever does something on touch. It supplies defaults
|
|
25735
|
-
* for `positionArea`, `marginWithContainer` and `
|
|
25736
|
-
*
|
|
25863
|
+
* for `positionArea`, `marginWithContainer`, `expandX` and `scrollCapture`,
|
|
25864
|
+
* so any of them can still be pinned explicitly. Keyed off `(pointer: coarse)` (the
|
|
25737
25865
|
* input device, not a width breakpoint — a narrow desktop window is still a
|
|
25738
25866
|
* mouse) via `coarsePointerSignal`, so it re-resolves live.
|
|
25739
25867
|
* @param {string} [props.positionArea="center"] - Where to dock the dialog
|
|
@@ -25770,7 +25898,7 @@ const css$T = /* css */`
|
|
|
25770
25898
|
* A `layer="local"` dialog always locks its own positioned ancestor's
|
|
25771
25899
|
* scroll while open (its backdrop only covers the scrollport, so scrolling
|
|
25772
25900
|
* there would reveal uncovered content); this prop extends the lock to the
|
|
25773
|
-
* whole page.
|
|
25901
|
+
* whole page. Defaults to `true` for a dialog docked by `dockedOnTouch`.
|
|
25774
25902
|
* @param {boolean|"auto"|"fading"|"scaling"|"sliding"|`slide-from-${string}`} [props.animation]
|
|
25775
25903
|
* - `true`/`"auto"` resolves to `"scaling"` for a centered `positionArea`,
|
|
25776
25904
|
* or a concrete `"slide-from-*"` direction otherwise. Any other explicit
|
|
@@ -25939,7 +26067,11 @@ const DialogLocal = props => {
|
|
|
25939
26067
|
const DOCKED = {
|
|
25940
26068
|
positionArea: "bottom",
|
|
25941
26069
|
marginWithContainer: 0,
|
|
25942
|
-
expandX: true
|
|
26070
|
+
expandX: true,
|
|
26071
|
+
// A sheet resting on the bottom edge is dragged with a thumb, and a drag that
|
|
26072
|
+
// runs past its own edge must not land on the page behind it: the same
|
|
26073
|
+
// reasoning as "bottom" above, applied to the gesture instead of the shape.
|
|
26074
|
+
scrollCapture: true
|
|
25943
26075
|
};
|
|
25944
26076
|
|
|
25945
26077
|
// The first control inside `dialogEl` that is mid-action, if any. Walks the
|
|
@@ -25985,7 +26117,7 @@ const useDialogProps = props => {
|
|
|
25985
26117
|
// there's no native inert-ing, so the real backdrop below is what
|
|
25986
26118
|
// actually makes "capture"/"none" behave the same way here too.
|
|
25987
26119
|
pointerInteractionOutsideEffect = "close",
|
|
25988
|
-
scrollCapture,
|
|
26120
|
+
scrollCapture: scrollCaptureProp,
|
|
25989
26121
|
animation,
|
|
25990
26122
|
// Only ever affects --anchor-width/--anchor-height (see this file's top
|
|
25991
26123
|
// comment) — Dialog's own positioning is never relative to it.
|
|
@@ -26020,6 +26152,7 @@ const useDialogProps = props => {
|
|
|
26020
26152
|
const expandXUnset = expand === undefined && expandXProp === undefined;
|
|
26021
26153
|
const expandX = expandXUnset ? isDocked && DOCKED.expandX : Boolean(expand) || Boolean(expandXProp);
|
|
26022
26154
|
const expandY = Boolean(expand) || Boolean(expandYProp);
|
|
26155
|
+
const scrollCapture = scrollCaptureProp ?? (isDocked ? DOCKED.scrollCapture : false);
|
|
26023
26156
|
const backdropRef = useRef();
|
|
26024
26157
|
// Disarms a still-pending backdrop hide from a previous close (see
|
|
26025
26158
|
// armPointerDownOutsideClose below) — same pattern as popover.jsx's own.
|
|
@@ -26579,8 +26712,13 @@ const DIALOG_PSEUDO_CLASSES = [":hover", ":active", ":focus", ":focus-visible",
|
|
|
26579
26712
|
|
|
26580
26713
|
// Lets consumers pass animationDuration="0.5s" as a regular prop; Box maps
|
|
26581
26714
|
// it to the CSS var for us (see box.jsx's styleCSSVars handling).
|
|
26715
|
+
// borderRadius goes through --dialog-border-radius rather than the
|
|
26716
|
+
// border-radius property itself so the flush-corner rules above (a plain
|
|
26717
|
+
// stylesheet) can still square the corners that land on the container's own —
|
|
26718
|
+
// an inline border-radius would outrank them.
|
|
26582
26719
|
const DIALOG_STYLE_CSS_VARS = {
|
|
26583
26720
|
animationDuration: "--popup-animation-duration",
|
|
26721
|
+
borderRadius: "--dialog-border-radius",
|
|
26584
26722
|
minWidth: "--dialog-min-width",
|
|
26585
26723
|
maxWidth: "--dialog-max-width",
|
|
26586
26724
|
minHeight: "--dialog-min-height",
|
|
@@ -31540,12 +31678,30 @@ const useActionAsyncData = (action, {
|
|
|
31540
31678
|
const runningState = action.runningStateSignal.peek();
|
|
31541
31679
|
const [, setTick] = useState(0);
|
|
31542
31680
|
useEffect(() => {
|
|
31543
|
-
|
|
31681
|
+
const unsubscribeFromRunningState = action.runningStateSignal.subscribe(state => {
|
|
31544
31682
|
if (state === RUNNING) {
|
|
31545
31683
|
dismissedActionWeakSet.delete(action);
|
|
31546
31684
|
}
|
|
31547
31685
|
setTick(n => n + 1);
|
|
31548
31686
|
});
|
|
31687
|
+
// The data does not come from this action's runs alone: dataSignal is a
|
|
31688
|
+
// computed over the resource store, so an other action writing that store
|
|
31689
|
+
// (a PUT upserting an item that a GET_MANY list already holds) changes the
|
|
31690
|
+
// data while this action stays COMPLETED. Subscribing here re-renders
|
|
31691
|
+
// through the same controlled path as the run state, instead of `.value`.
|
|
31692
|
+
let dataNotificationIsInitial = true;
|
|
31693
|
+
const unsubscribeFromData = action.dataSignal.subscribe(() => {
|
|
31694
|
+
if (dataNotificationIsInitial) {
|
|
31695
|
+
// subscribe() calls back synchronously with the current value
|
|
31696
|
+
dataNotificationIsInitial = false;
|
|
31697
|
+
return;
|
|
31698
|
+
}
|
|
31699
|
+
setTick(n => n + 1);
|
|
31700
|
+
});
|
|
31701
|
+
return () => {
|
|
31702
|
+
unsubscribeFromRunningState();
|
|
31703
|
+
unsubscribeFromData();
|
|
31704
|
+
};
|
|
31549
31705
|
}, []);
|
|
31550
31706
|
if (runningState === COMPLETED) {
|
|
31551
31707
|
return [action.dataSignal.peek(), false, undefined];
|
|
@@ -48009,13 +48165,10 @@ const css$u = /* css */`
|
|
|
48009
48165
|
font-size: 1em;
|
|
48010
48166
|
line-height: 1.4;
|
|
48011
48167
|
}
|
|
48012
|
-
/*
|
|
48013
|
-
|
|
48014
|
-
|
|
48015
|
-
|
|
48016
|
-
and disappear as things load. Targeted on the outline itself rather than
|
|
48017
|
-
inherited from the item, so a control nested deeper (which has room around
|
|
48018
|
-
it, and does not reach the edges) keeps the outline it asked for. */
|
|
48168
|
+
/* Same rule as [data-scrollable] in box.jsx, said again for this scroller:
|
|
48169
|
+
what an item holds IS against the edge of the scroll container — the list
|
|
48170
|
+
element between the two is markup, not spacing — so its loading outline
|
|
48171
|
+
stays inside its own box rather than raising a scrollbar. */
|
|
48019
48172
|
.navi_list_item > .navi_loading_outline_wrapper,
|
|
48020
48173
|
.navi_list_item > * > .navi_loading_outline_wrapper,
|
|
48021
48174
|
.navi_list_item_header > * > .navi_loading_outline_wrapper,
|
|
@@ -53769,6 +53922,18 @@ const css$n = /* css */`
|
|
|
53769
53922
|
/* The control grows itself; resizable below hands the handle back. */
|
|
53770
53923
|
resize: none;
|
|
53771
53924
|
overflow: auto;
|
|
53925
|
+
/* A placeholder must be readable in full before anything is typed: a
|
|
53926
|
+
field that opens already scrolled reads as a field that already has
|
|
53927
|
+
text in it. Its wrapped height is measured (see usePlaceholderHeight)
|
|
53928
|
+
because it only exists once laid out, and it only raises the floor
|
|
53929
|
+
while the placeholder is what is being shown — what is typed sizes the
|
|
53930
|
+
box on its own. */
|
|
53931
|
+
&:placeholder-shown {
|
|
53932
|
+
min-height: max(
|
|
53933
|
+
calc(var(--textarea-min-rows, 1.5) * 1lh),
|
|
53934
|
+
var(--x-textarea-placeholder-height, 0px)
|
|
53935
|
+
);
|
|
53936
|
+
}
|
|
53772
53937
|
}
|
|
53773
53938
|
&[data-resizable] .navi_control_input {
|
|
53774
53939
|
height: calc(var(--textarea-min-rows, 1.5) * 1lh);
|
|
@@ -53806,7 +53971,9 @@ const css$n = /* css */`
|
|
|
53806
53971
|
* @param {number} [maxRows] Lines after which the control stops growing and
|
|
53807
53972
|
* scrolls instead. Without it the control grows with its content.
|
|
53808
53973
|
* @param {boolean} [resizable] Give the browser's vertical resize handle back.
|
|
53809
|
-
*
|
|
53974
|
+
* An exchange, not an addition: the hand takes over from the automatic
|
|
53975
|
+
* growth, so the control stops following what is typed and stays at the
|
|
53976
|
+
* height it was last dragged to (starting at `minRows`).
|
|
53810
53977
|
* @param {number} [maxLength] The character limit, validated at submit. Pair
|
|
53811
53978
|
* with `maxLengthGuard` to block typing past it, and render a
|
|
53812
53979
|
* TextareaCharCount to show it.
|
|
@@ -53828,6 +53995,7 @@ const Textarea = ({
|
|
|
53828
53995
|
import.meta.css = [inputCss + css$n, "@jsenv/navi/src/control/input/textarea.jsx"];
|
|
53829
53996
|
const defaultRef = useRef(null);
|
|
53830
53997
|
props.ref = props.ref || defaultRef;
|
|
53998
|
+
usePlaceholderHeight(props.ref, props.placeholder);
|
|
53831
53999
|
const [rootProps, hostProps, childrenWrapperProps] = useControlProps(props, {
|
|
53832
54000
|
controlType: "input"
|
|
53833
54001
|
});
|
|
@@ -53908,6 +54076,61 @@ const TextareaCharCount = ({
|
|
|
53908
54076
|
children: maxLength === undefined ? length : `${length}/${maxLength}`
|
|
53909
54077
|
});
|
|
53910
54078
|
};
|
|
54079
|
+
|
|
54080
|
+
// `field-sizing: content` sizes the box from the value, and an empty field has
|
|
54081
|
+
// none — the placeholder is text the browser refuses to make room for. So the
|
|
54082
|
+
// height it wraps to is measured and published as --x-textarea-placeholder-height
|
|
54083
|
+
// for the CSS above to use as a floor.
|
|
54084
|
+
const usePlaceholderHeight = (ref, placeholder) => {
|
|
54085
|
+
useLayoutEffect(() => {
|
|
54086
|
+
const textareaEl = ref.current;
|
|
54087
|
+
if (!placeholder) {
|
|
54088
|
+
textareaEl.style.removeProperty("--x-textarea-placeholder-height");
|
|
54089
|
+
return null;
|
|
54090
|
+
}
|
|
54091
|
+
let widthMeasured;
|
|
54092
|
+
const measure = () => {
|
|
54093
|
+
// What is typed sizes the box itself; the placeholder is not displayed
|
|
54094
|
+
// then, and scrollHeight would report the value's height instead.
|
|
54095
|
+
if (textareaEl.value !== "") {
|
|
54096
|
+
return;
|
|
54097
|
+
}
|
|
54098
|
+
const {
|
|
54099
|
+
paddingTop,
|
|
54100
|
+
paddingBottom
|
|
54101
|
+
} = getComputedStyle(textareaEl);
|
|
54102
|
+
// Cleared before reading: scrollHeight can never report less than the
|
|
54103
|
+
// height already applied, so measuring on top of a previous measure could
|
|
54104
|
+
// only ever grow the box, never let it shrink back on a wider viewport.
|
|
54105
|
+
textareaEl.style.setProperty("--x-textarea-placeholder-height", "0px");
|
|
54106
|
+
const contentHeight = textareaEl.scrollHeight - parseFloat(paddingTop) - parseFloat(paddingBottom);
|
|
54107
|
+
widthMeasured = textareaEl.clientWidth;
|
|
54108
|
+
textareaEl.style.setProperty("--x-textarea-placeholder-height", `${contentHeight}px`);
|
|
54109
|
+
};
|
|
54110
|
+
measure();
|
|
54111
|
+
// The placeholder wraps against the available width, so a new width is a
|
|
54112
|
+
// new number of lines. Height changes are ignored: this measure is what
|
|
54113
|
+
// causes them, and reacting to them would be reacting to ourselves.
|
|
54114
|
+
const resizeObserver = new ResizeObserver(() => {
|
|
54115
|
+
if (textareaEl.clientWidth !== widthMeasured) {
|
|
54116
|
+
measure();
|
|
54117
|
+
}
|
|
54118
|
+
});
|
|
54119
|
+
resizeObserver.observe(textareaEl);
|
|
54120
|
+
// The width may have changed while the field held a value, when measuring
|
|
54121
|
+
// was impossible — emptying it is when the placeholder comes back.
|
|
54122
|
+
const onInput = () => {
|
|
54123
|
+
if (textareaEl.value === "") {
|
|
54124
|
+
measure();
|
|
54125
|
+
}
|
|
54126
|
+
};
|
|
54127
|
+
textareaEl.addEventListener("input", onInput);
|
|
54128
|
+
return () => {
|
|
54129
|
+
resizeObserver.disconnect();
|
|
54130
|
+
textareaEl.removeEventListener("input", onInput);
|
|
54131
|
+
};
|
|
54132
|
+
}, [placeholder]);
|
|
54133
|
+
};
|
|
53911
54134
|
const RealTextarea = ({
|
|
53912
54135
|
maxLength,
|
|
53913
54136
|
...domProps
|