Skip to content

Commit

Permalink
feat: introduce additional components for product variation select di…
Browse files Browse the repository at this point in the history
…splay (#1317)

* moved existing `ProductVariationSelectComponent` rendering as standard select box to new `ProductVariationSelectDefaultComponent`
* `ProductVariationSelectComponent` now contains the logic to select the fitting variation select rendering component
* added `ProductVariationSelectSwatchComponent` for colorCode and swatchImage variation select rendering
* added `ProductVariationSelectEnhancedComponent` for a select box rendering with color codes or swatch images and a mobile optimization

BREAKING CHANGES: Changed the rendering of the `ProductVariationSelectComponent` and introduced additional product variation select rendering components (see [Migrations / 3.1 to 3.2](https://github.com/intershop/intershop-pwa/blob/develop/docs/guides/migrations.md#31-to-32) for more details).

Co-authored-by: Stefan Hauke <s.hauke@intershop.de>
  • Loading branch information
tdonthu and shauke committed Dec 16, 2022
1 parent d9e4b17 commit ddd6e2e
Show file tree
Hide file tree
Showing 18 changed files with 675 additions and 62 deletions.
3 changes: 3 additions & 0 deletions docs/guides/migrations.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ You can keep the existing behavior by modifying the updateBasketItemsDesiredDeli

The `ProductsService` was changed to use `extended=true` REST calls for product details and variations to fetch variation attributes with additional `attributeType` and `metaData` information that can be used to control the rendering of different variation select types.
The added `VariationAttributeMapper` maps the additional information in a backwards compatible way.
To handle the different variation select rendering types the existing `ProductVariationSelectComponent` now contains the logic to select the fitting variation select rendering component.
The rendering and behavior of the existing `ProductVariationSelectComponent` as a standard select box was moved to the new `ProductVariationSelectDefaultComponent`.
A `ProductVariationSelectSwatchComponent` for colorCode and swatchImage variation select rendering and a `ProductVariationSelectEnhancedComponent` for a select box rendering with color codes or swatch images and a mobile optimization were added.

## 3.0 to 3.1

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<select
class="form-control"
[id]="uuid + group.id"
[attr.data-testing-id]="group.id"
(change)="optionChange(group.id, $event.target)"
>
<ng-container *ngFor="let option of group.options">
<option
*ngIf="!option.alternativeCombination || multipleOptions"
[value]="option.value"
[selected]="option.active"
[attr.data-testing-id]="group.id + '-' + option.value"
>
{{ option.label }}
<ng-container *ngIf="option.alternativeCombination">
- {{ 'product.available_in_different_configuration' | translate }}
</ng-container>
</option>
</ng-container>
</select>
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { anything, capture, spy, verify } from 'ts-mockito';

import { VariationOptionGroup } from 'ish-core/models/product-variation/variation-option-group.model';
import { findAllDataTestingIDs } from 'ish-core/utils/dev/html-query-utils';

import { ProductVariationSelectDefaultComponent } from './product-variation-select-default.component';

describe('Product Variation Select Default Component', () => {
let component: ProductVariationSelectDefaultComponent;
let fixture: ComponentFixture<ProductVariationSelectDefaultComponent>;
let element: HTMLElement;

const group = { id: 'a', options: [{ value: 'B' }, { value: 'C' }] } as VariationOptionGroup;

beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ProductVariationSelectDefaultComponent],
}).compileComponents();
});

beforeEach(() => {
fixture = TestBed.createComponent(ProductVariationSelectDefaultComponent);
component = fixture.componentInstance;
element = fixture.nativeElement;
component.group = group;
});

it('should be created', () => {
expect(component).toBeTruthy();
expect(element).toBeTruthy();
expect(() => fixture.detectChanges()).not.toThrow();
});

it('should initialize form of option group', () => {
fixture.detectChanges();

expect(findAllDataTestingIDs(fixture)).toMatchInlineSnapshot(`
Array [
"a",
"a-B",
"a-C",
]
`);
});

it('should set active values for form', () => {
fixture.detectChanges();

expect(fixture.debugElement.query(By.css('select[data-testing-id=a]')).nativeElement.value).toMatchInlineSnapshot(
`"B"`
);
});

