MOBILE-4268 core: Update error accordion UI

main
Noel De Martin 2024-03-18 16:07:08 +01:00
parent e1be8caace
commit 6b461b035e
5 changed files with 244 additions and 77 deletions

View File

@ -1,71 +1,143 @@
.core-error-accordion { .core-error-accordion {
background: var(--gray-200); --toggle-icon-animation-duration: 300ms;
--toggle-icon-animation-function: ease-in;
--background-color: var(--gray-300);
--toggle-icon-size: 16px;
background: var(--background-color);
border-radius: var(--radius-xs); border-radius: var(--radius-xs);
font-size: var(--body-font-size-sm);
color: var(--gray-900);
p:first-child {
margin-top: 0;
}
p:last-child {
margin-bottom: 0;
}
.core-error-accordion--code { .core-error-accordion--code {
padding: var(--spacing-2) var(--spacing-2) 0 var(--spacing-2); margin: 0;
font-size: var(--body-font-size-md); text-align: start;
color: var(--text-color-main);
font: var(--subtitle-md-font);
padding-top: var(--spacing-2);
padding-bottom: var(--spacing-2);
padding-inline-start: var(--spacing-3);
padding-inline-end: var(--spacing-3);
} }
.core-error-accordion--details p { .core-error-accordion--details {
padding: var(--spacing-2) var(--spacing-2) 0 var(--spacing-2); opacity: 0;
color: var(--gray-500); display: flex;
}
.core-error-accordion--checkbox {
display: none;
& + .core-error-accordion--details,
& + .core-error-accordion--code + .core-error-accordion--details {
max-height: 0;
overflow: hidden; overflow: hidden;
transition: max-height 600ms ease-in-out; flex-direction: column;
justify-content: center;
transition-property: opacity, height;
transition-duration: var(--toggle-icon-animation-duration);
transition-timing-function: var(--toggle-icon-animation-function);
& + .core-error-accordion--toggle { p {
margin: 0;
padding-top: var(--spacing-2);
padding-bottom: var(--spacing-2);
padding-inline-start: var(--spacing-3);
padding-inline-end: var(--spacing-3);
text-align: start;
font: var(--body-md-font);
color: var(--text-color-secondary);
}
}
.core-error-accordion--toggle {
display: flex; display: flex;
padding: var(--spacing-2);
min-height: var(--a11y-min-target-size);
align-items: center;
span {
width: 100%; width: 100%;
display: flex; align-items: center;
background: transparent;
justify-content: space-between; justify-content: space-between;
} color: var(--text-color-secondary);
font: var(--label-lg-font);
padding-top: var(--spacing-2);
padding-bottom: var(--spacing-2);
padding-inline-start: var(--spacing-3);
padding-inline-end: var(--spacing-3);
svg { .core-error-accordion--toggle-text {
fill: currentColor;
width: 11px;
}
.core-error-accordion--hide-content {
display: none;
}
}
}
&:checked + .core-error-accordion--details,
&:checked + .core-error-accordion--code + .core-error-accordion--details {
max-height: 110px;
& + .core-error-accordion--toggle .core-error-accordion--hide-content {
display: flex; display: flex;
flex-direction: column;
} }
& + .core-error-accordion--toggle .core-error-accordion--show-content { .core-error-accordion--show-details,
display: none; .core-error-accordion--hide-details {
text-align: start;
transition: opacity var(--toggle-icon-animation-duration) var(--toggle-icon-animation-function);
}
ion-icon {
width: var(--toggle-icon-size);
margin-inline-start: var(--spacing-4);
transition: transform var(--toggle-icon-animation-duration) var(--toggle-icon-animation-function);
transform: rotate(0);
}
&:hover {
background: var(--state-color-hover);
}
&:focus {
box-shadow: none;
background: var(--state-color-focused);
}
&:focus-visible {
box-shadow: none;
outline: 2px solid var(--state-color-keyboard-focus);
}
&:active {
background: var(--state-color-pressed);
}
}
&.hydrated {
width: var(--width);
.core-error-accordion--details {
height: 0;
}
.core-error-accordion--toggle {
.core-error-accordion--toggle-text {
flex-grow: 1;
position: relative;
}
.core-error-accordion--hide-details {
position: absolute;
opacity: 0;
top: 0;
bottom: 0;
left: 0;
right: 0;
}
}
}
&.expanded {
.core-error-accordion--details {
opacity: 1;
height: var(--description-height);
}
.core-error-accordion--toggle {
.core-error-accordion--hide-details {
opacity: 1;
}
.core-error-accordion--show-details {
opacity: 0;
}
ion-icon {
transform: rotate(180deg);
} }
} }
@ -73,3 +145,7 @@
} }
} }
html.dark .core-error-accordion {
--background-color: var(--gray-700);
}

