diff --git a/apps/datafeeder/src/app/app.module.ts b/apps/datafeeder/src/app/app.module.ts
index 19bb95c30f..9af9382362 100644
--- a/apps/datafeeder/src/app/app.module.ts
+++ b/apps/datafeeder/src/app/app.module.ts
@@ -36,6 +36,7 @@ import { SummarizeIllustrationComponent } from './presentation/components/svg/su
import { SummarizeBackgroundComponent } from './presentation/components/svg/summarize-background/summarize-background.component'
import { DATAFEEDER_STATE_KEY, reducer } from './store/datafeeder.reducer'
import { FeatureAuthModule } from '@geonetwork-ui/feature/auth'
+import { MatIconModule } from '@angular/material/icon'
export function apiConfigurationFactory() {
return new Configuration({
@@ -72,6 +73,7 @@ export function apiConfigurationFactory() {
UiInputsModule,
UiWidgetsModule,
HttpClientModule,
+ MatIconModule,
UtilI18nModule,
FeatureEditorModule,
ApiModule.forRoot(apiConfigurationFactory),
diff --git a/apps/datafeeder/src/app/presentation/components/data-import-validation-map-panel/data-import-validation-map-panel.component.html b/apps/datafeeder/src/app/presentation/components/data-import-validation-map-panel/data-import-validation-map-panel.component.html
index b08e5c1475..1758fdfa98 100644
--- a/apps/datafeeder/src/app/presentation/components/data-import-validation-map-panel/data-import-validation-map-panel.component.html
+++ b/apps/datafeeder/src/app/presentation/components/data-import-validation-map-panel/data-import-validation-map-panel.component.html
@@ -23,6 +23,7 @@
[choices]="footerList"
(selectValue)="selectValue($event)"
[selected]="selectedValue"
+ [extraBtnClass]="'secondary min-w-full'"
ariaName="search-sort-by"
*ngIf="footerList.length > 0"
>
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 5542051c77..1367531e67 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
@@ -5,6 +5,7 @@
',
})
export class MockDropdownSelectorComponent {
+ @Input() selected: any
@Input() choices: unknown[]
@Output() selectValue = new EventEmitter()
}
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..5d01ed32aa 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,6 +64,7 @@
#dropdown
[id]="wizardFieldConfig.id"
[title]="''"
+ [extraBtnClass]="'secondary min-w-full'"
[showTitle]="false"
[choices]="dropdownChoices"
[selected]="wizardFieldData"
diff --git a/libs/feature/editor/src/lib/components/wizard-field/wizard-field.component.ts b/libs/feature/editor/src/lib/components/wizard-field/wizard-field.component.ts
index 1bfe78ab20..fe45598346 100644
--- a/libs/feature/editor/src/lib/components/wizard-field/wizard-field.component.ts
+++ b/libs/feature/editor/src/lib/components/wizard-field/wizard-field.component.ts
@@ -103,7 +103,7 @@ export class WizardFieldComponent implements AfterViewInit, OnDestroy {
return data ? new Date(Number(data)) : new Date()
}
case WizardFieldType.DROPDOWN: {
- return data ? JSON.parse(data) : this.dropdownChoices[1]
+ return data ? JSON.parse(data) : this.dropdownChoices[0]?.value
}
}
}
diff --git a/libs/feature/record/src/lib/data-view/data-view.component.html b/libs/feature/record/src/lib/data-view/data-view.component.html
index c13524d976..0a647a42d2 100644
--- a/libs/feature/record/src/lib/data-view/data-view.component.html
+++ b/libs/feature/record/src/lib/data-view/data-view.component.html
@@ -3,7 +3,7 @@
*ngIf="dropdownChoices$ | async as choices"
[title]="'table.select.data' | translate"
class="mb-7 w-auto ml-auto"
- extraClass="!text-primary font-sans font-medium"
+ extraBtnClass="!text-primary font-sans font-medium"
[choices]="choices"
(selectValue)="selectLink($event)"
>
diff --git a/libs/feature/record/src/lib/map-view/map-view.component.html b/libs/feature/record/src/lib/map-view/map-view.component.html
index f857435c17..81d0155f5d 100644
--- a/libs/feature/record/src/lib/map-view/map-view.component.html
+++ b/libs/feature/record/src/lib/map-view/map-view.component.html
@@ -1,7 +1,7 @@
-
+
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 25250fc3ec..a2f2758e75 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,5 +1,21 @@
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
@@ -7,7 +23,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..f52c31448e 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,71 @@
-
-
-
+
+ expand_less
+ expand_more
+
+
+
+
+
+
+
+
+
+ {{ 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..a4ce0b1836 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,89 @@ describe('DropdownSelectorComponent', () => {
{ label: 'B', value: 'b' },
{ label: 'C', value: 'c' },
]
+ 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))
})
- it('shows one element per item in the dropdown', () => {
- expect(choicesEl.length).toBe(component.choices.length)
+ 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('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('when an existing value is provided', () => {
+ beforeEach(() => {
+ component.selected = 'b'
+ })
+ it('selects the corresponding choice', () => {
+ expect(component.selectedChoice).toEqual({ label: 'B', value: 'b' })
+ })
})
- 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('when no selected value is provided', () => {
+ beforeEach(() => {
+ component.selected = undefined
+ })
+ it('selects the first choice', () => {
+ expect(component.selectedChoice).toEqual({ label: 'A', value: 'a' })
+ })
+ })
+
+ describe('when the selected value is not part of the choices', () => {
+ beforeEach(() => {
+ component.selected = 'blarg'
+ })
+ it('selects the first choice', () => {
+ expect(component.selectedChoice).toEqual({ label: 'A', value: 'a' })
+ })
+ })
+ })
+
+ 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')
+ })
+ })
+ 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 99c1377c0e..4175ff67b2 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
@@ -5,11 +5,13 @@ import {
StoryObj,
} 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',
@@ -19,6 +21,8 @@ export default {
declarations: [],
imports: [
UtilI18nModule,
+ OverlayModule,
+ MatCheckboxModule,
TranslateModule.forRoot(TRANSLATE_DEFAULT_CONFIG),
],
}),
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 8d37d2e9f5..3a08f44ad1 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,37 +1,106 @@
import {
- AfterViewInit,
+ CdkConnectedOverlay,
+ CdkOverlayOrigin,
+ ConnectedPosition,
+} from '@angular/cdk/overlay'
+import {
ChangeDetectionStrategy,
Component,
EventEmitter,
Input,
+ OnInit,
Output,
+ ViewChild,
} from '@angular/core'
+import { Choice } from '../dropdown-multiselect/dropdown-multiselect.model'
export type DDChoices = Array<{
label: string
value: string
}>
+const DEFAULT_ROW_NUMBERS = 6
+
@Component({
selector: 'gn-ui-dropdown-selector',
templateUrl: './dropdown-selector.component.html',
styleUrls: ['./dropdown-selector.component.css'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
-export class DropdownSelectorComponent {
+export class DropdownSelectorComponent implements OnInit {
@Input() title: string
@Input() showTitle = true
@Input() ariaName: string
@Input() choices: DDChoices
@Input() selected: any
- @Input() extraClass = ''
+ @Input() maxRows: number
+ @Input() extraBtnClass = ''
+ @Input() minWidth = ''
@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,
+ },
+ ]
+ get selectedChoice(): Choice {
+ return (
+ this.choices.find((choice) => choice.value === this.selected) ??
+ this.choices[0]
+ )
+ }
get id() {
return this.title.toLowerCase().replace(/[^a-z]+/g, '-')
}
+ getChoiceLabel(): string {
+ return this.selectedChoice?.label
+ }
+
+ ngOnInit(): void {
+ if (!this.maxRows) this.maxRows = DEFAULT_ROW_NUMBERS
+ if (!this.choices || this.choices.length === 0) {
+ this.choices = []
+ }
+ }
+
isSelected(choice) {
- return choice.value === this.selected
+ return choice === this.selectedChoice
+ }
+
+ onSelectValue(choice: Choice): void {
+ this.closeOverlay()
+ this.selected = choice.value
+ this.selectValue.emit(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
}
}