-
Notifications
You must be signed in to change notification settings - Fork 451
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat(core): implement presence cursors #6081
Merged
Merged
Changes from 1 commit
Commits
Show all changes
36 commits
Select commit
Hold shift + click to select a range
4056387
feat(portable-text-editor): improve range decorations perf by compari…
skogsmaskin 7911308
test(portable-text-editor): add tests for range decorations rendering
skogsmaskin fe4a016
fixup! feat(portable-text-editor): improve range decorations perf by …
skogsmaskin 7babe0f
test(portable-text-editor): improve some minor test things
skogsmaskin 11d9fde
fix(core): update z-index on PTE activate overlay
hermanwikner 91f5ff2
feat(core): extend presence data model with `selection`
hermanwikner 175fc86
feat(structure): include `selection` in presence data
hermanwikner df92de1
feat(core): implement presence cursors in PTE
hermanwikner 295455c
test(core): add presence cursor workshop story
hermanwikner 25a5e24
refactor(core): use `getTheme_v2` to access theme values
hermanwikner 0df4e98
fix(core): introduce `OnPathFocusPayload` to improve typing
hermanwikner c82d9fd
fix(form/inputs): perf optimization for PT-input decorators
skogsmaskin def0d46
refactor(form/inputs): also display user presence when user is select…
skogsmaskin 6256217
fix(core): render presence cursors inline instead of in a portal
hermanwikner b8ec79c
fix(core): broken workshop story
hermanwikner 8e72405
feat(core): reset presence selection on blur in PTE
hermanwikner 8f1fe3b
fix(structure): add accidentially removed setFocusPath (DocumentPaneP…
skogsmaskin a8e21ca
fix(core): use `useFormFieldPresence` in `usePresenceCursorDecorations`
hermanwikner 6d87a9e
fix(core): set `focusPath` when receiving a mutation event if there a…
hermanwikner 1c48355
fix(form/inputs): break don't return
skogsmaskin 7f4e25f
fix(form/inputs): remove ref that should not be set here
skogsmaskin ce64287
fix(form/inputs): sort hook deps list
skogsmaskin 8d913b4
fix(form/inputs): reconcile presence decorations for PT-Input
skogsmaskin 4852256
fix(form/inputs): return early if focusPath is already selected in PT…
skogsmaskin 1416e9e
refactor(form/inputs): throttle reporting of focusPath and presence u…
skogsmaskin 4edd935
fix(core): import of `FormNodePresence`
hermanwikner 5aa6583
fix(form/inputs): remove lastActive as dep for PT-presence range deco…
skogsmaskin a30362c
test(core): add presence cursor workshop story
hermanwikner 134d33a
fix(core): remove unnecessary z-index in PTE activate overlay
hermanwikner b91ccf2
fix(core): prevent presence cursor user name from being selected
hermanwikner 04838dc
fix(form/inputs): fix issue where perf opt on focusPath tracking brok…
skogsmaskin 90634e7
test(core): update snapshots in `Studio.test`
hermanwikner 865fd72
refactor(core): move presence decorations according to user edits
skogsmaskin 4cd93f6
refactor(core/form): remove complexity from PortableTextInput
skogsmaskin 1f8fc66
refactor(structure): announce presence throttled
skogsmaskin 6c8a26c
refactor(core): clean up presence cursors code + add comments
hermanwikner File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
feat(core): implement presence cursors in PTE
- Loading branch information
commit df92de13a80041b848a45189f57e972d4e1006f9
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
212 changes: 212 additions & 0 deletions
212
packages/sanity/src/core/form/inputs/PortableText/presence-cursors/UserPresenceCursor.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,212 @@ | ||
import {type ColorTints} from '@sanity/color' | ||
import {type User} from '@sanity/types' | ||
import { | ||
Box, | ||
Flex, | ||
// eslint-disable-next-line no-restricted-imports | ||
Popover, | ||
Text, | ||
} from '@sanity/ui' | ||
import {type Theme} from '@sanity/ui/theme' | ||
import {AnimatePresence, motion, type Transition, type Variants} from 'framer-motion' | ||
import {useCallback, useState} from 'react' | ||
import {css, styled} from 'styled-components' | ||
|
||
import {useUserColor} from '../../../../user-color/hooks' | ||
|
||
const DEBUG_HOVER_TARGET = false | ||
|
||
const DOT_SIZE = 6 | ||
|
||
const LIGHT_SCHEME_TINT = 500 | ||
const DARK_SCHEME_TINT = 400 | ||
|
||
const CONTENT_BOX_VARIANTS: Variants = { | ||
animate: {opacity: 1, scaleX: 1, scaleY: 1}, | ||
exit: {opacity: 0, scaleX: 0, scaleY: 0.5}, | ||
initial: {opacity: 0, scaleX: 0, scaleY: 0.5}, | ||
} | ||
|
||
const CONTENT_BOX_TRANSITION: Transition = { | ||
duration: 0.3, | ||
ease: 'easeInOut', | ||
type: 'spring', | ||
bounce: 0, | ||
} | ||
|
||
const CONTENT_TEXT_VARIANTS: Variants = { | ||
animate: {opacity: 1}, | ||
exit: {opacity: 0}, | ||
initial: {opacity: 0}, | ||
} | ||
|
||
const CONTENT_TEXT_TRANSITION: Transition = { | ||
duration: 0.2, | ||
delay: 0.15, | ||
} | ||
|
||
const getTint = (isDark: boolean) => (isDark ? DARK_SCHEME_TINT : LIGHT_SCHEME_TINT) | ||
|
||
interface StyledProps { | ||
theme: Theme | ||
$tints: ColorTints | ||
} | ||
|
||
// The dot needs to be positioned using a Popover to ensure it doesn't interfere | ||
// with the editor's text. Utilizing a Popover allows rendering it atop the cursor | ||
// in a portal. This is essential as it won't be rendered inside the editor, | ||
// thus preventing any interference with the text nodes. | ||
const DotPopover = styled(Popover)<StyledProps>(({theme, $tints}) => { | ||
const isDark = theme.sanity.color.dark | ||
const bg = $tints[getTint(isDark)].hex | ||
|
||
return css` | ||
background-color: ${bg}; | ||
position: relative; | ||
z-index: 0; | ||
|
||
// We only want to show the popover (i.e. the dot) on the top of | ||
// the cursor. Therefore, we hide the popover when it's not placed on top | ||
// due to auto-placement. | ||
&:not([data-placement='top']) { | ||
display: none; | ||
} | ||
` | ||
}) | ||
|
||
const PopoverContentFlex = styled(Flex)<StyledProps>(({theme, $tints}) => { | ||
const isDark = theme.sanity.color.dark | ||
const bg = $tints[getTint(isDark)].hex | ||
const fg = $tints[isDark ? 950 : 50].hex | ||
|
||
return css` | ||
position: absolute; | ||
|
||
// Increase the hover target area to make it easier | ||
// to make it easier to display the user's name. | ||
width: calc(${DOT_SIZE}px * 2.5); | ||
height: calc(${DOT_SIZE}px * 4); | ||
|
||
top: -${DOT_SIZE * 1.5}px; | ||
left: 50%; | ||
transform: translateX(-50%); | ||
|
||
--presence-cursor-fg: ${fg}; | ||
--presence-cursor-bg: ${bg}; | ||
|
||
&[data-debug-hover-target='true'] { | ||
outline: 1px solid magenta; | ||
} | ||
` | ||
}) | ||
|
||
const CursorLine = styled.span<StyledProps>(({theme, $tints}) => { | ||
const isDark = theme.sanity.color.dark | ||
pedrobonamin marked this conversation as resolved.
Show resolved
Hide resolved
|
||
const bg = $tints[getTint(isDark)].hex | ||
|
||
return css` | ||
border-left: 1px solid transparent; | ||
margin-left: -1px; | ||
pointer-events: none; | ||
position: relative; | ||
word-break: normal; | ||
border-color: ${bg}; | ||
box-sizing: border-box; | ||
` | ||
}) | ||
|
||
const CursorDot = styled.div` | ||
background-color: var(--presence-cursor-bg); | ||
border-radius: 50%; | ||
width: ${DOT_SIZE}px; | ||
height: ${DOT_SIZE}px; | ||
` | ||
|
||
const UserBox = styled(motion(Box))(({theme}) => { | ||
const radius = theme.sanity.radius[4] | ||
|
||
return css` | ||
position: absolute; | ||
top: ${DOT_SIZE * 0.5}px; | ||
left: ${DOT_SIZE * 0.5}px; | ||
transform-origin: left; | ||
white-space: nowrap; | ||
padding: 0.2em 0.25em; | ||
box-sizing: border-box; | ||
border-radius: ${radius}px; | ||
background-color: var(--presence-cursor-bg); | ||
` | ||
}) | ||
|
||
const UserText = styled(motion(Text))` | ||
color: var(--presence-cursor-fg); | ||
` | ||
|
||
interface UserPresenceCursorProps { | ||
boundaryElement: HTMLElement | null | ||
user: User | ||
} | ||
|
||
export function UserPresenceCursor(props: UserPresenceCursorProps): JSX.Element { | ||
const {boundaryElement, user} = props | ||
const {tints} = useUserColor(user.id) | ||
const [hovered, setHovered] = useState<boolean>(false) | ||
|
||
const handleMouseEnter = useCallback(() => setHovered(true), []) | ||
const handleMouseLeave = useCallback(() => setHovered(false), []) | ||
|
||
const popoverContent = ( | ||
<PopoverContentFlex | ||
$tints={tints} | ||
align="center" | ||
contentEditable={false} | ||
data-debug-hover-target={DEBUG_HOVER_TARGET} | ||
justify="center" | ||
onMouseEnter={handleMouseEnter} | ||
onMouseLeave={handleMouseLeave} | ||
> | ||
<CursorDot contentEditable={false} /> | ||
|
||
<AnimatePresence> | ||
{hovered && ( | ||
<UserBox | ||
animate="animate" | ||
exit="exit" | ||
flex={1} | ||
initial="initial" | ||
transition={CONTENT_BOX_TRANSITION} | ||
variants={CONTENT_BOX_VARIANTS} | ||
> | ||
<UserText | ||
animate="animate" | ||
exit="exit" | ||
initial="initial" | ||
size={0} | ||
transition={CONTENT_TEXT_TRANSITION} | ||
variants={CONTENT_TEXT_VARIANTS} | ||
weight="medium" | ||
> | ||
{user.displayName} | ||
</UserText> | ||
</UserBox> | ||
)} | ||
</AnimatePresence> | ||
</PopoverContentFlex> | ||
) | ||
|
||
return ( | ||
<DotPopover | ||
$tints={tints} | ||
content={popoverContent} | ||
contentEditable={false} | ||
floatingBoundary={boundaryElement} | ||
open | ||
placement="top" | ||
portal | ||
referenceBoundary={boundaryElement} | ||
shadow={0} | ||
> | ||
<CursorLine $tints={tints} contentEditable={false} /> | ||
</DotPopover> | ||
) | ||
} |
1 change: 1 addition & 0 deletions
1
packages/sanity/src/core/form/inputs/PortableText/presence-cursors/index.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export * from './usePresenceCursorDecorations' |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we please have this with explicit comments as it were? It's explaining the "hack".