View File

@ -14,7 +14,12 @@
import { Component, ElementRef, Input, OnChanges, OnInit } from '@angular/core'; import { Component, ElementRef, Input, OnChanges, OnInit } from '@angular/core';
import { Translate } from '@singletons'; import { Translate } from '@singletons';
import { CoreUtils } from '@services/utils/utils';
import { CoreDom } from '@singletons/dom';
import { CoreForms } from '@singletons/form'; import { CoreForms } from '@singletons/form';
import { CoreLogger } from '@singletons/logger';
const logger = CoreLogger.getInstance('CoreErrorAccordionComponent');
/** /**
* Component to show error details. * Component to show error details.
@ -32,41 +37,91 @@ export class CoreErrorAccordionComponent implements OnInit, OnChanges {
/** /**
* Render an instance of the component into an HTML string. * Render an instance of the component into an HTML string.
* *
* @param errorDetails Error details. * @param element Root element.
* @param errorCode Error code. * @param errorCode Error code.
* @returns Component HTML. * @param errorDetails Error details.
*/ */
static render(errorDetails: string, errorCode: string): string { static async render(element: Element, errorCode: string, errorDetails: string): Promise<void> {
const toggleId = CoreForms.uniqueId('error-accordion-toggle'); const html = this.html(errorCode, errorDetails);
element.innerHTML = html;
await this.hydrate(element);
}
/**
* Get component html.
*
* @param errorCode Error code.
* @param errorDetails Error details.
* @returns HTML.
*/
static html(errorCode: string, errorDetails: string): string {
const contentId = CoreForms.uniqueId('error-accordion-content');
const errorCodeLabel = Translate.instant('core.errorcode', { errorCode }); const errorCodeLabel = Translate.instant('core.errorcode', { errorCode });
const hideDetailsLabel = Translate.instant('core.errordetailshide'); const hideDetailsLabel = Translate.instant('core.errordetailshide');
const showDetailsLabel = Translate.instant('core.errordetailsshow'); const showDetailsLabel = Translate.instant('core.errordetailsshow');
return ` return `
<div class="core-error-accordion"> <div class="core-error-accordion">
<input id="${toggleId}" type="checkbox" class="core-error-accordion--checkbox" /> <h3 class="core-error-accordion--code">${errorCodeLabel}</h3>
<div class="core-error-accordion--code"><strong>${errorCodeLabel}</strong></div> <div id="${contentId}" class="core-error-accordion--details" role="region" aria-hidden="true">
<div class="core-error-accordion--details">
<p>${errorDetails}</p> <p>${errorDetails}</p>
</div> </div>
<label for="${toggleId}" class="core-error-accordion--toggle" aria-hidden="true"> <button type="button" class="core-error-accordion--toggle" aria-expanded="false" aria-controls="${contentId}">
<span class="core-error-accordion--hide-content"> <div class="core-error-accordion--toggle-text">
${hideDetailsLabel} <span class="core-error-accordion--show-details">
<ion-icon name="chevron-up" />
</span>
<span class="core-error-accordion--show-content">
${showDetailsLabel} ${showDetailsLabel}
<ion-icon name="chevron-down" />
</span> </span>
</label> <span class="core-error-accordion--hide-details">
${hideDetailsLabel}
</span>
</div>
<ion-icon name="chevron-down" />
</button>
</div> </div>
`; `;
} }
@Input() errorDetails!: string; /**
@Input() errorCode!: string; * Hydrate component.
*
* @param element Root element.
*/
static async hydrate(element: Element): Promise<void> {
const wrapper = element.querySelector<HTMLDivElement>('.core-error-accordion');
const description = element.querySelector<HTMLParagraphElement>('.core-error-accordion--details');
const button = element.querySelector<HTMLButtonElement>('.core-error-accordion--toggle');
const hideText = element.querySelector<HTMLSpanElement>('.core-error-accordion--hide-details');
constructor(private element: ElementRef) {} if (!wrapper || !description || !button || !hideText) {
logger.error('Couldn\'t render error-accordion, one of the child elements is missing');
return;
}
await CoreDom.waitToBeVisible(wrapper);
button.onclick = () => {
wrapper.classList.toggle('expanded');
description.setAttribute('aria-hidden', description.getAttribute('aria-hidden') === 'true' ? 'false' : 'true');
button.setAttribute('aria-expanded', button.getAttribute('aria-expanded') === 'true' ? 'false' : 'true');
};
hideText.style.display = 'none';
wrapper.style.setProperty('--width', `${wrapper.clientWidth}px`);
wrapper.style.setProperty('--description-height', `${description.clientHeight}px`);
wrapper.classList.add('hydrated');
await CoreUtils.nextTick();
hideText.style.display = 'revert';
}
@Input() errorCode!: string;
@Input() errorDetails!: string;
constructor(private element: ElementRef<HTMLElement>) {}
/** /**
* @inheritdoc * @inheritdoc
@ -85,8 +140,8 @@ export class CoreErrorAccordionComponent implements OnInit, OnChanges {
/** /**
* Render component html in the element created by Angular. * Render component html in the element created by Angular.
*/ */
private render(): void { private async render(): Promise<void> {
this.element.nativeElement.innerHTML = CoreErrorAccordionComponent.render(this.errorDetails, this.errorCode); await CoreErrorAccordionComponent.render(this.element.nativeElement, this.errorCode, this.errorDetails);
} }
} }

