Skip to content

Commit 5ea3a7c

Browse files
committed
feat: add EditorInput single line and performance testing stories
1 parent 16b0734 commit 5ea3a7c

1 file changed

Lines changed: 149 additions & 0 deletions

File tree

src/components/form/Input.stories.tsx

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -460,6 +460,49 @@ export const EditorBadgeTags = () => {
460460
</Card>
461461
}
462462

463+
// ---- Single line ----
464+
// Renders the EditorInput as a one-line field (like a text input): line breaks
465+
// are blocked and pasted newlines collapse to spaces, while token highlighting
466+
// still works.
467+
export const EditorSingleLine = () => {
468+
469+
const [inputs, validate] = useForm({
470+
initialValues: {
471+
expression: ""
472+
},
473+
validate: {
474+
expression: (value) => {
475+
if (!value?.trim()) return "Please enter an expression"
476+
return null
477+
}
478+
},
479+
onSubmit: (values) => {
480+
console.log(values)
481+
}
482+
})
483+
484+
const tokenRules: EditorTokenRule[] = [
485+
{
486+
pattern: /\{\{([^}]+)\}\}/g,
487+
wrap: (_text, children) => (
488+
<Badge color={"info"}>{children}</Badge>
489+
),
490+
},
491+
]
492+
493+
return <Card color={"secondary"} w={"400px"}>
494+
<EditorInput
495+
{...inputs.getInputProps("expression")}
496+
onChange={() => validate("expression")}
497+
singleLine
498+
title={"Expression"}
499+
description={"A single-line expression — Enter does not add a new line"}
500+
placeholder={"Hello {{ user.name }}!"}
501+
tokenRules={tokenRules}
502+
/>
503+
</Card>
504+
}
505+
463506
// ---- Suggestions via Radix menu ----
464507
// Demonstrates the built-in Radix DropdownMenu for autocomplete
465508
export const EditorSuggestions = () => {
@@ -694,4 +737,110 @@ export const File = () => {
694737
</FileInput>
695738
</Card>
696739

740+
}
741+
742+
// ---- Performance: many EditorInputs in a single form ----
743+
// Stress-tests the Slate-based EditorInput by rendering up to 50 instances at once,
744+
// each with token highlighting + suggestions, so we can spot rendering/typing lag.
745+
export const EditorPerformance = () => {
746+
747+
const MAX_INPUTS = 200
748+
749+
const [count, setCount] = React.useState(MAX_INPUTS)
750+
const [renderTime, setRenderTime] = React.useState<number | null>(null)
751+
752+
// Measure the time from the start of a render pass to the browser paint.
753+
const renderStart = React.useRef(performance.now())
754+
renderStart.current = performance.now()
755+
React.useLayoutEffect(() => {
756+
const start = renderStart.current
757+
requestAnimationFrame(() => {
758+
setRenderTime(prev => {
759+
const measured = performance.now() - start
760+
// Avoid an infinite measure->setState->measure loop.
761+
return prev === null || Math.abs(prev - measured) > 0.5 ? measured : prev
762+
})
763+
})
764+
// eslint-disable-next-line react-hooks/exhaustive-deps
765+
}, [count])
766+
767+
const fields = React.useMemo(
768+
() => Array.from({length: MAX_INPUTS}, (_, i) => `field_${i}`),
769+
[]
770+
)
771+
772+
// Stable reference — useForm keys its internal state off the identity of
773+
// `initialValues`, so recreating this object each render would loop forever.
774+
const initialValues = React.useMemo(
775+
() => fields.reduce((acc, key) => {
776+
acc[key] = ""
777+
return acc
778+
}, {} as Record<string, string>),
779+
[fields]
780+
)
781+
782+
const [inputs, validate] = useForm<Record<string, string>>({
783+
initialValues,
784+
useInitialValidation: false,
785+
onSubmit: (values) => {
786+
console.log(values)
787+
}
788+
})
789+
790+
const tokenRules: EditorTokenRule[] = React.useMemo(() => [
791+
{
792+
pattern: /\{\{([^}]+)\}\}/g,
793+
wrap: (_text, children) => <Badge color={"info"}>{children}</Badge>,
794+
},
795+
], [])
796+
797+
const suggestions: InputSuggestion[] = React.useMemo(() => [
798+
{value: "user.name", children: "user.name", groupBy: "User"},
799+
{value: "user.email", children: "user.email", groupBy: "User"},
800+
{value: "order.id", children: "order.id", groupBy: "Order"},
801+
{value: "order.total", children: "order.total", groupBy: "Order"},
802+
], [])
803+
804+
return <Card color={"secondary"} w={"600px"}>
805+
<Flex align={"center"} justify={"space-between"} style={{marginBottom: "1rem"}}>
806+
<Text size={"sm"}>
807+
Rendering <b>{count}</b> EditorInput{count === 1 ? "" : "s"}
808+
{renderTime !== null && ` · last render ${renderTime.toFixed(1)}ms`}
809+
</Text>
810+
<ButtonGroup color={"primary"}>
811+
<Button paddingSize={"xxs"} onClick={() => setCount(c => Math.max(1, c - 10))}>
812+
-10
813+
</Button>
814+
<Button paddingSize={"xxs"} onClick={() => setCount(c => Math.min(MAX_INPUTS, c + 10))}>
815+
+10
816+
</Button>
817+
<Button paddingSize={"xxs"} onClick={() => setCount(MAX_INPUTS)}>
818+
Max ({MAX_INPUTS})
819+
</Button>
820+
</ButtonGroup>
821+
</Flex>
822+
823+
<Flex style={{flexDirection: "column", gap: "0.5rem"}}>
824+
{fields.slice(0, count).map((key, i) => (
825+
<EditorInput
826+
key={key}
827+
{...inputs.getInputProps(key)}
828+
onChange={() => validate(key)}
829+
title={`Expression ${i + 1}`}
830+
placeholder={"Hello {{ user.name }}!"}
831+
tokenRules={tokenRules}
832+
suggestions={suggestions}
833+
onSuggestionSelect={() => validate(key)}
834+
/>
835+
))}
836+
</Flex>
837+
838+
<br/>
839+
<div style={{display: "flex", justifyContent: "end"}}>
840+
<Button color={"info"} onClick={() => validate()}>
841+
<IconLogin size={13}/>
842+
Submit all
843+
</Button>
844+
</div>
845+
</Card>
697846
}

0 commit comments

Comments
 (0)