diff --git a/projects/design-angular-kit/src/lib/components/utils/gap/gap.directive.spec.ts b/projects/design-angular-kit/src/lib/components/utils/gap/gap.directive.spec.ts new file mode 100644 index 00000000..9170f01c --- /dev/null +++ b/projects/design-angular-kit/src/lib/components/utils/gap/gap.directive.spec.ts @@ -0,0 +1,125 @@ +import { Component } from '@angular/core'; +import { TestBed } from '@angular/core/testing'; +import { tb_base } from '../../../../test'; +import { ItGapDirective, ItGapXDirective, ItGapYDirective } from './gap.directive'; + +@Component({ + selector: 'it-test-gap0', + standalone: true, + imports: [ItGapDirective], + template: `
content
`, +}) +class Gap0Host {} + +@Component({ + selector: 'it-test-gap1', + standalone: true, + imports: [ItGapDirective], + template: `
content
`, +}) +class Gap1Host {} + +@Component({ + selector: 'it-test-gap3', + standalone: true, + imports: [ItGapDirective], + template: `
content
`, +}) +class Gap3Host {} + +@Component({ + selector: 'it-test-gap5', + standalone: true, + imports: [ItGapDirective], + template: `
content
`, +}) +class Gap5Host {} + +@Component({ + selector: 'it-test-gx2', + standalone: true, + imports: [ItGapXDirective], + template: `
content
`, +}) +class GapX2Host {} + +@Component({ + selector: 'it-test-gy4', + standalone: true, + imports: [ItGapYDirective], + template: `
content
`, +}) +class GapY4Host {} + +@Component({ + selector: 'it-test-combined', + standalone: true, + imports: [ItGapXDirective, ItGapYDirective], + template: `
content
`, +}) +class CombinedHost {} + +describe('ItGapDirective', () => { + it('should apply gap: 0 for scale 0', () => { + TestBed.configureTestingModule({ ...tb_base, imports: [...(tb_base.imports || []), Gap0Host] }); + const fix = TestBed.createComponent(Gap0Host); + fix.detectChanges(); + const div = fix.nativeElement.querySelector('div') as HTMLDivElement; + expect(div.style.gap).toBe('0px'); + }); + + it('should apply gap: 0.25rem for scale 1', () => { + TestBed.configureTestingModule({ ...tb_base, imports: [...(tb_base.imports || []), Gap1Host] }); + const fix = TestBed.createComponent(Gap1Host); + fix.detectChanges(); + const div = fix.nativeElement.querySelector('div') as HTMLDivElement; + expect(div.style.gap).toBe('0.25rem'); + }); + + it('should apply gap: 1rem for scale 3', () => { + TestBed.configureTestingModule({ ...tb_base, imports: [...(tb_base.imports || []), Gap3Host] }); + const fix = TestBed.createComponent(Gap3Host); + fix.detectChanges(); + const div = fix.nativeElement.querySelector('div') as HTMLDivElement; + expect(div.style.gap).toBe('1rem'); + }); + + it('should apply gap: 3rem for scale 5', () => { + TestBed.configureTestingModule({ ...tb_base, imports: [...(tb_base.imports || []), Gap5Host] }); + const fix = TestBed.createComponent(Gap5Host); + fix.detectChanges(); + const div = fix.nativeElement.querySelector('div') as HTMLDivElement; + expect(div.style.gap).toBe('3rem'); + }); +}); + +describe('ItGapXDirective', () => { + it('should apply column-gap: 0.5rem for scale 2', () => { + TestBed.configureTestingModule({ ...tb_base, imports: [...(tb_base.imports || []), GapX2Host] }); + const fix = TestBed.createComponent(GapX2Host); + fix.detectChanges(); + const div = fix.nativeElement.querySelector('div') as HTMLDivElement; + expect(div.style.columnGap).toBe('0.5rem'); + }); +}); + +describe('ItGapYDirective', () => { + it('should apply row-gap: 1.5rem for scale 4', () => { + TestBed.configureTestingModule({ ...tb_base, imports: [...(tb_base.imports || []), GapY4Host] }); + const fix = TestBed.createComponent(GapY4Host); + fix.detectChanges(); + const div = fix.nativeElement.querySelector('div') as HTMLDivElement; + expect(div.style.rowGap).toBe('1.5rem'); + }); +}); + +describe('ItGapX + ItGapY combined', () => { + it('should apply column-gap and row-gap independently', () => { + TestBed.configureTestingModule({ ...tb_base, imports: [...(tb_base.imports || []), CombinedHost] }); + const fix = TestBed.createComponent(CombinedHost); + fix.detectChanges(); + const div = fix.nativeElement.querySelector('div') as HTMLDivElement; + expect(div.style.columnGap).toBe('0.5rem'); + expect(div.style.rowGap).toBe('1.5rem'); + }); +}); diff --git a/projects/design-angular-kit/src/lib/components/utils/gap/gap.directive.ts b/projects/design-angular-kit/src/lib/components/utils/gap/gap.directive.ts new file mode 100644 index 00000000..36d8d949 --- /dev/null +++ b/projects/design-angular-kit/src/lib/components/utils/gap/gap.directive.ts @@ -0,0 +1,83 @@ +import { Directive, HostBinding, Input } from '@angular/core'; + +/** Bootstrap Italia spacer scale (0-5). */ +export type GapScale = 0 | 1 | 2 | 3 | 4 | 5; + +/** + * Maps Bootstrap Italia spacer scale values (0-5) to their actual + * CSS sizes following the $spacer variable ($spacer = 1rem = 16px). + */ +const SPACER_MAP: Record = { + 0: '0', + 1: '0.25rem', + 2: '0.5rem', + 3: '1rem', + 4: '1.5rem', + 5: '3rem', +}; + +/** + * Directive that applies the CSS `gap` property to the host element. + * + * Bootstrap Italia's `.g-*` classes only set CSS variables consumed by `.row`. + * This directive applies real CSS `gap`, making it usable on any flex or grid container. + * + * @example + * ```html + *
...
+ * ``` + */ +@Directive({ + selector: '[itGap]', + standalone: true, +}) +export class ItGapDirective { + @Input('itGap') value: GapScale = 0; + + @HostBinding('style.gap') + get gap(): string { + return SPACER_MAP[this.value] ?? '0'; + } +} + +/** + * Directive that applies the CSS `column-gap` property to the host element. + * + * @example + * ```html + *
...
+ * ``` + */ +@Directive({ + selector: '[itGapX]', + standalone: true, +}) +export class ItGapXDirective { + @Input('itGapX') value: GapScale = 0; + + @HostBinding('style.column-gap') + get columnGap(): string { + return SPACER_MAP[this.value] ?? '0'; + } +} + +/** + * Directive that applies the CSS `row-gap` property to the host element. + * + * @example + * ```html + *
...
+ * ``` + */ +@Directive({ + selector: '[itGapY]', + standalone: true, +}) +export class ItGapYDirective { + @Input('itGapY') value: GapScale = 0; + + @HostBinding('style.row-gap') + get rowGap(): string { + return SPACER_MAP[this.value] ?? '0'; + } +} diff --git a/projects/design-angular-kit/src/public_api.ts b/projects/design-angular-kit/src/public_api.ts index 344db7a6..b19f8ccd 100644 --- a/projects/design-angular-kit/src/public_api.ts +++ b/projects/design-angular-kit/src/public_api.ts @@ -123,6 +123,7 @@ export * from './lib/components/navigation/skiplink/skiplink/skiplink.component' export * from './lib/components/utils/error-page/error-page.component'; export * from './lib/components/utils/icon/icon.component'; export * from './lib/components/utils/language-switcher/language-switcher.component'; +export * from './lib/components/utils/gap/gap.directive'; // Services export * from './lib/services/notification/notification.service';