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 {
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);
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 {
padding: var(--spacing-2) var(--spacing-2) 0 var(--spacing-2);
font-size: var(--body-font-size-md);
margin: 0;
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 {
padding: var(--spacing-2) var(--spacing-2) 0 var(--spacing-2);
color: var(--gray-500);
}
.core-error-accordion--checkbox {
display: none;
& + .core-error-accordion--details,
& + .core-error-accordion--code + .core-error-accordion--details {
max-height: 0;
.core-error-accordion--details {
opacity: 0;
display: flex;
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;
padding: var(--spacing-2);
min-height: var(--a11y-min-target-size);
align-items: center;
span {
width: 100%;
display: flex;
align-items: center;
background: transparent;
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 {
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 {
.core-error-accordion--toggle-text {
display: flex;
flex-direction: column;
}
& + .core-error-accordion--toggle .core-error-accordion--show-content {
display: none;
.core-error-accordion--show-details,
.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 { Translate } from '@singletons';
import { CoreUtils } from '@services/utils/utils';
import { CoreDom } from '@singletons/dom';
import { CoreForms } from '@singletons/form';
import { CoreLogger } from '@singletons/logger';
const logger = CoreLogger.getInstance('CoreErrorAccordionComponent');
/**
* 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.
*
* @param errorDetails Error details.
* @param element Root element.
* @param errorCode Error code.
* @returns Component HTML.
* @param errorDetails Error details.
*/
static render(errorDetails: string, errorCode: string): string {
const toggleId = CoreForms.uniqueId('error-accordion-toggle');
static async render(element: Element, errorCode: string, errorDetails: string): Promise<void> {
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 hideDetailsLabel = Translate.instant('core.errordetailshide');
const showDetailsLabel = Translate.instant('core.errordetailsshow');
return `
<div class="core-error-accordion">
<input id="${toggleId}" type="checkbox" class="core-error-accordion--checkbox" />
<div class="core-error-accordion--code"><strong>${errorCodeLabel}</strong></div>
<div class="core-error-accordion--details">
<h3 class="core-error-accordion--code">${errorCodeLabel}</h3>
<div id="${contentId}" class="core-error-accordion--details" role="region" aria-hidden="true">
<p>${errorDetails}</p>
</div>
<label for="${toggleId}" class="core-error-accordion--toggle" aria-hidden="true">
<span class="core-error-accordion--hide-content">
${hideDetailsLabel}
<ion-icon name="chevron-up" />
</span>
<span class="core-error-accordion--show-content">
<button type="button" class="core-error-accordion--toggle" aria-expanded="false" aria-controls="${contentId}">
<div class="core-error-accordion--toggle-text">
<span class="core-error-accordion--show-details">
${showDetailsLabel}
<ion-icon name="chevron-down" />
</span>
</label>
<span class="core-error-accordion--hide-details">
${hideDetailsLabel}
</span>
</div>
<ion-icon name="chevron-down" />
</button>
</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
@ -85,8 +140,8 @@ export class CoreErrorAccordionComponent implements OnInit, OnChanges {
/**
* Render component html in the element created by Angular.
*/
private render(): void {
this.element.nativeElement.innerHTML = CoreErrorAccordionComponent.render(this.errorDetails, this.errorCode);
private async render(): Promise<void> {
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');
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');
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;
}
// Font sizes
// Body font size
// Typography
--font-size-sm: 12px;
--font-size-md: 14px;
--font-size-lg: 16px;
@ -15,19 +13,29 @@ html {
--font-weight-normal: 400;
--font-weight-medium: 500;
// Typography - Body
--body-font-size-sm: var(--font-size-sm);
--body-font-size-md: var(--font-size-md);
--body-font-size-lg: var(--font-size-lg);
--body-font-weight: var(--font-weight-normal);
--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-md-font-size: var(--font-size-md);
--link-lg-font-size: var(--font-size-lg);
--link-font-weight: var(--font-weight-normal);
--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-md-font-size: 12px;
--label-lg-font-size: 14px;
@ -37,7 +45,11 @@ html {
--label-md-line-height: 16px;
--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-md-font-size: 16px;
--subtitle-lg-font-size: 20px;
@ -45,7 +57,11 @@ html {
--subtitle-font-weight: var(--font-weight-medium);
--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-2-font-size: 24px;
--heading-3-font-size: 22px;
@ -94,6 +110,26 @@ html {
// A11y
--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 **/