From f89ac8b28830fd14342e0978acc492d58b24d467 Mon Sep 17 00:00:00 2001 From: Florian Necas Date: Wed, 5 Jul 2023 14:39:00 +0200 Subject: [PATCH] feat: update select dropdown to match multiselect one --- apps/datafeeder/src/app/app.module.ts | 2 + apps/datafeeder/src/index.html | 4 + apps/datafeeder/src/styles.css | 5 + .../lib/chart-view/chart-view.component.html | 12 ++- .../chart-view/chart-view.component.spec.ts | 1 + .../chart-view.component.stories.ts | 2 + .../dataviz/src/lib/feature-dataviz.module.ts | 2 + .../wizard-field/wizard-field.component.html | 2 + .../organisations-sort.component.html | 7 +- .../organisations-sort.component.spec.ts | 19 +++- .../dropdown-selector.component.html | 79 +++++++++++---- .../dropdown-selector.component.spec.ts | 82 +++++++++++----- .../dropdown-selector.component.stories.ts | 7 +- .../dropdown-selector.component.ts | 98 ++++++++++++++++++- 14 files changed, 266 insertions(+), 56 deletions(-) diff --git a/apps/datafeeder/src/app/app.module.ts b/apps/datafeeder/src/app/app.module.ts index f48617a8a7..9f3513a931 100644 --- a/apps/datafeeder/src/app/app.module.ts +++ b/apps/datafeeder/src/app/app.module.ts @@ -35,6 +35,7 @@ import { SummarizePageComponent } from './presentation/pages/summarize-page/summ import { SummarizeIllustrationComponent } from './presentation/components/svg/summarize-illustration/summarize-illustration.component' import { SummarizeBackgroundComponent } from './presentation/components/svg/summarize-background/summarize-background.component' import { DATAFEEDER_STATE_KEY, reducer } from './store/datafeeder.reducer' +import { MatIconModule } from '@angular/material/icon' export function apiConfigurationFactory() { return new Configuration({ @@ -71,6 +72,7 @@ export function apiConfigurationFactory() { UiInputsModule, UiWidgetsModule, HttpClientModule, + MatIconModule, UtilI18nModule, FeatureEditorModule, ApiModule.forRoot(apiConfigurationFactory), diff --git a/apps/datafeeder/src/index.html b/apps/datafeeder/src/index.html index 0fb328569d..2d1b4680d1 100644 --- a/apps/datafeeder/src/index.html +++ b/apps/datafeeder/src/index.html @@ -11,6 +11,10 @@ href="https://fonts.googleapis.com/css2?family=Open+Sans:wght@400;700&family=Permanent+Marker&display=swap" rel="stylesheet" /> + diff --git a/apps/datafeeder/src/styles.css b/apps/datafeeder/src/styles.css index 7a92c35275..c7ef160664 100644 --- a/apps/datafeeder/src/styles.css +++ b/apps/datafeeder/src/styles.css @@ -43,6 +43,11 @@ gn-ui-button button[type='button'].secondary { border-color: var(--color-primary); border-width: 1px; } + +gn-ui-dropdown-selector gn-ui-button button[type='button'].secondary { + border-width: 2px; +} + gn-ui-button button[type='button'].secondary:hover { background: var(--color-primary-darker); color: white; diff --git a/libs/feature/dataviz/src/lib/chart-view/chart-view.component.html b/libs/feature/dataviz/src/lib/chart-view/chart-view.component.html index 0a35eb6f34..4e9dc5af86 100644 --- a/libs/feature/dataviz/src/lib/chart-view/chart-view.component.html +++ b/libs/feature/dataviz/src/lib/chart-view/chart-view.component.html @@ -1,23 +1,25 @@
', }) export class MockDropdownSelectorComponent { + @Input() selected: any @Input() choices: unknown[] @Output() selectValue = new EventEmitter() } diff --git a/libs/feature/dataviz/src/lib/chart-view/chart-view.component.stories.ts b/libs/feature/dataviz/src/lib/chart-view/chart-view.component.stories.ts index 1614349040..7b0e6fcbc4 100644 --- a/libs/feature/dataviz/src/lib/chart-view/chart-view.component.stories.ts +++ b/libs/feature/dataviz/src/lib/chart-view/chart-view.component.stories.ts @@ -12,6 +12,7 @@ import { ChartViewComponent } from './chart-view.component' import { ChartComponent, UiDatavizModule } from '@geonetwork-ui/ui/dataviz' import { UiWidgetsModule } from '@geonetwork-ui/ui/widgets' import { MetadataLinkType } from '@geonetwork-ui/util/shared' +import { OverlayModule } from '@angular/cdk/overlay' export default { title: 'Smart/Dataviz/ChartView', @@ -21,6 +22,7 @@ export default { imports: [ ChartComponent, HttpClientModule, + OverlayModule, UiDatavizModule, UiWidgetsModule, BrowserAnimationsModule, diff --git a/libs/feature/dataviz/src/lib/feature-dataviz.module.ts b/libs/feature/dataviz/src/lib/feature-dataviz.module.ts index d414715c09..f5e79bc658 100644 --- a/libs/feature/dataviz/src/lib/feature-dataviz.module.ts +++ b/libs/feature/dataviz/src/lib/feature-dataviz.module.ts @@ -14,6 +14,7 @@ import { ChartViewComponent } from './chart-view/chart-view.component' import { TranslateModule } from '@ngx-translate/core' import { UiWidgetsModule } from '@geonetwork-ui/ui/widgets' import { UiInputsModule } from '@geonetwork-ui/ui/inputs' +import { OverlayModule } from '@angular/cdk/overlay' @NgModule({ imports: [ @@ -25,6 +26,7 @@ import { UiInputsModule } from '@geonetwork-ui/ui/inputs' UiWidgetsModule, TranslateModule, ChartComponent, + OverlayModule, UiInputsModule, ], declarations: [ diff --git a/libs/feature/editor/src/lib/components/wizard-field/wizard-field.component.html b/libs/feature/editor/src/lib/components/wizard-field/wizard-field.component.html index 0296751f56..671374af1e 100644 --- a/libs/feature/editor/src/lib/components/wizard-field/wizard-field.component.html +++ b/libs/feature/editor/src/lib/components/wizard-field/wizard-field.component.html @@ -64,8 +64,10 @@ #dropdown [id]="wizardFieldConfig.id" [title]="''" + [extraClass]="'secondary min-w-full'" [showTitle]="false" [choices]="dropdownChoices" + [selectedValueExpectedAsObject]="true" [selected]="wizardFieldData" ariaName="search-sort-by" > diff --git a/libs/ui/catalog/src/lib/organisations-sort/organisations-sort.component.html b/libs/ui/catalog/src/lib/organisations-sort/organisations-sort.component.html index c95bcb5f44..60ad9c85b4 100644 --- a/libs/ui/catalog/src/lib/organisations-sort/organisations-sort.component.html +++ b/libs/ui/catalog/src/lib/organisations-sort/organisations-sort.component.html @@ -5,14 +5,11 @@

