MOBILE-4268 core: Update error accordion UI
parent
e1be8caace
commit
6b461b035e
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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 **/
|
||||
|
|
Loading…
Reference in New Issue