feature(digi-migrering): ui popover flyttat till vår kod. Påverkar info-rutorna på deltagareflikarna (TV-852)

Merge in TEA/mina-sidor-fa-web from feature/TV-852-ui-popover-with-angular-cdk to develop

Squashed commit of the following:

commit 2fc389330b473e53916c1505c15d129c97239e7a
Author: Daniel Appelgren <daniel.appelgren@arbetsformedlingen.se>
Date:   Fri Dec 3 13:32:52 2021 +0100

    Update ui-popover.component.scss

commit d88c5e8cbb2d517f6463ed2ca22a4dfe4c21e696
Author: Daniel Appelgren <daniel.appelgren@arbetsformedlingen.se>
Date:   Fri Dec 3 13:31:41 2021 +0100

    refactoir

commit 400eae3e2732252dae1be8576f9231418007c9e7
Author: Daniel Appelgren <daniel.appelgren@arbetsformedlingen.se>
Date:   Fri Dec 3 13:30:09 2021 +0100

    refactor

commit d9c76abd40edd8071324f6c51dab2ecdc5759883
Author: Daniel Appelgren <daniel.appelgren@arbetsformedlingen.se>
Date:   Fri Dec 3 13:27:12 2021 +0100

    inline button

commit 0acabc39c42556936300b862c976328479084752
Author: Daniel Appelgren <daniel.appelgren@arbetsformedlingen.se>
Date:   Fri Dec 3 09:47:32 2021 +0100

    wip

commit b7c51ba386c1b3dd771bdebe1d5a4219c0702dc6
Author: Daniel Appelgren <daniel.appelgren@arbetsformedlingen.se>
Date:   Fri Dec 3 08:14:48 2021 +0100

    Update ui-popover.component.ts

commit 9e63b116b694c94daa4cb3b9a3dd898891d00c2b
Author: Daniel Appelgren <daniel.appelgren@arbetsformedlingen.se>
Date:   Fri Dec 3 07:49:21 2021 +0100

    first version
This commit is contained in:
Daniel Appelgren
2021-12-03 14:42:47 +01:00
parent 57113f45f3
commit ec63435fc5
15 changed files with 299 additions and 44 deletions

View File

@@ -40,19 +40,19 @@
<dt>Behov av tolk:</dt>
<dd class="deltagare-tab-personal-information__info">
{{avropInformation.tolkbehov || 'Inget tolkbehov'}}
<digi-ng-popover class="deltagare-tab-personal-information__popover" [afRelativeIconSize]="true">
<ui-popover>
<p>
Tolk är en typ av språkstöd som kan behöva anlitas vid behov. Ibland står Arbetsförmedlingen för
tolkkostnaden och ibland står leverantören för tolkkostnaden. Det finns skillnader mellan vilket språkstöd
som ingår i upphandlingen av olika tjänster och utbildningar. Du hittar mer information om språkstöd och
tolk i förfrågningsunderlaget för specifik upphandling.
</p>
</digi-ng-popover>
</ui-popover>
</dd>
<dt>Behov av språkstöd:</dt>
<dd class="deltagare-tab-personal-information__info">
{{avropInformation.sprakstod || 'Inget språkstöd'}}
<digi-ng-popover class="deltagare-tab-personal-information__popover" [afRelativeIconSize]="true">
<ui-popover>
<p>
Det finns flera olika typer av språkstöd. Till exempel användande av flerspråkig personal, tillgång till
material på lätt svenska eller olika språk, anlitande av tolk med mera. Dessa kan erbjudas och kombineras
@@ -60,7 +60,7 @@
tjänster och utbildningar. Du hittar mer information om språkstöd i förfrågningsunderlaget för specifik
upphandling.
</p>
</digi-ng-popover>
</ui-popover>
</dd>
</ng-container>
</dl>
@@ -108,12 +108,12 @@
class="deltagare-tab-personal-information__info"
>
{{ avropInformation.genomforandeReferens }}
<digi-ng-popover class="deltagare-tab-personal-information__popover" [afRelativeIconSize]="true">
<ui-popover [uiPopoverPosition]="UiPopoverPosition.Bottom">
<p>
Genomförandereferens är det referensnummer du ska använda dig av i kontakten med Arbetsförmedlingen. Du
kan också använda genomförandereferensen till att leta fram en order i leverantörsportalen.
</p>
</digi-ng-popover>
</ui-popover>
</dd>
</dl>
</div>

