1
+ import { mergeRefs } from "@react-aria/utils" ;
1
2
import { lexer } from "css-tree" ;
2
3
import { colord } from "colord" ;
3
4
import {
@@ -6,8 +7,11 @@ import {
6
7
useEffect ,
7
8
useRef ,
8
9
useState ,
10
+ type ChangeEvent ,
9
11
type ComponentProps ,
12
+ type KeyboardEvent ,
10
13
type ReactNode ,
14
+ type RefObject ,
11
15
} from "react" ;
12
16
import { useStore } from "@nanostores/react" ;
13
17
import { computed } from "nanostores" ;
@@ -26,6 +30,7 @@ import {
26
30
InputField ,
27
31
Label ,
28
32
NestedInputButton ,
33
+ SearchField ,
29
34
SectionTitle ,
30
35
SectionTitleButton ,
31
36
SectionTitleLabel ,
@@ -236,8 +241,9 @@ const AddProperty = forwardRef<
236
241
onClose : ( ) => void ;
237
242
onSubmit : ( css : string ) => void ;
238
243
onFocus : ( ) => void ;
244
+ onBlur : ( ) => void ;
239
245
}
240
- > ( ( { onClose, onSubmit, onFocus } , forwardedRef ) => {
246
+ > ( ( { onClose, onSubmit, onFocus, onBlur } , forwardedRef ) => {
241
247
const [ item , setItem ] = useState < SearchItem > ( {
242
248
property : "" ,
243
249
label : "" ,
@@ -309,11 +315,15 @@ const AddProperty = forwardRef<
309
315
return (
310
316
< ComboboxRoot open = { combobox . isOpen } >
311
317
< div { ...combobox . getComboboxProps ( ) } >
312
- < input type = "submit" hidden />
313
318
< ComboboxAnchor >
314
319
< InputField
315
320
{ ...inputProps }
321
+ autoFocus
316
322
onFocus = { onFocus }
323
+ onBlur = { ( event ) => {
324
+ inputProps . onBlur ( event ) ;
325
+ onBlur ( ) ;
326
+ } }
317
327
inputRef = { forwardedRef }
318
328
onKeyDown = { handleKeyDown }
319
329
placeholder = "Add styles"
@@ -414,12 +424,14 @@ const AdvancedPropertyValue = ({
414
424
autoFocus,
415
425
property,
416
426
onChangeComplete,
427
+ inputRef : inputRefProp ,
417
428
} : {
418
429
autoFocus ?: boolean ;
419
430
property : StyleProperty ;
420
431
onChangeComplete : ComponentProps <
421
432
typeof CssValueInputContainer
422
433
> [ "onChangeComplete" ] ;
434
+ inputRef ?: RefObject < HTMLInputElement > ;
423
435
} ) => {
424
436
const styleDecl = useComputedStyleDecl ( property ) ;
425
437
const inputRef = useRef < HTMLInputElement > ( null ) ;
@@ -432,7 +444,7 @@ const AdvancedPropertyValue = ({
432
444
const isColor = colord ( toValue ( styleDecl . usedValue ) ) . isValid ( ) ;
433
445
return (
434
446
< CssValueInputContainer
435
- inputRef = { inputRef }
447
+ inputRef = { mergeRefs ( inputRef , inputRefProp ) }
436
448
variant = "chromeless"
437
449
text = "mono"
438
450
fieldSizing = "content"
@@ -559,13 +571,15 @@ const AdvancedProperty = memo(
559
571
autoFocus,
560
572
onChangeComplete,
561
573
onReset,
574
+ valueInputRef,
562
575
} : {
563
576
property : StyleProperty ;
564
577
autoFocus ?: boolean ;
565
578
onReset ?: ( ) => void ;
566
579
onChangeComplete ?: ComponentProps <
567
580
typeof CssValueInputContainer
568
581
> [ "onChangeComplete" ] ;
582
+ valueInputRef ?: RefObject < HTMLInputElement > ;
569
583
} ) => {
570
584
const visibilityChangeEventSupported = useClientSupports (
571
585
( ) => "oncontentvisibilityautostatechange" in document . body
@@ -636,6 +650,7 @@ const AdvancedProperty = memo(
636
650
autoFocus = { autoFocus }
637
651
property = { property }
638
652
onChangeComplete = { onChangeComplete }
653
+ inputRef = { valueInputRef }
639
654
/>
640
655
</ Box >
641
656
</ >
@@ -650,75 +665,131 @@ export const Section = () => {
650
665
const advancedProperties = useStore ( $advancedProperties ) ;
651
666
const [ recentProperties , setRecentProperties ] = useState < StyleProperty [ ] > ( [ ] ) ;
652
667
const addPropertyInputRef = useRef < HTMLInputElement > ( null ) ;
668
+ const recentValueInputRef = useRef < HTMLInputElement > ( null ) ;
669
+ const searchInputRef = useRef < HTMLInputElement > ( null ) ;
670
+ const [ searchProperties , setSearchProperties ] =
671
+ useState < Array < StyleProperty > > ( ) ;
672
+ const containerRef = useRef < HTMLDivElement > ( null ) ;
673
+ const [ minHeight , setMinHeight ] = useState < number > ( 0 ) ;
653
674
654
- const addRecentProperties = ( properties : StyleProperty [ ] ) => {
655
- setRecentProperties (
656
- Array . from ( new Set ( [ ...recentProperties , ...properties ] ) )
657
- ) ;
675
+ const currentProperties = searchProperties ?? advancedProperties ;
676
+
677
+ const showRecentProperties =
678
+ recentProperties . length > 0 && searchProperties === undefined ;
679
+
680
+ const memorizeMinHeight = ( ) => {
681
+ setMinHeight ( containerRef . current ?. getBoundingClientRect ( ) . height ?? 0 ) ;
658
682
} ;
659
683
660
- const showAddProperty = ( ) => {
684
+ const handleShowAddStylesInput = ( ) => {
661
685
setIsAdding ( true ) ;
662
686
// User can click twice on the add button, so we need to focus the input on the second click after autoFocus isn't working.
663
687
addPropertyInputRef . current ?. focus ( ) ;
664
688
} ;
665
689
690
+ const handleAbortSearch = ( ) => {
691
+ setMinHeight ( 0 ) ;
692
+ setSearchProperties ( undefined ) ;
693
+ } ;
694
+
695
+ const handleSubmitStyles = ( cssText : string ) => {
696
+ setIsAdding ( false ) ;
697
+ const styles = insertStyles ( cssText ) ;
698
+ const insertedProperties = styles . map ( ( { property } ) => property ) ;
699
+ setRecentProperties (
700
+ Array . from ( new Set ( [ ...recentProperties , ...insertedProperties ] ) )
701
+ ) ;
702
+ } ;
703
+
704
+ const handleSearch = ( event : ChangeEvent < HTMLInputElement > ) => {
705
+ const search = event . target . value . trim ( ) ;
706
+ if ( search === "" ) {
707
+ return handleAbortSearch ( ) ;
708
+ }
709
+ memorizeMinHeight ( ) ;
710
+ const matched = matchSorter ( advancedProperties , search ) ;
711
+ setSearchProperties ( matched ) ;
712
+ } ;
713
+
714
+ const handleAbortAddStyles = ( ) => {
715
+ setIsAdding ( false ) ;
716
+ requestAnimationFrame ( ( ) => {
717
+ // We are either focusing the last value input from the recent list if available or the search input.
718
+ const element = recentValueInputRef . current ?? searchInputRef . current ;
719
+ element ?. focus ( ) ;
720
+ } ) ;
721
+ } ;
722
+
666
723
return (
667
724
< AdvancedStyleSection
668
725
label = "Advanced"
669
726
properties = { advancedProperties }
670
- onAdd = { showAddProperty }
727
+ onAdd = { handleShowAddStylesInput }
671
728
>
672
729
< Box css = { { paddingInline : theme . panel . paddingInline } } >
673
- { recentProperties . map ( ( property , index , properties ) => (
674
- < AdvancedProperty
675
- key = { property }
676
- property = { property }
677
- autoFocus = { index === properties . length - 1 }
678
- onChangeComplete = { ( event ) => {
679
- if ( event . type === "enter" ) {
680
- showAddProperty ( ) ;
681
- }
682
- } }
683
- onReset = { ( ) => {
684
- setRecentProperties ( ( properties ) => {
685
- return properties . filter (
686
- ( recentProperty ) => recentProperty !== property
687
- ) ;
688
- } ) ;
689
- } }
690
- />
691
- ) ) }
692
- < Box
693
- css = {
694
- isAdding
695
- ? { paddingTop : theme . spacing [ 3 ] }
696
- : // We hide it visually so you can tab into it to get shown.
697
- { overflow : "hidden" , height : 0 }
698
- }
699
- >
700
- < AddProperty
701
- onSubmit = { ( value ) => {
702
- setIsAdding ( false ) ;
703
- const styles = insertStyles ( value ) ;
704
- const insertedProperties = styles . map ( ( { property } ) => property ) ;
705
- addRecentProperties ( insertedProperties ) ;
706
- } }
707
- onClose = { ( ) => {
708
- setIsAdding ( false ) ;
709
- } }
710
- onFocus = { ( ) => {
711
- if ( isAdding === false ) {
712
- showAddProperty ( ) ;
713
- }
714
- } }
715
- ref = { addPropertyInputRef }
716
- />
717
- </ Box >
730
+ < SearchField
731
+ inputRef = { searchInputRef }
732
+ onChange = { handleSearch }
733
+ onAbort = { handleAbortSearch }
734
+ />
718
735
</ Box >
719
- { recentProperties . length > 0 && < Separator /> }
720
736
< Box css = { { paddingInline : theme . panel . paddingInline } } >
721
- { advancedProperties
737
+ { showRecentProperties &&
738
+ recentProperties . map ( ( property , index , properties ) => {
739
+ const isLast = index === properties . length - 1 ;
740
+ return (
741
+ < AdvancedProperty
742
+ valueInputRef = { isLast ? recentValueInputRef : undefined }
743
+ key = { property }
744
+ property = { property }
745
+ autoFocus = { isLast }
746
+ onChangeComplete = { ( event ) => {
747
+ if ( event . type === "enter" ) {
748
+ handleShowAddStylesInput ( ) ;
749
+ }
750
+ } }
751
+ onReset = { ( ) => {
752
+ setRecentProperties ( ( properties ) => {
753
+ return properties . filter (
754
+ ( recentProperty ) => recentProperty !== property
755
+ ) ;
756
+ } ) ;
757
+ } }
758
+ />
759
+ ) ;
760
+ } ) }
761
+ { ( showRecentProperties || isAdding ) && (
762
+ < Box
763
+ style = {
764
+ isAdding
765
+ ? { paddingTop : theme . spacing [ 3 ] }
766
+ : // We hide it visually so you can tab into it to get shown.
767
+ { overflow : "hidden" , height : 0 }
768
+ }
769
+ >
770
+ < AddProperty
771
+ onSubmit = { handleSubmitStyles }
772
+ onClose = { handleAbortAddStyles }
773
+ onFocus = { ( ) => {
774
+ if ( isAdding === false ) {
775
+ handleShowAddStylesInput ( ) ;
776
+ }
777
+ } }
778
+ onBlur = { ( ) => {
779
+ setIsAdding ( false ) ;
780
+ } }
781
+ ref = { addPropertyInputRef }
782
+ />
783
+ </ Box >
784
+ ) }
785
+ </ Box >
786
+ { showRecentProperties && < Separator /> }
787
+ < Box
788
+ css = { { paddingInline : theme . panel . paddingInline } }
789
+ style = { { minHeight } }
790
+ ref = { containerRef }
791
+ >
792
+ { currentProperties
722
793
. filter ( ( property ) => recentProperties . includes ( property ) === false )
723
794
. map ( ( property ) => (
724
795
< AdvancedProperty key = { property } property = { property } />
0 commit comments