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

@@ -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 {}