notifications
TosiNotification provides a singleton custom <tosi-notification> element that manages
a list of notifications.
Notifications stack up from the bottom of the screen, newest on top. Up to
TosiNotification.maxVisible (3) they sit in a column. Past that, the newest three stay in view
and the lowest of them becomes the front of a stack: older notifications sit underneath it,
newest first, each one's lower edge showing below
the card in front by TosiNotification.stackOffsets ([16, 8, 4], in px: each peeks out half
as far as the last). Any beyond that are hidden until the ones in front are closed. Close the
front card and the next one comes forward.
When more than one notification is showing, a Dismiss all button sits centred at the bottom,
over the edges of any stacked cards, outlined in the colour of the most severe notification open,
hidden ones included. It closes every notification, firing each one's close callback as if the
user had closed it; TosiNotification.dismissAll() does the same from code.
A long message is cut off with an ellipsis after four lines (--notification-max-lines), so the
close button always stays in view; the full text is the notification's tooltip, and screen
readers get all of it.
You can post a notification simply with TosiNotification.post() or postNotification().
interface NotificationSpec {
message: string
type?: 'success' | 'info' | 'log' | 'warn' | 'error' | 'progress' // default 'info'
icon?: SVGElement | string // defaults to an info icon
duration?: number
progress?: () => number // return percentage completion
close?: () => void
color?: string // specify color
}
If you provide a progress callback (which is assumed to return a number from 0-100, with
100+ indicating completion) then TosiNotification will poll it every second until the
task completes or the notification is closed. Returning 100 or more will automatically close
the notification.
If you configure a notification's type = "progress" but don't provide a progress callback
then an indefinite <progress> element will be displayed.
If you provide a close callback, it will be fired if the user closes the notification.
postNotification returns a callback function that closes the note programmatically (e.g.
when an operation completes). This will also call any close callback function you
provided. (The progress demos in the example exercise this functionality.)
import { postNotification, TosiNotification, icons } from 'tosijs-ui'
const form = preview.querySelector('tosi-form')
const submit = preview.querySelector('.submit')
const closeButton = preview.querySelector('.close')
let close
// read the field itself: the form's value omits a field nobody has edited
const maxVisible = () =>
Number(preview.querySelector('tosi-field[key=maxVisible] input').value) || 3
form.submitCallback = (value, isValid) => {
if (!isValid) return
TosiNotification.maxVisible = maxVisible()
if (value.type.startsWith('progress')) {
startTime = Date.now()
const { message, duration, icon } = value
close = postNotification({
message,
type: 'progress',
icon,
progress: value.type === 'progress' ? () => (Date.now() - startTime) / (10 * duration) : undefined,
close: () => { postNotification(`${value.message} cancelled`) },
})
} else {
close = postNotification(value)
}
closeButton.disabled = false
}
submit.addEventListener('click', form.submit)
preview.querySelector('.burst').addEventListener('click', () => {
TosiNotification.maxVisible = maxVisible()
const types = ['error', 'warn', 'info', 'success', 'log']
for (let i = 1; i <= 8; i++) {
postNotification({ message: `Notification ${i} of 8`, type: types[i % types.length] })
}
})
closeButton.addEventListener('click', () => {
if (close) {
close()
}
})
postNotification({
message: 'Welcome to tosijs-ui notifications, this message will disappear in 2s',
duration: 2
})
<tosi-form>
<h3 slot="header">Notification Test</h3>
<tosi-field caption="Message" key="message" type="string" value="This is a test…"></tosi-field>
<tosi-field caption="Type" key="type" value="info">
<tosi-select slot="input"
options="error,warn,info,success,log,,progress,progress (indefinite)"
></tosi-select>
</tosi-field>
<tosi-field caption="Icon" key="icon" value="info">
<tosi-select slot="input"
options="info,bug,thumbsUp,thumbsDown,message,spin120Loader"
></tosi-select>
</tosi-field>
<tosi-field caption="Duration" key="duration" type="number" value="2"></tosi-field>
<tosi-field caption="Max visible" key="maxVisible" type="number" value="3"></tosi-field>
<button slot="footer" class="close" disabled>Close Last Notification</button>
<button slot="footer" class="burst">Post 8</button>
<span slot="footer" class="elastic"></span>
<button slot="footer" class="submit">Post Notification</button>
</tosi-form>
tosi-form {
height: 100%;
}
tosi-form::part(content) {
display: flex;
flex-direction: column;
padding: 10px;
gap: 10px;
background: var(--background);
}
tosi-form::part(header),
tosi-form::part(footer) {
background: #eee;
justify-content: center;
padding: 10px;
}
tosi-form h3 {
margin: 0;
}
tosi-form label {
display: grid;
grid-template-columns: 120px 1fr;
}
test('notification singleton exists', async () => {
await waitMs(100)
const notification = document.querySelector('tosi-notification')
expect(notification).toBeTruthy()
})
test('form renders', () => {
const form = preview.querySelector('tosi-form')
expect(form).toBeTruthy()
})
postNotification(spec: NotificationSpec | string)
This is simply a wrapper for TosiNotification.post().