organisation.sort.intro

- - organisation.sort.sortBy - diff --git a/libs/ui/catalog/src/lib/organisations-sort/organisations-sort.component.spec.ts b/libs/ui/catalog/src/lib/organisations-sort/organisations-sort.component.spec.ts index ad5b342c75..47997afc13 100644 --- a/libs/ui/catalog/src/lib/organisations-sort/organisations-sort.component.spec.ts +++ b/libs/ui/catalog/src/lib/organisations-sort/organisations-sort.component.spec.ts @@ -1,6 +1,22 @@ import { ComponentFixture, TestBed } from '@angular/core/testing' import { OrganisationsSortComponent } from './organisations-sort.component' +import { Component, EventEmitter, Input, Output } from '@angular/core' +import { TranslateModule } from '@ngx-translate/core' + +@Component({ + selector: 'gn-ui-dropdown-selector', + template: '', +}) +class DropdownSelectorMockComponent { + @Input() showTitle: unknown + @Input() choices: { + value: unknown + label: string + }[] + @Input() selected: unknown + @Output() selectValue = new EventEmitter() +} describe('OrganisationsOrderComponent', () => { let component: OrganisationsSortComponent @@ -8,7 +24,8 @@ describe('OrganisationsOrderComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - declarations: [OrganisationsSortComponent], + declarations: [OrganisationsSortComponent, DropdownSelectorMockComponent], + imports: [TranslateModule.forRoot()], }).compileComponents() fixture = TestBed.createComponent(OrganisationsSortComponent) diff --git a/libs/ui/inputs/src/lib/dropdown-selector/dropdown-selector.component.html b/libs/ui/inputs/src/lib/dropdown-selector/dropdown-selector.component.html index 62326cf1d2..05a91a6f8c 100644 --- a/libs/ui/inputs/src/lib/dropdown-selector/dropdown-selector.component.html +++ b/libs/ui/inputs/src/lib/dropdown-selector/dropdown-selector.component.html @@ -1,27 +1,68 @@ -
- - -
-
+ + + {{ choice.label | translate }} + + +
+ diff --git a/libs/ui/inputs/src/lib/dropdown-selector/dropdown-selector.component.spec.ts b/libs/ui/inputs/src/lib/dropdown-selector/dropdown-selector.component.spec.ts index b66a69c21f..0cc8561b5a 100644 --- a/libs/ui/inputs/src/lib/dropdown-selector/dropdown-selector.component.spec.ts +++ b/libs/ui/inputs/src/lib/dropdown-selector/dropdown-selector.component.spec.ts @@ -1,8 +1,9 @@ import { ComponentFixture, TestBed } from '@angular/core/testing' import { TranslateModule } from '@ngx-translate/core' import { ButtonComponent } from '../button/button.component' - import { DropdownSelectorComponent } from './dropdown-selector.component' +import { OverlayModule } from '@angular/cdk/overlay' +import { MatIconModule } from '@angular/material/icon' describe('DropdownSelectorComponent', () => { let component: DropdownSelectorComponent @@ -10,7 +11,7 @@ describe('DropdownSelectorComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [TranslateModule.forRoot()], + imports: [OverlayModule, MatIconModule, TranslateModule.forRoot()], declarations: [DropdownSelectorComponent, ButtonComponent], }).compileComponents() }) @@ -24,37 +25,74 @@ describe('DropdownSelectorComponent', () => { { label: 'B', value: 'b' }, { label: 'C', value: 'c' }, ] + component.selectedValueExpectedAsObject = false + fixture.detectChanges() }) it('should create', () => { - fixture.detectChanges() expect(component).toBeTruthy() }) - describe('items array', () => { - let choicesEl - let selectEl + describe('items selection', () => { + let emitted beforeEach(() => { component.selected = 'b' - fixture.detectChanges() - choicesEl = fixture.nativeElement.querySelectorAll('option') - selectEl = fixture.nativeElement.querySelector('select') + emitted = null + component.selectValue.subscribe((v) => (emitted = v)) + }) + describe('when clicking an item with selectedValueExpectedAsObject', () => { + it('emits the correct item as Json object', () => { + component.onSelectValue({ label: 'A', value: 'a' }) + expect(emitted).toEqual('a') + }) }) - it('shows one element per item in the dropdown', () => { - expect(choicesEl.length).toBe(component.choices.length) + + describe('when clicking an item with selectedValueExpectedAsObject', () => { + beforeEach(() => { + component.selectedValueExpectedAsObject = true + }) + it('emits the correct item as Json object', () => { + component.selectedValueExpectedAsObject = true + component.onSelectValue({ label: 'A', value: 'a' }) + expect(emitted).toEqual(JSON.stringify({ label: 'A', value: 'a' })) + }) }) - it('displays the active element as such', () => { - expect(selectEl.value).toBe('b') - expect(choicesEl[0].selected).toBeFalsy() - expect(choicesEl[1].selected).toBeTruthy() - expect(choicesEl[2].selected).toBeFalsy() + }) + + describe('overlay sizing', () => { + describe('width', () => { + beforeEach(() => { + const originEl: HTMLElement = + component.overlayOrigin.elementRef.nativeElement + originEl.getBoundingClientRect = () => + ({ + width: 25, + height: 20, + } as any) + component.openOverlay() + }) + it('sets the width according to the toggle element', () => { + expect(component.overlayWidth).toBe('25px') + }) }) - it('emits the value of the clicked item', () => { - let emitted - component.selectValue.subscribe((v) => (emitted = v)) - selectEl.value = component.choices[0].value - selectEl.dispatchEvent(new Event('change')) - expect(emitted).toBe(component.choices[0].value) + describe('max height (with maxRows set)', () => { + beforeEach(() => { + component.maxRows = 10 + component.openOverlay() + }) + it('sets the max height according to the max rows input', () => { + expect(component.overlayMaxHeight).toMatch('350px') + }) + }) + describe('max height (with maxRows unset)', () => { + beforeEach(() => { + component.maxRows = undefined + component.openOverlay() + }) + it('sets the max height according to the max rows input', () => { + // we don't need the exact measurement, just to make sure it's an actual value + expect(component.overlayMaxHeight).toBe('none') + }) }) }) }) diff --git a/libs/ui/inputs/src/lib/dropdown-selector/dropdown-selector.component.stories.ts b/libs/ui/inputs/src/lib/dropdown-selector/dropdown-selector.component.stories.ts index 21c4b2e697..d3c7fb3459 100644 --- a/libs/ui/inputs/src/lib/dropdown-selector/dropdown-selector.component.stories.ts +++ b/libs/ui/inputs/src/lib/dropdown-selector/dropdown-selector.component.stories.ts @@ -1,10 +1,12 @@ import { Meta, moduleMetadata, Story } from '@storybook/angular' import { DropdownSelectorComponent } from './dropdown-selector.component' +import { OverlayModule } from '@angular/cdk/overlay' import { TranslateModule } from '@ngx-translate/core' import { TRANSLATE_DEFAULT_CONFIG, UtilI18nModule, } from '@geonetwork-ui/util/i18n' +import { MatCheckboxModule } from '@angular/material/checkbox' export default { title: 'Inputs/DropdownSelectorComponent', @@ -13,6 +15,8 @@ export default { moduleMetadata({ imports: [ UtilI18nModule, + OverlayModule, + MatCheckboxModule, TranslateModule.forRoot(TRANSLATE_DEFAULT_CONFIG), ], }), @@ -44,7 +48,8 @@ Primary.args = { value: 'choice3', }, ], - selected: 'choice1', + selected: { label: 'My Choice 1', value: 'choice1' }, + selectedValueExpectedAsObject: true, showTitle: true, } Primary.argTypes = { diff --git a/libs/ui/inputs/src/lib/dropdown-selector/dropdown-selector.component.ts b/libs/ui/inputs/src/lib/dropdown-selector/dropdown-selector.component.ts index 2c187ebd13..761bac564a 100644 --- a/libs/ui/inputs/src/lib/dropdown-selector/dropdown-selector.component.ts +++ b/libs/ui/inputs/src/lib/dropdown-selector/dropdown-selector.component.ts @@ -1,11 +1,19 @@ import { - AfterViewInit, + CdkConnectedOverlay, + CdkOverlayOrigin, + ConnectedPosition, +} from '@angular/cdk/overlay' +import { + AfterContentChecked, ChangeDetectionStrategy, Component, EventEmitter, Input, + OnInit, Output, + ViewChild, } from '@angular/core' +import { Choice } from '../dropdown-multiselect/dropdown-multiselect.model' export type DDChoices = Array<{ label: string @@ -18,20 +26,104 @@ export type DDChoices = Array<{ styleUrls: ['./dropdown-selector.component.css'], changeDetection: ChangeDetectionStrategy.OnPush, }) -export class DropdownSelectorComponent { +export class DropdownSelectorComponent implements AfterContentChecked, OnInit { @Input() title: string @Input() showTitle = true @Input() ariaName: string @Input() choices: DDChoices @Input() selected: any + @Input() selectedValueExpectedAsObject: boolean + @Input() maxRows: number @Input() extraClass = '' @Output() selectValue = new EventEmitter() + @ViewChild('overlayOrigin') overlayOrigin: CdkOverlayOrigin + @ViewChild(CdkConnectedOverlay) overlay: CdkConnectedOverlay + overlayOpen = false + overlayWidth = 'auto' + overlayMaxHeight = 'none' + overlayPositions: ConnectedPosition[] = [ + { + originX: 'start', + originY: 'bottom', + overlayX: 'start', + overlayY: 'top', + offsetY: 8, + }, + { + originX: 'start', + originY: 'top', + overlayX: 'start', + overlayY: 'bottom', + offsetY: -8, + }, + ] + selectedObject: Choice get id() { return this.title.toLowerCase().replace(/[^a-z]+/g, '-') } + getChoiceLabel(): string { + return this.selectedObject ? this.selectedObject.label : '' + } + + ngOnInit(): void { + if (!this.maxRows) this.maxRows = 6 + } + + ngAfterContentChecked(): void { + if (!this.selected && this.choices) { + this.selected = this.getExpectedSelectionFromChoice(this.choices[0]) + } + if (this.selected && this.choices && this.selectedObjecthasToBeSet()) { + const newSelectionObject = this.selectedValueExpectedAsObject + ? this.selected + : this.choices?.filter((choice) => choice.value == this.selected)[0] + if (newSelectionObject) { + this.selectedObject = newSelectionObject + } + } + } + isSelected(choice) { - return choice.value === this.selected + return choice === this.selectedObject + } + + onSelectValue(choice: Choice): void { + this.closeOverlay() + this.selectedObject = choice + this.selected = this.getExpectedSelectionFromChoice(choice) + this.selectValue.emit( + this.selectedValueExpectedAsObject + ? JSON.stringify(this.selected) + : this.selected + ) + } + + openOverlay() { + this.overlayWidth = + this.overlayOrigin.elementRef.nativeElement.getBoundingClientRect() + .width + 'px' + this.overlayMaxHeight = this.maxRows + ? `${this.maxRows * 29 + 60}px` + : 'none' + this.overlayOpen = true + } + + closeOverlay() { + this.overlayOpen = false + } + + private getExpectedSelectionFromChoice(choice: Choice): any { + return this.selectedValueExpectedAsObject ? choice : choice.value + } + + private selectedObjecthasToBeSet(): boolean { + if (this.selectedObject === undefined) return true + return ( + (this.selectedValueExpectedAsObject + ? this.selected.value + : this.selected) !== this.selectedObject.value + ) } }