/** * Copyright Aquera Inc 2023 * * This source code is licensed under the BSD-3-Clause license found in the * LICENSE file in the root directory of this source tree. */ import { html, nothing, CSSResultArray, TemplateResult } from 'lit'; import { customElement, property } from 'lit/decorators.js'; import { classMap } from 'lit/directives/class-map.js'; import { styles } from './nile-empty-state.css'; import NileElement from '../internal/nile-element'; import { ifDefined } from 'lit/directives/if-defined.js'; /** What happened. Selects an icon, tone, default copy and ARIA role. */ export type NileEmptyStateStatus = | 'empty' | 'no-results' | 'not-found' | 'no-access' | 'error' | 'offline'; /** How loud the state looks. Defaults from `status`, always overridable. */ export type NileEmptyStateTone = 'neutral' | 'info' | 'warning' | 'error'; /** What shape the state occupies. */ export type NileEmptyStateLayout = 'stack' | 'inline' | 'page'; interface StatusPreset { icon: string; tone: NileEmptyStateTone; text: string; subText: string; role: 'status' | 'alert'; live: 'polite' | 'assertive'; } const STATUS_PRESETS: Record = { 'empty': { icon: 'ng-database', tone: 'neutral', role: 'status', live: 'polite', text: 'No data yet', subText: `Rows appear here once there's something to show.`, }, 'no-results': { icon: 'ng-search-lg', tone: 'neutral', role: 'status', live: 'polite', text: 'No matches', subText: 'Try removing a filter or broadening your search.', }, 'not-found': { icon: 'ng-file-06', tone: 'warning', role: 'status', live: 'polite', text: `We couldn't find that`, subText: 'It may have been moved or deleted.', }, 'no-access': { icon: 'ng-lock', tone: 'warning', role: 'status', live: 'polite', text: `You don't have access`, subText: 'Ask an admin to grant you access to this resource.', }, 'error': { icon: 'ng-alert-circle', tone: 'error', role: 'alert', live: 'assertive', text: 'Something went wrong', subText: `We couldn't load this. Try again.`, }, 'offline': { icon: 'ng-cloud-off', tone: 'error', role: 'alert', live: 'assertive', text: 'Connection lost', subText: 'Check your connection and try again.', }, }; const TONE_GLYPH_COLOR: Record = { neutral: undefined, info: 'var(--nile-colors-primary-600, var(--ng-colors-fg-brand-primary-600))', warning: 'var(--nile-colors-yellow-700, var(--ng-colors-fg-warning-primary))', error: 'var(--nile-colors-red-700, var(--ng-colors-fg-error-primary))', }; const DEFAULT_ICON = 'ng-x-circle'; const DEFAULT_TEXT = 'Empty State'; const DEFAULT_SUB_TEXT = 'No Data'; /** * Nile empty-state component. * * @tag nile-empty-state * */ @customElement('nile-empty-state') export class NileEmptyState extends NileElement { /** * The styles for nile-empty-state * @remarks If you are extending this class you can extend the base styles with super. Eg `return [super(), myCustomStyles]` */ public static get styles(): CSSResultArray { return [styles]; } @property({ type: String, attribute: true }) size: 'sm' | 'md' | 'lg' = 'md'; @property({ type: String, attribute: true }) variant: | 'flat' | 'content' | 'tonal' = 'tonal'; @property({ type: String, attribute: true, reflect: true }) status?: NileEmptyStateStatus; @property({ type: String, attribute: true, reflect: true }) layout: NileEmptyStateLayout = 'stack'; @property({ type: String, attribute: true }) code?: string; @property({ type: Boolean, attribute: true, reflect: true }) bordered: boolean = false; @property({ type: String, attribute: true }) svgIconUrl: string; /** * Overrides the layout-driven glyph size for a custom `svgIconUrl`. Left unset, * the SVG matches whatever size the current variant/layout draws at. */ @property({ type: Number, attribute: true }) svgIconSize?: number; @property({ type: String, attribute: true }) svgIconColor: string; private _icon?: string; private _text?: string; private _subText?: string; private _tone?: NileEmptyStateTone; @property({ type: String, attribute: true }) get icon(): string { return this._icon ?? this.preset?.icon ?? DEFAULT_ICON; } set icon(value: string | null) { const old = this.icon; this._icon = value ?? undefined; this.requestUpdate('icon', old); } /** Headline. Resolves through the `status` preset to `DEFAULT_TEXT`. */ @property({ type: String, attribute: true }) get text(): string { return this._text ?? this.preset?.text ?? DEFAULT_TEXT; } set text(value: string | null) { const old = this.text; this._text = value ?? undefined; this.requestUpdate('text', old); } /** Supporting line. Resolves through the `status` preset to `DEFAULT_SUB_TEXT`. */ @property({ type: String, attribute: 'sub-text' }) get subText(): string { return this._subText ?? this.preset?.subText ?? DEFAULT_SUB_TEXT; } set subText(value: string | null) { const old = this.subText; this._subText = value ?? undefined; this.requestUpdate('subText', old); } @property({ type: String, attribute: true, reflect: true }) get tone(): NileEmptyStateTone { return this._tone ?? this.preset?.tone ?? 'neutral'; } set tone(value: NileEmptyStateTone | null) { const old = this.tone; this._tone = value ?? undefined; this.requestUpdate('tone', old); } /* #endregion */ /* #region Methods */ /** The preset backing the current `status`, or undefined when none is set. */ private get preset(): StatusPreset | undefined { return this.status ? STATUS_PRESETS[this.status] : undefined; } private get glyphColor(): string | undefined { return this.svgIconColor ?? TONE_GLYPH_COLOR[this.tone]; } /** ARIA wiring */ private get liveRegion(): { role: string; live: string } | undefined { const preset = this.preset; if (!preset) return undefined; return { role: preset.role, live: preset.live }; } private renderGlyph(size: number | string): TemplateResult { const color = this.glyphColor; return this.svgIconUrl ? html` ` : html` `; } /** * Render method * @slot This is a slot test */ public render(): TemplateResult { const live = this.liveRegion; const inline = this.layout === 'inline'; return html`
${inline ? html` ${this.renderGlyph(20)} ` : nothing } ${!inline && this.variant == 'content' ? html`
${!this.icon && !this.svgIconUrl ? nothing : html`
${ this.renderGlyph(this.size == 'lg' ? 28 : 24)}
` }
`: nothing } ${!inline && this.variant == 'tonal' ? html`
${this.renderGlyph(40)}
`: nothing } ${!inline && this.variant == 'flat' ? html`
${this.renderGlyph(this.size == 'lg' ? 28 : 24)}
`: nothing }
${this.code ? html`
${this.code}
` : nothing}
${this.text}
${this.subText}
`; } /* #endregion */ } export default NileEmptyState; declare global { interface HTMLElementTagNameMap { 'nile-empty-state': NileEmptyState; } }