Skip to content

Commit

Permalink
Collab UX Polish
Browse files Browse the repository at this point in the history
* Simplified integration between prose/index and da-title.
* prose/index only responsible for data, da-title is presentation.
* Turned user icon into clickable initial pill. Clicking shows full name popover.
* Cloud is also clickable for more detailed status
  • Loading branch information
auniverseaway committed Mar 6, 2024
1 parent faacea5 commit 4406106
Show file tree
Hide file tree
Showing 10 changed files with 178 additions and 119 deletions.
78 changes: 76 additions & 2 deletions blocks/edit/da-title/da-title.css
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@
padding: 48px 0;
}

:host > svg {
display: none;
}

h1 {
margin-top: 0;
}
Expand Down Expand Up @@ -54,7 +58,7 @@ h1 {
opacity: 1;
}

.da-title-name-label:before {
.da-title-name-label::before {
display: block;
content: '';
position: absolute;
Expand All @@ -65,8 +69,78 @@ h1 {
background: url('/blocks/edit/img/left-large.svg') center/18px no-repeat;
}

.da-title-actions {
.da-title-collab-actions-wrapper {
display: flex;
margin-bottom: 0.67em;
}

.collab-status {
display: flex;
align-items: center;
justify-content: end;
}

.collab-icon {
position: relative;
font-size: 12px;
font-weight: 700;
}

.collab-icon:hover {
z-index: 2;
}

.collab-icon.collab-popup::after {
display: block;
content: attr(data-popup-content);
position: absolute;
bottom: -32px;
left: 50%;
transform: translateX(-50%);
text-align: center;
text-transform: capitalize;
background: #676767;
color: #FFF;
white-space: nowrap;
padding: 0 8px;
border-radius: 4px;
}

.collab-icon-user {
height: 24px;
border-radius: 12px;
background: rgb(171 171 171 / 50%);
display: flex;
align-items: center;
justify-content: center;
text-transform: uppercase;
color: #676767;
margin-right: -6px;
padding: 0 12px;
}

.collab-icon-user:hover {
background: rgb(150 150 150 / 50%);
}

.collab-status-cloud {
height: 27px;
margin-left: -4px;
margin-bottom: -3px;
color: rgb(20 115 230 / 80%);
}

.collab-icon.collab-status-cloud.collab-popup::after {
bottom: -29px;
}

.collab-status-cloud svg {
pointer-events: none;
width: 37.5px;
height: 27px;
}

.da-title-actions {
right: -12px;
position: relative;
border: 12px solid transparent;
Expand Down
89 changes: 69 additions & 20 deletions blocks/edit/da-title/da-title.js
Original file line number Diff line number Diff line change
@@ -1,19 +1,36 @@
import { LitElement, html } from '../../../deps/lit/lit-core.min.js';
import { LitElement, html, nothing } from '../../../deps/lit/lit-core.min.js';
import { saveToDa, saveToAem } from '../utils/helpers.js';
import inlinesvg from '../../shared/inlinesvg.js';
import getSheet from '../../shared/sheet.js';

const sheet = await getSheet('/blocks/edit/da-title/da-title.css');

const ICONS = [
'/blocks/edit/img/Smock_Cloud_18_N.svg',
'/blocks/edit/img/Smock_CloudDisconnected_18_N.svg',
'/blocks/edit/img/Smock_CloudError_18_N.svg',
];

const CLOUD_ICONS = {
connected: 'spectrum-Cloud-connected',
offline: 'spectrum-Cloud-offline',
connecting: 'spectrum-Cloud-error',
error: 'spectrum-Cloud-error',
};

export default class DaTitle extends LitElement {
static properties = {
details: { attribute: false },
collabStatus: { attribute: false },
collabUsers: { attribute: false },
_actionsVis: {},
};

connectedCallback() {
super.connectedCallback();
this.shadowRoot.adoptedStyleSheets = [sheet];
this._actionsVis = false;
inlinesvg({ parent: this.shadowRoot, paths: ICONS });
}

async handleAction(action) {
Expand Down Expand Up @@ -48,6 +65,35 @@ export default class DaTitle extends LitElement {
this._actionsVis = !this._actionsVis;
}

popover({ target }) {
// If toggling off, simply remove;
if (target.classList.contains('collab-popup')) {
target.classList.remove('collab-popup');
return;
}
// Find all open popups and close them
const openPopups = this.shadowRoot.querySelectorAll('.collab-popup');
openPopups.forEach((pop) => { pop.classList.remove('collab-popup'); });
target.classList.add('collab-popup');
}

renderColabUsers() {
return html`${this.collabUsers.map((user) => {
const initials = user.split(' ').map((name) => name.toString().substring(0, 1));
return html`<div class="collab-icon collab-icon-user" data-popup-content="${user}" @click=${this.popover}>${initials.join('')}</div>`;
})}`;
}

renderCollab() {
return html`
<div class="collab-status">
${this.collabUsers && this.collabUsers.length > 1 ? this.renderColabUsers() : nothing}
<div class="collab-icon collab-status-cloud collab-status-${this.collabStatus}" data-popup-content="${this.collabStatus}" @click=${this.popover}>
<svg class="icon"><use href="#${CLOUD_ICONS[this.collabStatus]}"/></svg>
</div>
</div>`;
}

render() {
return html`
<div class="da-title-inner">
Expand All @@ -58,25 +104,28 @@ export default class DaTitle extends LitElement {
class="da-title-name-label">${this.details.parentName}</a>
<h1>${this.details.name}</h1>
</div>
<div class="da-title-actions${this._actionsVis ? ' is-open' : ''}">
<button
@click=${this.handlePreview}
class="con-button blue da-title-action"
aria-label="Send">
Preview
</button>
<button
@click=${this.handlePublish}
class="con-button blue da-title-action"
aria-label="Send">
Publish
</button>
<button
@click=${this.toggleActions}
class="con-button blue da-title-action-send"
aria-label="Send">
<span class="da-title-action-send-icon"></span>
</button>
<div class="da-title-collab-actions-wrapper">
${this.collabStatus ? this.renderCollab() : nothing}
<div class="da-title-actions${this._actionsVis ? ' is-open' : ''}">
<button
@click=${this.handlePreview}
class="con-button blue da-title-action"
aria-label="Send">
Preview
</button>
<button
@click=${this.handlePublish}
class="con-button blue da-title-action"
aria-label="Send">
Publish
</button>
<button
@click=${this.toggleActions}
class="con-button blue da-title-action-send"
aria-label="Send">
<span class="da-title-action-send-icon"></span>
</button>
</div>
</div>
</div>
`;
Expand Down
7 changes: 7 additions & 0 deletions blocks/edit/img/Smock_CloudDisconnected_18_N.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
7 changes: 7 additions & 0 deletions blocks/edit/img/Smock_CloudError_18_N.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
7 changes: 7 additions & 0 deletions blocks/edit/img/Smock_Cloud_18_N.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
12 changes: 0 additions & 12 deletions blocks/edit/prose/img/Smock_CloudDisconnected_18_N.svg

This file was deleted.

12 changes: 0 additions & 12 deletions blocks/edit/prose/img/Smock_CloudError_18_N.svg

This file was deleted.

11 changes: 0 additions & 11 deletions blocks/edit/prose/img/Smock_Cloud_18_N.svg

This file was deleted.

11 changes: 0 additions & 11 deletions blocks/edit/prose/img/Smock_RealTimeCustomerProfile_18_N.svg

This file was deleted.

63 changes: 12 additions & 51 deletions blocks/edit/prose/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -90,27 +90,8 @@ function setAEMDocInEditor(aemDoc, yXmlFragment, schema) {
prosemirrorToYXmlFragment(fin, yXmlFragment);
}

export function setConnectionStatus(connectionImg, status) {
switch (status) {
case 'connected':
case 'online':
connectionImg.src = '/blocks/edit/prose/img/Smock_Cloud_18_N.svg';
break;
case 'connecting':
case 'offline':
connectionImg.src = '/blocks/edit/prose/img/Smock_CloudDisconnected_18_N.svg';
break;
default:
connectionImg.src = '/blocks/edit/prose/img/Smock_CloudError_18_N.svg';
break;
}
connectionImg.alt = status;
connectionImg.title = status;
}

function handleAwarenessUpdates(wsProvider, statusDiv, win) {
function handleAwarenessUpdates(wsProvider, daTitle, win) {
const users = new Set();
const usersDiv = statusDiv.querySelector('div.collab-users');

wsProvider.awareness.on('update', (delta) => {
const awarenessStates = wsProvider.awareness.getStates();
Expand All @@ -119,40 +100,19 @@ function handleAwarenessUpdates(wsProvider, statusDiv, win) {
delta.updated.forEach((u) => users.add(u));
delta.removed.forEach((u) => users.delete(u));

let html = '';
for (const u of Array.from(users).sort()) {
const name = awarenessStates.get(u)?.user?.name;
if (name) {
const initial = name.toString().substring(0, 1);
html = html.concat(`<div class="collab-initial" title="${name}"><p>${initial}</p></div>`);
} else {
html = html.concat(`<div class="collab-icon">
<img src="/blocks/edit/prose/img/Smock_RealTimeCustomerProfile_18_N.svg"
alt="Other active user" class="collab-icon" alt="${u}" title="${u}"/></div>`);
}
}
usersDiv.innerHTML = html;
const sortedUsers = [...users].sort();
daTitle.collabUsers = sortedUsers.map((u) => awarenessStates.get(u)?.user?.name || 'Anonymous');
});

const connectionImg = statusDiv.querySelector('img.collab-connection');
wsProvider.on('status', (st) => setConnectionStatus(connectionImg, st.status));
win.addEventListener('online', () => setConnectionStatus(connectionImg, 'online'));
win.addEventListener('offline', () => setConnectionStatus(connectionImg, 'offline'));
wsProvider.on('status', (st) => { daTitle.collabStatus = st.status; });
win.addEventListener('online', () => { daTitle.collabStatus = 'online'; });
win.addEventListener('offline', () => { daTitle.collabStatus = 'offline'; });
}

export function createAwarenessStatusWidget(wsProvider, win = window) {
const statusDiv = win.document.createElement('div');
statusDiv.classList = 'collab-awareness';
statusDiv.innerHTML = `<div class="collab-other-users">
<div><img class="collab-connection collab-icon"></div>
<div class="collab-users"></div>
</div>`;

const container = win.document.querySelector('da-title').shadowRoot.children[0];
container.insertBefore(statusDiv, container.children[1]);

handleAwarenessUpdates(wsProvider, statusDiv, win);
return statusDiv;
const daTitle = win.document.querySelector('da-title');
handleAwarenessUpdates(wsProvider, daTitle, win);
return daTitle;
}

export default function initProse({ editor, path }) {
Expand All @@ -170,7 +130,7 @@ export default function initProse({ editor, path }) {
}

const wsProvider = new WebsocketProvider(server, roomName, ydoc, opts);
const statusDiv = createAwarenessStatusWidget(wsProvider);
const daTitle = createAwarenessStatusWidget(wsProvider);

const yXmlFragment = ydoc.getXmlFragment('prosemirror');

Expand All @@ -195,7 +155,8 @@ export default function initProse({ editor, path }) {
const svrUpdate = ydoc.getMap('aem').get(serverInvKey);
if (svrUpdate) {
// push update from the server: re-init document
statusDiv.remove();
delete daTitle.collabStatus;
delete daTitle.collabUsers;
ydoc.destroy();
wsProvider.destroy();
editor.innerHTML = '';
Expand Down

0 comments on commit 4406106

Please sign in to comment.