From 6b461b035e347da72c00ef8a5f61f2968b1986bd Mon Sep 17 00:00:00 2001 From: Noel De Martin Date: Mon, 18 Mar 2024 16:07:08 +0100 Subject: [PATCH] MOBILE-4268 core: Update error accordion UI --- .../error-accordion/error-accordion.scss | 170 +++++++++++++----- .../error-accordion/error-accordion.ts | 99 +++++++--- src/core/features/login/pages/site/site.ts | 2 +- src/core/services/utils/dom.ts | 2 +- src/theme/theme.design-system.scss | 48 ++++- 5 files changed, 244 insertions(+), 77 deletions(-) diff --git a/src/core/components/error-accordion/error-accordion.scss b/src/core/components/error-accordion/error-accordion.scss index a16ec2ac9..4bc7f20af 100644 --- a/src/core/components/error-accordion/error-accordion.scss +++ b/src/core/components/error-accordion/error-accordion.scss @@ -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--details { + opacity: 0; + display: flex; + overflow: hidden; + 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); + + 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--checkbox { - display: none; + .core-error-accordion--toggle { + display: flex; + width: 100%; + 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); - & + .core-error-accordion--details, - & + .core-error-accordion--code + .core-error-accordion--details { - max-height: 0; - overflow: hidden; - transition: max-height 600ms ease-in-out; + .core-error-accordion--toggle-text { + display: flex; + flex-direction: column; + } - & + .core-error-accordion--toggle { - display: flex; - padding: var(--spacing-2); - min-height: var(--a11y-min-target-size); - align-items: center; + .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); + } - span { - width: 100%; - display: flex; - justify-content: space-between; - } + 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); + } - svg { - fill: currentColor; - width: 11px; - } + &:hover { + background: var(--state-color-hover); + } - .core-error-accordion--hide-content { - display: none; - } + &: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; } } - &: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; + &.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--toggle .core-error-accordion--show-content { - display: none; + .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); +} diff --git a/src/core/components/error-accordion/error-accordion.ts b/src/core/components/error-accordion/error-accordion.ts index b2f31d845..8efbf1553 100644 --- a/src/core/components/error-accordion/error-accordion.ts +++ b/src/core/components/error-accordion/error-accordion.ts @@ -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 { + 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 `
- -
${errorCodeLabel}
-
+

${errorCodeLabel}

+ - +
`; } - @Input() errorDetails!: string; - @Input() errorCode!: string; + /** + * Hydrate component. + * + * @param element Root element. + */ + static async hydrate(element: Element): Promise { + const wrapper = element.querySelector('.core-error-accordion'); + const description = element.querySelector('.core-error-accordion--details'); + const button = element.querySelector('.core-error-accordion--toggle'); + const hideText = element.querySelector('.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) {} /** * @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 { + await CoreErrorAccordionComponent.render(this.element.nativeElement, this.errorCode, this.errorDetails); } } diff --git a/src/core/features/login/pages/site/site.ts b/src/core/features/login/pages/site/site.ts index 2226dbafe..8262ff90c 100644 --- a/src/core/features/login/pages/site/site.ts +++ b/src/core/features/login/pages/site/site.ts @@ -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); } } } diff --git a/src/core/services/utils/dom.ts b/src/core/services/utils/dom.ts index d3f6b5a43..ad792223c 100644 --- a/src/core/services/utils/dom.ts +++ b/src/core/services/utils/dom.ts @@ -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); } } diff --git a/src/theme/theme.design-system.scss b/src/theme/theme.design-system.scss index 06fe09b70..3ef11bc87 100644 --- a/src/theme/theme.design-system.scss +++ b/src/theme/theme.design-system.scss @@ -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 **/