it('should trigger changeOption output handler if select value changes', () => {
fixture.detectChanges();
const emitter = spy(component.changeOption);
const select = fixture.debugElement.query(By.css('select')).nativeElement;
select.value = 'C';
select.dispatchEvent(new Event('change'));

verify(emitter.emit(anything())).once();
const [arg] = capture(emitter.emit).last();
expect(arg).toMatchInlineSnapshot(`
Object {
"group": "a",
"value": "C",
}
`);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core';

import { VariationOptionGroup } from 'ish-core/models/product-variation/variation-option-group.model';

@Component({
selector: 'ish-product-variation-select-default',
templateUrl: './product-variation-select-default.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ProductVariationSelectDefaultComponent {
@Input() group: VariationOptionGroup;
@Input() uuid: string;
@Input() multipleOptions: boolean;

@Output() changeOption = new EventEmitter<{ group: string; value: string }>();

optionChange(group: string, target: EventTarget) {
this.changeOption.emit({ group, value: (target as HTMLDataElement).value });
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
<!-- desktop: display enhanced select boxes with color codes or swatch images with labels -->
<ng-container *ngIf="(deviceType$ | async) === 'desktop'; else mobile">
<div ngbDropdown>
<button ngbDropdownToggle type="button" class="btn variation-select" [id]="uuid + group.id">
<ng-container *ngFor="let option of group.options">
<ng-container *ngIf="option.active">
<ng-container *ngTemplateOutlet="optionTemplate; context: { group, option }"></ng-container>
</ng-container>
</ng-container>
</button>
<div ngbDropdownMenu class="variation-options" [attr.aria-labelledby]="uuid + group.id + 'label'">
<ng-container *ngFor="let option of group.options">
<button
ngbDropdownItem
*ngIf="!option.alternativeCombination || multipleOptions"
[value]="option.value"
[attr.data-testing-id]="group.id + '-' + option.value"
(click)="optionChange(group.id, option.value)"
>
<ng-container *ngTemplateOutlet="optionTemplate; context: { group, option }"></ng-container>
</button>
</ng-container>
</div>
</div>
</ng-container>

<!-- mobile/tablet: display a list of color codes or swatch images with labels -->
<ng-template #mobile>
<div class="mobile-variation-select">
<div *ngFor="let option of group.options" class="mobile-variation-option">
<a (click)="optionChange(group.id, option.value)">
<ng-container *ngTemplateOutlet="optionTemplate; context: { group, option }"></ng-container>
</a>
</div>
</div>
</ng-template>

<!-- reusable template to render the individual options as color code or image swatch with label -->
<ng-template #optionTemplate let-group="group" let-option="option">
<span
*ngIf="group.attributeType === 'defaultAndColorCode'"
class="color-code"
[ngStyle]="{ 'background-color': '#' + option.metaData }"
[ngClass]="{ 'light-color': option.label.toLowerCase() === 'white' }"
></span>
<img
*ngIf="group.attributeType === 'defaultAndSwatchImage'"
class="image-swatch"
[src]="option.metaData"
alt="{{ option.label }}"
/>
<span class="label" [ngClass]="{ selected: option.active }"
>{{ option.label }}
<ng-container *ngIf="option.alternativeCombination">
- {{ 'product.available_in_different_configuration' | translate }}
</ng-container>
</span>
</ng-template>
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
@import 'variables';

.variation-select {
width: 100%;
padding-right: 12px;
padding-left: 12px;
overflow: hidden;
text-align: left;
border: 1px solid $gray-400;

&.dropdown-toggle::after {
position: absolute;
top: 17px;
right: 10px;
}

.label.selected {
font-family: $font-family-regular;
}
}

.variation-options {
width: 100%;

.dropdown-item {
padding-right: 12px;
padding-left: 12px;
}
}

.mobile-variation-select {
margin: $space-default * 0.5 0 $space-default 0;
font-family: $font-family-regular;

.mobile-variation-option {
margin-bottom: $space-default * 0.5;
}
}

span.color-code,
img.image-swatch {
display: inline-block;
width: 24px;
height: 24px;
margin-right: $space-default * 0.5;
vertical-align: middle;
}

span.color-code {
border: 1px solid transparent;
border-radius: 50%;

&.light-color {
border: 1px solid $border-color-light;
}
}

span.label {
font-family: $font-family-regular;
color: $text-color-primary;
text-transform: none;

&.selected {
font-family: $font-family-bold;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { of } from 'rxjs';
import { anything, capture, instance, mock, spy, verify, when } from 'ts-mockito';

import { AppFacade } from 'ish-core/facades/app.facade';
import { VariationOptionGroup } from 'ish-core/models/product-variation/variation-option-group.model';

import { ProductVariationSelectEnhancedComponent } from './product-variation-select-enhanced.component';

describe('Product Variation Select Enhanced Component', () => {
let component: ProductVariationSelectEnhancedComponent;
let fixture: ComponentFixture<ProductVariationSelectEnhancedComponent>;
let element: HTMLElement;
let appFacade: AppFacade;

const group_colorCode = {
id: 'color',
attributeType: 'defaultAndColorCode',
options: [
{ value: 'black', label: 'Black', metaData: '000000', active: true },
{ value: 'white', label: 'White', metaData: 'FFFFFF' },
],
} as VariationOptionGroup;

const group_swatchImage = {
id: 'swatch',
attributeType: 'defaultAndSwatchImage',
options: [
{ value: 'Y', label: 'yyy', metaData: 'imageY.png' },
{ value: 'Z', label: 'zzz', metaData: 'imageZ.png', active: true },
],
} as VariationOptionGroup;

beforeEach(async () => {
appFacade = mock(AppFacade);
await TestBed.configureTestingModule({
declarations: [ProductVariationSelectEnhancedComponent],
providers: [{ provide: AppFacade, useFactory: () => instance(appFacade) }],
}).compileComponents();
});

beforeEach(() => {
fixture = TestBed.createComponent(ProductVariationSelectEnhancedComponent);
component = fixture.componentInstance;
element = fixture.nativeElement;
component.group = group_colorCode;
component.uuid = 'uuid';
});

it('should be created', () => {
expect(component).toBeTruthy();
expect(element).toBeTruthy();
expect(() => fixture.detectChanges()).not.toThrow();
});

it('should render a color code select when the attribute type is "defaultAndColorCode" for mobile', () => {
component.group = group_colorCode;
fixture.detectChanges();
expect(element).toMatchInlineSnapshot(`
<div class="mobile-variation-select">
<div class="mobile-variation-option">
<a
><span class="color-code" style="background-color: rgb(0, 0, 0)"></span
><span class="label selected">Black </span></a
>
</div>
<div class="mobile-variation-option">
<a
><span class="color-code light-color" style="background-color: rgb(255, 255, 255)"></span
><span class="label">White </span></a
>
</div>
</div>
`);
});

it('should render a swatch image select when the attribute type is "defaultAndColorCode" for desktop', () => {
when(appFacade.deviceType$).thenReturn(of('desktop'));
component.group = group_swatchImage;
fixture.detectChanges();
expect(element).toMatchInlineSnapshot(`
<div ngbdropdown="">
<button ngbdropdowntoggle="" type="button" class="btn variation-select" id="uuidswatch">
<img class="image-swatch" alt="zzz" src="imageZ.png" /><span class="label selected">zzz </span>
</button>
<div ngbdropdownmenu="" class="variation-options" aria-labelledby="uuidswatchlabel">
<button ngbdropdownitem="" value="Y" data-testing-id="swatch-Y">
<img class="image-swatch" alt="yyy" src="imageY.png" /><span class="label">yyy </span></button
><button ngbdropdownitem="" value="Z" data-testing-id="swatch-Z">
<img class="image-swatch" alt="zzz" src="imageZ.png" /><span class="label selected"
>zzz
</span>
</button>
</div>
</div>
`);
});

it('should trigger changeOption output handler if color code element is clicked (mobile)', () => {
component.group = group_colorCode;
fixture.detectChanges();
const emitter = spy(component.changeOption);
const link = fixture.debugElement.query(By.css('.label.selected')).parent.nativeElement;
link.dispatchEvent(new Event('click'));

verify(emitter.emit(anything())).once();
const [arg] = capture(emitter.emit).last();
expect(arg).toMatchInlineSnapshot(`
Object {
"group": "color",
"value": "black",
}
`);
});

it('should trigger changeOption output handler if swatch image element is clicked (desktop)', () => {
when(appFacade.deviceType$).thenReturn(of('desktop'));
component.group = group_swatchImage;
fixture.detectChanges();
const emitter = spy(component.changeOption);
const link = fixture.debugElement.queryAll(By.css('.label.selected')).pop().parent.nativeElement;
link.dispatchEvent(new Event('click'));

verify(emitter.emit(anything())).once();
const [arg] = capture(emitter.emit).last();
expect(arg).toMatchInlineSnapshot(`
Object {
"group": "swatch",
"value": "Z",
}
`);
});
});
Loading

0 comments on commit ddd6e2e

Please sign in to comment.