feature(Teknisk skuld): Refaktorisera overlay och multiselect (används i filtren i Avrop). Förberedande arbete inför att göra egna komponenter för dialog och popover (TV-845)
Merge in TEA/mina-sidor-fa-web from feature/TV-845-move-overlays-(dialog,-popover)-to-@ui to develop
Squashed commit of the following:
commit b98faf62aa47a155acb5609cdbdbc31a2c726ee9
Author: Daniel Appelgren <daniel.appelgren@arbetsformedlingen.se>
Date: Tue Nov 30 09:13:48 2021 +0100
Update overlay-trigger-for.directive.ts
commit 3945b98f702e82e100cb223da8da0be02fd48d17
Author: Daniel Appelgren <daniel.appelgren@arbetsformedlingen.se>
Date: Mon Nov 29 11:45:33 2021 +0100
Update avrop.service.ts
commit 4c65724cac9825a599feb003c725ac37a40d03fa
Author: Daniel Appelgren <daniel.appelgren@arbetsformedlingen.se>
Date: Mon Nov 29 11:40:08 2021 +0100
cleanup
commit aad6c2fc64e7d8c4d19da5f3c061f92a953be59e
Author: Daniel Appelgren <daniel.appelgren@arbetsformedlingen.se>
Date: Mon Nov 29 11:06:21 2021 +0100
refactor overlay and multiselect
commit cf57126f36416f6e9de60d0048e2494bc4141188
Merge: 94257008 609698eb
Author: Daniel Appelgren <daniel.appelgren@arbetsformedlingen.se>
Date: Mon Nov 29 08:41:55 2021 +0100
Merge branch 'develop' into feature/TV-845-move-overlays-(dialog,-popover)-to-@ui
commit 942570081f4bad6e6dc68f5416619cd64d9d6bb4
Author: Daniel Appelgren <daniel.appelgren@arbetsformedlingen.se>
Date: Wed Nov 24 15:44:56 2021 +0100
migrate Dropdown to UI and extract Overlay part for other usage
This commit is contained in:
@@ -1,34 +1,34 @@
|
||||
<div class="avrop-filters" *ngIf="(filtersAreLoading$ | async) === false; else loadingRef">
|
||||
<div class="avrop-filters__filter-wrapper" *ngIf="availableTjanster$ | async as availableTjanster">
|
||||
<msfa-multiselect
|
||||
<ui-multiselect
|
||||
[multiselectTitle]="'Tjänster'"
|
||||
[availableOptions]="availableTjanster"
|
||||
confirmButtonText="Uppdatera filter"
|
||||
[selectedOptions]="filteredTjanster$ | async"
|
||||
(selectedOptionsChange)="updateSelectedTjanster($event)"
|
||||
></msfa-multiselect>
|
||||
></ui-multiselect>
|
||||
</div>
|
||||
<div
|
||||
class="avrop-filters__filter-wrapper"
|
||||
*ngIf="availableUtforandeVerksamheter$ | async as availableUtforandeVerksamheter"
|
||||
>
|
||||
<msfa-multiselect
|
||||
<ui-multiselect
|
||||
[multiselectTitle]="'Utförande verksamheter'"
|
||||
confirmButtonText="Uppdatera filter"
|
||||
[availableOptions]="availableUtforandeVerksamheter"
|
||||
[selectedOptions]="filteredUtforandeVerksamheter$ | async"
|
||||
(selectedOptionsChange)="updateSelectedUtforandeVerksamheter($event)"
|
||||
></msfa-multiselect>
|
||||
></ui-multiselect>
|
||||
</div>
|
||||
|
||||
<div class="avrop-filters__filter-wrapper" *ngIf="availableKommuner$ | async as availableKommuner">
|
||||
<msfa-multiselect
|
||||
<ui-multiselect
|
||||
[multiselectTitle]="'Kommuner'"
|
||||
[availableOptions]="availableKommuner"
|
||||
[selectedOptions]="filteredKommuner$ | async"
|
||||
confirmButtonText="Uppdatera filter"
|
||||
(selectedOptionsChange)="updateSelectedKommuner($event)"
|
||||
></msfa-multiselect>
|
||||
></ui-multiselect>
|
||||
</div>
|
||||
</div>
|
||||
<br /><br />
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { ChangeDetectionStrategy, Component } from '@angular/core';
|
||||
import { AvropService } from '@msfa-services/avrop.service';
|
||||
import { MultiselectFilterOption } from '@msfa-shared/components/multiselect/multiselect-filter-option';
|
||||
import { combineLatest, Observable } from 'rxjs';
|
||||
import { map } from 'rxjs/operators';
|
||||
import { MultiselectFilterOption } from '@ui/multiselect/multiselect-filter-option';
|
||||
|
||||
@Component({
|
||||
selector: 'msfa-avrop-filters',
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
||||
import { MultiselectModule } from '@msfa-shared/components/multiselect/multiselect.module';
|
||||
import { UiLoaderModule } from '@ui/loader/loader.module';
|
||||
import { AvropFiltersComponent } from './avrop-filters.component';
|
||||
import { UiMultiselectModule } from '@ui/multiselect/multiselect.module';
|
||||
|
||||
@NgModule({
|
||||
schemas: [CUSTOM_ELEMENTS_SCHEMA],
|
||||
declarations: [AvropFiltersComponent],
|
||||
imports: [CommonModule, MultiselectModule, UiLoaderModule],
|
||||
imports: [CommonModule, UiMultiselectModule, UiLoaderModule],
|
||||
exports: [AvropFiltersComponent],
|
||||
})
|
||||
export class AvropFiltersModule {}
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
import { EventEmitter, TemplateRef } from '@angular/core';
|
||||
|
||||
export interface DropdownPanel {
|
||||
templateRef: TemplateRef<never>;
|
||||
readonly closed: EventEmitter<void>;
|
||||
}
|
||||
@@ -1,101 +0,0 @@
|
||||
import { Overlay, OverlayRef } from '@angular/cdk/overlay';
|
||||
import { TemplatePortal } from '@angular/cdk/portal';
|
||||
import { Directive, ElementRef, HostListener, Input, OnDestroy, ViewContainerRef } from '@angular/core';
|
||||
import { merge, Observable, Subscription } from 'rxjs';
|
||||
import { DropdownPanel } from './dropdown-panel';
|
||||
|
||||
@Directive({
|
||||
selector: '[msfaDropdownTriggerFor]',
|
||||
})
|
||||
export class DropdownTriggerForDirective implements OnDestroy {
|
||||
@Input('msfaDropdownTriggerFor') public dropdownPanel: DropdownPanel;
|
||||
|
||||
private isDropdownOpen = false;
|
||||
public overlayRef: OverlayRef;
|
||||
private dropdownClosingActionsSub = Subscription.EMPTY;
|
||||
|
||||
private onClosedCallback: () => void = () => {
|
||||
return;
|
||||
};
|
||||
private onOpenCallback: () => void = () => {
|
||||
return;
|
||||
};
|
||||
|
||||
@HostListener('click') onClick(): void {
|
||||
return this.toggleDropdown();
|
||||
}
|
||||
@HostListener('document:keyup.escape', ['$event']) onKeydownHandler(): void {
|
||||
this.closeDropdown();
|
||||
}
|
||||
|
||||
constructor(
|
||||
private overlay: Overlay,
|
||||
private elementRef: ElementRef<HTMLElement>,
|
||||
private viewContainerRef: ViewContainerRef
|
||||
) {}
|
||||
|
||||
toggleDropdown(): void {
|
||||
this.isDropdownOpen ? this.closeDropdown() : this.openDropdown();
|
||||
}
|
||||
|
||||
openDropdown(): void {
|
||||
this.isDropdownOpen = true;
|
||||
this.overlayRef = this.overlay.create({
|
||||
hasBackdrop: true,
|
||||
backdropClass: 'cdk-overlay-transparent-backdrop',
|
||||
scrollStrategy: this.overlay.scrollStrategies.close(),
|
||||
positionStrategy: this.overlay
|
||||
.position()
|
||||
.flexibleConnectedTo(this.elementRef)
|
||||
.withPositions([
|
||||
{
|
||||
originX: 'start',
|
||||
originY: 'bottom',
|
||||
overlayX: 'start',
|
||||
overlayY: 'top',
|
||||
offsetY: 3,
|
||||
},
|
||||
]),
|
||||
});
|
||||
|
||||
const templatePortal = new TemplatePortal(this.dropdownPanel.templateRef, this.viewContainerRef);
|
||||
|
||||
this.overlayRef.attach(templatePortal);
|
||||
this.onOpenCallback();
|
||||
|
||||
this.dropdownClosingActionsSub = this.dropdownClosingActions().subscribe(() => this.closeDropdown());
|
||||
}
|
||||
|
||||
private dropdownClosingActions(): Observable<MouseEvent | void> {
|
||||
const backdropClick$ = this.overlayRef.backdropClick();
|
||||
const detachment$ = this.overlayRef.detachments();
|
||||
const dropdownClosed = this.dropdownPanel.closed;
|
||||
|
||||
return merge(backdropClick$, detachment$, dropdownClosed);
|
||||
}
|
||||
|
||||
closeDropdown(): void {
|
||||
if (!this.overlayRef || !this.isDropdownOpen) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.onClosedCallback();
|
||||
this.dropdownClosingActionsSub.unsubscribe();
|
||||
this.isDropdownOpen = false;
|
||||
this.overlayRef.detach();
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
if (this.overlayRef) {
|
||||
this.overlayRef.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
onClosed(fn: () => void): void {
|
||||
this.onClosedCallback = fn;
|
||||
}
|
||||
|
||||
onOpen(fn: () => void): void {
|
||||
this.onOpenCallback = fn;
|
||||
}
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
<ng-template>
|
||||
<div class="dropdown-content" cdkTrapFocus cdkTrapFocusAutoCapture>
|
||||
<ng-content></ng-content>
|
||||
</div>
|
||||
</ng-template>
|
||||
@@ -1,7 +0,0 @@
|
||||
@import "variables/shadows";
|
||||
|
||||
.dropdown-content {
|
||||
background-color: white;
|
||||
border-radius: var(--digi--ui--border--radius);
|
||||
box-shadow: $msfa__shadow;
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { DropdownComponent } from './dropdown.component';
|
||||
|
||||
describe('DropdownComponent', () => {
|
||||
let component: DropdownComponent;
|
||||
let fixture: ComponentFixture<DropdownComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [ DropdownComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(DropdownComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -1,13 +0,0 @@
|
||||
import { ChangeDetectionStrategy, Component, EventEmitter, Output, TemplateRef, ViewChild } from '@angular/core';
|
||||
import { DropdownPanel } from './dropdown-panel';
|
||||
|
||||
@Component({
|
||||
selector: 'msfa-dropdown',
|
||||
templateUrl: './dropdown.component.html',
|
||||
styleUrls: ['./dropdown.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class DropdownComponent implements DropdownPanel {
|
||||
@ViewChild(TemplateRef) templateRef: TemplateRef<never>;
|
||||
@Output() closed = new EventEmitter<void>();
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { DropdownTriggerForDirective } from './dropdown-trigger-for.directive';
|
||||
import { DropdownComponent } from './dropdown.component';
|
||||
import { OverlayModule } from '@angular/cdk/overlay';
|
||||
import { A11yModule } from '@angular/cdk/a11y';
|
||||
|
||||
@NgModule({
|
||||
declarations: [DropdownTriggerForDirective, DropdownComponent],
|
||||
imports: [CommonModule, OverlayModule, A11yModule],
|
||||
exports: [DropdownTriggerForDirective, DropdownComponent],
|
||||
})
|
||||
export class DropdownModule {}
|
||||
@@ -1,6 +0,0 @@
|
||||
export interface MultiselectFilterOption {
|
||||
label?: string;
|
||||
id?: string;
|
||||
count?: number;
|
||||
secondary?: boolean;
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
<form autocomplete="off">
|
||||
<div class="multiselect-panel">
|
||||
<div class="multiselect-panel__content">
|
||||
<fieldset
|
||||
class="multiselect-panel__fieldset"
|
||||
*ngIf="availableOptions?.length > 0 else noOptionsWithCurrentFilters"
|
||||
>
|
||||
<legend class="multiselect-panel__legend">{{ multiselectTitle }}</legend>
|
||||
<p *ngIf="description" class="multiselect-panel__description">{{ description }}</p>
|
||||
|
||||
<div class="multiselect-panel__checkboxes">
|
||||
<ul class="multiselect-panel__checkboxes-list">
|
||||
<li
|
||||
class="multiselect-panel__checkboxes-item"
|
||||
*ngFor="let multiselectOption of availableOptions; let i = index"
|
||||
>
|
||||
<digi-ng-form-checkbox
|
||||
[afLabel]="multiselectOption.label + ' (' + (multiselectOption.count || 0) + ')'"
|
||||
(change)="setOptionState(multiselectOption, $event.target.checked)"
|
||||
[ngModel]="isSelected(multiselectOption)"
|
||||
[name]="multiselectOption.id"
|
||||
></digi-ng-form-checkbox>
|
||||
</li>
|
||||
<ng-container *ngIf="selectedUnavailableOptions.length">
|
||||
<li
|
||||
class="multiselect-panel__checkboxes-item"
|
||||
[ngClass]="{'multiselect-panel__checkboxes-item--unavailable': i === 0}"
|
||||
*ngFor="let multiselectOption of selectedUnavailableOptions; let i = index"
|
||||
>
|
||||
<digi-ng-form-checkbox
|
||||
[afLabel]="multiselectOption.label + ' (0)'"
|
||||
(change)="setOptionState(multiselectOption, $event.target.checked)"
|
||||
[ngModel]="isSelected(multiselectOption)"
|
||||
[name]="multiselectOption.id"
|
||||
[afSecondary]="true"
|
||||
></digi-ng-form-checkbox>
|
||||
</li>
|
||||
</ng-container>
|
||||
</ul>
|
||||
</div>
|
||||
</fieldset>
|
||||
</div>
|
||||
|
||||
<div class="multiselect-panel__footer">
|
||||
<digi-button (click)="emitSelectedOptions()" af-size="m">
|
||||
<span>{{confirmButtonText}}</span>
|
||||
<span class="msfa__a11y-sr-only"> och stäng dialogen för filter {{ multiselectTitle }}</span>
|
||||
</digi-button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<ng-template #noOptionsWithCurrentFilters>
|
||||
<div class="multiselect-panel__no-options-available">
|
||||
<p>
|
||||
I kombination med dina nuvarande filterval, finns det inga nya deltagare med {{ multiselectTitlePlural }} att
|
||||
filtrera på.
|
||||
</p>
|
||||
</div>
|
||||
</ng-template>
|
||||
@@ -1,111 +0,0 @@
|
||||
@import 'mixins/form';
|
||||
|
||||
@import 'mixins/a11y';
|
||||
.multiselect-panel {
|
||||
&__checkboxes {
|
||||
max-width: var(--digi--typography--text--max-width);
|
||||
padding: var(--digi--layout--padding--15);
|
||||
}
|
||||
|
||||
&__fieldset {
|
||||
@include msfa__fieldset;
|
||||
}
|
||||
|
||||
&__checkboxes-list {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
&__checkboxes-item {
|
||||
margin-top: var(--digi--layout--gutter--s);
|
||||
|
||||
&:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
&--unavailable {
|
||||
margin-top: var(--digi--layout--gutter);
|
||||
}
|
||||
}
|
||||
|
||||
&__search-wrapper {
|
||||
position: relative;
|
||||
margin-bottom: var(--digi--layout--gutter--l);
|
||||
|
||||
&--searched {
|
||||
margin-bottom: var(--digi--layout--gutter--s);
|
||||
}
|
||||
}
|
||||
|
||||
&__select-all {
|
||||
margin-bottom: var(--digi--layout--gutter--l);
|
||||
}
|
||||
|
||||
&__input-reset-button {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 1px;
|
||||
}
|
||||
}
|
||||
|
||||
.multiselect-panel {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
::ng-deep &__backdrop {
|
||||
background-color: rgba(var(--digi--ui--color--background--dark), 40%);
|
||||
}
|
||||
|
||||
&__fieldset {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border-width: 0;
|
||||
}
|
||||
|
||||
&__legend {
|
||||
position: relative;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
&__heading,
|
||||
&__legend {
|
||||
width: 100%;
|
||||
height: 45px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin: 0;
|
||||
font-weight: var(--digi--typography--font-weight--bold);
|
||||
font-size: var(--digi--typography--font-size);
|
||||
padding: var(--digi--layout--gutter--s) var(--digi--layout--gutter);
|
||||
border-bottom: 1px solid var(--digi--ui--color--background--tertiary);
|
||||
}
|
||||
|
||||
&__content {
|
||||
position: relative;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
&__inner-content {
|
||||
width: 100%;
|
||||
min-width: 300px;
|
||||
max-width: 800px;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
&__footer {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background-color: var(--digi--ui--color--background--tertiary);
|
||||
padding: $digi--layout--gutter $digi--layout--gutter--l;
|
||||
}
|
||||
|
||||
&__no-options-available {
|
||||
text-align: center;
|
||||
padding: var(--digi--layout--gutter--s);
|
||||
max-width: 300px;
|
||||
}
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { MultiselectPanelComponent } from './multiselect-panel.component';
|
||||
|
||||
describe('MultiselectPanelComponent', () => {
|
||||
let component: MultiselectPanelComponent;
|
||||
let fixture: ComponentFixture<MultiselectPanelComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [MultiselectPanelComponent],
|
||||
schemas: [CUSTOM_ELEMENTS_SCHEMA],
|
||||
}).compileComponents();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(MultiselectPanelComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -1,71 +0,0 @@
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
EventEmitter,
|
||||
Input,
|
||||
OnChanges,
|
||||
Output,
|
||||
SimpleChanges,
|
||||
} from '@angular/core';
|
||||
import { DropdownTriggerForDirective } from '@msfa-shared/components/dropdown/dropdown-trigger-for.directive';
|
||||
import { MultiselectFilterOption } from '@msfa-shared/components/multiselect/multiselect-filter-option';
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
|
||||
@Component({
|
||||
selector: 'msfa-multiselect-panel',
|
||||
templateUrl: './multiselect-panel.component.html',
|
||||
styleUrls: ['./multiselect-panel.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class MultiselectPanelComponent implements OnChanges {
|
||||
@Input() dropdownRef: DropdownTriggerForDirective;
|
||||
@Input() confirmButtonText = 'Spara';
|
||||
@Input() multiselectTitle: string;
|
||||
@Input() multiselectTitlePlural: string;
|
||||
@Input() description: string;
|
||||
@Input() availableOptions: MultiselectFilterOption[];
|
||||
@Input() selectedOptions: MultiselectFilterOption[];
|
||||
@Output() selectedOptionsChange = new EventEmitter<MultiselectFilterOption[]>();
|
||||
|
||||
private _pendingSelectedOptions$ = new BehaviorSubject<MultiselectFilterOption[]>([]);
|
||||
private _selectedUnavailableOptions$ = new BehaviorSubject<MultiselectFilterOption[]>([]);
|
||||
|
||||
ngOnChanges(changes: SimpleChanges): void {
|
||||
if (changes.availableOptions || changes.selectedOptions) {
|
||||
this._setPendingSelectedOptions();
|
||||
}
|
||||
}
|
||||
get selectedUnavailableOptions(): MultiselectFilterOption[] {
|
||||
return this._selectedUnavailableOptions$.getValue();
|
||||
}
|
||||
private get _pendingSelectedOptions(): MultiselectFilterOption[] {
|
||||
return this._pendingSelectedOptions$.getValue();
|
||||
}
|
||||
|
||||
private _setPendingSelectedOptions(): void {
|
||||
this._pendingSelectedOptions$.next(this.selectedOptions || []);
|
||||
this._selectedUnavailableOptions$.next(
|
||||
this.selectedOptions?.filter(
|
||||
selectedOption => !this.availableOptions.some(availableOption => availableOption.id === selectedOption.id)
|
||||
) || []
|
||||
);
|
||||
}
|
||||
|
||||
isSelected(filterOption: MultiselectFilterOption): boolean {
|
||||
return this._pendingSelectedOptions$.value?.some(selectedOption => selectedOption.id === filterOption.id) ?? false;
|
||||
}
|
||||
|
||||
emitSelectedOptions(): void {
|
||||
this.selectedOptionsChange.emit(this._pendingSelectedOptions);
|
||||
}
|
||||
|
||||
setOptionState(filterOption: MultiselectFilterOption, isSelected: boolean): void {
|
||||
if (isSelected) {
|
||||
this._pendingSelectedOptions$.next([...this._pendingSelectedOptions, filterOption]);
|
||||
} else {
|
||||
this._pendingSelectedOptions$.next(
|
||||
this._pendingSelectedOptions.filter(item => item.id !== filterOption.id) ?? []
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
<button
|
||||
#multiselectButton
|
||||
[msfaDropdownTriggerFor]="dropdown"
|
||||
[attr.id]="buttonElementId"
|
||||
[attr.aria-controls]="panelId"
|
||||
[ngClass]="{'multiselect__toggle-btn--invalid': isInvalid && showValidation}"
|
||||
type="button"
|
||||
class="multiselect__toggle-btn"
|
||||
>
|
||||
<span class="multiselect__toggle-btn__text">{{multiselectTitle}}</span>
|
||||
<digi-icon-arrow-down aria-hidden="true" class="multiselect__toggle-btn__arrow-down"></digi-icon-arrow-down>
|
||||
<span class="msfa__a11y-sr-only"> Filtrera på {{multiselectTitle}}. </span>
|
||||
</button>
|
||||
|
||||
<msfa-dropdown #dropdown [attr.id]="panelId">
|
||||
<msfa-multiselect-panel
|
||||
[availableOptions]="availableOptions"
|
||||
[selectedOptions]="selectedOptions"
|
||||
[dropdownRef]="dropdown"
|
||||
[multiselectTitle]="multiselectTitle"
|
||||
[confirmButtonText]="confirmButtonText"
|
||||
(selectedOptionsChange)="emitSelectedOptions($event)"
|
||||
></msfa-multiselect-panel>
|
||||
</msfa-dropdown>
|
||||
|
||||
<div class="msfa__a11y-sr-only" aria-live="polite">
|
||||
<span *ngIf="selectedOptions"
|
||||
>Antal valda {{multiselectTitle.toLocaleLowerCase()}} är {{selectedOptions.length}}</span
|
||||
>
|
||||
</div>
|
||||
<div class="msfa__a11y-sr-only" aria-live="polite">
|
||||
<ul *ngFor="let option of selectedOptions">
|
||||
<li>{{option.label}}</li>
|
||||
</ul>
|
||||
</div>
|
||||
@@ -1,60 +0,0 @@
|
||||
@import 'variables/colors';
|
||||
@import 'mixins/list';
|
||||
@import 'variables/gutters';
|
||||
|
||||
.multiselect {
|
||||
&__toggle-btn {
|
||||
margin-right: var(--digi--layout--gutter);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
appearance: none;
|
||||
width: 100%;
|
||||
height: var(--digi--ui--input--height);
|
||||
padding: var(--digi--ui--input--padding);
|
||||
border: 1px solid var(--digi--ui--input--border--color);
|
||||
cursor: pointer;
|
||||
font-size: var(--digi--typography--font-size--m);
|
||||
color: var(--digi--ui--color--background--overlay--opaque);
|
||||
background-color: var(--digi--ui--color--background);
|
||||
|
||||
&:focus {
|
||||
border-color: var(--digi--ui--color--focus--light);
|
||||
box-shadow: 0 0 0.1rem 0.05rem var(--digi--ui--color--focus);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
cursor: default;
|
||||
background-color: var(--digi--ui--color--background--disabled);
|
||||
border-color: var(--digi--ui--color--border--disabled);
|
||||
color: var(--digi--typography--color--text--disabled);
|
||||
}
|
||||
|
||||
&--invalid {
|
||||
background-color: var(--digi--ui--color--background--error);
|
||||
border-color: var(--digi--ui--color--border--error);
|
||||
box-shadow: 0 0 0 1px var(--digi--ui--color--border--error);
|
||||
}
|
||||
|
||||
&__text {
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
|
||||
&__toggle-btn__arrow-down {
|
||||
padding: var(--digi--layout--padding);
|
||||
}
|
||||
|
||||
&__validation-messages {
|
||||
@include msfa__reset-list;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: var(--digi--layout--gutter--s) 0;
|
||||
flex-direction: column;
|
||||
gap: var(--digi--layout--gutter--s);
|
||||
}
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { MultiselectComponent } from './multiselect.component';
|
||||
import { DropdownModule } from '@msfa-shared/components/dropdown/dropdown.module';
|
||||
|
||||
describe('MultiselectComponent', () => {
|
||||
let component: MultiselectComponent;
|
||||
let fixture: ComponentFixture<MultiselectComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [DropdownModule],
|
||||
declarations: [MultiselectComponent],
|
||||
schemas: [CUSTOM_ELEMENTS_SCHEMA],
|
||||
}).compileComponents();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(MultiselectComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -1,104 +0,0 @@
|
||||
import {
|
||||
AfterViewInit,
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
ElementRef,
|
||||
EventEmitter,
|
||||
forwardRef,
|
||||
Input,
|
||||
Output,
|
||||
Renderer2,
|
||||
ViewChild,
|
||||
} from '@angular/core';
|
||||
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
|
||||
import { DropdownTriggerForDirective } from '@msfa-shared/components/dropdown/dropdown-trigger-for.directive';
|
||||
import { MultiselectFilterOption } from '@msfa-shared/components/multiselect/multiselect-filter-option';
|
||||
import { uuid } from '@utils/uuid.util';
|
||||
|
||||
interface PropagateChangeFn {
|
||||
(_: unknown): void;
|
||||
}
|
||||
|
||||
interface PropagateTouchedFn {
|
||||
(): void;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'msfa-multiselect',
|
||||
templateUrl: './multiselect.component.html',
|
||||
styleUrls: ['./multiselect.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
providers: [
|
||||
{
|
||||
provide: NG_VALUE_ACCESSOR,
|
||||
useExisting: forwardRef(() => MultiselectComponent),
|
||||
multi: true,
|
||||
},
|
||||
],
|
||||
})
|
||||
export class MultiselectComponent implements AfterViewInit, ControlValueAccessor {
|
||||
@ViewChild(DropdownTriggerForDirective) dropdownRef: DropdownTriggerForDirective;
|
||||
panelId = `panel-${uuid()}`;
|
||||
@Input() buttonElementId: string = uuid();
|
||||
@Input() isInvalid = false;
|
||||
@Input() showValidation = false;
|
||||
@Input() confirmButtonText = 'Spara';
|
||||
@Input() description: string;
|
||||
@Input() multiselectTitle: string;
|
||||
@Input() availableOptions: MultiselectFilterOption[];
|
||||
@Input() selectedOptions: MultiselectFilterOption[];
|
||||
@Output() selectedOptionsChange = new EventEmitter<MultiselectFilterOption[]>();
|
||||
@ViewChild('multiselectButton') multiselectButton: ElementRef;
|
||||
isDisabled = false;
|
||||
|
||||
private propagateChange: PropagateChangeFn;
|
||||
private propagateTouched: PropagateTouchedFn;
|
||||
|
||||
constructor(private renderer: Renderer2) {}
|
||||
|
||||
// Allows Angular to update the model.
|
||||
// Update the model and changes needed for the view here.
|
||||
writeValue(selectedOptions: MultiselectFilterOption[]): void {
|
||||
this.selectedOptions = selectedOptions;
|
||||
}
|
||||
|
||||
// Allows Angular to register a function to call when the model changes.
|
||||
// Save the function as a property to call later here.
|
||||
registerOnChange(fn: PropagateChangeFn): void {
|
||||
this.propagateChange = fn;
|
||||
}
|
||||
|
||||
// Allows Angular to register a function to call when the input has been touched.
|
||||
// Save the function as a property to call later here.
|
||||
registerOnTouched(fn: PropagateTouchedFn): void {
|
||||
this.propagateTouched = fn;
|
||||
}
|
||||
|
||||
// Allows Angular to disable the input.
|
||||
setDisabledState?(isDisabled: boolean): void {
|
||||
this.renderer.setProperty(this.multiselectButton.nativeElement, 'disabled', isDisabled);
|
||||
this.dropdownRef.closeDropdown();
|
||||
}
|
||||
|
||||
emitSelectedOptions(selectedOptions: MultiselectFilterOption[]): void {
|
||||
// If used with FormsModule/ReactiveFormsModule
|
||||
if (this.propagateChange && this.propagateTouched) {
|
||||
this.propagateChange(selectedOptions);
|
||||
this.propagateTouched();
|
||||
} else {
|
||||
this.selectedOptionsChange.emit(selectedOptions);
|
||||
}
|
||||
|
||||
this.dropdownRef.closeDropdown();
|
||||
}
|
||||
|
||||
ngAfterViewInit(): void {
|
||||
if (!this.dropdownRef) {
|
||||
return;
|
||||
}
|
||||
this.dropdownRef.onOpen(() => {
|
||||
// force refresh of selected options if it's reopened
|
||||
this.selectedOptions = [...(this.selectedOptions || [])];
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
import { DigiNgFormCheckboxModule } from '@af/digi-ng/_form/form-checkbox';
|
||||
import { A11yModule } from '@angular/cdk/a11y';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { DropdownModule } from '@msfa-shared/components/dropdown/dropdown.module';
|
||||
import { MultiselectComponent } from '@msfa-shared/components/multiselect/multiselect.component';
|
||||
import { MultiselectPanelComponent } from './multiselect-panel/multiselect-panel.component';
|
||||
|
||||
@NgModule({
|
||||
declarations: [MultiselectComponent, MultiselectPanelComponent],
|
||||
imports: [CommonModule, DropdownModule, A11yModule, DigiNgFormCheckboxModule, FormsModule],
|
||||
exports: [MultiselectComponent],
|
||||
schemas: [CUSTOM_ELEMENTS_SCHEMA],
|
||||
})
|
||||
export class MultiselectModule {}
|
||||
@@ -1,7 +1,7 @@
|
||||
import { SortOrder } from '@msfa-enums/sort-order.enum';
|
||||
import { DeltagareCompact } from '@msfa-models/deltagare.model';
|
||||
import { MultiselectFilterOption } from '@msfa-shared/components/multiselect/multiselect-filter-option';
|
||||
import { EmployeeCompactResponse } from './employee.response.model';
|
||||
import { MultiselectFilterOption } from '@ui/multiselect/multiselect-filter-option';
|
||||
|
||||
export interface Params {
|
||||
[param: string]: string | string[];
|
||||
|
||||
@@ -3,10 +3,10 @@ import { AvropParams, Params } from '@msfa-models/api/params.model';
|
||||
import { Avrop, AvropAndMeta } from '@msfa-models/avrop.model';
|
||||
import { Handledare } from '@msfa-models/handledare.model';
|
||||
import { AvropApiService } from '@msfa-services/api/avrop-api.service';
|
||||
import { MultiselectFilterOption } from '@msfa-shared/components/multiselect/multiselect-filter-option';
|
||||
import { BehaviorSubject, combineLatest, Observable, of } from 'rxjs';
|
||||
import { distinctUntilChanged, filter, map, shareReplay, switchMap, tap } from 'rxjs/operators';
|
||||
import { HandledareApiService } from './api/handledare.api.service';
|
||||
import { MultiselectFilterOption } from '@ui/multiselect/multiselect-filter-option';
|
||||
|
||||
type Step = 1 | 2 | 3 | 4;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user