@@ -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
465508export 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