<div id="example-notification--stacked" class="cwf-notification cwf-notification--secondary ">
<div class="cwf-notification__container cwf-notification__container--stacked">
<div class="cwf-notification__header">
<div class="cwf-notification__icon">
🐏
</div>
<span class="cwf-notification__title">Notifications can also be stacked!</span>
</div>
<div class="cwf-notification__body">
Depending on the content, a stacked version may be good because it utilizes more space for the title and body content.
</div>
</div>
</div>
{% set icons = {
alert: '<i class="fas fa-bullhorn"></i>',
info: '<i class="far fa-sticky-note"></i>',
primary: '🐏',
secondary: '🐏',
success: '<i class="far fa-check-circle"></i>'
} %}
<div id="{{ id ?? 'cwf-notification' }}"
class="cwf-notification cwf-notification--{{ theme }} {{
compact
? 'cwf-notification--compact'
-}}"
{{ dismissable ? 'aria-expanded="false"' }}>
<div class="cwf-notification__container {{
stacked
? 'cwf-notification__container--stacked'
}}">
{% if title %}
<div class="cwf-notification__header">
<div class="cwf-notification__icon">
{{ attribute(icons, theme) }}
</div>
<span class="cwf-notification__title">{{ title }}</span>
</div>
{% endif %}
<div class="cwf-notification__body">
{{ body }}
</div>
{% if dismissable %}
<button class="cwf-notification__close"
aria-label="Close notification">
<span>Close</span>
{% include '../../shared/icons/times-solid.svg' with {
class: 'cwf-notification__times',
role: 'presentation'
} %}
</button>
{% endif %}
</div>
</div>
{
"id": "example-notification--stacked",
"theme": "secondary",
"title": "Notifications can also be stacked!",
"body": " Depending on the content, a stacked version may be good because it utilizes more space for the title and body content.",
"stacked": true,
"dismissable": false,
"compact": false
}
// Context notification component styles
@use "sass:map";
@use "../../shared/media";
@use "../../shared/style";
@use "../../shared/theme";
@use "../../utilities/marker/shared" as marker;
// Selector prefix
$prefix: "cwf" !default;
.#{$prefix}-notification {
margin-bottom: 1rem;
padding: 0.5rem 0;
background-color: var(--cwf-notification--primary-color);
border-bottom: 10px solid var(--cwf-notification--secondary-color);
font-family: theme.font--sans-serif();
color: var(--cwf-notification--body-color);
@include media.breakpoint {
padding: 1rem 0;
}
}
.#{$prefix}-notification[aria-expanded="false"] {
display: none;
}
// Notification alert colors
$color--body--alert: style.color("white") !default;
$color--primary--alert: style.color("red", "accent") !default;
$color--secondary--alert: style.darken(
style.color("red", "accent"),
28%
) !default;
$color--title--alert: style.color("white") !default;
// Notification info colors
$color--body--info: style.color("white") !default;
$color--primary--info: style.color("blue", "accent") !default;
$color--secondary--info: style.darken(
style.color("blue", "accent"),
34.75%
) !default;
$color--title--info: style.color("white") !default;
// Notification primary colors
$color--body--primary: style.color("white") !default;
$color--primary--primary: style.darken("gray-dark", 33%) !default; // #222
$color--secondary--primary: style.color("gray") !default;
$color--title--primary: style.color("gold") !default;
// Notification secondary colors
$color--body--secondary: style.color("black") !default;
$color--primary--secondary: style.color("gold") !default;
$color--secondary--secondary: style.darken("gold", 20.5%) !default;
$color--title--secondary: style.color("black") !default;
// Notification success colors
$color--body--success: style.color("white") !default;
$color--primary--success: style.color("green", "accent") !default;
$color--secondary--success: style.darken(
style.color("green", "accent"),
40.5%
) !default;
$color--title--success: style.color("white") !default;
// Notification themes
$themes: (
"alert": (
"body": $color--body--alert,
"primary": $color--primary--alert,
"secondary": $color--secondary--alert,
"title": $color--title--alert
),
"info": (
"body": $color--body--info,
"primary": $color--primary--info,
"secondary": $color--secondary--info,
"title": $color--title--info
),
"primary": (
"body": $color--body--primary,
"primary": $color--primary--primary,
"secondary": $color--secondary--primary,
"title": $color--title--primary
),
"secondary": (
"body": $color--body--secondary,
"primary": $color--primary--secondary,
"secondary": $color--secondary--secondary,
"title": $color--title--secondary
),
"success": (
"body": $color--body--success,
"primary": $color--primary--success,
"secondary": $color--secondary--success,
"title": $color--title--success
)
);
$theme--keys: "body", "primary", "secondary", "title", "theme";
@function theme--validate($instructions) {
@return map.keys($instructions) == $theme--keys;
}
@mixin theme($instructions) {
@if theme--validate($instructions) {
$theme: map.get($instructions, "theme");
$body: map.get($instructions, "body");
$primary: map.get($instructions, "primary");
$secondary: map.get($instructions, "secondary");
$title: map.get($instructions, "title");
.#{$prefix}-notification--#{$theme} {
--cwf-notification--body-color: #{$body};
--cwf-notification--primary-color: #{$primary};
--cwf-notification--secondary-color: #{$secondary};
--cwf-notification--title-color: #{$title};
}
} @else {
@warn "Invalid notification themes provided!";
}
}
@mixin theme--official($theme) {
$colors: map.get($themes, $theme);
$instructions: map.merge(
$colors,
(
"theme": $theme
)
);
@include theme($instructions);
}
@each $theme, $colors in $themes {
@include theme--official($theme);
}
.#{$prefix}-notification__container {
align-items: center;
display: flex;
flex-direction: column;
flex-wrap: wrap;
justify-content: space-between;
@include theme.contain;
@include media.breakpoint {
flex-direction: row;
flex-wrap: nowrap;
}
}
.#{$prefix}-notification__header {
align-items: center;
color: var(--cwf-notification--title-color);
display: flex;
flex-wrap: nowrap;
font-size: 1.25rem;
font-weight: 700;
justify-content: center;
order: 1;
padding: 0.5rem 1rem;
@include media.breakpoint {
align-items: flex-start;
border-right: 1px solid var(--cwf-notification--secondary-color);
justify-content: flex-start;
max-width: 480px;
margin: 0 1rem 0 0;
order: 0;
}
}
.#{$prefix}-notification__icon,
.#{$prefix}-notification__title {
padding: 0 0.5rem;
}
.#{$prefix}-notification__body {
align-items: center;
justify-content: center;
order: 1;
padding: 0 0.5rem 1rem;
text-align: center;
@include media.breakpoint {
margin-right: auto;
order: 0;
padding: 0 1rem;
text-align: left;
}
@include style.children;
a {
color: var(--cwf-notification--title-color);
}
.#{$prefix}-button {
background-color: var(--cwf-notification--primary-color) !important;
border-color: var(--cwf-notification--secondary-color) !important;
border-width: 2px;
color: var(--cwf-notification--body-color) !important;
margin: 0 1rem;
&:focus,
&:hover {
background-color: var(
--cwf-notification--secondary-color
) !important;
}
}
}
$close__background-color: transparent !default;
$close__color: style.color("black") !default;
.#{$prefix}-notification__close {
align-items: center;
align-self: flex-end;
background-color: var(--cwf-notification__close--background-color);
border: none;
border-radius: 4px;
color: var(--cwf-notification__close--color);
display: flex;
font-size: var(--cwf-notification--font-size);
justify-content: right;
line-height: 1;
margin: 0rem 1rem;
padding: 0.25rem;
color: var(--cwf-notification--body-color);
--cwf-notification__close--background-color: #{$close__background-color};
--cwf-notification__close--color: #{$close__color};
@include media.breakpoint {
align-self: flex-start;
width: auto;
}
@include style.cursor;
&:hover,
&:focus {
background-color: var(--cwf-notification--secondary-color);
}
}
.#{$prefix}-notification__close span {
align-content: flex-end;
}
.#{$prefix}-notification__times {
flex: 1;
min-width: 0.8rem;
margin-left: 0.5rem;
width: 0.8rem;
}
// Compact variation
.#{$prefix}-notification--compact {
padding: 0.5rem 0;
.#{$prefix}-notification__header {
border: none;
font-size: 1rem;
margin-right: 0;
}
.#{$prefix}-notification__close {
font-size: 0.9rem;
}
}
// Stacked variation
@include media.breakpoint {
.#{$prefix}-notification__container--stacked {
flex-direction: row-reverse;
flex-wrap: wrap;
padding: 0 1rem;
.#{$prefix}-notification__header,
.#{$prefix}-notification__body {
border-right: none;
max-width: none;
padding: 0;
width: 100%;
}
.#{$prefix}-notification__header {
flex: 1;
}
.#{$prefix}-notification__body {
padding-left: 3rem;
}
.#{$prefix}-notification__close {
align-self: flex-start;
order: -1;
}
}
.#{$prefix}-notification--compact
.#{$prefix}-notification__container--stacked
.#{$prefix}-notification__body {
padding-left: 2.6rem !important;
}
}
// The default component class
import { Component } from '../../shared/component.js';
// Cookie library
import Cookies from 'js-cookie';
// Provide functionality to all accordions
export class Notification extends Component {
constructor({
prefix = 'cwf',
notification = 'notification',
dismissable = 'notification__close'
} = {}) {
super({
prefix,
classes: {
notification,
dismissable
}
});
// Bind "this" to the necessary methods
this.onClick = this.onClick.bind(this);
}
// Find an the parent notification of a dismissable button.
findParentNotification(dismissable) {
const notification = dismissable.parentNode.parentNode;
if (notification.classList.contains(this.classes.notification))
return notification;
return null;
}
// Get references to all accordion elements
getReferences() {
// Attempt to grab all dismissables...
this.references = Array.from(
document.querySelectorAll(this.selectors.dismissable)
);
// ... and if none are found, do nothing else
if (!this.references.length) return;
// Finally, convert each reference to an object containing the dismissable and its notification
this.references = this.references
.map((dismissable) => {
const notification = this.findParentNotification(dismissable);
if (!notification) return null;
return {
notification,
dismissable
};
})
.filter(Boolean);
}
// If the user doesn't already have a dismissable cookie set, then toggle the aria-expanded attribute
toggleNotificationDisplay({ notification }) {
if (!notification) return;
// Check if the user has already dismissed this notification
const notificationAlreadyDismissed =
this.checkExistenceOfNotificationDismissedCookie(notification);
if (notificationAlreadyDismissed) return;
// ... if not, then toggle the aria-expanded attribute
const attribute = 'aria-expanded';
const currentValue = notification.getAttribute(attribute) === 'true';
notification.setAttribute(attribute, !currentValue);
}
// Check if the user has a cookie already set that corresponds to it's unique id
checkExistenceOfNotificationDismissedCookie(notification) {
return Cookies.get(notification.id) === 'false';
}
// Sets a cookie with notification's unique id as the name and a value of false. Note, cookie values are always strings.
setNotificationDismissedCookie(notification) {
Cookies.set(notification.id, 'false', {
path: '/',
sameSite: 'strict'
});
}
// Handle click events
onClick(event) {
// Grab the current target of the event
const { currentTarget } = event;
// Get the parent notification of the clicked dismissable
const { notification } =
this.references.find(
({ dismissable }) => dismissable === currentTarget
) || {};
// If no notification was found, do nothing else
if (!notification) return;
// ... and toggle the notification
this.toggleNotificationDisplay({ notification });
// ... and set a cookie so we know this notification has been dismissed
this.setNotificationDismissedCookie(notification);
// Finally, prevent the default behavior...
event.preventDefault();
}
// Mount/unmount the notification functionality
mount(run) {
super.mount(run);
// If mounting, get/store all notification references...
if (run) this.getReferences();
// ... and if none were found, do nothing else
if (!this.references.length) return;
// If mounting,...
if (run)
// ... toggle all aria-expanded attributes to true
this.references.forEach(this.toggleNotificationDisplay.bind(this));
// For each referenced modal's dismissable,...
this.references.forEach(({ dismissable }) =>
// ... bind/unbind to its click event
dismissable[this.listener]('click', this.onClick)
);
// Finally, if unmounting, reset all references
if (!run) this.references = {};
}
}
The notification component can be used at either the top of the page (below the VCU branding bar), the main content area or the sidebar. It should be used to draw the user’s attention to something important.
On small screens the content in the notification will be center aligned. On larger screens content will be left aligned and wrapped in a container with a max-width of 1400px.
This is the general body of content for the notification. Your basic text based WYSIWYG type of content will generally look okay in this area. It is not recommended to insert images or blockquotes into the notification.
The notification component has the ability to display with a variety of background colors depending on the theme attribute. Carefully consider the theme of the notification based on the context of the content.
The associated icon that is displayed next to the title is defined by the notification’s theme. Please only use icons defined in this framework.
Notifications are configurable and provide additional options past the two required fields.
The notification component has the option to display in compact mode, which will reduce the font size and padding of the wrapping block.
Individual notifications have the option to be dismissable by the user. If this option is selected ensure you’ve added a specific id and the aria-expanded="false"
attribute for each unique dismissable notification.
When a page that includes dismissable notifications is loaded, the aria-expanded="false"
attributes on each notification are automatically toggled to aria-expanded="true"
via the toggleNotificationDisplay()
method, resulting in the user being able to interact with the notification content.
When a user dismisses a specific notification, a session based cookie will be set in the browser that corresponds to the notification’s unique id with a value of ‘false’ via the setNotificationDismissedCookie()
method. Additionally, the notification will have it’s aria-expanded
attribute toggled back to aria-expanded="false"
, resulting in the notification being hidden.
When the page is reloaded or a new page is navigated to, the notification javascript will again prepare to toggle all aria-expanded
attributes to aria-expanded="true"
, but this time will only toggle notifications open that do not have a corresponding dissmissed cookie already set.
Notification cookies are good for the duration of a user’s session and only correspond to that particular domain.
The notification has an option to stack the title and body on top of each other instead of side by side. This option may be good for notifications that require long body or title content. To stack the notification add the .cwf-notification__container--row
class to the .cwf-notification__container
.
The title is optional in notifications. If empty, the notification body will fill the rest of the space and no icon will be displayed.
Notifications are implemented in T4 as the “Notification” plugin, meaning its classes are .plugin-
prefixed instead of .cwf-
prefixed.
This plugin can be used within the global “Site-Header”, “Site-Sidebar”, and “Site-Footer” sections to have it displayed globally within the header, sidebar, and footer areas respectively.
In the “Injectors” field of the “Notification” plugin, the following injectors can be used:
area:{sidebar}
- Moves the card to the sidebar area (right of the main content).id:{custom_id}
- Overrides the default, T4 ID of the notification with a custom ID.class:{custom_classes}
- Adds custom classes to the notification.style:{custom_styles}
- Adds custom styles to a style
attribute of the notification.before:{custom_html}
- Adds custom HTML before the notification.after:{custom_html}
- Adds custom HTML after the notifcation.