From 4406106ff20b5b2100d82f3243877ec5ac3b6742 Mon Sep 17 00:00:00 2001 From: Chris Millar Date: Tue, 5 Mar 2024 19:26:50 -0700 Subject: [PATCH] Collab UX Polish * 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 --- blocks/edit/da-title/da-title.css | 78 +++++++++++++++- blocks/edit/da-title/da-title.js | 89 ++++++++++++++----- .../edit/img/Smock_CloudDisconnected_18_N.svg | 7 ++ blocks/edit/img/Smock_CloudError_18_N.svg | 7 ++ blocks/edit/img/Smock_Cloud_18_N.svg | 7 ++ .../img/Smock_CloudDisconnected_18_N.svg | 12 --- .../edit/prose/img/Smock_CloudError_18_N.svg | 12 --- blocks/edit/prose/img/Smock_Cloud_18_N.svg | 11 --- .../Smock_RealTimeCustomerProfile_18_N.svg | 11 --- blocks/edit/prose/index.js | 63 +++---------- 10 files changed, 178 insertions(+), 119 deletions(-) create mode 100644 blocks/edit/img/Smock_CloudDisconnected_18_N.svg create mode 100644 blocks/edit/img/Smock_CloudError_18_N.svg create mode 100644 blocks/edit/img/Smock_Cloud_18_N.svg delete mode 100644 blocks/edit/prose/img/Smock_CloudDisconnected_18_N.svg delete mode 100644 blocks/edit/prose/img/Smock_CloudError_18_N.svg delete mode 100644 blocks/edit/prose/img/Smock_Cloud_18_N.svg delete mode 100644 blocks/edit/prose/img/Smock_RealTimeCustomerProfile_18_N.svg diff --git a/blocks/edit/da-title/da-title.css b/blocks/edit/da-title/da-title.css index 73de77f1..6ec64a98 100644 --- a/blocks/edit/da-title/da-title.css +++ b/blocks/edit/da-title/da-title.css @@ -3,6 +3,10 @@ padding: 48px 0; } +:host > svg { + display: none; +} + h1 { margin-top: 0; } @@ -54,7 +58,7 @@ h1 { opacity: 1; } -.da-title-name-label:before { +.da-title-name-label::before { display: block; content: ''; position: absolute; @@ -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; diff --git a/blocks/edit/da-title/da-title.js b/blocks/edit/da-title/da-title.js index 17a738a9..d1cf05fd 100644 --- a/blocks/edit/da-title/da-title.js +++ b/blocks/edit/da-title/da-title.js @@ -1,12 +1,28 @@ -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: {}, }; @@ -14,6 +30,7 @@ export default class DaTitle extends LitElement { super.connectedCallback(); this.shadowRoot.adoptedStyleSheets = [sheet]; this._actionsVis = false; + inlinesvg({ parent: this.shadowRoot, paths: ICONS }); } async handleAction(action) { @@ -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`
${initials.join('')}
`; + })}`; + } + + renderCollab() { + return html` +
+ ${this.collabUsers && this.collabUsers.length > 1 ? this.renderColabUsers() : nothing} +
+ +
+
`; + } + render() { return html`
@@ -58,25 +104,28 @@ export default class DaTitle extends LitElement { class="da-title-name-label">${this.details.parentName}

${this.details.name}

-
- - - +
+ ${this.collabStatus ? this.renderCollab() : nothing} +
+ + + +
`; diff --git a/blocks/edit/img/Smock_CloudDisconnected_18_N.svg b/blocks/edit/img/Smock_CloudDisconnected_18_N.svg new file mode 100644 index 00000000..7132c866 --- /dev/null +++ b/blocks/edit/img/Smock_CloudDisconnected_18_N.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/blocks/edit/img/Smock_CloudError_18_N.svg b/blocks/edit/img/Smock_CloudError_18_N.svg new file mode 100644 index 00000000..e2bb47eb --- /dev/null +++ b/blocks/edit/img/Smock_CloudError_18_N.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/blocks/edit/img/Smock_Cloud_18_N.svg b/blocks/edit/img/Smock_Cloud_18_N.svg new file mode 100644 index 00000000..a4ea1434 --- /dev/null +++ b/blocks/edit/img/Smock_Cloud_18_N.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/blocks/edit/prose/img/Smock_CloudDisconnected_18_N.svg b/blocks/edit/prose/img/Smock_CloudDisconnected_18_N.svg deleted file mode 100644 index 810c4782..00000000 --- a/blocks/edit/prose/img/Smock_CloudDisconnected_18_N.svg +++ /dev/null @@ -1,12 +0,0 @@ - - - - - S CloudDisconnected 18 N - - - \ No newline at end of file diff --git a/blocks/edit/prose/img/Smock_CloudError_18_N.svg b/blocks/edit/prose/img/Smock_CloudError_18_N.svg deleted file mode 100644 index 10c36214..00000000 --- a/blocks/edit/prose/img/Smock_CloudError_18_N.svg +++ /dev/null @@ -1,12 +0,0 @@ - - - - - S CloudError 18 N - - - \ No newline at end of file diff --git a/blocks/edit/prose/img/Smock_Cloud_18_N.svg b/blocks/edit/prose/img/Smock_Cloud_18_N.svg deleted file mode 100644 index b647d589..00000000 --- a/blocks/edit/prose/img/Smock_Cloud_18_N.svg +++ /dev/null @@ -1,11 +0,0 @@ - - - - - S Cloud 18 N - - \ No newline at end of file diff --git a/blocks/edit/prose/img/Smock_RealTimeCustomerProfile_18_N.svg b/blocks/edit/prose/img/Smock_RealTimeCustomerProfile_18_N.svg deleted file mode 100644 index f2d7bfe6..00000000 --- a/blocks/edit/prose/img/Smock_RealTimeCustomerProfile_18_N.svg +++ /dev/null @@ -1,11 +0,0 @@ - - - - - S RealTimeCustomerProfile 18 N - - \ No newline at end of file diff --git a/blocks/edit/prose/index.js b/blocks/edit/prose/index.js index 8d5ee9dd..2c65c258 100644 --- a/blocks/edit/prose/index.js +++ b/blocks/edit/prose/index.js @@ -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(); @@ -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(`

${initial}

`); - } else { - html = html.concat(`
- Other active user
`); - } - } - 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 = `
-
-
-
`; - - 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 }) { @@ -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'); @@ -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 = '';