<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">
                &#x1F40F;
            </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: '&#x1F40F;',
    secondary: '&#x1F40F;',
    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
}
  • Content:
    // 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;
        }
    }
    
  • URL: /components/raw/notification/_index.scss
  • Filesystem Path: components/notification/_index.scss
  • Size: 8.2 KB
  • Content:
    // 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 = {};
        }
    }
    
  • URL: /components/raw/notification/index.js
  • Filesystem Path: components/notification/index.js
  • Size: 4.5 KB

Notification

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.

Requirements

Body

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.

Theme

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.

Options

Notifications are configurable and provide additional options past the two required fields.

Compact

The notification component has the option to display in compact mode, which will reduce the font size and padding of the wrapping block.

Dismissable

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.

Dismissable javascript workflow

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.

Stacked

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.

Title

The title is optional in notifications. If empty, the notification body will fill the rest of the space and no icon will be displayed.

T4 implementation

Notifications are implemented in T4 as the “Notification” plugin, meaning its classes are .plugin- prefixed instead of .cwf- prefixed.

Areas

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.

Injectors

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.