@marimo-team/frontend 0.23.10-dev37 → 0.23.10-dev41
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/assets/{ImperativeModal-CDPfRIx5.js → ImperativeModal-BW1dmntO.js} +1 -1
- package/dist/assets/{JsonOutput-BemZ015x.js → JsonOutput-DXzjDImT.js} +2 -2
- package/dist/assets/{MarimoErrorOutput-BQyzDTet.js → MarimoErrorOutput-DIx3jD4Z.js} +1 -1
- package/dist/assets/{RSPContexts-QoVyPvhN.js → RSPContexts-C3vBe8nL.js} +1 -1
- package/dist/assets/{RunButton-ACHY83op.js → RunButton-DLMH3KLF.js} +1 -1
- package/dist/assets/{add-cell-with-ai-C1gPcb_Q.js → add-cell-with-ai-CeYsuinK.js} +1 -1
- package/dist/assets/{add-connection-dialog-ZjsgZSOV.js → add-connection-dialog-CTTPp4DA.js} +1 -1
- package/dist/assets/{agent-panel-CEJFK1Jm.js → agent-panel-CQcFMptO.js} +1 -1
- package/dist/assets/{ai-model-dropdown-BhCUiMlB.js → ai-model-dropdown-CXNYq3J_.js} +1 -1
- package/dist/assets/{app-config-button-CJlCOCbq.js → app-config-button-BI_m1lzO.js} +1 -1
- package/dist/assets/{cache-panel-Cs308ish.js → cache-panel-C2wbL8-g.js} +1 -1
- package/dist/assets/{cell-editor-DbvkP4j5.js → cell-editor-CdlAPM5y.js} +1 -1
- package/dist/assets/{cell-link-BFEa98Sc.js → cell-link-spbX-mlL.js} +1 -1
- package/dist/assets/{chat-display-3RqKe-PZ.js → chat-display-CiAnYN2V.js} +1 -1
- package/dist/assets/{chat-panel-ChWIPBm1.js → chat-panel-CWJ3edyp.js} +1 -1
- package/dist/assets/{chat-ui-BX8NORpM.js → chat-ui-BrpoAGED.js} +1 -1
- package/dist/assets/{column-preview-DYVirisk.js → column-preview-cHpj1JI3.js} +1 -1
- package/dist/assets/{command-BkXWJLBP.js → command-BmOGG6-2.js} +1 -1
- package/dist/assets/{command-palette-Vkj8jbRj.js → command-palette-BF3Tp6Qs.js} +1 -1
- package/dist/assets/{common-C9lH0PD9.js → common-Zqa_rd3u.js} +1 -1
- package/dist/assets/{dependency-graph-panel-gmv5dXTu.js → dependency-graph-panel-1c-_VkGM.js} +1 -1
- package/dist/assets/{edit-page-BrUsc_AX.js → edit-page-CWYAjQfm.js} +4 -4
- package/dist/assets/{error-panel-lbt2yT6e.js → error-panel-DmfYCEYR.js} +1 -1
- package/dist/assets/{field-BWxnw0_c.js → field-DCdxnSKm.js} +1 -1
- package/dist/assets/{file-explorer-panel-CgALaLLS.js → file-explorer-panel-CITmIApE.js} +1 -1
- package/dist/assets/{file-name-input-B-pk2GlG.js → file-name-input-D-E8LtaP.js} +1 -1
- package/dist/assets/{form-BWJXHKy1.js → form-BUVEy6A8.js} +1 -1
- package/dist/assets/formatting-DuH2AQgo.js +1 -0
- package/dist/assets/{gallery-page-L--A5Tic.js → gallery-page-iZXf43Df.js} +1 -1
- package/dist/assets/{glide-data-editor-C_ammsJR.js → glide-data-editor-BbI6JGpY.js} +1 -1
- package/dist/assets/{home-page-VA-VAYME.js → home-page-CVKp7bjJ.js} +1 -1
- package/dist/assets/{hooks-ClbTcW-s.js → hooks-B4MwgGVt.js} +1 -1
- package/dist/assets/index-DB64hzBp.css +2 -0
- package/dist/assets/index-DukQCuHG.js +38 -0
- package/dist/assets/input-DA8xlX70.js +1 -0
- package/dist/assets/{layout-Di_1u3yT.js → layout-By2E0DxG.js} +3 -3
- package/dist/assets/{logs-panel-c4pWZhi-.js → logs-panel-KFtLFK9O.js} +1 -1
- package/dist/assets/{name-cell-input-mVvyyh5P.js → name-cell-input-DyUkpM9d.js} +1 -1
- package/dist/assets/numbers-DGxQfQ-A.js +1 -0
- package/dist/assets/{packages-panel-BspJRth2.js → packages-panel-Bt88BnuF.js} +1 -1
- package/dist/assets/{panels-C2Lu-9Am.js → panels-BMVwv9bb.js} +1 -1
- package/dist/assets/{reveal-component-D69mChLl.js → reveal-component-UAqGa1wV.js} +1 -1
- package/dist/assets/{run-page-sD-tVkQP.js → run-page-Cmzv9g5Z.js} +1 -1
- package/dist/assets/{scratchpad-panel-C-uK6ogD.js → scratchpad-panel-C5mnecjj.js} +1 -1
- package/dist/assets/{secrets-panel-BMKccJMu.js → secrets-panel-BJEb16JJ.js} +1 -1
- package/dist/assets/{session-panel-DqF_iSx7.js → session-panel-DJxyhD5Y.js} +1 -1
- package/dist/assets/{snippets-panel-CCXy7OX0.js → snippets-panel-BGtSLFha.js} +1 -1
- package/dist/assets/{tracing-DhjGEWqy.js → tracing-DNuuRjp_.js} +1 -1
- package/dist/assets/{tracing-panel-Dyt4cvQF.js → tracing-panel-CnlPrcNd.js} +2 -2
- package/dist/assets/{useCellActionButton-DH4naKvY.js → useCellActionButton-DNDYDKvS.js} +1 -1
- package/dist/assets/{useDependencyPanelTab-DI6IO_9Y.js → useDependencyPanelTab-Bu76tcmV.js} +1 -1
- package/dist/assets/{useNotebookActions-DNIvhWif.js → useNotebookActions-CMYQ5bMR.js} +1 -1
- package/dist/assets/{write-secret-modal-CwsDn7jz.js → write-secret-modal-DM7t6LNp.js} +1 -1
- package/dist/index.html +13 -13
- package/package.json +1 -1
- package/src/components/ui/number-field.tsx +5 -1
- package/src/plugins/impl/SliderPlugin.tsx +132 -18
- package/src/plugins/impl/__tests__/SliderPlugin.test.tsx +278 -8
- package/src/utils/__tests__/numbers.test.ts +20 -0
- package/src/utils/numbers.ts +27 -0
- package/dist/assets/formatting-30FX-rC-.js +0 -1
- package/dist/assets/index-BEUIgFV-.js +0 -38
- package/dist/assets/index-Ct3LgsQ8.css +0 -2
- package/dist/assets/input-DckzEovC.js +0 -1
- package/dist/assets/numbers-C3BAdHZE.js +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
import{s as V}from"./chunk-LvLJmgfZ.js";import{t as B}from"./react-Bj1aDYRI.js";import{t as G}from"./compiler-runtime-B3qBwwSJ.js";import{t as H}from"./jsx-runtime-BqBOg78p.js";import{c as I,i as J,n as O,s as Q,t as U}from"./select-6suE6YM2.js";import{t as P}from"./label-C2_YQRio.js";import{t as W}from"./button-BbCh-29a.js";import{r as X}from"./requests-DIwGYs0l.js";import{r as D}from"./input-
|
|
1
|
+
import{s as V}from"./chunk-LvLJmgfZ.js";import{t as B}from"./react-Bj1aDYRI.js";import{t as G}from"./compiler-runtime-B3qBwwSJ.js";import{t as H}from"./jsx-runtime-BqBOg78p.js";import{c as I,i as J,n as O,s as Q,t as U}from"./select-6suE6YM2.js";import{t as P}from"./label-C2_YQRio.js";import{t as W}from"./button-BbCh-29a.js";import{r as X}from"./requests-DIwGYs0l.js";import{r as D}from"./input-DA8xlX70.js";import{t as E}from"./use-toast-CJ1Tj2eC.js";import{a as Z,c as $,i as ee,n as te,r as re}from"./dialog-DRnNAWYl.js";import{t as ae}from"./links-D1JoyKTt.js";import{r as L}from"./field-DCdxnSKm.js";var oe=G(),T=V(B(),1),t=V(H(),1);function ne(a){return a.toSorted((e,r)=>e.provider==="env"?1:r.provider==="env"?-1:0)}const le=a=>{let e=(0,oe.c)(43),{providerNames:r,onClose:A,onSuccess:F}=a,{writeSecret:Y}=X(),[o,M]=T.useState(""),[l,R]=T.useState(""),[n,z]=T.useState(r[0]),g;e[0]!==o||e[1]!==n||e[2]!==F||e[3]!==l||e[4]!==Y?(g=async i=>{if(i.preventDefault(),!n){E({title:"Error",description:"No location selected for the secret.",variant:"danger"});return}if(!o||!l||!n){E({title:"Error",description:"Please fill in all fields.",variant:"danger"});return}try{await Y({key:o,value:l,provider:"dotenv",name:n}),E({title:"Secret created",description:"The secret has been created successfully."}),F(o)}catch{E({title:"Error",description:"Failed to create secret. Please try again.",variant:"danger"})}},e[0]=o,e[1]=n,e[2]=F,e[3]=l,e[4]=Y,e[5]=g):g=e[5];let q=g,j;e[6]===Symbol.for("react.memo_cache_sentinel")?(j=(0,t.jsxs)(Z,{children:[(0,t.jsx)($,{children:"Add Secret"}),(0,t.jsx)(re,{children:"Add a new secret to your environment variables."})]}),e[6]=j):j=e[6];let y;e[7]===Symbol.for("react.memo_cache_sentinel")?(y=(0,t.jsx)(P,{htmlFor:"key",children:"Key"}),e[7]=y):y=e[7];let S;e[8]===Symbol.for("react.memo_cache_sentinel")?(S=i=>{M(ie(i.target.value))},e[8]=S):S=e[8];let s;e[9]===o?s=e[10]:(s=(0,t.jsxs)("div",{className:"grid gap-2",children:[y,(0,t.jsx)(D,{id:"key",value:o,onChange:S,placeholder:"MY_SECRET_KEY",required:!0})]}),e[9]=o,e[10]=s);let _;e[11]===Symbol.for("react.memo_cache_sentinel")?(_=(0,t.jsx)(P,{htmlFor:"value",children:"Value"}),e[11]=_):_=e[11];let b;e[12]===Symbol.for("react.memo_cache_sentinel")?(b=i=>R(i.target.value),e[12]=b):b=e[12];let c;e[13]===l?c=e[14]:(c=(0,t.jsx)(D,{id:"value",type:"password",value:l,onChange:b,required:!0,autoComplete:"off"}),e[13]=l,e[14]=c);let C;e[15]===Symbol.for("react.memo_cache_sentinel")?(C=se()&&(0,t.jsx)(L,{children:"Note: You are sending this key over http."}),e[15]=C):C=e[15];let d;e[16]===c?d=e[17]:(d=(0,t.jsxs)("div",{className:"grid gap-2",children:[_,c,C]}),e[16]=c,e[17]=d);let N;e[18]===Symbol.for("react.memo_cache_sentinel")?(N=(0,t.jsx)(P,{htmlFor:"location",children:"Location"}),e[18]=N):N=e[18];let m;e[19]===r.length?m=e[20]:(m=r.length===0&&(0,t.jsx)("p",{className:"text-sm text-muted-foreground",children:"No dotenv locations configured."}),e[19]=r.length,e[20]=m);let h;e[21]!==n||e[22]!==r?(h=r.length>0&&(0,t.jsxs)(U,{value:n,onValueChange:i=>z(i),children:[(0,t.jsx)(Q,{children:(0,t.jsx)(I,{placeholder:"Select a provider"})}),(0,t.jsx)(O,{children:r.map(ce)})]}),e[21]=n,e[22]=r,e[23]=h):h=e[23];let k;e[24]===Symbol.for("react.memo_cache_sentinel")?(k=(0,t.jsxs)(L,{children:["You can configure the location by setting the"," ",(0,t.jsx)(ae,{href:"https://links.marimo.app/dotenv",children:"dotenv configuration"}),"."]}),e[24]=k):k=e[24];let p;e[25]!==m||e[26]!==h?(p=(0,t.jsxs)("div",{className:"grid gap-2",children:[N,m,h,k]}),e[25]=m,e[26]=h,e[27]=p):p=e[27];let u;e[28]!==d||e[29]!==p||e[30]!==s?(u=(0,t.jsxs)("div",{className:"grid gap-4 py-4",children:[s,d,p]}),e[28]=d,e[29]=p,e[30]=s,e[31]=u):u=e[31];let f;e[32]===A?f=e[33]:(f=(0,t.jsx)(W,{type:"button",variant:"outline",onClick:A,children:"Cancel"}),e[32]=A,e[33]=f);let K=!o||!l||!n,v;e[34]===K?v=e[35]:(v=(0,t.jsx)(W,{type:"submit",disabled:K,children:"Add Secret"}),e[34]=K,e[35]=v);let x;e[36]!==f||e[37]!==v?(x=(0,t.jsxs)(ee,{children:[f,v]}),e[36]=f,e[37]=v,e[38]=x):x=e[38];let w;return e[39]!==q||e[40]!==u||e[41]!==x?(w=(0,t.jsx)(te,{children:(0,t.jsxs)("form",{onSubmit:q,children:[j,u,x]})}),e[39]=q,e[40]=u,e[41]=x,e[42]=w):w=e[42],w};function ie(a){return a.replaceAll(/\W/g,"_")}function se(){return window.location.href.startsWith("http://")}function ce(a){return(0,t.jsx)(J,{value:a,children:a},a)}export{ne as n,le as t};
|
package/dist/index.html
CHANGED
|
@@ -66,7 +66,7 @@
|
|
|
66
66
|
<marimo-server-token data-token="{{ server_token }}" hidden></marimo-server-token>
|
|
67
67
|
<!-- /TODO -->
|
|
68
68
|
<title>{{ title }}</title>
|
|
69
|
-
<script type="module" crossorigin src="./assets/index-
|
|
69
|
+
<script type="module" crossorigin src="./assets/index-DukQCuHG.js"></script>
|
|
70
70
|
<link rel="modulepreload" crossorigin href="./assets/preload-helper-BPPi7vOr.js">
|
|
71
71
|
<link rel="modulepreload" crossorigin href="./assets/chunk-LvLJmgfZ.js">
|
|
72
72
|
<link rel="modulepreload" crossorigin href="./assets/react-Bj1aDYRI.js">
|
|
@@ -140,14 +140,14 @@
|
|
|
140
140
|
<link rel="modulepreload" crossorigin href="./assets/alert-dialog-5BGIfskO.js">
|
|
141
141
|
<link rel="modulepreload" crossorigin href="./assets/dialog-DRnNAWYl.js">
|
|
142
142
|
<link rel="modulepreload" crossorigin href="./assets/useDebounce-7xZwVnnu.js">
|
|
143
|
-
<link rel="modulepreload" crossorigin href="./assets/numbers-
|
|
143
|
+
<link rel="modulepreload" crossorigin href="./assets/numbers-DGxQfQ-A.js">
|
|
144
144
|
<link rel="modulepreload" crossorigin href="./assets/SSRProvider-CkFAAHXw.js">
|
|
145
145
|
<link rel="modulepreload" crossorigin href="./assets/context-QkTujrKn.js">
|
|
146
146
|
<link rel="modulepreload" crossorigin href="./assets/useNumberFormatter-ByUv-u9o.js">
|
|
147
147
|
<link rel="modulepreload" crossorigin href="./assets/usePress-DQ_tAz5W.js">
|
|
148
|
-
<link rel="modulepreload" crossorigin href="./assets/input-
|
|
149
|
-
<link rel="modulepreload" crossorigin href="./assets/ImperativeModal-
|
|
150
|
-
<link rel="modulepreload" crossorigin href="./assets/cell-link-
|
|
148
|
+
<link rel="modulepreload" crossorigin href="./assets/input-DA8xlX70.js">
|
|
149
|
+
<link rel="modulepreload" crossorigin href="./assets/ImperativeModal-BW1dmntO.js">
|
|
150
|
+
<link rel="modulepreload" crossorigin href="./assets/cell-link-spbX-mlL.js">
|
|
151
151
|
<link rel="modulepreload" crossorigin href="./assets/multi-map-C4OANz8X.js">
|
|
152
152
|
<link rel="modulepreload" crossorigin href="./assets/alert-yTS3WpF4.js">
|
|
153
153
|
<link rel="modulepreload" crossorigin href="./assets/chevron-right-CG5QYXYk.js">
|
|
@@ -162,7 +162,7 @@
|
|
|
162
162
|
<link rel="modulepreload" crossorigin href="./assets/state-C9ggN1rs.js">
|
|
163
163
|
<link rel="modulepreload" crossorigin href="./assets/package-Tv6ztuzw.js">
|
|
164
164
|
<link rel="modulepreload" crossorigin href="./assets/sparkles-lWUAsPhp.js">
|
|
165
|
-
<link rel="modulepreload" crossorigin href="./assets/MarimoErrorOutput-
|
|
165
|
+
<link rel="modulepreload" crossorigin href="./assets/MarimoErrorOutput-DIx3jD4Z.js">
|
|
166
166
|
<link rel="modulepreload" crossorigin href="./assets/eye-off-Cm6DdkeR.js">
|
|
167
167
|
<link rel="modulepreload" crossorigin href="./assets/refresh-cw-DHwG4Mac.js">
|
|
168
168
|
<link rel="modulepreload" crossorigin href="./assets/tree-actions-XvgFV23w.js">
|
|
@@ -174,8 +174,8 @@
|
|
|
174
174
|
<link rel="modulepreload" crossorigin href="./assets/micromark-factory-space-BygYYKhs.js">
|
|
175
175
|
<link rel="modulepreload" crossorigin href="./assets/chunk-5FQGJX7Z-Bp9g5nD2.js">
|
|
176
176
|
<link rel="modulepreload" crossorigin href="./assets/markdown-renderer-BwjWZESf.js">
|
|
177
|
-
<link rel="modulepreload" crossorigin href="./assets/command-
|
|
178
|
-
<link rel="modulepreload" crossorigin href="./assets/field-
|
|
177
|
+
<link rel="modulepreload" crossorigin href="./assets/command-BmOGG6-2.js">
|
|
178
|
+
<link rel="modulepreload" crossorigin href="./assets/field-DCdxnSKm.js">
|
|
179
179
|
<link rel="modulepreload" crossorigin href="./assets/popover-CNCYAwbC.js">
|
|
180
180
|
<link rel="modulepreload" crossorigin href="./assets/errors-D3F5g1xA.js">
|
|
181
181
|
<link rel="modulepreload" crossorigin href="./assets/download-BS31LU22.js">
|
|
@@ -190,7 +190,7 @@
|
|
|
190
190
|
<link rel="modulepreload" crossorigin href="./assets/emotion-is-prop-valid.esm-C59xfSYt.js">
|
|
191
191
|
<link rel="modulepreload" crossorigin href="./assets/useDateFormatter-ICIDGgHf.js">
|
|
192
192
|
<link rel="modulepreload" crossorigin href="./assets/react-icons.esm-BNzu6e7h.js">
|
|
193
|
-
<link rel="modulepreload" crossorigin href="./assets/RSPContexts-
|
|
193
|
+
<link rel="modulepreload" crossorigin href="./assets/RSPContexts-C3vBe8nL.js">
|
|
194
194
|
<link rel="modulepreload" crossorigin href="./assets/table-6NxtGaCm.js">
|
|
195
195
|
<link rel="modulepreload" crossorigin href="./assets/ellipsis-KAhMaCIV.js">
|
|
196
196
|
<link rel="modulepreload" crossorigin href="./assets/external-link-BTNxSavU.js">
|
|
@@ -198,11 +198,11 @@
|
|
|
198
198
|
<link rel="modulepreload" crossorigin href="./assets/plus-BgB18UzY.js">
|
|
199
199
|
<link rel="modulepreload" crossorigin href="./assets/trash-2-rVklqqFF.js">
|
|
200
200
|
<link rel="modulepreload" crossorigin href="./assets/react-resizable-panels.browser.esm-CV8-hvjx.js">
|
|
201
|
-
<link rel="modulepreload" crossorigin href="./assets/JsonOutput-
|
|
201
|
+
<link rel="modulepreload" crossorigin href="./assets/JsonOutput-DXzjDImT.js">
|
|
202
202
|
<link rel="modulepreload" crossorigin href="./assets/chart-no-axes-column-nqk474t8.js">
|
|
203
203
|
<link rel="modulepreload" crossorigin href="./assets/square-function-uY_yJr5g.js">
|
|
204
204
|
<link rel="modulepreload" crossorigin href="./assets/spec-CPQR_o92.js">
|
|
205
|
-
<link rel="modulepreload" crossorigin href="./assets/column-preview-
|
|
205
|
+
<link rel="modulepreload" crossorigin href="./assets/column-preview-cHpj1JI3.js">
|
|
206
206
|
<link rel="modulepreload" crossorigin href="./assets/icons-Ol38nIbL.js">
|
|
207
207
|
<link rel="modulepreload" crossorigin href="./assets/radio-group-DS_mmqHJ.js">
|
|
208
208
|
<link rel="modulepreload" crossorigin href="./assets/floating-outline-CCH9y0X9.js">
|
|
@@ -225,7 +225,7 @@
|
|
|
225
225
|
<link rel="modulepreload" crossorigin href="./assets/label-C2_YQRio.js">
|
|
226
226
|
<link rel="modulepreload" crossorigin href="./assets/textarea-BfDUjd9c.js">
|
|
227
227
|
<link rel="modulepreload" crossorigin href="./assets/refresh-ccw-C-n2VFP5.js">
|
|
228
|
-
<link rel="modulepreload" crossorigin href="./assets/form-
|
|
228
|
+
<link rel="modulepreload" crossorigin href="./assets/form-BUVEy6A8.js">
|
|
229
229
|
<link rel="modulepreload" crossorigin href="./assets/renderShortcut--ku7VECI.js">
|
|
230
230
|
<link rel="modulepreload" crossorigin href="./assets/useBoolean-CE6PfxGj.js">
|
|
231
231
|
<link rel="modulepreload" crossorigin href="./assets/useDeepCompareMemoize-CVmg-GkP.js">
|
|
@@ -246,7 +246,7 @@
|
|
|
246
246
|
<link rel="stylesheet" crossorigin href="./assets/cells-pu5xzE_v.css">
|
|
247
247
|
<link rel="stylesheet" crossorigin href="./assets/markdown-renderer-DdDKmWlR.css">
|
|
248
248
|
<link rel="stylesheet" crossorigin href="./assets/JsonOutput-B7vuddcd.css">
|
|
249
|
-
<link rel="stylesheet" crossorigin href="./assets/index-
|
|
249
|
+
<link rel="stylesheet" crossorigin href="./assets/index-DB64hzBp.css">
|
|
250
250
|
</head>
|
|
251
251
|
<body>
|
|
252
252
|
<div id="root"></div>
|
package/package.json
CHANGED
|
@@ -25,7 +25,10 @@ export interface NumberFieldProps extends AriaNumberFieldProps {
|
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
export const NumberField = React.forwardRef<HTMLInputElement, NumberFieldProps>(
|
|
28
|
-
(
|
|
28
|
+
(
|
|
29
|
+
{ placeholder, variant = "default", onInputText, formatOptions, ...props },
|
|
30
|
+
ref,
|
|
31
|
+
) => {
|
|
29
32
|
const { locale } = useLocale();
|
|
30
33
|
return (
|
|
31
34
|
<AriaNumberField
|
|
@@ -33,6 +36,7 @@ export const NumberField = React.forwardRef<HTMLInputElement, NumberFieldProps>(
|
|
|
33
36
|
formatOptions={{
|
|
34
37
|
minimumFractionDigits: 0,
|
|
35
38
|
maximumFractionDigits: maxFractionalDigits(locale),
|
|
39
|
+
...formatOptions,
|
|
36
40
|
}}
|
|
37
41
|
>
|
|
38
42
|
<div
|
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
-
import { type JSX, useEffect, useId, useState } from "react";
|
|
2
|
+
import { type JSX, useEffect, useId, useMemo, useState } from "react";
|
|
3
3
|
import { useLocale } from "react-aria";
|
|
4
4
|
import { z } from "zod";
|
|
5
5
|
import { NumberField } from "@/components/ui/number-field";
|
|
6
6
|
import { cn } from "@/utils/cn";
|
|
7
|
-
import {
|
|
7
|
+
import { clamp } from "@/utils/math";
|
|
8
|
+
import {
|
|
9
|
+
maxFractionDigitsForSteps,
|
|
10
|
+
prettyScientificNumber,
|
|
11
|
+
roundToFractionDigits,
|
|
12
|
+
} from "@/utils/numbers";
|
|
8
13
|
import { Slider } from "../../components/ui/slider";
|
|
9
14
|
import type { IPlugin, IPluginProps, Setter } from "../types";
|
|
10
15
|
import { Labeled } from "./common/labeled";
|
|
@@ -46,8 +51,9 @@ export class SliderPlugin implements IPlugin<T, Data> {
|
|
|
46
51
|
render(props: IPluginProps<T, Data>): JSX.Element {
|
|
47
52
|
// Create the valueMap function
|
|
48
53
|
const valueMap = (sliderValue: number): number => {
|
|
49
|
-
|
|
50
|
-
|
|
54
|
+
const { steps } = props.data;
|
|
55
|
+
if (steps && steps.length > 0) {
|
|
56
|
+
return steps[clamp(sliderValue, 0, steps.length - 1)];
|
|
51
57
|
}
|
|
52
58
|
return sliderValue;
|
|
53
59
|
};
|
|
@@ -69,6 +75,66 @@ interface SliderProps extends Data {
|
|
|
69
75
|
valueMap: (sliderValue: number) => number;
|
|
70
76
|
}
|
|
71
77
|
|
|
78
|
+
interface StepsConfig {
|
|
79
|
+
steps: T[];
|
|
80
|
+
fractionDigits: number;
|
|
81
|
+
minValue: number;
|
|
82
|
+
maxValue: number;
|
|
83
|
+
inputStep: number;
|
|
84
|
+
formatOptions: {
|
|
85
|
+
minimumFractionDigits: number;
|
|
86
|
+
maximumFractionDigits: number;
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Index of the step whose value is closest to `target`.
|
|
91
|
+
function nearestStepIndex(stepValues: T[], target: number): number {
|
|
92
|
+
let bestIndex = 0;
|
|
93
|
+
let bestDistance = Number.POSITIVE_INFINITY;
|
|
94
|
+
for (let i = 0; i < stepValues.length; i++) {
|
|
95
|
+
const distance = Math.abs(stepValues[i] - target);
|
|
96
|
+
if (distance < bestDistance) {
|
|
97
|
+
bestDistance = distance;
|
|
98
|
+
bestIndex = i;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return bestIndex;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// When `steps` are provided, the slider runs in *index* space (start=0,
|
|
105
|
+
// stop=len-1, step=1) while the input must display and accept the *actual*
|
|
106
|
+
// step values. This config maps the input back into value space.
|
|
107
|
+
function computeStepsConfig(steps: T[] | null): StepsConfig | null {
|
|
108
|
+
if (!steps || steps.length === 0) {
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
const sorted = steps.toSorted((a, b) => a - b);
|
|
112
|
+
// Smallest positive gap between consecutive step values, used as the input's
|
|
113
|
+
// increment/decrement amount. Falls back to 1 when undefined.
|
|
114
|
+
let inputStep = Number.POSITIVE_INFINITY;
|
|
115
|
+
for (let i = 1; i < sorted.length; i++) {
|
|
116
|
+
const gap = sorted[i] - sorted[i - 1];
|
|
117
|
+
if (gap > 0 && gap < inputStep) {
|
|
118
|
+
inputStep = gap;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
if (!Number.isFinite(inputStep) || inputStep <= 0) {
|
|
122
|
+
inputStep = 1;
|
|
123
|
+
}
|
|
124
|
+
const fractionDigits = maxFractionDigitsForSteps(steps, inputStep);
|
|
125
|
+
return {
|
|
126
|
+
steps,
|
|
127
|
+
fractionDigits,
|
|
128
|
+
minValue: roundToFractionDigits(sorted[0], fractionDigits),
|
|
129
|
+
maxValue: roundToFractionDigits(sorted[sorted.length - 1], fractionDigits),
|
|
130
|
+
inputStep: roundToFractionDigits(inputStep, fractionDigits),
|
|
131
|
+
formatOptions: {
|
|
132
|
+
minimumFractionDigits: 0,
|
|
133
|
+
maximumFractionDigits: fractionDigits,
|
|
134
|
+
},
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
72
138
|
const SliderComponent = ({
|
|
73
139
|
label,
|
|
74
140
|
setValue,
|
|
@@ -95,6 +161,64 @@ const SliderComponent = ({
|
|
|
95
161
|
setInternalValue(value);
|
|
96
162
|
}, [value]);
|
|
97
163
|
|
|
164
|
+
const stepsConfig = useMemo(() => computeStepsConfig(steps), [steps]);
|
|
165
|
+
|
|
166
|
+
const handleInputChange = (nextValue: number | null | undefined): void => {
|
|
167
|
+
if (stepsConfig) {
|
|
168
|
+
const { steps: stepValues, fractionDigits } = stepsConfig;
|
|
169
|
+
// Cleared input -> reset to the first step.
|
|
170
|
+
if (nextValue == null || Number.isNaN(nextValue)) {
|
|
171
|
+
setInternalValue(0);
|
|
172
|
+
setValue(0);
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
const roundedNext = roundToFractionDigits(nextValue, fractionDigits);
|
|
176
|
+
let index = nearestStepIndex(stepValues, roundedNext);
|
|
177
|
+
// If the nearest step is the current one but the value actually moved
|
|
178
|
+
// (e.g. stepper arrows on non-uniform steps), nudge one index in the
|
|
179
|
+
// direction of travel so the input never gets "stuck". Caveat: a typed
|
|
180
|
+
// value closest to the current step but past it nudges too, so on
|
|
181
|
+
// non-uniform steps typing a nearby number can jump to the adjacent step.
|
|
182
|
+
const currentValue = roundToFractionDigits(
|
|
183
|
+
stepValues[internalValue] ?? stepValues[0],
|
|
184
|
+
fractionDigits,
|
|
185
|
+
);
|
|
186
|
+
if (index === internalValue && roundedNext !== currentValue) {
|
|
187
|
+
index =
|
|
188
|
+
roundedNext > currentValue
|
|
189
|
+
? Math.min(internalValue + 1, stepValues.length - 1)
|
|
190
|
+
: Math.max(internalValue - 1, 0);
|
|
191
|
+
}
|
|
192
|
+
setInternalValue(index);
|
|
193
|
+
setValue(index);
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// No steps: the input value is the slider value directly.
|
|
198
|
+
const resolved =
|
|
199
|
+
nextValue == null || Number.isNaN(nextValue) ? Number(start) : nextValue;
|
|
200
|
+
setInternalValue(resolved);
|
|
201
|
+
setValue(resolved);
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
const inputProps = stepsConfig
|
|
205
|
+
? {
|
|
206
|
+
value: roundToFractionDigits(
|
|
207
|
+
valueMap(internalValue),
|
|
208
|
+
stepsConfig.fractionDigits,
|
|
209
|
+
),
|
|
210
|
+
minValue: stepsConfig.minValue,
|
|
211
|
+
maxValue: stepsConfig.maxValue,
|
|
212
|
+
step: stepsConfig.inputStep,
|
|
213
|
+
formatOptions: stepsConfig.formatOptions,
|
|
214
|
+
}
|
|
215
|
+
: {
|
|
216
|
+
value: valueMap(internalValue),
|
|
217
|
+
minValue: start,
|
|
218
|
+
maxValue: stop,
|
|
219
|
+
step,
|
|
220
|
+
};
|
|
221
|
+
|
|
98
222
|
const sliderElement = (
|
|
99
223
|
<Labeled
|
|
100
224
|
label={label}
|
|
@@ -123,14 +247,14 @@ const SliderComponent = ({
|
|
|
123
247
|
step={step}
|
|
124
248
|
orientation={orientation}
|
|
125
249
|
// Triggered on all value changes
|
|
126
|
-
onValueChange={([nextValue]) => {
|
|
250
|
+
onValueChange={([nextValue]: number[]) => {
|
|
127
251
|
setInternalValue(nextValue);
|
|
128
252
|
if (!debounce) {
|
|
129
253
|
setValue(nextValue);
|
|
130
254
|
}
|
|
131
255
|
}}
|
|
132
256
|
// Triggered on mouse up
|
|
133
|
-
onValueCommit={([nextValue]) => {
|
|
257
|
+
onValueCommit={([nextValue]: [number]) => {
|
|
134
258
|
if (debounce) {
|
|
135
259
|
setValue(nextValue);
|
|
136
260
|
}
|
|
@@ -145,18 +269,8 @@ const SliderComponent = ({
|
|
|
145
269
|
)}
|
|
146
270
|
{includeInput && (
|
|
147
271
|
<NumberField
|
|
148
|
-
|
|
149
|
-
onChange={
|
|
150
|
-
// If nextValue is null/undefined/NaN (input cleared), set to start
|
|
151
|
-
if (nextValue == null || Number.isNaN(nextValue)) {
|
|
152
|
-
nextValue = Number(start);
|
|
153
|
-
}
|
|
154
|
-
setInternalValue(nextValue);
|
|
155
|
-
setValue(nextValue);
|
|
156
|
-
}}
|
|
157
|
-
minValue={start}
|
|
158
|
-
maxValue={stop}
|
|
159
|
-
step={step}
|
|
272
|
+
{...inputProps}
|
|
273
|
+
onChange={handleInputChange}
|
|
160
274
|
className="w-24"
|
|
161
275
|
aria-label={`${label || "Slider"} value input`}
|
|
162
276
|
isDisabled={disabled}
|
|
@@ -50,11 +50,18 @@ describe("SliderPlugin", () => {
|
|
|
50
50
|
vi.useRealTimers();
|
|
51
51
|
});
|
|
52
52
|
|
|
53
|
-
const createProps = (
|
|
54
|
-
debounce
|
|
55
|
-
includeInput
|
|
56
|
-
setValue
|
|
57
|
-
|
|
53
|
+
const createProps = ({
|
|
54
|
+
debounce,
|
|
55
|
+
includeInput,
|
|
56
|
+
setValue,
|
|
57
|
+
}: {
|
|
58
|
+
debounce: boolean;
|
|
59
|
+
includeInput: boolean;
|
|
60
|
+
setValue: ReturnType<typeof vi.fn>;
|
|
61
|
+
}): IPluginProps<
|
|
62
|
+
number,
|
|
63
|
+
z.infer<typeof SliderPlugin.prototype.validator>
|
|
64
|
+
> => {
|
|
58
65
|
return {
|
|
59
66
|
host: document.createElement("div"),
|
|
60
67
|
value: 5,
|
|
@@ -76,10 +83,57 @@ describe("SliderPlugin", () => {
|
|
|
76
83
|
};
|
|
77
84
|
};
|
|
78
85
|
|
|
86
|
+
// When `steps` are provided, the slider works in *index* space: `value`,
|
|
87
|
+
// `start`, `stop` and `step` are all indices into the `steps` array, while
|
|
88
|
+
// the editable input shows/accepts the actual step values.
|
|
89
|
+
const createStepsProps = ({
|
|
90
|
+
steps,
|
|
91
|
+
valueIndex,
|
|
92
|
+
setValue,
|
|
93
|
+
}: {
|
|
94
|
+
steps: number[];
|
|
95
|
+
valueIndex: number;
|
|
96
|
+
setValue: ReturnType<typeof vi.fn>;
|
|
97
|
+
}): IPluginProps<
|
|
98
|
+
number,
|
|
99
|
+
z.infer<typeof SliderPlugin.prototype.validator>
|
|
100
|
+
> => {
|
|
101
|
+
return {
|
|
102
|
+
host: document.createElement("div"),
|
|
103
|
+
value: valueIndex,
|
|
104
|
+
setValue,
|
|
105
|
+
data: {
|
|
106
|
+
initialValue: valueIndex,
|
|
107
|
+
start: 0,
|
|
108
|
+
stop: steps.length - 1,
|
|
109
|
+
step: 1,
|
|
110
|
+
label: "Test Slider",
|
|
111
|
+
debounce: false,
|
|
112
|
+
orientation: "horizontal" as const,
|
|
113
|
+
showValue: false,
|
|
114
|
+
fullWidth: false,
|
|
115
|
+
includeInput: true,
|
|
116
|
+
steps,
|
|
117
|
+
},
|
|
118
|
+
functions: {},
|
|
119
|
+
};
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
const typeAndCommit = (input: HTMLElement, value: string) => {
|
|
123
|
+
act(() => {
|
|
124
|
+
fireEvent.change(input, { target: { value } });
|
|
125
|
+
fireEvent.blur(input);
|
|
126
|
+
});
|
|
127
|
+
};
|
|
128
|
+
|
|
79
129
|
it("slider triggers setValue immediately when debounce is false", () => {
|
|
80
130
|
const plugin = new SliderPlugin();
|
|
81
131
|
const setValue = vi.fn();
|
|
82
|
-
const props = createProps(
|
|
132
|
+
const props = createProps({
|
|
133
|
+
debounce: false,
|
|
134
|
+
includeInput: false,
|
|
135
|
+
setValue,
|
|
136
|
+
});
|
|
83
137
|
const { getByRole } = render(plugin.render(props));
|
|
84
138
|
|
|
85
139
|
act(() => {
|
|
@@ -98,7 +152,11 @@ describe("SliderPlugin", () => {
|
|
|
98
152
|
it("slider waits until commit before calling setValue when debounce is true", () => {
|
|
99
153
|
const plugin = new SliderPlugin();
|
|
100
154
|
const setValue = vi.fn();
|
|
101
|
-
const props = createProps(
|
|
155
|
+
const props = createProps({
|
|
156
|
+
debounce: true,
|
|
157
|
+
includeInput: false,
|
|
158
|
+
setValue,
|
|
159
|
+
});
|
|
102
160
|
const { getByRole } = render(plugin.render(props));
|
|
103
161
|
|
|
104
162
|
act(() => {
|
|
@@ -124,7 +182,7 @@ describe("SliderPlugin", () => {
|
|
|
124
182
|
it("editable input triggers setValue immediately even when slider debounce is true", () => {
|
|
125
183
|
const plugin = new SliderPlugin();
|
|
126
184
|
const setValue = vi.fn();
|
|
127
|
-
const props = createProps(true, true, setValue);
|
|
185
|
+
const props = createProps({ debounce: true, includeInput: true, setValue });
|
|
128
186
|
const { getByRole } = render(plugin.render(props));
|
|
129
187
|
|
|
130
188
|
act(() => {
|
|
@@ -145,4 +203,216 @@ describe("SliderPlugin", () => {
|
|
|
145
203
|
// setValue should be called immediately regardless of debounce=true.
|
|
146
204
|
expect(setValue).toHaveBeenCalledWith(9);
|
|
147
205
|
});
|
|
206
|
+
|
|
207
|
+
describe("editable input with steps (regression for #9850)", () => {
|
|
208
|
+
it("displays the actual step value, not the index", () => {
|
|
209
|
+
const plugin = new SliderPlugin();
|
|
210
|
+
const setValue = vi.fn();
|
|
211
|
+
// steps[0] === -4, displayed value must be -4 (not the index 0).
|
|
212
|
+
const props = createStepsProps({
|
|
213
|
+
steps: [-4, -3, -2, -1],
|
|
214
|
+
valueIndex: 0,
|
|
215
|
+
setValue,
|
|
216
|
+
});
|
|
217
|
+
const { getByRole } = render(plugin.render(props));
|
|
218
|
+
|
|
219
|
+
act(() => {
|
|
220
|
+
vi.advanceTimersByTime(0);
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
const input = getByRole("textbox") as HTMLInputElement;
|
|
224
|
+
expect(input.value).toBe("-4");
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
it("displays decimal step values without float artifacts", () => {
|
|
228
|
+
const plugin = new SliderPlugin();
|
|
229
|
+
const setValue = vi.fn();
|
|
230
|
+
const props = createStepsProps({
|
|
231
|
+
steps: [0.1, 0.2, 0.3, 0.4],
|
|
232
|
+
valueIndex: 2,
|
|
233
|
+
setValue,
|
|
234
|
+
});
|
|
235
|
+
const { getByRole } = render(plugin.render(props));
|
|
236
|
+
|
|
237
|
+
act(() => {
|
|
238
|
+
vi.advanceTimersByTime(0);
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
const input = getByRole("textbox") as HTMLInputElement;
|
|
242
|
+
expect(input.value).toBe("0.3");
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
it("steps decimal input increment and decrement buttons move between steps", () => {
|
|
246
|
+
const plugin = new SliderPlugin();
|
|
247
|
+
const setValue = vi.fn();
|
|
248
|
+
const props = createStepsProps({
|
|
249
|
+
steps: [0.1, 0.2, 0.3, 0.4],
|
|
250
|
+
valueIndex: 2,
|
|
251
|
+
setValue,
|
|
252
|
+
});
|
|
253
|
+
const { getByRole } = render(plugin.render(props));
|
|
254
|
+
|
|
255
|
+
act(() => {
|
|
256
|
+
vi.advanceTimersByTime(0);
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
const decrement = getByRole("button", {
|
|
260
|
+
name: "Decrease Test Slider value input",
|
|
261
|
+
});
|
|
262
|
+
const increment = getByRole("button", {
|
|
263
|
+
name: "Increase Test Slider value input",
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
expect(decrement).not.toBeDisabled();
|
|
267
|
+
|
|
268
|
+
act(() => {
|
|
269
|
+
fireEvent.click(decrement);
|
|
270
|
+
});
|
|
271
|
+
expect(setValue.mock.calls).toEqual([[1]]);
|
|
272
|
+
|
|
273
|
+
act(() => {
|
|
274
|
+
fireEvent.click(increment);
|
|
275
|
+
});
|
|
276
|
+
expect(setValue).toHaveBeenLastCalledWith(2);
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
it("maps a typed integer step value back to its index", () => {
|
|
280
|
+
const plugin = new SliderPlugin();
|
|
281
|
+
const setValue = vi.fn();
|
|
282
|
+
const props = createStepsProps({
|
|
283
|
+
steps: [1, 2, 3, 4],
|
|
284
|
+
valueIndex: 0,
|
|
285
|
+
setValue,
|
|
286
|
+
});
|
|
287
|
+
const { getByRole } = render(plugin.render(props));
|
|
288
|
+
|
|
289
|
+
act(() => {
|
|
290
|
+
vi.advanceTimersByTime(0);
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
const input = getByRole("textbox");
|
|
294
|
+
// Typing "4" should select the last step (index 3), not index 4.
|
|
295
|
+
typeAndCommit(input, "4");
|
|
296
|
+
expect(setValue).toHaveBeenLastCalledWith(3);
|
|
297
|
+
|
|
298
|
+
// Typing "2" should select index 1, not get "stuck" on 3.
|
|
299
|
+
typeAndCommit(input, "2");
|
|
300
|
+
expect(setValue).toHaveBeenLastCalledWith(1);
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
it("maps fractional step values back to their index", () => {
|
|
304
|
+
const plugin = new SliderPlugin();
|
|
305
|
+
const setValue = vi.fn();
|
|
306
|
+
const props = createStepsProps({
|
|
307
|
+
steps: [0.1, 0.2, 0.3, 0.4],
|
|
308
|
+
valueIndex: 0,
|
|
309
|
+
setValue,
|
|
310
|
+
});
|
|
311
|
+
const { getByRole } = render(plugin.render(props));
|
|
312
|
+
|
|
313
|
+
act(() => {
|
|
314
|
+
vi.advanceTimersByTime(0);
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
const input = getByRole("textbox");
|
|
318
|
+
typeAndCommit(input, "0.3");
|
|
319
|
+
expect(setValue).toHaveBeenLastCalledWith(2);
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
it("maps negative step values back to their index", () => {
|
|
323
|
+
const plugin = new SliderPlugin();
|
|
324
|
+
const setValue = vi.fn();
|
|
325
|
+
const props = createStepsProps({
|
|
326
|
+
steps: [-4, -3, -2, -1],
|
|
327
|
+
valueIndex: 0,
|
|
328
|
+
setValue,
|
|
329
|
+
});
|
|
330
|
+
const { getByRole } = render(plugin.render(props));
|
|
331
|
+
|
|
332
|
+
act(() => {
|
|
333
|
+
vi.advanceTimersByTime(0);
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
const input = getByRole("textbox");
|
|
337
|
+
typeAndCommit(input, "-2");
|
|
338
|
+
expect(setValue).toHaveBeenLastCalledWith(2);
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
it("clamps out-of-range typed values to the nearest step index", () => {
|
|
342
|
+
const plugin = new SliderPlugin();
|
|
343
|
+
const setValue = vi.fn();
|
|
344
|
+
const props = createStepsProps({
|
|
345
|
+
steps: [1, 2, 3, 4],
|
|
346
|
+
valueIndex: 0,
|
|
347
|
+
setValue,
|
|
348
|
+
});
|
|
349
|
+
const { getByRole } = render(plugin.render(props));
|
|
350
|
+
|
|
351
|
+
act(() => {
|
|
352
|
+
vi.advanceTimersByTime(0);
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
const input = getByRole("textbox");
|
|
356
|
+
// Above the max step -> clamps to the last index.
|
|
357
|
+
typeAndCommit(input, "100");
|
|
358
|
+
expect(setValue).toHaveBeenLastCalledWith(3);
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
it("does not crash when steps shrink while the index is temporarily out of range", () => {
|
|
362
|
+
const plugin = new SliderPlugin();
|
|
363
|
+
const setValue = vi.fn();
|
|
364
|
+
// Start at the last index of a 5-element steps array.
|
|
365
|
+
const props = createStepsProps({
|
|
366
|
+
steps: [10, 20, 30, 40, 50],
|
|
367
|
+
valueIndex: 4,
|
|
368
|
+
setValue,
|
|
369
|
+
});
|
|
370
|
+
const { getByRole, rerender } = render(plugin.render(props));
|
|
371
|
+
|
|
372
|
+
act(() => {
|
|
373
|
+
vi.advanceTimersByTime(0);
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
// Shrink `steps` so the held index (4) is now out of range. The index
|
|
377
|
+
// syncs in an effect after this render, so the render must not throw on
|
|
378
|
+
// the stale, out-of-range index.
|
|
379
|
+
const shrunkProps = createStepsProps({
|
|
380
|
+
steps: [10, 20],
|
|
381
|
+
valueIndex: 1,
|
|
382
|
+
setValue,
|
|
383
|
+
});
|
|
384
|
+
expect(() => {
|
|
385
|
+
act(() => {
|
|
386
|
+
rerender(plugin.render(shrunkProps));
|
|
387
|
+
vi.advanceTimersByTime(0);
|
|
388
|
+
});
|
|
389
|
+
}).not.toThrow();
|
|
390
|
+
|
|
391
|
+
const input = getByRole("textbox") as HTMLInputElement;
|
|
392
|
+
expect(input.value).toBe("20");
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
it("nudges to the adjacent step on non-uniform steps even when the current step is nearest (known caveat)", () => {
|
|
396
|
+
const plugin = new SliderPlugin();
|
|
397
|
+
const setValue = vi.fn();
|
|
398
|
+
// Non-uniform steps; start at index 2 (value 10).
|
|
399
|
+
const props = createStepsProps({
|
|
400
|
+
steps: [0, 1, 10, 100],
|
|
401
|
+
valueIndex: 2,
|
|
402
|
+
setValue,
|
|
403
|
+
});
|
|
404
|
+
const { getByRole } = render(plugin.render(props));
|
|
405
|
+
|
|
406
|
+
act(() => {
|
|
407
|
+
vi.advanceTimersByTime(0);
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
const input = getByRole("textbox");
|
|
411
|
+
// 10 is far closer to 12 than 100 is, but because 12 > 10 the input nudges
|
|
412
|
+
// up one index. This documents the stepper-vs-typing limitation: onChange
|
|
413
|
+
// can't distinguish a typed value from a stepper-button increment.
|
|
414
|
+
typeAndCommit(input, "12");
|
|
415
|
+
expect(setValue).toHaveBeenLastCalledWith(3);
|
|
416
|
+
});
|
|
417
|
+
});
|
|
148
418
|
});
|