View File

@ -460,7 +460,7 @@ export class CoreLoginSitePage implements OnInit {
const containerElement = alertElement.querySelector('.core-error-accordion-container'); const containerElement = alertElement.querySelector('.core-error-accordion-container');
if (containerElement) { if (containerElement) {
containerElement.innerHTML = CoreErrorAccordionComponent.render(debug.details, debug.code); await CoreErrorAccordionComponent.render(containerElement, debug.code, debug.details);
} }
} }
} }

View File

@ -1065,7 +1065,7 @@ export class CoreDomUtilsProvider {
const containerElement = alertElement.querySelector('.core-error-accordion-container'); const containerElement = alertElement.querySelector('.core-error-accordion-container');
if (containerElement) { if (containerElement) {
containerElement.innerHTML = CoreErrorAccordionComponent.render(error.debug.details, error.debug.code); await CoreErrorAccordionComponent.render(containerElement, error.debug.code, error.debug.details);
} }
} }

View File

@ -5,9 +5,7 @@ html {
--spacing-#{$i}: #{$i*4}px; --spacing-#{$i}: #{$i*4}px;
} }
// Font sizes // Typography
// Body font size
--font-size-sm: 12px; --font-size-sm: 12px;
--font-size-md: 14px; --font-size-md: 14px;
--font-size-lg: 16px; --font-size-lg: 16px;
@ -15,19 +13,29 @@ html {
--font-weight-normal: 400; --font-weight-normal: 400;
--font-weight-medium: 500; --font-weight-medium: 500;
// Typography - Body
--body-font-size-sm: var(--font-size-sm); --body-font-size-sm: var(--font-size-sm);
--body-font-size-md: var(--font-size-md); --body-font-size-md: var(--font-size-md);
--body-font-size-lg: var(--font-size-lg); --body-font-size-lg: var(--font-size-lg);
--body-font-weight: var(--font-weight-normal); --body-font-weight: var(--font-weight-normal);
--body-line-height: 150%; --body-line-height: 150%;
--body-sm-font: normal normal var(--body-font-weight) var(--body-font-size-sm)/var(--body-line-height) var(--ion-font-family);
--body-md-font: normal normal var(--body-font-weight) var(--body-font-size-md)/var(--body-line-height) var(--ion-font-family);
--body-lg-font: normal normal var(--body-font-weight) var(--body-font-size-lg)/var(--body-line-height) var(--ion-font-family);
// Typography - Links
--link-sm-font-size: var(--font-size-sm); --link-sm-font-size: var(--font-size-sm);
--link-md-font-size: var(--font-size-md); --link-md-font-size: var(--font-size-md);
--link-lg-font-size: var(--font-size-lg); --link-lg-font-size: var(--font-size-lg);
--link-font-weight: var(--font-weight-normal); --link-font-weight: var(--font-weight-normal);
--link-line-height: 150%; --link-line-height: 150%;
// Labels --link-sm-font: normal normal var(--link-font-weight) var(--link-sm-font-size)/var(--link-line-height) var(--ion-font-family);
--link-md-font: normal normal var(--link-font-weight) var(--link-md-font-size)/var(--link-line-height) var(--ion-font-family);
--link-lg-font: normal normal var(--link-font-weight) var(--link-lg-font-size)/var(--link-line-height) var(--ion-font-family);
// Typography - Labels
--label-sm-font-size: 10px; --label-sm-font-size: 10px;
--label-md-font-size: 12px; --label-md-font-size: 12px;
--label-lg-font-size: 14px; --label-lg-font-size: 14px;
@ -37,7 +45,11 @@ html {
--label-md-line-height: 16px; --label-md-line-height: 16px;
--label-lg-line-height: 20px; --label-lg-line-height: 20px;
// Subtitles --label-sm-font: normal normal var(--label-font-weight) var(--label-sm-font-size)/var(--label-sm-line-height) var(--ion-font-family);
--label-md-font: normal normal var(--label-font-weight) var(--label-md-font-size)/var(--label-md-line-height) var(--ion-font-family);
--label-lg-font: normal normal var(--label-font-weight) var(--label-lg-font-size)/var(--label-lg-line-height) var(--ion-font-family);
// Typography - Subtitles
--subtitle-sm-font-size: 14px; --subtitle-sm-font-size: 14px;
--subtitle-md-font-size: 16px; --subtitle-md-font-size: 16px;
--subtitle-lg-font-size: 20px; --subtitle-lg-font-size: 20px;
@ -45,7 +57,11 @@ html {
--subtitle-font-weight: var(--font-weight-medium); --subtitle-font-weight: var(--font-weight-medium);
--subtitle-line-height: 150%; --subtitle-line-height: 150%;
// Headings --subtitle-sm-font: normal normal var(--subtitle-font-weight) var(--subtitle-sm-font-size)/var(--subtitle-line-height) var(--ion-font-family);
--subtitle-md-font: normal normal var(--subtitle-font-weight) var(--subtitle-md-font-size)/var(--subtitle-line-height) var(--ion-font-family);
--subtitle-lg-font: normal normal var(--subtitle-font-weight) var(--subtitle-lg-font-size)/var(--subtitle-line-height) var(--ion-font-family);
// Typography - Headings
--heading-1-font-size: 28px; --heading-1-font-size: 28px;
--heading-2-font-size: 24px; --heading-2-font-size: 24px;
--heading-3-font-size: 22px; --heading-3-font-size: 22px;
@ -94,6 +110,26 @@ html {
// A11y // A11y
--a11y-min-target-size: 44px; --a11y-min-target-size: 44px;
// Colors
--blue: #0f6cbf;
--text-color-main: var(--gray-900);
--text-color-secondary: var(--gray-800);
--state-color-hover: rgb(40 40 40, 4%); // --gray-900 4%
--state-color-pressed: rgb(40 40 40, 12%); // --gray-900 12%
--state-color-focused: rgb(40 40 40, 12%); // --gray-900 12%
--state-color-keyboard-focus: var(--blue);
}
html.dark {
// Colors
--text-color-main: var(--gray-200);
--text-color-secondary: var(--gray-300);
} }
/** @deprecated since 4.3 **/ /** @deprecated since 4.3 **/