View File

@@ -22,13 +22,4 @@
display: flex;
align-items: center;
}
&__popover {
display: inline-block;
margin-left: var(--digi--layout--gutter--s);
::ng-deep .digi-ng-popover__container {
z-index: $msfa__z-index-popover;
}
}
}

View File

@@ -9,6 +9,7 @@ import { distinctUntilChanged, filter, map, startWith, switchMap } from 'rxjs/op
import { DeltagareCardService } from '../../deltagare-card.service';
import { UiIconType } from '@ui/icon/icon-type.enum';
import { UiIconSize } from '@ui/icon/icon-size.enum';
import { UiPopoverPosition } from '@ui/popover/ui-popover.component';
@Component({
selector: 'msfa-deltagare-tab-personal-information',
@@ -21,6 +22,7 @@ export class DeltagareTabPersonalInformationComponent {
@Input() handledarePickerVisible: boolean;
IconType = UiIconType;
IconSize = UiIconSize;
UiPopoverPosition = UiPopoverPosition;
avropInformation$: Observable<DeltagareAvrop> = this.deltagareCardService.avropInformation$;
contactInformation$: Observable<ContactInformation> = this.deltagareCardService.contactInformation$;

View File

@@ -6,12 +6,9 @@
<ng-container *ngFor="let disability of disabilities; let index = index">
<dt>Funktionsnedsättning {{index + 1}}</dt>
<dd>
<span>{{ disability.title }}</span>
<digi-ng-popover
*ngIf="disability.description"
class="deltagare-tab-sensitive-information__popover"
[afRelativeIconSize]="true"
>{{ disability.description }}</digi-ng-popover
{{ disability.title }}
<ui-popover *ngIf="disability.description" class="deltagare-tab-sensitive-information__popover"
>{{ disability.description }}</ui-popover
>
</dd>
</ng-container>

View File

@@ -15,9 +15,5 @@
&__popover {
display: inline-block;
margin-left: var(--digi--layout--gutter--s);
::ng-deep .digi-ng-popover__container {
z-index: $msfa__z-index-popover;
}
}
}

View File

@@ -19,6 +19,7 @@ import { DeltagareTabReportsComponent } from './components/deltagare-tab-reports
import { DeltagareTabSensitiveInformationComponent } from './components/deltagare-tab-sensitive-information/deltagare-tab-sensitive-information.component';
import { DeltagareCardComponent } from './deltagare-card.component';
import { DeltagareCardService } from './deltagare-card.service';
import { UiPopoverModule } from '@ui/popover/ui-popover.module';
@NgModule({
schemas: [CUSTOM_ELEMENTS_SCHEMA],
@@ -41,6 +42,7 @@ import { DeltagareCardService } from './deltagare-card.service';
HandledarePickerFormModule,
DigiNgLayoutExpansionPanelModule,
DigiNgPopoverModule,
UiPopoverModule,
UiSkeletonModule,
UiIconModule,
UiLinkButtonModule,

View File

@@ -21,6 +21,8 @@ export enum UiIconType {
ARROW_RIGHT = 'arrow-right',
EYE = 'eye',
EYESLASH = 'eyeslash',
INFO_CIRCLE_SOLID = 'info-circle-solid',
INFO_CIRCLE_OUTLINE = 'info-circle-outline',
ARCHIVE = 'archive',
PAPERCLIP = 'paperclip'
PAPERCLIP = 'paperclip',
}

View File

@@ -100,5 +100,13 @@
<digi-icon-eye-slash *ngSwitchCase="iconType.EYESLASH" [ngClass]="iconClass"></digi-icon-eye-slash>
<digi-icon-archive *ngSwitchCase="iconType.ARCHIVE" [ngClass]="iconClass"></digi-icon-archive>
<digi-icon-paperclip *ngSwitchCase="iconType.PAPERCLIP" [ngClass]="iconClass"></digi-icon-paperclip>
<digi-icon-info-circle-solid
*ngSwitchCase="iconType.INFO_CIRCLE_SOLID"
[ngClass]="iconClass"
></digi-icon-info-circle-solid>
<digi-icon-info-circle-reg
*ngSwitchCase="iconType.INFO_CIRCLE_OUTLINE"
[ngClass]="iconClass"
></digi-icon-info-circle-reg>
</ng-container>
</ng-template>

View File

@@ -38,7 +38,7 @@ interface PropagateTouchedFn {
],
})
export class MultiselectComponent implements AfterViewInit, ControlValueAccessor {
@ViewChild(OverlayTriggerForDirective) dropdownRef: OverlayTriggerForDirective;
@ViewChild(OverlayTriggerForDirective) overlayRef: OverlayTriggerForDirective;
panelId = `panel-${uuid()}`;
@Input() buttonElementId: string = uuid();
@Input() isInvalid = false;
@@ -88,7 +88,7 @@ export class MultiselectComponent implements AfterViewInit, ControlValueAccessor
// Allows Angular to disable the input.
setDisabledState?(isDisabled: boolean): void {
this.renderer.setProperty(this.multiselectButton.nativeElement, 'disabled', isDisabled);
this.dropdownRef.closeDropdown();
this.overlayRef.closeOverlay();
}
emitSelectedOptions(selectedOptions: MultiselectFilterOption[]): void {
@@ -100,14 +100,14 @@ export class MultiselectComponent implements AfterViewInit, ControlValueAccessor
this.selectedOptionsChange.emit(selectedOptions);
}
this.dropdownRef.closeDropdown();
this.overlayRef.closeOverlay();
}
ngAfterViewInit(): void {
if (!this.dropdownRef) {
if (!this.overlayRef) {
return;
}
this.dropdownRef.onOpen(() => {
this.overlayRef.onOpen(() => {
// force refresh of selected options if it's reopened
this.selectedOptions = [...(this.selectedOptions || [])];
});

View File

@@ -1,6 +1,15 @@
import { ConnectedPosition, Overlay, OverlayRef } from '@angular/cdk/overlay';
import { ConnectedPosition, Overlay, OverlayConfig, OverlayRef } from '@angular/cdk/overlay';
import { TemplatePortal } from '@angular/cdk/portal';
import { Directive, ElementRef, HostListener, Input, OnDestroy, ViewContainerRef } from '@angular/core';
import {
Directive,
ElementRef,
EventEmitter,
HostListener,
Input,
OnDestroy,
Output,
ViewContainerRef,
} from '@angular/core';
import { merge, Observable, Subscription } from 'rxjs';
import { OverlayPanel } from './overlay.model';
@@ -9,6 +18,7 @@ import { OverlayPanel } from './overlay.model';
})
export class OverlayTriggerForDirective implements OnDestroy {
@Input() public uiOverlayTriggerFor: OverlayPanel;
@Input() public uiOverlayPositions: ConnectedPosition[] = [
{
originX: 'start',
@@ -18,6 +28,13 @@ export class OverlayTriggerForDirective implements OnDestroy {
offsetY: 3,
},
];
/*
* uiOverlayConfig will override uiOverlayPreferredPositions
* */
@Input() public uiOverlayConfig: OverlayConfig;
@Output() public uiOverlayRealPositions = new EventEmitter<ConnectedPosition[]>();
private isOverlayOpen = false;
public overlayRef: OverlayRef;
private overlayClosingActionsSub = Subscription.EMPTY;
@@ -30,32 +47,36 @@ export class OverlayTriggerForDirective implements OnDestroy {
};
@HostListener('click') onClick(): void {
return this.toggleDropdown();
return this.toggleOverlay();
}
@HostListener('document:keyup.escape', ['$event']) onKeydownHandler(): void {
this.closeDropdown();
this.closeOverlay();
}
constructor(
private overlay: Overlay,
private elementRef: ElementRef<HTMLElement>,
private triggerRef: ElementRef<HTMLElement>,
private viewContainerRef: ViewContainerRef
) {}
toggleDropdown(): void {
this.isOverlayOpen ? this.closeDropdown() : this.openOverlay();
toggleOverlay(): void {
this.isOverlayOpen ? this.closeOverlay() : this.openOverlay();
}
openOverlay(): void {
this.isOverlayOpen = true;
const positionStrategy = this.overlay
.position()
.flexibleConnectedTo(this.triggerRef)
.withPositions(this.uiOverlayPositions);
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(this.uiOverlayPositions),
positionStrategy,
...(this.uiOverlayConfig ?? {}),
});
const templatePortal = new TemplatePortal(this.uiOverlayTriggerFor.templateRef, this.viewContainerRef);
@@ -63,7 +84,7 @@ export class OverlayTriggerForDirective implements OnDestroy {
this.overlayRef.attach(templatePortal);
this.onOpenCallback();
this.overlayClosingActionsSub = this.dropdownClosingActions().subscribe(() => this.closeDropdown());
this.overlayClosingActionsSub = this.dropdownClosingActions().subscribe(() => this.closeOverlay());
}
private dropdownClosingActions(): Observable<MouseEvent | void> {
@@ -74,7 +95,7 @@ export class OverlayTriggerForDirective implements OnDestroy {
return merge(backdropClick$, detachment$, overlayClosed);
}
closeDropdown(): void {
closeOverlay(): void {
if (!this.overlayRef || !this.isOverlayOpen) {
return;
}

View File

@@ -0,0 +1,27 @@
<div class="popover">
<button
class="popover__button"
type="button"
#triggerButton
[attr.aria-label]="uiAriaLabel"
[uiOverlayTriggerFor]="overlay"
[uiOverlayPositions]="overlayPositions"
[attr.aria-controls]="panelId"
>
<ui-icon [uiType]="UiIconType.INFO_CIRCLE_SOLID" [uiSize]="UiIconSize.L"></ui-icon>
<span class="popover__button-text">{{uiButtonText}}</span>
</button>
</div>
<ui-overlay #overlay>
<div class="popover__overlay-content" [attr.id]="panelId" #content cdkTrapFocus cdkTrapFocusAutoCapture>
<digi-typography>
<ng-content></ng-content>
</digi-typography>
</div>
<button (click)="closePopover()" class="popover__close-button" type="button">
<span class="popover__close-button-text">Stäng&nbsp;</span>
<ui-icon [uiType]="UiIconType.X" [uiSize]="UiIconSize.L"></ui-icon>
</button>
<div class="popover__container-arrow" #arrow></div>
</ui-overlay>

View File

@@ -0,0 +1,70 @@
@import 'libs/styles/src/variables/colors';
@import 'libs/styles/src/mixins/list';
@import 'libs/styles/src/variables/gutters';
@import 'libs/styles/src/variables/shadows';
@import 'libs/styles/src/variables/z-index';
.popover {
margin-left: var(--digi--layout--gutter);
vertical-align: middle;
display: inline-block;
&__button {
background-color: transparent;
color: var(--digi--typography--color--link);
font-size: var(--digi--typography--font-size--desktop);
border-width: 0;
display: flex;
align-items: center;
justify-content: center;
&:hover {
color: var(--digi--typography--color--link--active);
}
}
&__button-text {
margin-left: var(--digi--layout--gutter--s);
}
&__overlay-content {
max-width: var(--digi--typography--text--max-width);
padding: var(--digi--layout--padding--50) var(--digi--layout--padding--20) var(--digi--layout--padding--30);
}
&__close-button {
position: absolute;
top: var(--digi--layout--gutter);
right: var(--digi--layout--gutter--s);
background: transparent;
border: none;
display: flex;
justify-content: center;
align-items: center;
}
&__close-button-text {
font-size: var(--digi--typography--font-size--s);
}
&__container-arrow {
width: u(6);
height: u(6);
position: absolute;
bottom: 100%;
right: 50%;
z-index: $msfa__z-index-popover;
overflow: hidden;
&:after {
content: '';
position: absolute;
background: #fff;
border-width: u(3);
border-style: solid;
border-color: transparent #fff #fff;
right: 50%;
overflow: hidden;
transform: rotate(45deg);
box-shadow: 0 0 u(0.7) u(0.7) rgb(0 0 0 / 30%);
}
}
}

View File

@@ -0,0 +1,27 @@
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { UiPopoverComponent } from './ui-popover.component';
import { DropdownModule } from '@ui/dropdown/dropdown.module';
describe('popoverComponent', () => {
let component: UiPopoverComponent;
let fixture: ComponentFixture<UiPopoverComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [DropdownModule],
declarations: [UiPopoverComponent],
schemas: [CUSTOM_ELEMENTS_SCHEMA],
}).compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(UiPopoverComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,96 @@
import {
AfterViewInit,
ChangeDetectionStrategy,
Component,
ElementRef,
Input,
Renderer2,
ViewChild,
} from '@angular/core';
import { uuid } from '@utils/uuid.util';
import { OverlayTriggerForDirective } from '@ui/overlay/overlay-trigger-for.directive';
import { ConnectedPosition } from '@angular/cdk/overlay';
import { UiIconType } from '@ui/icon/icon-type.enum';
import { UiIconSize } from '@ui/icon/icon-size.enum';
export enum UiPopoverPosition {
Top,
Bottom,
}
@Component({
selector: 'ui-popover',
templateUrl: './ui-popover.component.html',
styleUrls: ['./ui-popover.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class UiPopoverComponent implements AfterViewInit {
UiIconType = UiIconType;
UiIconSize = UiIconSize;
@ViewChild(OverlayTriggerForDirective) overlayRef: OverlayTriggerForDirective;
panelId = `panel-${uuid()}`;
@Input() uiAriaLabel: string;
@Input() uiButtonText?: string = 'Info';
@Input() buttonElementId: string = uuid();
@Input() uiPopoverPosition: UiPopoverPosition = UiPopoverPosition.Top;
@ViewChild('triggerButton') triggerButton: ElementRef;
@ViewChild('content') content: ElementRef;
@ViewChild('arrow') arrow: ElementRef;
readonly offsetPixels = -30;
@ViewChild('popoverButton') popoverButton: ElementRef;
get overlayPositions(): ConnectedPosition[] {
switch (this.uiPopoverPosition) {
case UiPopoverPosition.Top:
return [
{
originX: 'center',
originY: 'top',
overlayX: 'center',
overlayY: 'bottom',
offsetY: this.offsetPixels,
},
];
case UiPopoverPosition.Bottom:
return [
{
originX: 'center',
originY: 'bottom',
overlayX: 'center',
overlayY: 'top',
offsetY: -this.offsetPixels,
},
];
}
}
constructor(private renderer: Renderer2) {}
closePopover(): void {
this.overlayRef.closeOverlay();
}
ngAfterViewInit(): void {
switch (this.uiPopoverPosition) {
case UiPopoverPosition.Top:
this.moveArrowBottom();
break;
case UiPopoverPosition.Bottom:
this.moveArrowTop();
break;
default:
throw new Error('uiPopoverPosition is mandatory');
}
}
moveArrowBottom() {
this.renderer.setStyle(this.arrow.nativeElement, 'transform', 'rotate(90deg)');
this.renderer.setStyle(this.arrow.nativeElement, 'bottom', `${this.offsetPixels + 6}px `);
}
moveArrowTop() {
this.renderer.setStyle(this.arrow.nativeElement, 'transform', 'rotate(-90deg)');
this.renderer.setStyle(this.arrow.nativeElement, 'top', `${this.offsetPixels + 7}px `);
}
}

View File

@@ -0,0 +1,16 @@
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 { UiOverlayModule } from '@ui/overlay/overlay.module';
import { UiPopoverComponent } from '@ui/popover/ui-popover.component';
import { UiIconModule } from '@ui/icon/icon.module';
@NgModule({
declarations: [UiPopoverComponent],
imports: [CommonModule, A11yModule, DigiNgFormCheckboxModule, FormsModule, UiOverlayModule, UiIconModule],
exports: [UiPopoverComponent],
schemas: [CUSTOM_ELEMENTS_SCHEMA],
})
export class UiPopoverModule {}