Better notification UX and UI

This commit is contained in:
sairaj mote 2023-09-27 15:45:04 +05:30
parent d655631df7
commit b5e69a4af8
3 changed files with 117 additions and 137 deletions

View File

@ -20,7 +20,7 @@ smNotifications.innerHTML = `
} }
.notification-panel{ .notification-panel{
display: grid; display: grid;
width: 100%; width: min(26rem, 100%);
gap: 0.5rem; gap: 0.5rem;
position: fixed; position: fixed;
left: 0; left: 0;
@ -29,65 +29,55 @@ smNotifications.innerHTML = `
max-height: 100%; max-height: 100%;
padding: 1rem; padding: 1rem;
overflow: hidden auto; overflow: hidden auto;
-ms-scroll-chaining: none; overscroll-behavior: contain;
overscroll-behavior: contain;
touch-action: none; touch-action: none;
} }
.notification-panel:empty{ .notification-panel:empty{
display:none; display:none;
} }
.notification{ .notification{
display: -webkit-box;
display: -ms-flexbox;
display: flex; display: flex;
position: relative; position: relative;
border-radius: 0.3rem; border-radius: 0.5rem;
background: rgba(var(--foreground-color, (255,255,255)), 1); background: rgba(var(--foreground-color, (255,255,255)), 1);
overflow: hidden; overflow: hidden;
overflow-wrap: break-word; overflow-wrap: break-word;
word-wrap: break-word; word-wrap: break-word;
-ms-word-break: break-all;
word-break: break-all; word-break: break-all;
word-break: break-word; word-break: break-word;
-ms-hyphens: auto;
-webkit-hyphens: auto;
hyphens: auto; hyphens: auto;
max-width: 100%; max-width: 100%;
padding: 1rem; padding: max(1rem,1.5vw);
align-items: center; align-items: center;
box-shadow: 0 0.5rem 1rem 0 rgba(0,0,0,0.14); box-shadow: 0 0.5rem 1rem 0 rgba(0,0,0,0.14);
touch-action: none; touch-action: none;
} }
.notification:not(.pinned)::before{
content: '';
position: absolute;
bottom: 0;
left: 0;
height: 0.2rem;
width: 100%;
background-color: var(--accent-color, teal);
transform: scaleX(0);
animation: loading var(--timeout, 5000ms) linear forwards;
transform-origin: left;
}
@keyframes loading{
0%{
transform: scaleX(0);
}
100%{
transform: scaleX(1);
}
}
.icon-container:not(:empty){ .icon-container:not(:empty){
margin-right: 0.5rem; margin-right: 0.5rem;
height: var(--icon-height); height: var(--icon-height);
width: var(--icon-width); width: var(--icon-width);
flex-shrink: 0; flex-shrink: 0;
} }
h4:first-letter,
p:first-letter{
text-transform: uppercase;
}
h4{
font-weight: 400;
}
p{
line-height: 1.6;
-webkit-box-flex: 1;
-ms-flex: 1;
flex: 1;
color: rgba(var(--text-color, (17,17,17)), 0.9);
overflow-wrap: break-word;
overflow-wrap: break-word;
word-wrap: break-word;
-ms-word-break: break-all;
word-break: break-all;
word-break: break-word;
-ms-hyphens: auto;
-webkit-hyphens: auto;
hyphens: auto;
max-width: 100%;
}
.notification:last-of-type{ .notification:last-of-type{
margin-bottom: 0; margin-bottom: 0;
} }
@ -98,11 +88,14 @@ smNotifications.innerHTML = `
} }
.icon--success { .icon--success {
fill: var(--green); fill: var(--green);
} }
.icon--failure, .icon--failure,
.icon--error { .icon--error {
fill: var(--danger-color); fill: var(--danger-color);
} }
output{
width: 100%;
}
.close{ .close{
height: 2rem; height: 2rem;
width: 2rem; width: 2rem;
@ -139,8 +132,6 @@ smNotifications.innerHTML = `
} }
@media screen and (min-width: 640px){ @media screen and (min-width: 640px){
.notification-panel{ .notification-panel{
max-width: 28rem;
width: max-content;
top: auto; top: auto;
bottom: 0; bottom: 0;
} }
@ -187,7 +178,7 @@ customElements.define('sm-notifications', class extends HTMLElement {
this.removeNotification = this.removeNotification.bind(this) this.removeNotification = this.removeNotification.bind(this)
this.clearAll = this.clearAll.bind(this) this.clearAll = this.clearAll.bind(this)
this.remove = this.remove.bind(this) this.remove = this.remove.bind(this)
this.handlePointerMove = this.handlePointerMove.bind(this) this.handleTouchMove = this.handleTouchMove.bind(this)
this.startX = 0; this.startX = 0;
@ -200,12 +191,16 @@ customElements.define('sm-notifications', class extends HTMLElement {
this.swipeTime = 0; this.swipeTime = 0;
this.swipeTimeThreshold = 200; this.swipeTimeThreshold = 200;
this.currentTarget = null; this.currentTarget = null;
this.notificationTimeout = 5000;
this.mediaQuery = window.matchMedia('(min-width: 640px)') this.mediaQuery = window.matchMedia('(min-width: 640px)')
this.handleOrientationChange = this.handleOrientationChange.bind(this) this.handleOrientationChange = this.handleOrientationChange.bind(this)
this.isLandscape = false this.isBigViewport = false
}
set timeout(value) {
if (isNaN(value)) return;
this.notificationTimeout = value;
} }
randString(length) { randString(length) {
let result = ''; let result = '';
const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'; const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
@ -215,46 +210,44 @@ customElements.define('sm-notifications', class extends HTMLElement {
} }
createNotification(message, options = {}) { createNotification(message, options = {}) {
const { pinned = false, icon = '', action } = options; const { pinned = false, icon, action, timeout = this.notificationTimeout } = options;
const notification = document.createElement('div') const notification = document.createElement('div')
notification.id = this.randString(8) notification.id = this.randString(8)
notification.className = `notification ${pinned ? 'pinned' : ''}` notification.className = `notification ${pinned ? 'pinned' : ''}`
const iconContainer = document.createElement('div') notification.style.setProperty('--timeout', `${timeout}ms`);
iconContainer.className = 'icon-container' notification.innerHTML = `
iconContainer.innerHTML = icon ${icon ? `<div class="icon-container">${icon}</div>` : ''}
const output = document.createElement('output') <output>${message}</output>
output.textContent = message ${action ? `<button class="action">${action.label}</button>` : ''}
notification.append(iconContainer, output) ${pinned ? `<button class="close">
<svg class="icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24"><path fill="none" d="M0 0h24v24H0z"/><path d="M12 10.586l4.95-4.95 1.414 1.414-4.95 4.95 4.95 4.95-1.414 1.414-4.95-4.95-4.95 4.95-1.414-1.414 4.95-4.95-4.95-4.95L7.05 5.636z"/></svg>
</button>` : ''}
`;
if (action) { if (action) {
const button = document.createElement('button') notification.querySelector('.action').addEventListener('click', action.callback)
button.className = 'action'
button.innerText = action.label
button.addEventListener('click', action.callback)
} }
if (pinned) { if (pinned) {
const button = document.createElement('button') notification.querySelector('.close').addEventListener('click', () => {
button.className = 'close' this.removeNotification(notification);
button.innerHTML = ` });
<svg class="icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24"><path fill="none" d="M0 0h24v24H0z"/><path d="M12 10.586l4.95-4.95 1.414 1.414-4.95 4.95 4.95 4.95-1.414 1.414-4.95-4.95-4.95 4.95-1.414-1.414 4.95-4.95-4.95-4.95L7.05 5.636z"/></svg> } else {
` setTimeout(() => {
button.addEventListener('click', () => { this.removeNotification(notification, this.isBigViewport ? 'left' : 'top');
this.remove(notification.id) }, timeout);
})
notification.append(button)
} }
return notification; return notification;
} }
push(message, options = {}) { push(message, options = {}) {
const notification = this.createNotification(message, options); const notification = this.createNotification(message, options);
if (this.isLandscape) if (this.isBigViewport)
this.notificationPanel.append(notification); this.notificationPanel.append(notification);
else else
this.notificationPanel.prepend(notification); this.notificationPanel.prepend(notification);
this.notificationPanel.animate( this.notificationPanel.animate(
[ [
{ {
transform: `translateY(${this.isLandscape ? '' : '-'}${notification.clientHeight}px)`, transform: `translateY(${this.isBigViewport ? '' : '-'}${notification.clientHeight}px)`,
}, },
{ {
transform: `none`, transform: `none`,
@ -274,26 +267,40 @@ customElements.define('sm-notifications', class extends HTMLElement {
e.target.commitStyles() e.target.commitStyles()
e.target.cancel() e.target.cancel()
} }
if (notification.querySelector('.action'))
notification.querySelector('.action').addEventListener('click', options.action.callback)
return notification.id; return notification.id;
} }
removeNotification(notification, direction = 'left') { removeNotification(notification, direction = 'left') {
if (!notification) return; if (!notification) return;
const sign = direction === 'left' ? '-' : '+'; const sign = direction === 'left' || direction === 'top' ? '-' : '+';
notification.animate([
{ if (!this.isBigViewport && direction === 'top') {
transform: this.currentX ? `translateX(${this.currentX}px)` : `none`, notification.animate([
opacity: '1' {
}, transform: this.currentX ? `translateY(${this.currentX}px)` : `none`,
{ opacity: '1'
transform: `translateX(calc(${sign}${Math.abs(this.currentX)}px ${sign} 1rem))`, },
opacity: '0' {
} transform: `translateY(calc(${sign}${Math.abs(this.currentX)}px ${sign} 1rem))`,
], this.animationOptions).onfinish = () => { opacity: '0'
notification.remove(); }
}; ], this.animationOptions).onfinish = () => {
notification.remove();
};
} else {
notification.animate([
{
transform: this.currentX ? `translateX(${this.currentX}px)` : `none`,
opacity: '1'
},
{
transform: `translateX(calc(${sign}${Math.abs(this.currentX)}px ${sign} 1rem))`,
opacity: '0'
}
], this.animationOptions).onfinish = () => {
notification.remove();
};
}
} }
remove(id) { remove(id) {
const notification = this.notificationPanel.querySelector(`#${id}`); const notification = this.notificationPanel.querySelector(`#${id}`);
@ -307,13 +314,13 @@ customElements.define('sm-notifications', class extends HTMLElement {
}); });
} }
handlePointerMove(e) { handleTouchMove(e) {
this.currentX = e.clientX - this.startX; this.currentX = e.touches[0].clientX - this.startX;
this.currentTarget.style.transform = `translateX(${this.currentX}px)`; this.currentTarget.style.transform = `translateX(${this.currentX}px)`;
} }
handleOrientationChange(e) { handleOrientationChange(e) {
this.isLandscape = e.matches this.isBigViewport = e.matches
if (e.matches) { if (e.matches) {
// landscape // landscape
@ -326,22 +333,21 @@ customElements.define('sm-notifications', class extends HTMLElement {
this.handleOrientationChange(this.mediaQuery); this.handleOrientationChange(this.mediaQuery);
this.mediaQuery.addEventListener('change', this.handleOrientationChange); this.mediaQuery.addEventListener('change', this.handleOrientationChange);
this.notificationPanel.addEventListener('pointerdown', e => { this.notificationPanel.addEventListener('touchstart', e => {
if (e.target.closest('.close')) { if (e.target.closest('.close')) {
this.removeNotification(e.target.closest('.notification')); this.removeNotification(e.target.closest('.notification'));
} else if (e.target.closest('.notification')) { } else if (e.target.closest('.notification')) {
this.swipeThreshold = e.target.closest('.notification').getBoundingClientRect().width / 2; this.swipeThreshold = e.target.closest('.notification').getBoundingClientRect().width / 2;
this.currentTarget = e.target.closest('.notification'); this.currentTarget = e.target.closest('.notification');
this.currentTarget.setPointerCapture(e.pointerId);
this.startTime = Date.now(); this.startTime = Date.now();
this.startX = e.clientX; this.startX = e.touches[0].clientX;
this.startY = e.clientY; this.startY = e.touches[0].clientY;
this.notificationPanel.addEventListener('pointermove', this.handlePointerMove); this.notificationPanel.addEventListener('touchmove', this.handleTouchMove, { passive: true });
} }
}); }, { passive: true });
this.notificationPanel.addEventListener('pointerup', e => { this.notificationPanel.addEventListener('touchend', e => {
this.endX = e.clientX; this.endX = e.changedTouches[0].clientX;
this.endY = e.clientY; this.endY = e.changedTouches[0].clientY;
this.swipeDistance = Math.abs(this.endX - this.startX); this.swipeDistance = Math.abs(this.endX - this.startX);
this.swipeTime = Date.now() - this.startTime; this.swipeTime = Date.now() - this.startTime;
if (this.endX > this.startX) { if (this.endX > this.startX) {
@ -369,24 +375,9 @@ customElements.define('sm-notifications', class extends HTMLElement {
} }
} }
} }
this.notificationPanel.removeEventListener('pointermove', this.handlePointerMove) this.notificationPanel.removeEventListener('touchmove', this.handleTouchMove)
this.notificationPanel.releasePointerCapture(e.pointerId);
this.currentX = 0; this.currentX = 0;
}); });
const observer = new MutationObserver(mutationList => {
mutationList.forEach(mutation => {
if (mutation.type === 'childList') {
if (mutation.addedNodes.length && !mutation.addedNodes[0].classList.contains('pinned')) {
setTimeout(() => {
this.removeNotification(mutation.addedNodes[0]);
}, 5000);
}
}
});
});
observer.observe(this.notificationPanel, {
childList: true,
});
} }
disconnectedCallback() { disconnectedCallback() {
mediaQueryList.removeEventListener('change', handleOrientationChange); mediaQueryList.removeEventListener('change', handleOrientationChange);

File diff suppressed because one or more lines are too long

View File

@ -21,6 +21,7 @@
<script src="dist/tags-input.js"></script> <script src="dist/tags-input.js"></script>
<script src="dist/strip-select.js"></script> <script src="dist/strip-select.js"></script>
<script src="dist/collapsed-text.js"></script> <script src="dist/collapsed-text.js"></script>
<script src="dist/notifications.js"></script>
<link rel="stylesheet" href="css/main.min.css"> <link rel="stylesheet" href="css/main.min.css">
<style> <style>
div { div {
@ -35,35 +36,23 @@
</head> </head>
<body> <body>
<button>Show popup</button> <sm-notifications id="notification_drawer"></sm-notifications>
<collapsed-text> <button onclick="pushNotification()">
Loreetur adipisicing elit. Architecto minima maiores autem iusto porro, odit Send Notification
iure </button>
ea emus dolor itaque unde sequi, reprehenderit ex aperiam
dinfgw egweb gnw
slideInRightgsdgs
</collapsed-text>
<sm-popup>
<input type="text" placeholder="fds">
<button>dsfsd</button>
</sm-popup>
<text-field value="eetur adipisicing elit. Architecto minima maiores autem iusto porro, odit
iure"></text-field>
</body> </body>
<script> <script>
const popup = document.querySelector('sm-popup'); function pushNotification() {
const pPromise = popup.show({ payload: { opened: new Date() } }) document.getElementById('notification_drawer').push(`<h4>This is a notification.</h4><p>d snfkjsdn sdf sdfsd</p>`, {
pPromise.opened.then((data) => { timeout: 10000,
console.log('popup opened', data); action: {
}); label: 'click me',
pPromise.closed.then((data) => { callback: () => {
console.log('popup closed', data); console.log('clicked')
}); }
document.querySelector('button').addEventListener('click', () => { }
popup.show(); })
}); }
</script> </script>
</html> </html>