@@ -43,6 +43,7 @@ import {
4343 IconTriangleFilled ,
4444} from '@tabler/icons-preact' ;
4545import { createContext } from 'preact' ;
46+ import { CSSProperties } from 'preact/compat' ;
4647import {
4748 useContext ,
4849 useEffect ,
@@ -551,6 +552,16 @@ DocEditor.FieldBody = (props: FieldProps) => {
551552 const targeted = deeplink . value === props . deepKey ;
552553 const ref = useRef < HTMLDivElement > ( null ) ;
553554
555+ // Presence highlight: color the field to match the (first other) viewer
556+ // focused on it, à la Google Docs. Applied declaratively (rather than via
557+ // imperative classList/style mutations) so it survives re-renders such as the
558+ // `deeplink-target` class toggling on/off.
559+ const fieldViewers = useFieldViewers ( props . deepKey ) ;
560+ const presenceColor = useMemo ( ( ) => {
561+ const other = fieldViewers . find ( ( viewer ) => ! viewer . isCurrentUser ) ;
562+ return ( other || fieldViewers [ 0 ] ) ?. color || '' ;
563+ } , [ fieldViewers ] ) ;
564+
554565 const showFieldHeader = useMemo ( ( ) => {
555566 if ( field . type === 'object' ) {
556567 // Default to the "drawer" variant.
@@ -573,8 +584,14 @@ DocEditor.FieldBody = (props: FieldProps) => {
573584 className = { joinClassNames (
574585 'DocEditor__field' ,
575586 field . deprecated && 'DocEditor__field--deprecated' ,
576- targeted && 'deeplink-target'
587+ targeted && 'deeplink-target' ,
588+ presenceColor && 'DocEditor__field--presence'
577589 ) }
590+ style = {
591+ presenceColor
592+ ? ( { '--presence-color' : presenceColor } as CSSProperties )
593+ : undefined
594+ }
578595 data-type = { field . type }
579596 data-level = { level }
580597 id = { props . deepKey }
@@ -683,47 +700,16 @@ DocEditor.FieldHeader = (props: FieldProps & {className?: string}) => {
683700/**
684701 * Renders miniature avatars of other viewers who currently have this field
685702 * focused. Sized to match the translate icon so it never causes layout shift.
686- *
687- * Also color-codes the field: the enclosing `.DocEditor__field` gets a colored
688- * border matching the (first other) viewer focused on it, à la Google Docs, so
689- * it's easy to tell at a glance who is on which field.
703+ * (The field's colored presence border is applied in `FieldBody`.)
690704 */
691705DocEditor . FieldHeaderViewers = ( props : { deepKey : string } ) => {
692706 const viewers = useFieldViewers ( props . deepKey ) ;
693- const ref = useRef < HTMLDivElement > ( null ) ;
694-
695- // The color to highlight the field with: prefer the first *other* viewer so
696- // the highlight reflects who else is here (not your own color).
697- const highlightColor = useMemo ( ( ) => {
698- const other = viewers . find ( ( viewer ) => ! viewer . isCurrentUser ) ;
699- return ( other || viewers [ 0 ] ) ?. color || '' ;
700- } , [ viewers ] ) ;
701-
702- useEffect ( ( ) => {
703- const fieldEl = ref . current ?. closest < HTMLElement > ( '.DocEditor__field' ) ;
704- if ( ! fieldEl ) {
705- return ;
706- }
707- if ( highlightColor ) {
708- fieldEl . style . setProperty ( '--presence-color' , highlightColor ) ;
709- fieldEl . classList . add ( 'DocEditor__field--presence' ) ;
710- } else {
711- fieldEl . style . removeProperty ( '--presence-color' ) ;
712- fieldEl . classList . remove ( 'DocEditor__field--presence' ) ;
713- }
714- return ( ) => {
715- fieldEl . style . removeProperty ( '--presence-color' ) ;
716- fieldEl . classList . remove ( 'DocEditor__field--presence' ) ;
717- } ;
718- } , [ highlightColor ] ) ;
719707
720708 if ( viewers . length === 0 ) {
721- // Keep an (empty) anchor element mounted so the cleanup effect above can
722- // still find the field and clear the highlight when viewers leave.
723- return < div ref = { ref } className = "DocEditor__FieldHeader__viewers" /> ;
709+ return null ;
724710 }
725711 return (
726- < div ref = { ref } className = "DocEditor__FieldHeader__viewers" >
712+ < div className = "DocEditor__FieldHeader__viewers" >
727713 { viewers . map ( ( viewer ) => (
728714 < UserAvatar
729715 key = { viewer . email }
0 commit comments