diff --git a/.gitignore b/.gitignore index 9ba6898df4..8763be9ad1 100644 --- a/.gitignore +++ b/.gitignore @@ -27,6 +27,7 @@ speed-measure-plugin.json .vscode # IDE - VSCode +.vscode .vscode/* !.vscode/settings.json !.vscode/tasks.json @@ -35,6 +36,7 @@ speed-measure-plugin.json .history/* # misc +.angular /.angular/cache /.sass-cache /connect.lock diff --git a/projects/app/src/app/app.module.ts b/projects/app/src/app/app.module.ts index 6123cf53d9..c4d03b9a2c 100644 --- a/projects/app/src/app/app.module.ts +++ b/projects/app/src/app/app.module.ts @@ -5,7 +5,6 @@ import { NgModule } from '@angular/core'; import { RouterModule } from '@angular/router'; import { PoModule } from '@po-ui/ng-components'; - import { AppComponent } from './app.component'; @NgModule({ diff --git a/projects/portal/src/app/app.component.ts b/projects/portal/src/app/app.component.ts index 5736160fb8..ea41214cd8 100644 --- a/projects/portal/src/app/app.component.ts +++ b/projects/portal/src/app/app.component.ts @@ -30,14 +30,13 @@ export class AppComponent implements OnInit, OnDestroy { private poTheme: PoThemeService, public router: Router ) { - const _poTheme = this.poTheme.persistThemeActive(); + const _poTheme = this.poTheme.applyTheme(); if (!_poTheme) { this.theme = poThemeConstant.active; + this.poTheme.setTheme(poThemeConstant, this.theme); } else { - this.theme = _poTheme.active || 0; + this.theme = typeof _poTheme.active === 'object' ? _poTheme.active.type : _poTheme.active; } - - this.poTheme.setTheme(poThemeConstant, this.theme); } async ngOnInit() { diff --git a/projects/portal/src/app/theme-builder/theme-builder.component.css b/projects/portal/src/app/theme-builder/theme-builder.component.css index 79c3e40964..781093c20b 100644 --- a/projects/portal/src/app/theme-builder/theme-builder.component.css +++ b/projects/portal/src/app/theme-builder/theme-builder.component.css @@ -238,10 +238,6 @@ p { } } -.widget-items > .po-widget { - height: 255px; -} - .widget-color > .po-widget { background-color: var(--color-neutral-light-10); } diff --git a/projects/portal/src/app/theme-builder/theme-builder.component.html b/projects/portal/src/app/theme-builder/theme-builder.component.html index bdd8d58760..1052a2b46e 100644 --- a/projects/portal/src/app/theme-builder/theme-builder.component.html +++ b/projects/portal/src/app/theme-builder/theme-builder.component.html @@ -152,7 +152,7 @@ --> -
+
diff --git a/projects/ui/src/lib/components/po-field/po-combo/po-combo.component.html b/projects/ui/src/lib/components/po-field/po-combo/po-combo.component.html index 2895619a66..1fa45ca3e9 100644 --- a/projects/ui/src/lib/components/po-field/po-combo/po-combo.component.html +++ b/projects/ui/src/lib/components/po-field/po-combo/po-combo.component.html @@ -7,60 +7,67 @@ [p-required]="required" [p-show-required]="showRequired" > -
-
- -
+
+
+
+ +
- + -
- - +
+ + -
- +
+ +
diff --git a/projects/ui/src/lib/components/po-field/po-datepicker-range/po-datepicker-range.component.html b/projects/ui/src/lib/components/po-field/po-datepicker-range/po-datepicker-range.component.html index baa4856159..4ddcc174c8 100644 --- a/projects/ui/src/lib/components/po-field/po-datepicker-range/po-datepicker-range.component.html +++ b/projects/ui/src/lib/components/po-field/po-datepicker-range/po-datepicker-range.component.html @@ -6,64 +6,68 @@ [p-required]="required" [p-show-required]="showRequired" > -
-
- -
+
+
+
+
+ +
-
-
+
-
-
- -
+
+ +
-
- -
+
+ +
-
- - +
+ + +
+
diff --git a/projects/ui/src/lib/components/po-field/po-lookup/po-lookup.component.html b/projects/ui/src/lib/components/po-field/po-lookup/po-lookup.component.html index 1f1124679a..c3cbb28940 100644 --- a/projects/ui/src/lib/components/po-field/po-lookup/po-lookup.component.html +++ b/projects/ui/src/lib/components/po-field/po-lookup/po-lookup.component.html @@ -6,39 +6,53 @@ [p-required]="required" [p-show-required]="showRequired" > -
- -
- - + [placeholder]="placeholder" + [required]="required" + (blur)="searchEvent()" + /> + +
+ + +
+ +
+
-
- - + +
diff --git a/projects/ui/src/lib/po.module.ts b/projects/ui/src/lib/po.module.ts index 9caec306e6..f2ffa99771 100644 --- a/projects/ui/src/lib/po.module.ts +++ b/projects/ui/src/lib/po.module.ts @@ -7,6 +7,7 @@ import { PoInterceptorsModule } from './interceptors/interceptors.module'; import { PoPipesModule } from './pipes/pipes.module'; import { PoServicesModule } from './services/services.module'; import { PoNotificationService } from './services/po-notification/po-notification.service'; +import { PoThemeA11yEnum, poThemeDefault, PoThemeService, PoThemeTypeEnum } from './services'; @NgModule({ declarations: [], diff --git a/projects/ui/src/lib/services/po-theme/enum/po-theme-a11y.enum.ts b/projects/ui/src/lib/services/po-theme/enum/po-theme-a11y.enum.ts new file mode 100644 index 0000000000..b985e09ace --- /dev/null +++ b/projects/ui/src/lib/services/po-theme/enum/po-theme-a11y.enum.ts @@ -0,0 +1,29 @@ +/** + * Enum para definir os níveis de acessibilidade suportados pelo serviço de temas. + * + * @usedBy PoThemeService + * + * @example + * + * Em um serviço de tema, você pode usar este enum de acessibilidade para alternar entre os níveis de temas suportados. + * + * ``` + * import { PoThemeA11yEnum } from '@po-ui/theme'; + * + * // Definindo o nível de acessibilidade ao setar o tema junto com o tipo + * themeService.setTheme(...theme, ...type, PoThemeA11yEnum.AA); + * + * // Definir o tema junto com o nível de acessibilidade + * themeService.setThemeA11y(...theme, PoThemeA11yEnum.AAA); + * + * // Definir o nível de acessibilidade de um tema já aplicado + * themeService.setCurrentThemeA11y(PoThemeA11yEnum.AAA); + * ``` + */ +export enum PoThemeA11yEnum { + /** Nível de acessibilidade duplo A (AA) */ + AA = 'AA', + + /** Nível de acessibilidade triplo A (AAA) */ + AAA = 'AAA' +} diff --git a/projects/ui/src/lib/services/po-theme/enum/po-theme-type.enum.ts b/projects/ui/src/lib/services/po-theme/enum/po-theme-type.enum.ts index d3c0689ad2..2a261bfc55 100644 --- a/projects/ui/src/lib/services/po-theme/enum/po-theme-type.enum.ts +++ b/projects/ui/src/lib/services/po-theme/enum/po-theme-type.enum.ts @@ -15,6 +15,9 @@ * * // Definindo o tipo de tema para dark * themeService.setTheme(...theme, PoThemeTypeEnum.dark); + * + * // Definir o tipo do tema de um tema já aplicado + * themeService.setCurrentThemeType(PoThemeTypeEnum.dark); * ``` */ export enum PoThemeTypeEnum { diff --git a/projects/ui/src/lib/services/po-theme/helpers/accessibilities/po-theme-default-aa.constant.ts b/projects/ui/src/lib/services/po-theme/helpers/accessibilities/po-theme-default-aa.constant.ts new file mode 100644 index 0000000000..8ea931ce69 --- /dev/null +++ b/projects/ui/src/lib/services/po-theme/helpers/accessibilities/po-theme-default-aa.constant.ts @@ -0,0 +1,13 @@ +/** + * Define estilos específicos por componente e onRoot para temas de acessibilidade AA. + */ +export const poThemeDefaultAA = { + perComponent: {}, + onRoot: { + /*------------------------------------*\ + COMMON + \*------------------------------------*/ + '--outline-width': 'var(--border-width-md)', + '--outline-width-focus-visible': 'var(--border-width-md)' + } +}; diff --git a/projects/ui/src/lib/services/po-theme/helpers/accessibilities/po-theme-default-aaa.constant.ts b/projects/ui/src/lib/services/po-theme/helpers/accessibilities/po-theme-default-aaa.constant.ts new file mode 100644 index 0000000000..f9b9ca4789 --- /dev/null +++ b/projects/ui/src/lib/services/po-theme/helpers/accessibilities/po-theme-default-aaa.constant.ts @@ -0,0 +1,17 @@ +/** + * Define estilos específicos por componente e onRoot para temas de acessibilidade AAA. + */ +export const poThemeDefaultAAA = { + perComponent: {}, + onRoot: { + /*------------------------------------*\ + FONT + \*------------------------------------*/ + '--font-family': 'Roboto', + '--font-family-theme': 'Roboto', + '--font-family-theme-bold': 'Roboto-Bold', + '--font-family-theme-extra-light': 'Roboto-Condensed-Light', + '--font-family-heading': 'Roboto', + '--font-family-code': 'Monospace' + } +}; diff --git a/projects/ui/src/lib/services/po-theme/helpers/po-theme-poui.constant.ts b/projects/ui/src/lib/services/po-theme/helpers/po-theme-poui.constant.ts index 2e89e71447..53054e8ad8 100644 --- a/projects/ui/src/lib/services/po-theme/helpers/po-theme-poui.constant.ts +++ b/projects/ui/src/lib/services/po-theme/helpers/po-theme-poui.constant.ts @@ -1,3 +1,4 @@ +import { PoThemeA11yEnum } from '../enum/po-theme-a11y.enum'; import { PoThemeTypeEnum } from '../enum/po-theme-type.enum'; import { PoThemeTokens } from '../interfaces/po-theme-tokens.interface'; import { PoTheme } from '../interfaces/po-theme.interface'; @@ -5,9 +6,14 @@ import { poThemeDefaultActions, poThemeDefaultBrands, poThemeDefaultFeedback, - poThemeDefaultLightValues, poThemeDefaultNeutrals -} from './po-theme-light-defaults.constant'; +} from './types/po-theme-light-defaults.constant'; +import { + poThemeDefaultActionsDark, + poThemeDefaultBrandsDark, + poThemeDefaultFeedbackDark, + poThemeDefaultNeutralsDark +} from './types/po-theme-dark-defaults.constant'; /** * Tokens de tema padrão para temas claros. @@ -18,12 +24,18 @@ const poThemeDefaultLight: PoThemeTokens = { action: poThemeDefaultActions, neutral: poThemeDefaultNeutrals, feedback: poThemeDefaultFeedback - }, - perComponent: { - ...poThemeDefaultLightValues.perComponent - }, - onRoot: { - ...poThemeDefaultLightValues.onRoot + } +}; + +/** + * Tokens de tema padrão para o tema escuro. + */ +const poThemeDefaultDark: PoThemeTokens = { + color: { + brand: poThemeDefaultBrandsDark, + action: poThemeDefaultActionsDark, + neutral: poThemeDefaultNeutralsDark, + feedback: poThemeDefaultFeedbackDark } }; @@ -32,10 +44,19 @@ const poThemeDefaultLight: PoThemeTokens = { */ const poThemeDefault: PoTheme = { name: 'default', - type: { - light: poThemeDefaultLight - }, - active: PoThemeTypeEnum.light + type: [ + { + light: poThemeDefaultLight, + dark: poThemeDefaultDark, + a11y: PoThemeA11yEnum.AAA + }, + { + light: poThemeDefaultLight, + dark: poThemeDefaultDark, + a11y: PoThemeA11yEnum.AA + } + ], + active: { type: PoThemeTypeEnum.light, a11y: PoThemeA11yEnum.AAA } }; -export { poThemeDefault, poThemeDefaultLight }; +export { poThemeDefault, poThemeDefaultDark, poThemeDefaultLight }; diff --git a/projects/ui/src/lib/services/po-theme/helpers/po-theme-dark-defaults.constant.ts b/projects/ui/src/lib/services/po-theme/helpers/types/po-theme-dark-defaults.constant.ts similarity index 90% rename from projects/ui/src/lib/services/po-theme/helpers/po-theme-dark-defaults.constant.ts rename to projects/ui/src/lib/services/po-theme/helpers/types/po-theme-dark-defaults.constant.ts index ad81512b13..058b8039d3 100644 --- a/projects/ui/src/lib/services/po-theme/helpers/po-theme-dark-defaults.constant.ts +++ b/projects/ui/src/lib/services/po-theme/helpers/types/po-theme-dark-defaults.constant.ts @@ -1,4 +1,9 @@ -import { PoThemeColorAction, PoThemeColorFeedback, PoThemeColorNeutral } from '../interfaces/po-theme-color.interface'; +import { + PoThemeColorAction, + poThemeColorBrand, + PoThemeColorFeedback, + PoThemeColorNeutral +} from '../../interfaces/po-theme-color.interface'; /** * Define as cores de ação padrão para temas escuros. @@ -61,7 +66,7 @@ const poThemeDefaultFeedbackDark: PoThemeColorFeedback = { lightest: '#081536', lighter: '#0f2557', light: '#173782', - base: '#23489f', + base: '#0079b8', dark: '#7996d7', darker: '#b0c1e8', darkest: '#e3e9f7' @@ -88,6 +93,24 @@ const poThemeDefaultFeedbackDark: PoThemeColorFeedback = { } }; +const poThemeDefaultBrandsDark: poThemeColorBrand = { + '01': { + lightest: '#260538', + lighter: '#400e58', + light: '#5b1c7d', + base: '#753399', + dark: '#bd94d1', + darker: '#d9c2e5', + darkest: '#f2eaf6' + }, + '02': { + base: '#b92f72' + }, + '03': { + base: '#ffd464' + } +}; + /** * Define estilos específicos por componente e onRoot para temas escuros. */ @@ -125,10 +148,6 @@ const poThemeDefaultDarkValues = { 'po-chart po-chart-container > svg .po-chart-axis-x-label, .po-chart-axis-y-label': { 'fill': 'var(--color-neutral-dark-95)' }, - // RICH-TEXT: color button border - 'po-rich-text-toolbar .po-button-default.po-rich-text-toolbar-color-picker-button': { - 'border-style': 'solid' - }, // LINK: item visitado 'po-link': { '--text-color-visited': 'var(--color-action-default)' @@ -162,11 +181,9 @@ const poThemeDefaultDarkValues = { 'po-overlay, po-page-slide': { '--color-overlay': 'var(--color-neutral-light-05)' }, + /** SELECT */ 'po-select': { '--color-hover': 'var(--color-action-hover);' - }, - 'po-select select': { - '--color': 'var(--color-neutral-light-30);' } }, onRoot: { @@ -189,8 +206,8 @@ const poThemeDefaultDarkValues = { '--color-secondary-dark-60-alpha-70': 'color-mix(in srgb, var(--color-neutral-mid-60) 70%, white)', '--color-tertiary-light-90': 'color-mix(in srgb, var(--color-brand-03-base) 90%, black)', '--color-tertiary-dark-5': 'color-mix(in srgb, var(--color-brand-03-base) 5%, white)', - /* PO-PAGE */ - '--color-page-background-color-page': 'var(--color-neutral-light-00)', + /* PAGE */ + '--color-page-background-color-page': 'var(--color-neutral-light-05)', /* TOOLBAR BADGE */ '--color-toolbar-color-badge-text': 'var(--color-neutral-dark-95)', /* POPOVER */ @@ -200,12 +217,14 @@ const poThemeDefaultDarkValues = { '--color-calendar-background-color-box-background-range': 'var(--color-brand-01-lightest)', /* STEPPER */ '--color-stepper-circle-disabled': 'var(--color-neutral-mid-40)', - '--color-stepper-bar-disabled': 'var(--color-neutral-mid-40)', - /* TAB */ - '--po-tab-smart-active': 'var(--color-neutral-dark-95)', - '--po-tab-smart-background-item-selected': 'var(--color-brand-01-lighter)', - '--po-tab-smart-background-hover': 'var(--color-brand-01-lightest)' + '--color-stepper-bar-disabled': 'var(--color-neutral-mid-40)' } }; -export { poThemeDefaultActionsDark, poThemeDefaultFeedbackDark, poThemeDefaultNeutralsDark, poThemeDefaultDarkValues }; +export { + poThemeDefaultBrandsDark, + poThemeDefaultActionsDark, + poThemeDefaultFeedbackDark, + poThemeDefaultNeutralsDark, + poThemeDefaultDarkValues +}; diff --git a/projects/ui/src/lib/services/po-theme/helpers/po-theme-light-defaults.constant.ts b/projects/ui/src/lib/services/po-theme/helpers/types/po-theme-light-defaults.constant.ts similarity index 63% rename from projects/ui/src/lib/services/po-theme/helpers/po-theme-light-defaults.constant.ts rename to projects/ui/src/lib/services/po-theme/helpers/types/po-theme-light-defaults.constant.ts index 6e1457e461..f426903207 100644 --- a/projects/ui/src/lib/services/po-theme/helpers/po-theme-light-defaults.constant.ts +++ b/projects/ui/src/lib/services/po-theme/helpers/types/po-theme-light-defaults.constant.ts @@ -3,7 +3,7 @@ import { PoThemeColorFeedback, PoThemeColorNeutral, poThemeColorBrand -} from '../interfaces/po-theme-color.interface'; +} from '../../interfaces/po-theme-color.interface'; /** * Define as cores de ação padrão para temas claros. @@ -93,6 +93,9 @@ const poThemeDefaultFeedback: PoThemeColorFeedback = { } }; +/** + * Define as cores da Brand padrão para temas claros. + */ const poThemeDefaultBrands: poThemeColorBrand = { '01': { lightest: '#f2eaf6', @@ -112,34 +115,11 @@ const poThemeDefaultBrands: poThemeColorBrand = { }; /** - * Define estilos específicos por componente e onRoot para temas claros. + * Define estilos específicos por componente e onRoot para temas claros para AAA. */ const poThemeDefaultLightValues = { perComponent: {}, - onRoot: { - /* WIDGET */ - '--color-widget-color-action-active': 'var(--color-primary-dark-20)', - '--color-widget-color-action-hover': 'var(--color-primary-dark-20)', - '--color-widget-color-action': 'var(--color-brand-02-base)', - '--color-widget-color-default': 'var(--color-neutral-dark-70)', - '--color-widget-color-title-action': 'var(--color-brand-02-base)', - '--color-widget-color-widget-primary': 'var(--color-neutral-dark-90)', - /* CALENDAR */ - '--color-calendar-arrow': 'var(--color-brand-02-base)', - '--color-calendar-title': 'var(--color-brand-02-base)', - '--color-calendar-text-box-background-active': 'var(--color-neutral-dark-90)', - '--color-calendar-background-color-border-today': 'var(--color-brand-02-base)', - '--color-calendar-color-box-foreground': 'var(--color-neutral-dark-70)', - '--color-calendar-color-box-foreground-selected': 'var(--color-neutral-dark-90)', - '--color-calendar-color-box-foreground-pressed': 'var(--color-neutral-dark-90)', - '--color-calendar-color-box-foreground-range': 'var(--color-brand-02-base)', - '--color-calendar-color-box-foreground-today': 'var(--color-brand-02-base)', - /* TOOLBAR */ - '--color-toolbar-color-default': 'var(--color-brand-02-base)', - '--color-toolbar-color-title': 'var(--color-action-default)', - /* CALENDAR */ - '--color-calendar-background-color-box-background-range': 'var(--color-primary-light-80)' - } + onRoot: {} }; export { diff --git a/projects/ui/src/lib/services/po-theme/index.ts b/projects/ui/src/lib/services/po-theme/index.ts index a2c91daaad..a83afe8b2c 100644 --- a/projects/ui/src/lib/services/po-theme/index.ts +++ b/projects/ui/src/lib/services/po-theme/index.ts @@ -1,12 +1,15 @@ -export * from './helpers/po-theme-dark-defaults.constant'; -export * from './helpers/po-theme-light-defaults.constant'; -export * from './helpers/po-theme-poui.constant'; - export * from './enum/po-theme-type.enum'; +export * from './enum/po-theme-a11y.enum'; export * from './interfaces/po-theme-tokens.interface'; export * from './interfaces/po-theme-color.interface'; export * from './interfaces/po-theme.interface'; +export * from './helpers/types/po-theme-light-defaults.constant'; +export * from './helpers/types/po-theme-dark-defaults.constant'; +export * from './helpers/accessibilities/po-theme-default-aaa.constant'; +export * from './helpers/accessibilities/po-theme-default-aa.constant'; +export * from './helpers/po-theme-poui.constant'; + export * from './po-theme.service'; export * from './po-theme.module'; diff --git a/projects/ui/src/lib/services/po-theme/interfaces/po-theme.interface.ts b/projects/ui/src/lib/services/po-theme/interfaces/po-theme.interface.ts index 96de2bbeb1..1049665bbb 100644 --- a/projects/ui/src/lib/services/po-theme/interfaces/po-theme.interface.ts +++ b/projects/ui/src/lib/services/po-theme/interfaces/po-theme.interface.ts @@ -1,3 +1,4 @@ +import { PoThemeA11yEnum } from '../enum/po-theme-a11y.enum'; import { PoThemeTypeEnum } from '../enum/po-theme-type.enum'; import { PoThemeTokens } from './po-theme-tokens.interface'; @@ -12,10 +13,10 @@ export interface PoTheme { name: string; /** Tipos de tema: 'light' e 'dark' */ - type: PoThemeType; + type: PoThemeType | Array; - /** Tipo de tema ativo */ - active?: PoThemeTypeEnum; + /** Tipo e nivel de acessibilidade de tema ativo */ + active?: PoThemeTypeEnum | PoThemeActive; } /** @@ -23,10 +24,26 @@ export interface PoTheme { * @description * Interface para os tipos de tema ('light' e 'dark'). */ -interface PoThemeType { +export interface PoThemeType { /** Tipo de tipo 'light' */ light?: PoThemeTokens; /** Tipo de tipo 'dark' */ dark?: PoThemeTokens; + + /** Nivel de Acessibilidade */ + a11y?: PoThemeA11yEnum; +} + +/** + * @docsPrivate + * @description + * Interface para o tipo de tema ativo. + */ +export interface PoThemeActive { + /** Tipo de tema ativo */ + type?: PoThemeTypeEnum; + + /** Nivel de Acessibilidade */ + a11y?: PoThemeA11yEnum; } diff --git a/projects/ui/src/lib/services/po-theme/po-theme.service.spec.ts b/projects/ui/src/lib/services/po-theme/po-theme.service.spec.ts index 6df5b1203b..c54b8a0e70 100644 --- a/projects/ui/src/lib/services/po-theme/po-theme.service.spec.ts +++ b/projects/ui/src/lib/services/po-theme/po-theme.service.spec.ts @@ -1,23 +1,103 @@ import { DOCUMENT } from '@angular/common'; -import { Renderer2 } from '@angular/core'; +import { Renderer2, RendererFactory2, RendererStyleFlags2 } from '@angular/core'; import { TestBed } from '@angular/core/testing'; import { ICONS_DICTIONARY, PoIconDictionary } from '../../components/po-icon'; +import { PoThemeA11yEnum } from './enum/po-theme-a11y.enum'; import { PoThemeTypeEnum } from './enum/po-theme-type.enum'; import { poThemeDefault } from './helpers/po-theme-poui.constant'; import { PoTheme } from './interfaces/po-theme.interface'; import { PoThemeService } from './po-theme.service'; -class MockRenderer2 { - createElement(): any {} - appendChild(): any {} - removeChild(): any {} - addClass(): any {} - removeClass(): any {} - setStyle(): any {} +class MockRenderer2 implements Renderer2 { + data = {}; + destroyNode: ((node: any) => void) | null = null; + + selectRootElement(selectorOrNode: string | any, preserveContent?: boolean) { + throw new Error('Method not implemented.'); + } + parentNode(node: any) { + throw new Error('Method not implemented.'); + } + nextSibling(node: any) { + throw new Error('Method not implemented.'); + } + + destroy(): void {} + + createElement(tagName: string, namespace?: string | null): any { + return document.createElement(tagName); + } + + createComment(value: string): any { + return document.createComment(value); + } + + createText(value: string): any { + return document.createTextNode(value); + } + + appendChild(parent: any, newChild: any): void { + parent.appendChild(newChild); + } + + removeChild(parent: any, oldChild: any): void { + parent.removeChild(oldChild); + } + + insertBefore(parent: any, newChild: any, refChild: any): void { + parent.insertBefore(newChild, refChild); + } + + setAttribute(el: any, name: string, value: string, namespace?: string | null): void { + el.setAttribute(name, value); + } + + removeAttribute(el: any, name: string, namespace?: string | null): void { + el.removeAttribute(name); + } + + addClass(el: any, name: string): void { + el.classList.add(name); + } + + removeClass(el: any, name: string): void { + el.classList.remove(name); + } + + setStyle(el: any, style: string, value: any, flags?: RendererStyleFlags2): void { + el.style[style] = value; + } + + removeStyle(el: any, style: string, flags?: RendererStyleFlags2): void { + el.style[style] = ''; + } + + setProperty(el: any, name: string, value: any): void { + el[name] = value; + } + + setValue(node: any, value: string): void { + node.nodeValue = value; + } + + listen( + target: 'document' | 'window' | 'body' | any, + eventName: string, + callback: (event: any) => boolean | void + ): () => void { + return () => {}; + } +} +class MockRendererFactory2 { + createRenderer(): Renderer2 { + return new MockRenderer2(); + } } describe('PoThemeService:', () => { let service: PoThemeService; + let renderer: MockRenderer2; + let styleElement: HTMLStyleElement; beforeEach(() => { TestBed.configureTestingModule({ @@ -25,13 +105,16 @@ describe('PoThemeService:', () => { PoThemeService, { provide: 'Window', useValue: window }, { provide: DOCUMENT, useValue: document }, - { provide: Renderer2, useClass: MockRenderer2 }, + { provide: RendererFactory2, useClass: MockRendererFactory2 }, { provide: 'poThemeDefault', useValue: poThemeDefault }, { provide: ICONS_DICTIONARY, useValue: PoIconDictionary } ] }); + renderer = TestBed.inject(RendererFactory2).createRenderer(null, null) as MockRenderer2; service = TestBed.inject(PoThemeService); + + spyOn(renderer, 'createText').and.callFake(css => document.createTextNode(css)); }); it('should be created', () => { @@ -43,7 +126,10 @@ describe('PoThemeService:', () => { const theme = service.getThemeActive(); expect(theme).toBeTruthy(); - expect(theme.active).toEqual(PoThemeTypeEnum.light); + expect(theme.active).toEqual({ + type: PoThemeTypeEnum.light, + a11y: PoThemeA11yEnum.AAA + }); }); it('should change current theme type', () => { @@ -52,11 +138,15 @@ describe('PoThemeService:', () => { const theme = service.getThemeActive(); - expect(theme.active).toEqual(PoThemeTypeEnum.dark); + expect(theme.active).toEqual({ + type: PoThemeTypeEnum.dark, + a11y: PoThemeA11yEnum.AAA + }); }); describe('Custom Theme:', () => { let poThemeTest: PoTheme; + let poThemeInit: PoTheme; let poThemeTestDark: PoTheme; let poThemeWithJustAdditionalStyles: PoTheme; @@ -119,19 +209,56 @@ describe('PoThemeService:', () => { '--background': 'var(--color-neutral-light-00);' } } - } + }, + dark: {}, + a11y: PoThemeA11yEnum.AAA }, - active: PoThemeTypeEnum.light + active: { + type: PoThemeTypeEnum.light, + a11y: PoThemeA11yEnum.AAA + } }; - poThemeTestDark = { ...poThemeTest, active: PoThemeTypeEnum.dark }; + + poThemeTestDark = { + ...poThemeTest, + active: { + type: PoThemeTypeEnum.dark, + a11y: PoThemeA11yEnum.AAA + } + }; + poThemeWithJustAdditionalStyles = { name: 'justAdditionalStyles', type: { light: { '--color-page-background-color-page': 'var(--color-neutral-light-00)' - } + }, + a11y: PoThemeA11yEnum.AAA }, - active: PoThemeTypeEnum.light + active: { + type: PoThemeTypeEnum.light, + a11y: PoThemeA11yEnum.AAA + } + }; + + poThemeInit = { + name: 'custom', + type: [ + { + light: {}, + dark: {}, + a11y: PoThemeA11yEnum.AA + }, + { + light: {}, + dark: {}, + a11y: PoThemeA11yEnum.AAA + } + ], + active: { + type: PoThemeTypeEnum.light, + a11y: PoThemeA11yEnum.AAA + } }; }); @@ -162,21 +289,103 @@ describe('PoThemeService:', () => { const theme = service.getThemeActive(); expect(theme).toEqual(poThemeTest); - expect(theme.active).toEqual(PoThemeTypeEnum.dark); + expect(typeof theme.active === 'object' ? theme.active.type : theme.active).toEqual(PoThemeTypeEnum.dark); + }); + + it('should set theme default, and get the active type and accessibility', () => { + const _theme = { + name: 'customOld', + type: { + light: {}, + dark: {} + }, + active: PoThemeTypeEnum.light + }; + spyOn(service, 'getThemeActive').and.returnValue(_theme); + service.changeCurrentThemeType(PoThemeTypeEnum.dark); + + const activeTypeFromTheme = service['getActiveTypeFromTheme'](_theme.active); + const activeAccessibilityFromTheme = service['getActiveA11yFromTheme'](_theme.active); + + const theme = service.getThemeActive(); + expect(theme).toEqual(_theme); + expect(activeTypeFromTheme).toEqual(PoThemeTypeEnum.dark); + expect(activeAccessibilityFromTheme).toEqual(PoThemeA11yEnum.AAA); + }); + + it('should set custom theme, theme type and accessibility', () => { + service.setTheme(poThemeInit, PoThemeTypeEnum.light, PoThemeA11yEnum.AA); + service.setCurrentThemeType(PoThemeTypeEnum.dark); + service.setCurrentThemeA11y(PoThemeA11yEnum.AAA); + + const theme = service.getThemeActive(); + expect(theme).toEqual(poThemeInit); + expect(typeof theme.active === 'object' ? theme.active.type : theme.active).toEqual(PoThemeTypeEnum.dark); + expect(typeof theme.active === 'object' ? theme.active.a11y : PoThemeA11yEnum.AAA).toEqual(PoThemeA11yEnum.AAA); + }); + + it('setTheme: should return if the type dont exist', () => { + spyOn(service, 'getThemeActive').and.returnValue(undefined); + const theme = service.setTheme(poThemeInit, 2 as PoThemeTypeEnum, PoThemeA11yEnum.AA); + + expect(theme).toEqual(undefined); }); describe('Local Saved Theme Methods:', () => { - it('persistThemeActive: should persist and define the active theme', () => { + it('applyTheme: should persist and define the active theme', () => { spyOn(service, 'getThemeActive').and.returnValue(poThemeTest); spyOn(service, 'setTheme'); - const result = service.persistThemeActive(); + const result = service.applyTheme(); expect(service.getThemeActive).toHaveBeenCalled(); - expect(service.setTheme).toHaveBeenCalledWith(poThemeTest, poThemeTest.active); + expect(service.setTheme).toHaveBeenCalledWith( + poThemeTest, + typeof poThemeTest.active === 'object' ? poThemeTest.active.type : poThemeTest.active, + typeof poThemeTest.active === 'object' ? poThemeTest.active.a11y : PoThemeA11yEnum.AAA + ); expect(result).toEqual(poThemeTest); }); + it('applyTheme: should apply local theme, if theme passed is the same saved', () => { + spyOn(service, 'getThemeActive').and.returnValue(poThemeTest); + spyOn(service, 'setTheme'); + + const result = service.applyTheme(poThemeTest); + + expect(service.getThemeActive).toHaveBeenCalled(); + expect(service.setTheme).toHaveBeenCalledWith( + poThemeTest, + typeof poThemeTest.active === 'object' ? poThemeTest.active.type : poThemeTest.active, + typeof poThemeTest.active === 'object' ? poThemeTest.active.a11y : PoThemeA11yEnum.AAA + ); + expect(result).toEqual(poThemeTest); + }); + + it('applyTheme: should apply new theme and persist and define as active theme', () => { + spyOn(service, 'getThemeActive').and.returnValue(poThemeTest); + spyOn(service, 'setTheme'); + + const result = service.applyTheme(poThemeInit); + + expect(service.setTheme).toHaveBeenCalledWith( + poThemeInit, + typeof poThemeInit.active === 'object' ? poThemeInit.active.type : poThemeInit.active, + typeof poThemeInit.active === 'object' ? poThemeInit.active.a11y : PoThemeA11yEnum.AAA + ); + expect(result).toEqual(poThemeInit); + }); + + it('applyTheme: should not apply theme if none is passed and there is none local saved', () => { + spyOn(service, 'getThemeActive').and.returnValue(undefined); + spyOn(service, 'setTheme'); + + const result = service.applyTheme(); + + expect(service.getThemeActive).toHaveBeenCalled(); + expect(result).toEqual(undefined); + }); + it('changeCurrentThemeType: should change current theme type', () => { spyOn(service, 'getThemeActive').and.returnValue(poThemeTest); spyOn(service, 'changeThemeType'); @@ -187,23 +396,26 @@ describe('PoThemeService:', () => { }); it('cleanThemeActive: should clean active theme', () => { - spyOn(service, 'getThemeActive').and.returnValue(poThemeTest); + poThemeTest.name = 'test'; + poThemeTest.active['type'] = PoThemeTypeEnum.light; + poThemeTest.active['a11y'] = PoThemeA11yEnum.AAA; - const removeSpy = spyOn(document.getElementsByTagName('html')[0].classList, 'remove'); + const htmlElement = document.getElementsByTagName('html')[0]; + htmlElement.classList.add('test-light-AAA'); + const removeClassSpy = spyOn(htmlElement.classList, 'remove').and.callThrough(); const localStorageSpy = spyOn(localStorage, 'removeItem'); service.cleanThemeActive(); - expect(service.getThemeActive).toHaveBeenCalled(); - expect(removeSpy).toHaveBeenCalledWith('test-light'); + expect(removeClassSpy).toHaveBeenCalledWith('test-light-AAA'); expect(localStorageSpy).toHaveBeenCalledWith('totvs-theme'); }); - it('setThemeActive: should set active theme', () => { + it('setThemeLocal: should set active theme', () => { const theme: PoTheme = poThemeTest; const setItemSpy = spyOn(localStorage, 'setItem'); - service['setThemeActive'](theme); + service['setThemeLocal'](theme); expect(setItemSpy).toHaveBeenCalledWith('totvs-theme', JSON.stringify(theme)); expect(service['theme']).toEqual(theme); @@ -226,6 +438,262 @@ describe('PoThemeService:', () => { expect(consoleSpy).toHaveBeenCalledWith('Erro ao obter o tema do armazenamento local:', jasmine.any(Error)); }); }); + + describe('setThemeType', () => { + beforeEach(() => { + spyOn(service, 'setTheme'); + }); + + it('should set theme type to light by default, if it is not defined', () => { + const theme = { ...poThemeInit, active: PoThemeTypeEnum.light } as PoTheme; + + service.setThemeType(theme); + + expect(service.setTheme).toHaveBeenCalledWith(theme, PoThemeTypeEnum.light, PoThemeA11yEnum.AAA); + }); + + it('should set theme type to dark', () => { + const theme = { ...poThemeInit, active: PoThemeTypeEnum.dark } as PoTheme; + + service.setThemeType(theme, PoThemeTypeEnum.dark); + + expect(service.setTheme).toHaveBeenCalledWith(theme, PoThemeTypeEnum.dark, PoThemeA11yEnum.AAA); + }); + + it('should use accessibility from theme if available', () => { + const theme = { + ...poThemeInit, + active: { type: PoThemeTypeEnum.light, a11y: PoThemeA11yEnum.AA } + } as PoTheme; + + service.setThemeType(theme); + + expect(service.setTheme).toHaveBeenCalledWith(theme, PoThemeTypeEnum.light, PoThemeA11yEnum.AA); + }); + }); + + describe('setCurrentThemeType', () => { + beforeEach(() => { + spyOn(service, 'getThemeActive').and.returnValue({ ...poThemeInit, active: PoThemeTypeEnum.light } as PoTheme); + spyOn(service, 'setThemeType'); + }); + + it('should set current theme type to light by default', () => { + service.setCurrentThemeType(); + + expect(service.setThemeType).toHaveBeenCalledWith( + { ...poThemeInit, active: PoThemeTypeEnum.light } as PoTheme, + PoThemeTypeEnum.light + ); + }); + + it('should set current theme type to dark', () => { + service.setCurrentThemeType(PoThemeTypeEnum.dark); + + expect(service.setThemeType).toHaveBeenCalledWith( + { ...poThemeInit, active: PoThemeTypeEnum.light } as PoTheme, + PoThemeTypeEnum.dark + ); + }); + }); + + describe('setThemeA11y', () => { + beforeEach(() => { + spyOn(service, 'setTheme'); + }); + + it('should set theme accessibility to AAA by default, if it is not defined', () => { + const theme = { ...poThemeInit, active: PoThemeTypeEnum.light } as PoTheme; + + service.setThemeA11y(theme); + + expect(service.setTheme).toHaveBeenCalledWith(theme, PoThemeTypeEnum.light, PoThemeA11yEnum.AAA); + }); + + it('should set theme accessibility to AA', () => { + const theme = { ...poThemeInit, active: PoThemeTypeEnum.dark } as PoTheme; + + service.setThemeA11y(theme, PoThemeA11yEnum.AA); + + expect(service.setTheme).toHaveBeenCalledWith(theme, PoThemeTypeEnum.dark, PoThemeA11yEnum.AA); + }); + + it('should use type from theme if available', () => { + const theme = { + ...poThemeInit, + active: { type: PoThemeTypeEnum.dark, a11y: PoThemeA11yEnum.AA } + } as PoTheme; + + service.setThemeA11y(theme, PoThemeA11yEnum.AA); + + expect(service.setTheme).toHaveBeenCalledWith(theme, PoThemeTypeEnum.dark, PoThemeA11yEnum.AA); + }); + }); + + describe('setCurrentThemeA11y', () => { + beforeEach(() => { + spyOn(service, 'getThemeActive').and.returnValue({ + ...poThemeInit, + active: { type: PoThemeTypeEnum.light } + } as PoTheme); + spyOn(service, 'setThemeA11y'); + }); + + it('should set current theme accessibility to AAA by default', () => { + service.setCurrentThemeA11y(); + + expect(service.setThemeA11y).toHaveBeenCalledWith( + { ...poThemeInit, active: { type: PoThemeTypeEnum.light } } as PoTheme, + PoThemeA11yEnum.AAA + ); + }); + + it('should set current theme accessibility to AA', () => { + service.setCurrentThemeA11y(PoThemeA11yEnum.AA); + + expect(service.setThemeA11y).toHaveBeenCalledWith( + { ...poThemeInit, active: { type: PoThemeTypeEnum.light } } as PoTheme, + PoThemeA11yEnum.AA + ); + }); + }); + + describe('setPerComponentAndOnRoot: ', () => { + let active: any; + let perComponent: any; + let onRoot: any; + + beforeEach(() => { + document.head.querySelector('#baseStyle')?.remove(); + + active = { type: PoThemeTypeEnum.light, a11y: PoThemeA11yEnum.AAA }; + perComponent = { 'po-listbox': { display: 'flex' } }; + onRoot = { '--font-family': 'Roboto' }; + }); + + const validateStyleContent = (expectedCss: string) => { + const styleElement = document.head.querySelector('#baseStyle'); + expect(styleElement).toBeTruthy(); // Verifica se o elemento foi criado + expect(styleElement.textContent?.replace(/\s+/g, ' ').trim()).toContain( + expectedCss.replace(/\s+/g, ' ').trim() + ); + }; + + it('should apply both type and accessibility when both are present', () => { + service.setPerComponentAndOnRoot(active, perComponent, onRoot); + + const expectedCss = ` + html[class*="-light-AAA"]:root { + po-listbox {display: flex;}; + --font-family: Roboto; + } + `; + validateStyleContent(expectedCss); + }); + + it('should apply accessibility only when type is not present', () => { + active.type = undefined; + + service.setPerComponentAndOnRoot(active, perComponent, onRoot); + + const expectedCss = ` + html[class$="-AAA"]:root { + po-listbox {display: flex;}; + --font-family: Roboto; + } + `; + validateStyleContent(expectedCss); + }); + + it('should apply type only when accessibility is not present', () => { + active.a11y = undefined; + active.type = PoThemeTypeEnum.dark; + onRoot = { '--font-family': 'Arial' }; + + service.setPerComponentAndOnRoot(active, perComponent, onRoot); + + const expectedCss = ` + html[class*="-dark"]:root { + po-listbox {display: flex;}; + --font-family: Arial; + } + `; + validateStyleContent(expectedCss); + }); + + it('should not apply type or accessibility when both are undefined', () => { + active = { type: undefined, a11y: undefined }; + perComponent = { 'po-listbox': { display: 'none' } }; + onRoot = { '--font-family': 'Helvetica' }; + + service.setPerComponentAndOnRoot(active, perComponent, onRoot); + + const expectedCss = ` + html:root { + po-listbox {display: none;}; + --font-family: Helvetica; + } + `; + validateStyleContent(expectedCss); + }); + + it('should append styles to existing style element if it exists', () => { + // Simula estilo pré-existente + const existingStyle = document.createElement('style'); + existingStyle.id = 'baseStyle'; + document.head.appendChild(existingStyle); + + service.setPerComponentAndOnRoot(active, perComponent, onRoot); + + const expectedCss = ` + html[class*="-light-AAA"]:root { + po-listbox {display: flex;}; + --font-family: Roboto; + } + `; + validateStyleContent(expectedCss); + }); + + it('should use empty perComponentStyles and onRootStyles when both are undefined', () => { + perComponent = undefined; + onRoot = undefined; + + service.setPerComponentAndOnRoot(active, perComponent, onRoot); + + const expectedCss = ` + html[class*="-light-AAA"]:root { + + } + `; + validateStyleContent(expectedCss); + }); + + it('should use empty perComponentStyles when perComponent is undefined', () => { + perComponent = undefined; + + service.setPerComponentAndOnRoot(active, perComponent, onRoot); + + const expectedCss = ` + html[class*="-light-AAA"]:root { + --font-family: Roboto; + } + `; + validateStyleContent(expectedCss); + }); + + it('should use empty onRootStyles when onRoot is undefined', () => { + onRoot = undefined; + + service.setPerComponentAndOnRoot(active, perComponent, onRoot); + + const expectedCss = ` + html[class*="-light-AAA"]:root { + po-listbox {display: flex;}; + } + `; + validateStyleContent(expectedCss); + }); + }); }); }); @@ -255,6 +723,6 @@ describe(`PoThemeService with 'PhosphorIconDictionary':`, () => { const theme = service.getThemeActive(); expect(theme).toBeTruthy(); - expect(theme.active).toEqual(PoThemeTypeEnum.light); + expect(typeof theme.active === 'object' ? theme.active.type : theme.active).toEqual(PoThemeTypeEnum.light); }); }); diff --git a/projects/ui/src/lib/services/po-theme/po-theme.service.ts b/projects/ui/src/lib/services/po-theme/po-theme.service.ts index 4bf359a39f..dc3d419f13 100644 --- a/projects/ui/src/lib/services/po-theme/po-theme.service.ts +++ b/projects/ui/src/lib/services/po-theme/po-theme.service.ts @@ -1,11 +1,17 @@ import { DOCUMENT } from '@angular/common'; import { Inject, Injectable, Optional, Renderer2, RendererFactory2 } from '@angular/core'; import { ICONS_DICTIONARY, PhosphorIconDictionary } from '../../components/po-icon/index'; -import { PoThemeTypeEnum } from './enum/po-theme-type.enum'; -import { poThemeDefault } from './helpers/po-theme-poui.constant'; + import { PoThemeColor } from './interfaces/po-theme-color.interface'; import { PoThemeTokens } from './interfaces/po-theme-tokens.interface'; -import { PoTheme } from './interfaces/po-theme.interface'; +import { PoTheme, PoThemeActive } from './interfaces/po-theme.interface'; +import { PoThemeA11yEnum } from './enum/po-theme-a11y.enum'; +import { PoThemeTypeEnum } from './enum/po-theme-type.enum'; +import { poThemeDefaultAA } from './helpers/accessibilities/po-theme-default-aa.constant'; +import { poThemeDefaultAAA } from './helpers/accessibilities/po-theme-default-aaa.constant'; +import { poThemeDefaultLightValues } from './helpers/types/po-theme-light-defaults.constant'; +import { poThemeDefaultDarkValues } from './helpers/types/po-theme-dark-defaults.constant'; +import { poThemeDefault } from './helpers/po-theme-poui.constant'; /** * @description @@ -31,7 +37,7 @@ import { PoTheme } from './interfaces/po-theme.interface'; }) export class PoThemeService { private renderer: Renderer2; - private theme: PoTheme = poThemeDefault; + private theme: PoTheme; private _iconToken: { [key: string]: string }; get iconNameLib() { @@ -45,28 +51,59 @@ export class PoThemeService { @Optional() @Inject(ICONS_DICTIONARY) value: { [key: string]: string } ) { this.renderer = rendererFactory.createRenderer(null, null); - this._iconToken = value ?? PhosphorIconDictionary; + + // set triple A for all themes (its the base theme) + // result: html:root + this.setPerComponentAndOnRoot(undefined, poThemeDefaultAAA.perComponent, poThemeDefaultAAA.onRoot); + + // set double A + // result: html[class*="-AA"]:root + this.setPerComponentAndOnRoot({ a11y: PoThemeA11yEnum.AA }, poThemeDefaultAA.perComponent, poThemeDefaultAA.onRoot); + + // set Light mode values + // result: html[class*="-light"]:root + this.setPerComponentAndOnRoot( + { type: PoThemeTypeEnum.light }, + poThemeDefaultLightValues.perComponent, + poThemeDefaultLightValues.onRoot + ); + + // set Dark mode values + // result: html[class*="-dark"]:root + this.setPerComponentAndOnRoot( + { type: PoThemeTypeEnum.dark }, + poThemeDefaultDarkValues.perComponent, + poThemeDefaultDarkValues.onRoot + ); } /** - * Define o tema a ser aplicado no componente, de acordo com o tipo de tema especificado. + * Aplica um tema ao componente de acordo com o tipo de tema e o nível de acessibilidade especificados. * - * Este método define o tema a ser aplicado no componente com base no objeto `theme` fornecido e no tipo de tema especificado. - * Ele atualiza as propriedades do componente para refletir o tema selecionado, como cores, estilos e comportamentos. + * Este método configura o tema do componente com base no objeto `themeConfig` fornecido, no `themeType` e no `a11yLevel`. + * Além disso, ele pode opcionalmente salvar a preferência de tema no localStorage, se solicitado. * - * @param {PoTheme} theme - Objeto contendo as definições de tema a serem aplicadas no componente. - * @param {PoThemeTypeEnum} [themeType=PoThemeTypeEnum.light] - (Opcional) Tipo de tema a ser aplicado, podendo ser 'light' (claro) ou 'dark' (escuro). Por padrão, o tema claro é aplicado. + * @param {PoTheme} themeConfig - Configuração de tema a ser aplicada ao componente. + * @param {PoThemeTypeEnum} [themeType=PoThemeTypeEnum.light] - (Opcional) Tipo de tema, podendo ser 'light' (claro) ou 'dark' (escuro). O tema claro é o padrão. + * @param {PoThemeA11yEnum} [a11yLevel=PoThemeA11yEnum.AAA] - (Opcional) Nível de acessibilidade do tema, podendo ser AA ou AAA. Padrão é AAA. + * @param {boolean} [persistPreference=true] - (Opcional) Define se a preferência de tema deve ser salva no localStorage para persistência. `true` para salvar, `false` para não salvar. */ - setTheme(theme: PoTheme, themeType: PoThemeTypeEnum = PoThemeTypeEnum.light): void { + setTheme( + themeConfig: PoTheme, + themeType: PoThemeTypeEnum = PoThemeTypeEnum.light, + a11yLevel: PoThemeA11yEnum = PoThemeA11yEnum.AAA, + persistPreference: boolean = true + ): void { // Change theme name, remove special characteres and number, replace space with dash - theme.name = theme.name - .toLowerCase() - .replace(/[^a-zA-Z ]/g, '') - .replace(/\s+/g, '-'); - theme.active = themeType; + this.formatTheme(themeConfig, themeType, a11yLevel); + + const _themeActive = + Array.isArray(themeConfig.type) && themeConfig.type.length >= 1 + ? themeConfig.type.find(e => e.a11y === a11yLevel) + : themeConfig.type; - const _themeType = theme.type[PoThemeTypeEnum[themeType]]; + const _themeType = _themeActive[PoThemeTypeEnum[themeType]]; if (!_themeType) { return; } @@ -77,7 +114,7 @@ export class PoThemeService { const additionalStyles = this.generateAdditionalStyles(_themeType); const combinedStyles = ` - .${theme.name}-${PoThemeTypeEnum[themeType]}:root { + html.${themeConfig.name}-${PoThemeTypeEnum[themeType]}-${a11yLevel}:root { ${colorStyles} ${perComponentStyles} ${onRootStyles} @@ -85,7 +122,86 @@ export class PoThemeService { }`; this.applyThemeStyles(combinedStyles); - this.changeThemeType(theme); + this.changeThemeType(themeConfig, persistPreference); + } + + /** + * @docsPrivate + * + * Aplica estilos customizados para o componente e para o root HTML, utilizando os tokens definidos. + * + * Esse método é chamado para inserir ou atualizar estilos no DOM, aplicando tanto tokens de `onRoot` (ex: `--font-family: 'Roboto'`) + * quanto estilos específicos de componentes (`perComponent`, como `po-listbox [hidden]: { display: 'flex !important' }`). + * + * O seletor CSS gerado leva em consideração o tema (`type`) e as configurações de acessibilidade (`a11y`) do tema ativo. + * A classe do tema é aplicada no HTML e pode ser formatada como `html[class*="-light-AA"]` para personalizações + * em temas específicos. + * + * @param {PoThemeActive} active - Objeto que define o tema ativo, com `type` e `a11y`. + * @param {any} perComponent - Objeto contendo os estilos específicos para componentes a serem aplicados. + * @param {any} onRoot - Objeto contendo tokens de estilo que serão aplicados diretamente no seletor `:root` do HTML. + * + * @example + * + * // Exemplo de utilização com um tema ativo e tokens de estilo + * const themeActive = { type: 'light', a11y: 'AA' }; + * const perComponentStyles = { + * 'po-listbox [hidden]': { + * 'display': 'flex !important' + * } + * }; + * const onRootStyles = { + * '--font-family': 'Roboto', + * '--background-color': '#fff' + * }; + * + * this.setPerComponentAndOnRoot(themeActive, perComponentStyles, onRootStyles); + * + * // Resultado: + * // Gera e aplica os seguintes estilos no DOM + * // html[class*="-light-AA"]:root { + * // --font-family: 'Roboto'; + * // --background-color: '#fff'; + * // po-listbox [hidden] { + * // display: flex !important; + * // } + * // } + * + */ + public setPerComponentAndOnRoot(active: PoThemeActive, perComponent: any, onRoot: any) { + const perComponentStyles = perComponent ? this.generatePerComponentStyles(perComponent) : ''; + const onRootStyles = onRoot ? this.generateAdditionalStyles(onRoot) : ''; + + let selector = 'html'; + const typeSelector = active?.type !== undefined ? `-${PoThemeTypeEnum[active.type]}` : ''; + const accessibilitySelector = active?.a11y !== undefined ? `-${PoThemeA11yEnum[active.a11y]}` : ''; + + if (typeSelector && accessibilitySelector) { + selector += `[class*="${typeSelector}${accessibilitySelector}"]`; + } else if (!typeSelector && accessibilitySelector) { + selector += `[class$="${accessibilitySelector}"]`; + } else if (typeSelector) { + selector += `[class*="${typeSelector}"]`; + } + + const styleCss = ` + ${selector}:root { + ${perComponentStyles} + ${onRootStyles} + } + `; + + let styleElement = this.document.head.querySelector('#baseStyle'); + if (!styleElement) { + styleElement = this.renderer.createElement('style'); + styleElement.id = 'baseStyle'; + this.renderer.appendChild(styleElement, this.renderer.createText(styleCss)); + this.renderer.appendChild(this.document.head, styleElement); + } else { + if (!styleElement.textContent.includes(styleCss.trim())) { + this.renderer.appendChild(styleElement, this.renderer.createText(styleCss)); + } + } } /** @@ -109,8 +225,8 @@ export class PoThemeService { * @param styleCss Os estilos CSS a serem aplicados. */ private applyThemeStyles(styleCss: string): void { - const styleElement = this.createStyleElement(styleCss); - const existingStyleElement = document.head.querySelector('#pouiTheme'); + const styleElement = this.createStyleElement(styleCss, 'theme'); + const existingStyleElement = document.head.querySelector('#theme'); if (existingStyleElement) { this.renderer.removeChild(document.head, existingStyleElement); @@ -119,10 +235,20 @@ export class PoThemeService { this.renderer.appendChild(document.head, styleElement); } - private changeThemeType(theme: PoTheme) { - this.cleanThemeActive(); - this.setThemeActive(theme); - document.getElementsByTagName('html')[0].classList.add(...[`${theme.name}-${PoThemeTypeEnum[theme.active]}`]); + private changeThemeType(theme: PoTheme, persistPreference: boolean = true) { + this.cleanThemeActive(persistPreference); + + if (persistPreference) { + this.setThemeLocal(theme); + } + + document + .getElementsByTagName('html')[0] + .classList.add( + ...[ + `${theme.name}-${PoThemeTypeEnum[this.getActiveTypeFromTheme(theme.active)]}-${PoThemeA11yEnum[this.getActiveA11yFromTheme(theme.active)]}` + ] + ); } /** @@ -134,10 +260,42 @@ export class PoThemeService { */ persistThemeActive() { const _theme = this.getThemeActive(); - this.setTheme(_theme, _theme.active); + this.setTheme(_theme, this.getActiveTypeFromTheme(_theme.active), this.getActiveA11yFromTheme(_theme.active)); return _theme; } + private formatTheme(themeConfig, themeType, a11yLevel) { + themeConfig.name = themeConfig.name + .toLowerCase() + .replace(/[^a-zA-Z ]/g, '') + .replace(/\s+/g, '-'); + themeConfig.active = { type: themeType, a11y: a11yLevel }; + } + + applyTheme(theme?: any): any { + const _localTheme = this.getThemeActive(); + + if (!theme) { + if (_localTheme) { + this.persistThemeActive(); + return _localTheme; + } + return undefined; + } + + const _type = this.getActiveTypeFromTheme(theme.active); + const _accessibility = this.getActiveA11yFromTheme(theme.active); + + if (_localTheme && JSON.stringify(_localTheme) === JSON.stringify(theme)) { + this.persistThemeActive(); + return _localTheme; + } + + this.formatTheme(theme, _type, _accessibility); + this.setTheme(theme, _type, _accessibility); + return theme; + } + /** * Altera o tipo do tema armazenado e aplica os novos estilos ao documento. * @@ -147,18 +305,40 @@ export class PoThemeService { */ changeCurrentThemeType(type: PoThemeTypeEnum): void { const _theme = this.getThemeActive(); - _theme.active = type; + typeof _theme.active === 'object' ? (_theme.active.type = type) : (_theme.active = type); this.changeThemeType(_theme); } /** * Método remove o tema armazenado e limpa todos os estilos de tema * aplicados ao documento. + * + * @param {boolean} [persistPreference=true] - (Opcional) Define se a preferência de tema não deve ser mantida no localStorage para persistência. `true` para remover, `false` para manter. */ - cleanThemeActive(): void { - const _theme = this.getThemeActive(); - document.getElementsByTagName('html')[0].classList.remove(`${_theme.name}-${PoThemeTypeEnum[_theme.active]}`); - localStorage.removeItem('totvs-theme'); + cleanThemeActive(persistPreference: boolean = true): void { + // Sufixo existentes hoje + const themeSuffixes = ['-light-', '-dark-']; + const htmlElement = document.getElementsByTagName('html')[0]; + + // Converte `classList` em um array e remove as classes que terminam com os sufixos especificados + Array.from(htmlElement.classList).forEach(className => { + if (themeSuffixes.some(suffix => className.includes(suffix))) { + htmlElement.classList.remove(className); + } + }); + + // Remove o tema ativo do localStorage + if (persistPreference) { + localStorage.removeItem('totvs-theme'); + } + } + + private getActiveTypeFromTheme(active): PoThemeTypeEnum { + return typeof active === 'object' ? active.type : active; + } + + private getActiveA11yFromTheme(active): PoThemeA11yEnum { + return typeof active === 'object' ? active.a11y : PoThemeA11yEnum.AAA; } /** @@ -167,7 +347,7 @@ export class PoThemeService { * Este método define um dados do tema e o armazena. * @param theme Os tokens de tema contendo os estilos adicionais a serem gerados. */ - private setThemeActive(theme: PoTheme): void { + private setThemeLocal(theme: PoTheme): void { if (theme) { localStorage.setItem('totvs-theme', JSON.stringify(theme)); this.theme = theme; @@ -194,12 +374,13 @@ export class PoThemeService { * @docsPrivate * * Gera estilos CSS com base nos tokens de cores fornecidos. - * @param themeColor Os tokens de cor a serem usados para gerar os estilos. + * @param css Os tokens de cor a serem usados para gerar os estilos. + * @param id id do style a ser aplicado. * @returns Uma string contendo os estilos CSS gerados. */ - private createStyleElement(css: string): HTMLStyleElement { + private createStyleElement(css: string, id: string): HTMLStyleElement { const styleElement = this.renderer.createElement('style'); - styleElement.id = 'pouiTheme'; + styleElement.id = id; this.renderer.appendChild(styleElement, this.renderer.createText(css)); return styleElement; } @@ -328,4 +509,46 @@ export class PoThemeService { return svg; } + + /** + * Define o tipo (light/dark) quando um tema está sendo aplicado. + * + * @param {PoTheme} theme - Objeto contendo as definições de tema a serem aplicadas no componente. + * @param {PoThemeTypeEnum} [themeType=PoThemeTypeEnum.light] - (Opcional) Tipo de tema a ser aplicado, podendo ser 'light' (claro) ou 'dark' (escuro). Por padrão, o tema claro é aplicado. + */ + setThemeType(theme: PoTheme, themeType: PoThemeTypeEnum = PoThemeTypeEnum.light) { + const _accessibility = typeof theme.active === 'object' ? theme.active.a11y : PoThemeA11yEnum.AAA; + this.setTheme(theme, themeType, _accessibility); + } + + /** + * Define o tipo (light/dark) para um tema já ativo. + * + * @param {PoThemeTypeEnum} [themeType=PoThemeTypeEnum.light] - (Opcional) Tipo de tema a ser aplicado, podendo ser 'light' (claro) ou 'dark' (escuro). Por padrão, o tema claro é aplicado. + */ + setCurrentThemeType(themeType: PoThemeTypeEnum = PoThemeTypeEnum.light) { + const _theme = this.getThemeActive(); + this.setThemeType(_theme, themeType); + } + + /** + * Define o nivel de acessibilidade quando um tema está sendo aplicado. + * + * @param {PoTheme} theme - Objeto contendo as definições de tema a serem aplicadas no componente. + * @param {PoThemeA11yEnum} [a11y=PoThemeA11yEnum.AAA] - (Opcional) Nível de acessibilidade a ser aplicado ao tema, como AA ou AAA. Se não for informado, por padrão a acessibilidade será AAA. + */ + setThemeA11y(theme: PoTheme, a11y: PoThemeA11yEnum = PoThemeA11yEnum.AAA) { + const _type = (typeof theme.active === 'object' ? theme.active.type : theme.active) || 0; + this.setTheme(theme, _type, a11y); + } + + /** + * Define o nivel de acessibilidade para um tema já ativo. + * + * @param {PoThemeA11yEnum} [a11y=PoThemeA11yEnum.AAA] - (Opcional) Nível de acessibilidade a ser aplicado ao tema, como AA ou AAA. Se não for informado, por padrão a acessibilidade será AAA. + */ + setCurrentThemeA11y(a11y: PoThemeA11yEnum = PoThemeA11yEnum.AAA) { + const _theme = this.getThemeActive(); + this.setThemeA11y(_theme, a11y); + } } diff --git a/projects/ui/src/lib/services/po-theme/samples/sample-po-theme-labs/sample-po-theme-labs.component.ts b/projects/ui/src/lib/services/po-theme/samples/sample-po-theme-labs/sample-po-theme-labs.component.ts index 5b79653168..bbf9bab3f1 100644 --- a/projects/ui/src/lib/services/po-theme/samples/sample-po-theme-labs/sample-po-theme-labs.component.ts +++ b/projects/ui/src/lib/services/po-theme/samples/sample-po-theme-labs/sample-po-theme-labs.component.ts @@ -186,14 +186,13 @@ export class SamplePoThemeLabsComponent implements OnInit, OnDestroy { private poNotification: PoNotificationService, private poTheme: PoThemeService ) { - const _poTheme = this.poTheme.persistThemeActive(); + const _poTheme = this.poTheme.applyTheme(); if (!_poTheme) { + this.poTheme.setTheme(this.poThemeSample, this.theme); this.theme = this.poThemeSample.active; } else { this.theme = _poTheme.active || 0; } - - this.poTheme.setTheme(this.poThemeSample, this.theme); } changeTheme(value: number, dispatchEvent = true) {