Repository navigation
Real-time Collaboration: Add collaborators presence UI #75065
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
Merged
Merged
Changes from all commits
Commits
Show all changes
14 commits
Select commit
Hold shift + click to select a range
45f43e6
Add UI components and hook up to real awareness data
maxschmeling d5a03f0
Fixed styling of the <CollaboratorsPresence> component
maxschmeling 74a2d49
Close the popover on escape
maxschmeling 5ae243b
Remove default provider so it doesn't get merged
maxschmeling 732450f
Remove commented CursorRegistry code
maxschmeling ecb7ab1
Some code cleanup
maxschmeling ffecfef
Removed some duplicate types and some code related to the cursor regi…
maxschmeling 1d0d43d
Remove unnecessary array map
maxschmeling ccb5a80
Extract AvatarProps interface for <Avatar>
maxschmeling 2f44103
Uses clsx in <Avatar> and updates CSSProperties type usage
maxschmeling c5e79e9
Updated <CollaboratorsPresence> props
maxschmeling cc07500
Localize strings in collaborators list
maxschmeling 8f0a8dd
React.FC => function
maxschmeling 5b48e52
useActiveUsers => useActiveCollaborators
maxschmeling 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
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or 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 hidden or 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
50 changes: 50 additions & 0 deletions
50
packages/editor/src/components/collaborators-presence/avatar.tsx
This file contains hidden or 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,50 @@ | ||
| import { clsx } from 'clsx'; | ||
| import { type PostEditorAwarenessState } from '../../../../core-data/src/awareness/types'; | ||
| import './styles/avatar.scss'; | ||
|
|
||
| type AvatarSize = 'small' | 'medium'; | ||
|
|
||
| interface AvatarProps { | ||
| userInfo: PostEditorAwarenessState[ 'userInfo' ]; | ||
| showUserColorBorder?: boolean; | ||
| size?: AvatarSize; | ||
| } | ||
|
|
||
| /** | ||
| * Renders a circular avatar bubble for a user with an optional border. | ||
| * | ||
| * @param {Object} props Component props. | ||
| * @param {Object} props.userInfo User information. | ||
| * @param {boolean} props.showUserColorBorder Whether to show the user color border. | ||
| * @param {string} props.size Size of the avatar. | ||
| */ | ||
| export function Avatar( { | ||
| userInfo, | ||
| showUserColorBorder, | ||
| size = 'small', | ||
| }: AvatarProps ) { | ||
| const className = clsx( | ||
| 'editor-collaborators-presence__avatar', | ||
| `editor-collaborators-presence__avatar--${ size }`, | ||
| showUserColorBorder && | ||
| 'editor-collaborators-presence__avatar--with-color-border' | ||
| ); | ||
|
|
||
| const avatarUrl = | ||
| userInfo.avatar_urls?.[ 48 ] || | ||
| userInfo.avatar_urls?.[ 96 ] || | ||
| userInfo.avatar_urls?.[ 24 ]; | ||
|
|
||
| const avatarStyles = { | ||
| '--avatar-url': `url(${ avatarUrl })`, | ||
| '--user-color': userInfo.color, | ||
| } as React.CSSProperties; | ||
|
|
||
| return ( | ||
| <div | ||
| className={ className } | ||
| style={ avatarStyles } | ||
| aria-hidden="true" | ||
| /> | ||
| ); | ||
| } | ||
94 changes: 94 additions & 0 deletions
94
packages/editor/src/components/collaborators-presence/index.tsx
This file contains hidden or 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,94 @@ | ||
| import { Button } from '@wordpress/components'; | ||
| import { useState } from '@wordpress/element'; | ||
| import { privateApis } from '@wordpress/core-data'; | ||
|
|
||
| import { Avatar } from './avatar'; | ||
| import { CollaboratorsList } from './list'; | ||
| import { unlock } from '../../lock-unlock'; | ||
| import { type PostEditorAwarenessState } from '../../../../core-data/src/awareness/types'; | ||
|
|
||
| import './styles/collaborators-presence.scss'; | ||
|
|
||
| const { useActiveCollaborators } = unlock( privateApis ); | ||
|
|
||
| interface CollaboratorsPresenceProps { | ||
| postId: number | null; | ||
| postType: string | null; | ||
| } | ||
|
|
||
| /** | ||
| * Renders a list of avatars for the active users, with a maximum of 3 visible avatars. | ||
| * Shows a popover with all users on hover. | ||
| * | ||
| * @param {Object} props CollaboratorsPresence component props | ||
| * @param {number} props.postId ID of the post | ||
| * @param {string} props.postType Type of the post | ||
| */ | ||
| export function CollaboratorsPresence( { | ||
| postId, | ||
| postType, | ||
| }: CollaboratorsPresenceProps ) { | ||
| const activeUsers = useActiveCollaborators( | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. users -> |
||
| postId, | ||
| postType | ||
| ) as PostEditorAwarenessState[]; | ||
|
|
||
| // Filter out current user - we never show ourselves in the list | ||
| const otherActiveUsers = activeUsers.filter( ( user ) => ! user.isMe ); | ||
|
|
||
| const [ isPopoverVisible, setIsPopoverVisible ] = useState( false ); | ||
| const [ popoverAnchor, setPopoverAnchor ] = useState< HTMLElement | null >( | ||
| null | ||
| ); | ||
|
|
||
| // When there are no other collaborators, this component should not render | ||
| // at all. This will always be the case when collaboration is not enabled, but | ||
| // also when the current user is the only editor with the post open. | ||
| if ( otherActiveUsers.length === 0 ) { | ||
| return null; | ||
| } | ||
|
|
||
| const visibleUsers = otherActiveUsers.slice( 0, 3 ); | ||
| const remainingUsers = otherActiveUsers.slice( 3 ); | ||
| const remainingUsersText = remainingUsers | ||
| .map( ( { userInfo } ) => userInfo.name ) | ||
| .join( ', ' ); | ||
|
|
||
| return visibleUsers.length > 0 ? ( | ||
| <div className="editor-collaborators-presence"> | ||
| <Button | ||
| __next40pxDefaultSize | ||
| className="editor-collaborators-presence__button" | ||
| onClick={ () => setIsPopoverVisible( ! isPopoverVisible ) } | ||
| isPressed={ isPopoverVisible } | ||
| ref={ setPopoverAnchor } | ||
| aria-label={ `Collaborators list, ${ otherActiveUsers.length } online` } | ||
| > | ||
| { visibleUsers.map( ( userState ) => ( | ||
| <Avatar | ||
| key={ userState.clientId } | ||
| userInfo={ userState.userInfo } | ||
| showUserColorBorder={ false } | ||
| size="small" | ||
| /> | ||
| ) ) } | ||
|
|
||
| { remainingUsers.length > 0 && ( | ||
| <div | ||
| className="editor-collaborators-presence__remaining" | ||
| title={ remainingUsersText } | ||
| > | ||
| +{ remainingUsers.length } | ||
| </div> | ||
| ) } | ||
| </Button> | ||
| { isPopoverVisible && ( | ||
| <CollaboratorsList | ||
| activeUsers={ otherActiveUsers } | ||
| popoverAnchor={ popoverAnchor } | ||
| setIsPopoverVisible={ setIsPopoverVisible } | ||
| /> | ||
| ) } | ||
| </div> | ||
| ) : null; | ||
| } | ||
79 changes: 79 additions & 0 deletions
79
packages/editor/src/components/collaborators-presence/list.tsx
This file contains hidden or 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,79 @@ | ||
| import { __ } from '@wordpress/i18n'; | ||
| import { Popover, Button } from '@wordpress/components'; | ||
| import { close } from '@wordpress/icons'; | ||
|
|
||
| import { Avatar } from './avatar'; | ||
|
|
||
| import './styles/collaborators-list.scss'; | ||
| import { type PostEditorAwarenessState } from '../../../../core-data/src/awareness/types'; | ||
|
|
||
| interface CollaboratorsListProps { | ||
| activeUsers: PostEditorAwarenessState[]; | ||
| popoverAnchor?: HTMLElement | null; | ||
| setIsPopoverVisible: ( isVisible: boolean ) => void; | ||
| } | ||
|
|
||
| /** | ||
| * Renders a list showing all active collaborators with their details. | ||
| * Note: activeUsers should already exclude the current user (filtered by parent component). | ||
| * @param props Component props | ||
| * @param props.activeUsers List of active users | ||
| * @param props.popoverAnchor Anchor element for the popover | ||
| * @param props.setIsPopoverVisible Callback to set the visibility of the popover | ||
| */ | ||
| export function CollaboratorsList( { | ||
| activeUsers, | ||
| popoverAnchor, | ||
| setIsPopoverVisible, | ||
| }: CollaboratorsListProps ) { | ||
| return ( | ||
| <Popover | ||
| anchor={ popoverAnchor } | ||
| placement="bottom" | ||
| offset={ 8 } | ||
| className="editor-collaborators-presence__list" | ||
| onClose={ () => setIsPopoverVisible( false ) } | ||
| > | ||
| <div className="editor-collaborators-presence__list-content"> | ||
| <div className="editor-collaborators-presence__list-header"> | ||
| <div className="editor-collaborators-presence__list-header-title"> | ||
| { __( 'Collaborators' ) } | ||
| <span> { activeUsers.length } </span> | ||
| </div> | ||
| <div className="editor-collaborators-presence__list-header-action"> | ||
| <Button | ||
| __next40pxDefaultSize | ||
| icon={ close } | ||
| iconSize={ 16 } | ||
| label={ __( 'Close Collaborators List' ) } | ||
| onClick={ () => setIsPopoverVisible( false ) } | ||
| /> | ||
| </div> | ||
| </div> | ||
| <div className="editor-collaborators-presence__list-items"> | ||
| { activeUsers.map( ( userState ) => ( | ||
| <button | ||
| key={ userState.clientId } | ||
| className="editor-collaborators-presence__list-item" | ||
| disabled | ||
| style={ { | ||
| opacity: userState.isConnected ? 1 : 0.5, | ||
| } } | ||
| > | ||
| <Avatar | ||
| userInfo={ userState.userInfo } | ||
| showUserColorBorder | ||
| size="medium" | ||
| /> | ||
| <div className="editor-collaborators-presence__list-item-info"> | ||
| <div className="editor-collaborators-presence__list-item-name"> | ||
| { userState.userInfo.name } | ||
| </div> | ||
| </div> | ||
| </button> | ||
| ) ) } | ||
| </div> | ||
| </div> | ||
| </Popover> | ||
| ); | ||
| } |
31 changes: 31 additions & 0 deletions
31
packages/editor/src/components/collaborators-presence/styles/avatar.scss
This file contains hidden or 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,31 @@ | ||
| @use "@wordpress/base-styles/colors" as *; | ||
| @use "@wordpress/base-styles/variables" as *; | ||
|
|
||
| .editor-collaborators-presence__avatar { | ||
| background-image: var(--avatar-url); | ||
| background-size: cover; | ||
| background-position: center; | ||
| border-radius: $grid-unit-05 + 2; | ||
| border-width: 1.5px; | ||
| border-style: solid; | ||
| border-color: $gray-300; | ||
| font-size: 0; | ||
|
|
||
| &--small { | ||
| width: $button-size-small; | ||
| height: $button-size-small; | ||
| border-radius: $grid-unit-05 + 2; | ||
| } | ||
|
|
||
| &--medium { | ||
| width: $button-size-compact; | ||
| height: $button-size-compact; | ||
| border-radius: $grid-unit-10; | ||
| } | ||
|
|
||
| &--with-color-border { | ||
| border-color: var(--user-color); | ||
| box-shadow: inset 0 0 0 1px rgba($white, 0.7); | ||
| background-clip: padding-box; | ||
| } | ||
| } |
96 changes: 96 additions & 0 deletions
96
packages/editor/src/components/collaborators-presence/styles/collaborators-list.scss
This file contains hidden or 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,96 @@ | ||
| @use "@wordpress/base-styles/colors" as *; | ||
| @use "@wordpress/base-styles/variables" as *; | ||
|
|
||
| .editor-collaborators-presence__list.components-popover { | ||
| // Popover wrapper styles | ||
| .components-popover__content { | ||
| border: 1px solid $gray-300; | ||
| border-width: 1px 0 0 1px; | ||
| border-radius: $radius-large; | ||
| background: $white; | ||
| box-shadow: $elevation-small; | ||
| } | ||
|
|
||
| .editor-collaborators-presence__list-content { | ||
| min-width: 280px; | ||
| } | ||
|
|
||
| // Header with title and action button | ||
| .editor-collaborators-presence__list-header { | ||
| display: flex; | ||
| justify-content: space-between; | ||
| align-items: center; | ||
| padding: 0 $grid-unit-20; | ||
|
|
||
| &-title { | ||
| font-size: $font-size-medium; | ||
| font-weight: $font-weight-medium; | ||
| text-transform: uppercase; | ||
| line-height: $font-line-height-small; | ||
| padding: 14px 0; | ||
|
|
||
| span { | ||
| color: $gray-700; | ||
| } | ||
| } | ||
|
|
||
| &-action { | ||
| padding: $grid-unit-10 0; | ||
|
|
||
| button { | ||
| color: $gray-900; | ||
| height: $button-size-small; | ||
| width: $button-size-small; | ||
| padding: 0; | ||
| } | ||
| } | ||
| } | ||
|
|
||
| // List of collaborators | ||
| .editor-collaborators-presence__list-items { | ||
| display: flex; | ||
| flex-direction: column; | ||
| padding: 0 10px $grid-unit-20; | ||
| } | ||
|
|
||
| .editor-collaborators-presence__list-item { | ||
| // Reset button styles | ||
| all: unset; | ||
| box-sizing: border-box; | ||
|
|
||
| // Layout styles | ||
| display: flex; | ||
| align-items: center; | ||
| padding: $grid-unit-05 + 2; | ||
| gap: $grid-unit-10; | ||
| border-radius: $radius-large + $grid-unit-05; | ||
| width: 100%; | ||
| cursor: pointer; | ||
| transition: background-color 0.2s ease; | ||
|
|
||
| &:hover:not(:disabled) { | ||
| background-color: rgba($black, 0.05); | ||
| } | ||
|
|
||
| &:active:not(:disabled) { | ||
| background-color: rgba($black, 0.08); | ||
| } | ||
|
|
||
| &:focus-visible { | ||
| outline: 2px solid var(--wp-admin-theme-color, #007cba); | ||
| outline-offset: -2px; | ||
| } | ||
|
|
||
| &:disabled { | ||
| cursor: default; | ||
| } | ||
|
|
||
| &-info { | ||
| display: flex; | ||
| flex-direction: column; | ||
| flex: 1; | ||
| line-height: $font-line-height-x-small; | ||
| font-size: $font-size-small; | ||
| } | ||
| } | ||
| } |
Oops, something went wrong.
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.
Why is it marked with
aria-hidden? Will we rely oncollaborators-listfor announceable text?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.
This was done by @shekharnwagh originally, but I do believe the reason is because you don't want to go through this list and then also the dropdown list. You just want the items in the interactive list to be announced.
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.
Yeah, the text in
collaborators-listis enough and this doesn't add meaningful content for screen readers