Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found

Target

Select target project
  • web-et-numerique/factory/pamn_plateforme-des-acteurs-de-la-mediation-numerique/pamn_client
1 result
Show changes
Commits on Source (7)
Showing
with 166 additions and 45 deletions
<div class="visually-hidden">
<p>Utilisez <strong>h</strong> (header) pour revenir au logo Res'in à tout moment.</p>
</div>
<app-header /> <app-header />
<main class="app-container"> <main class="app-container">
<div class="app-body" id="app-body" (scroll)="onScroll($event)"> <div class="app-body" id="app-body" (scroll)="onScroll($event)">
......
import { Component, OnInit } from '@angular/core'; import { Component, HostListener, OnInit, ViewChild } from '@angular/core';
import { import {
GuardsCheckStart, GuardsCheckStart,
NavigationCancel, NavigationCancel,
...@@ -8,6 +8,7 @@ import { ...@@ -8,6 +8,7 @@ import {
Router, Router,
} from '@angular/router'; } from '@angular/router';
import { MatomoInitializerService } from 'ngx-matomo-client'; import { MatomoInitializerService } from 'ngx-matomo-client';
import { HeaderComponent } from './header/header.component';
import { ProfileService } from './profile/services/profile.service'; import { ProfileService } from './profile/services/profile.service';
import { AuthService } from './services/auth.service'; import { AuthService } from './services/auth.service';
import { ConfigService } from './services/config.service'; import { ConfigService } from './services/config.service';
...@@ -24,6 +25,36 @@ import { WindowScrollService } from './shared/service/windowScroll.service'; ...@@ -24,6 +25,36 @@ import { WindowScrollService } from './shared/service/windowScroll.service';
export class AppComponent implements OnInit { export class AppComponent implements OnInit {
public loading = true; public loading = true;
@ViewChild(HeaderComponent) headerComponent!: HeaderComponent;
// Listener, keyboard shortcuts
@HostListener('window:keydown', ['$event'])
handleKeyboardEvent(event: KeyboardEvent): void {
const target = event.target as HTMLElement;
const tagName = target.tagName.toLowerCase();
// Check if the focus is within an input, textarea
if (tagName === 'input') {
const inputElement = target as HTMLInputElement;
const inputType = inputElement.type.toLowerCase();
// Ignore only text-based inputs, not others like radio, checkbox, etc.
if (!['radio', 'checkbox', 'button', 'submit', 'reset'].includes(inputType)) {
return;
}
} else if (tagName === 'textarea' || target.isContentEditable) {
return;
}
switch (event.key) {
case 'h': // 'h' to go to the header (resin logo)
this.headerComponent.focusLogo();
event.preventDefault();
break;
default:
break;
}
}
constructor( constructor(
public printService: PrintService, public printService: PrintService,
private authService: AuthService, private authService: AuthService,
......
...@@ -53,7 +53,6 @@ ...@@ -53,7 +53,6 @@
label { label {
@include font-bold-14; @include font-bold-14;
color: $red; color: $red;
min-width: 26px;
} }
} }
.topSpacing { .topSpacing {
......
<div class="footerForm"> <div class="footerForm">
<ng-container *ngIf="!failedOrientation"> <ng-container *ngIf="!failedOrientation">
<app-button <app-button
*ngIf="currentStep !== null && !(isPrevHidden || isLastStep)" *ngIf="showPrevButton()"
#prevButton
[variant]="'secondary'" [variant]="'secondary'"
[label]="'Précédent'" [label]="'Précédent'"
[iconName]="'arrowBack'" [iconName]="'arrowBack'"
(action)="prevPage()" (action)="prevPage()"
/> />
<app-button <app-button
*ngIf="!hideNavButtons" *ngIf="showNextButton()"
#nextButton
[variant]="'primary'" [variant]="'primary'"
[label]="isLastStep ? 'Imprimer' : 'Suivant'" [label]="isLastStep ? 'Imprimer' : 'Suivant'"
[iconName]="isLastStep ? 'printer' : 'arrowForward'" [iconName]="isLastStep ? 'printer' : 'arrowForward'"
......
import { Component, EventEmitter, Input, Output } from '@angular/core'; import { Component, EventEmitter, Input, Output, ViewChild } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router'; import { ActivatedRoute, Router } from '@angular/router';
import { ButtonComponent } from '../../../../shared/components';
import { NeedsType, OnlineDemarche } from '../../enums/orientation.enums'; import { NeedsType, OnlineDemarche } from '../../enums/orientation.enums';
import { MediationStepType, MediationType } from '../../types/orientation.types'; import { MediationStepType, MediationType } from '../../types/orientation.types';
...@@ -22,11 +23,21 @@ export class NavigationComponent { ...@@ -22,11 +23,21 @@ export class NavigationComponent {
@Output() goPrev = new EventEmitter<any>(); @Output() goPrev = new EventEmitter<any>();
@Output() goReset = new EventEmitter<any>(); @Output() goReset = new EventEmitter<any>();
@ViewChild('prevButton', { read: ButtonComponent }) prevButton: ButtonComponent;
@ViewChild('nextButton', { read: ButtonComponent }) nextButton: ButtonComponent;
public NeedsTypeEnum = NeedsType; public NeedsTypeEnum = NeedsType;
constructor( constructor(
private router: Router, private router: Router,
private route: ActivatedRoute, private route: ActivatedRoute,
) {} ) {}
public showPrevButton(): boolean {
return this.currentStep !== null && !(this.isPrevHidden || this.isLastStep);
}
public showNextButton(): boolean {
return !this.hideNavButtons;
}
public nextPage(isPrint?: boolean): void { public nextPage(isPrint?: boolean): void {
this.goNext.emit(isPrint); this.goNext.emit(isPrint);
} }
...@@ -41,4 +52,11 @@ export class NavigationComponent { ...@@ -41,4 +52,11 @@ export class NavigationComponent {
public resetOrientation(): void { public resetOrientation(): void {
this.goReset.emit(); this.goReset.emit();
} }
public focusFirstButton(): void {
if (this.showPrevButton()) {
this.prevButton.focus();
} else if (this.showNextButton()) {
this.nextButton.focus();
}
}
} }
...@@ -7,7 +7,7 @@ ...@@ -7,7 +7,7 @@
[id]="lang.key" [id]="lang.key"
[label]="lang.title" [label]="lang.title"
[selected]="selected === lang.key" [selected]="selected === lang.key"
(selectedEvent)="handleSelect(lang.key)" (click)="handleSelect(lang.key)"
/> />
</div> </div>
</div> </div>
...@@ -7,7 +7,7 @@ ...@@ -7,7 +7,7 @@
[label]="option.title" [label]="option.title"
[description]="option.hint" [description]="option.hint"
[selected]="selected === option.key" [selected]="selected === option.key"
(selectedEvent)="handleSelect(option.key)" (click)="handleSelect(option.key)"
/> />
</div> </div>
</div> </div>
<div class="orientation"> <div class="orientation">
<h1 class="visually-hidden">Orientation</h1> <h1 class="visually-hidden">Orientation</h1>
<div class="visually-hidden">
<p>Utilisez <strong>f</strong> (footer) pour aller directement aux boutons de validation.</p>
</div>
<app-progress-bar [currentPage]="currentStep" [nbSteps]="nbSteps" [formType]="formType.orientation" /> <app-progress-bar [currentPage]="currentStep" [nbSteps]="nbSteps" [formType]="formType.orientation" />
<div class="container" [ngClass]="{ 'no-max-width': fullScreen }"> <div class="container" [ngClass]="{ 'no-max-width': fullScreen }">
<app-needs-selection <app-needs-selection
......
import { AfterContentChecked, ChangeDetectorRef, Component, OnInit } from '@angular/core'; import { AfterContentChecked, ChangeDetectorRef, Component, HostListener, OnInit, ViewChild } from '@angular/core';
import { AbstractControl, FormGroup, UntypedFormGroup } from '@angular/forms'; import { AbstractControl, FormGroup, UntypedFormGroup } from '@angular/forms';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import { lastValueFrom } from 'rxjs'; import { lastValueFrom } from 'rxjs';
...@@ -28,6 +28,7 @@ import { ...@@ -28,6 +28,7 @@ import {
OnlineDemarchesCommonSteps, OnlineDemarchesCommonSteps,
StructuresListSteps, StructuresListSteps,
} from './enums/orientation.enums'; } from './enums/orientation.enums';
import { NavigationComponent } from './global-components/navigation/navigation.component';
import { IAppointment } from './interfaces/appointment.interface'; import { IAppointment } from './interfaces/appointment.interface';
import { FiltersForm } from './interfaces/filtersForm.interface'; import { FiltersForm } from './interfaces/filtersForm.interface';
import { IOnlineMediation } from './interfaces/onlineMediation.interface'; import { IOnlineMediation } from './interfaces/onlineMediation.interface';
...@@ -116,6 +117,36 @@ export class OrientationFormViewComponent implements OnInit, AfterContentChecked ...@@ -116,6 +117,36 @@ export class OrientationFormViewComponent implements OnInit, AfterContentChecked
public showConfirmationModal = false; public showConfirmationModal = false;
private resolve: CanExitResolver; private resolve: CanExitResolver;
@ViewChild(NavigationComponent) navComponent!: NavigationComponent;
// Listener, keyboard shortcuts
@HostListener('window:keydown', ['$event'])
handleKeyboardEvent(event: KeyboardEvent): void {
const target = event.target as HTMLElement;
const tagName = target.tagName.toLowerCase();
// Check if the focus is within an input, textarea
if (tagName === 'input') {
const inputElement = target as HTMLInputElement;
const inputType = inputElement.type.toLowerCase();
// Ignore only text-based inputs, not others like radio, checkbox, etc.
if (!['radio', 'checkbox', 'button', 'submit', 'reset'].includes(inputType)) {
return;
}
} else if (tagName === 'textarea' || target.isContentEditable) {
return;
}
switch (event.key) {
case 'f': // 'f' to go to the navigation footer
this.navComponent.focusFirstButton();
event.preventDefault();
break;
default:
break;
}
}
constructor( constructor(
public orientationService: OrientationService, public orientationService: OrientationService,
private notificationService: NotificationService, private notificationService: NotificationService,
...@@ -124,12 +155,13 @@ export class OrientationFormViewComponent implements OnInit, AfterContentChecked ...@@ -124,12 +155,13 @@ export class OrientationFormViewComponent implements OnInit, AfterContentChecked
private profileService: ProfileService, private profileService: ProfileService,
private searchService: SearchService, private searchService: SearchService,
private structureService: StructureService, private structureService: StructureService,
private cdref: ChangeDetectorRef, private cdRef: ChangeDetectorRef,
private indicatorService: IndicatorService, private indicatorService: IndicatorService,
private router: Router, private router: Router,
) { ) {
this.setCategories(); this.setCategories();
} }
async ngOnInit(): Promise<void> { async ngOnInit(): Promise<void> {
this.orientationService.rdvUser = null; this.orientationService.rdvUser = null;
if (history.state.rdvUser) { if (history.state.rdvUser) {
...@@ -167,7 +199,7 @@ export class OrientationFormViewComponent implements OnInit, AfterContentChecked ...@@ -167,7 +199,7 @@ export class OrientationFormViewComponent implements OnInit, AfterContentChecked
} }
ngAfterContentChecked(): void { ngAfterContentChecked(): void {
this.cdref.detectChanges(); this.cdRef.detectChanges();
} }
public validatePage(event: boolean): void { public validatePage(event: boolean): void {
......
...@@ -10,6 +10,7 @@ ...@@ -10,6 +10,7 @@
/> />
</div> </div>
<div <div
#clickableLogo
class="logo clickable" class="logo clickable"
aria-label="Retour accueil" aria-label="Retour accueil"
role="button" role="button"
......
import { animate, animateChild, query, style, transition, trigger } from '@angular/animations'; import { animate, animateChild, query, style, transition, trigger } from '@angular/animations';
import { Component } from '@angular/core'; import { Component, ElementRef, ViewChild } from '@angular/core';
import { NavigationEnd, Router } from '@angular/router'; import { NavigationEnd, Router } from '@angular/router';
import { Structure } from '../models/structure.model'; import { Structure } from '../models/structure.model';
import { ProfileService } from '../profile/services/profile.service'; import { ProfileService } from '../profile/services/profile.service';
...@@ -39,6 +39,8 @@ export class HeaderComponent { ...@@ -39,6 +39,8 @@ export class HeaderComponent {
private displayDataShare = false; private displayDataShare = false;
private loadingDataShare = false; private loadingDataShare = false;
@ViewChild('clickableLogo') clickableLogoDiv: ElementRef;
constructor( constructor(
private authService: AuthService, private authService: AuthService,
private profileService: ProfileService, private profileService: ProfileService,
...@@ -85,6 +87,12 @@ export class HeaderComponent { ...@@ -85,6 +87,12 @@ export class HeaderComponent {
} }
} }
public focusLogo(): void {
if (this.clickableLogoDiv) {
this.clickableLogoDiv.nativeElement.focus();
}
}
public get isLoggedIn(): boolean { public get isLoggedIn(): boolean {
return this.authService.isLoggedIn(); return this.authService.isLoggedIn();
} }
......
...@@ -132,16 +132,14 @@ ...@@ -132,16 +132,14 @@
(createValueChange)="onCreateValueChangeEmployer($event)" (createValueChange)="onCreateValueChangeEmployer($event)"
/> />
<div> <app-select-or-create
<app-select-or-create [name]="'Fonction'"
[name]="'Fonction'" [isFeminineWord]="true"
[isFeminineWord]="true" [autocompleteFunction]="profileService.getJobs.bind(profileService)"
[autocompleteFunction]="profileService.getJobs.bind(profileService)" [(value)]="jobName"
[(value)]="jobName" (selectItem)="selectJob($event)"
(selectItem)="selectJob($event)" (createValueChange)="onCreateValueChangeJob($event)"
(createValueChange)="onCreateValueChangeJob($event)" />
/>
</div>
<app-appointment-choice <app-appointment-choice
*ngIf="hasPersonalOffer" *ngIf="hasPersonalOffer"
......
...@@ -105,7 +105,8 @@ ...@@ -105,7 +105,8 @@
::ng-deep .selectOrCreate { ::ng-deep .selectOrCreate {
gap: 24px; gap: 24px;
padding: 24px; padding: 24px;
border: 1px solid $grey-6; border: 1px solid $grey-7;
border-radius: 4px;
} }
.credentialsTab { .credentialsTab {
......
<button <button
#buttonElement
[attr.aria-label]="ariaLabel" [attr.aria-label]="ariaLabel"
[type]="type" [type]="type"
[ngClass]="classes" [ngClass]="classes"
......
import { Component, EventEmitter, Input, Output } from '@angular/core'; import { Component, ElementRef, EventEmitter, Input, Output, ViewChild } from '@angular/core';
import { SpriteFolderType } from '../svg-icon/SpriteFolder.type'; import { SpriteFolderType } from '../svg-icon/SpriteFolder.type';
/** values will be used for css selectors */ /** values will be used for css selectors */
...@@ -54,7 +54,13 @@ export class ButtonComponent { ...@@ -54,7 +54,13 @@ export class ButtonComponent {
/** Click handler */ /** Click handler */
@Output() action = new EventEmitter<Event>(); @Output() action = new EventEmitter<Event>();
@ViewChild('buttonElement') buttonElement!: ElementRef;
public get classes(): string[] { public get classes(): string[] {
return [this.variant, this.size, this.wide ? 'wide' : '']; return [this.variant, this.size, this.wide ? 'wide' : ''];
} }
public focus(): void {
this.buttonElement.nativeElement.focus();
}
} }
...@@ -7,8 +7,10 @@ ...@@ -7,8 +7,10 @@
<div *ngIf="!day.value.open" class="openingTime">Fermé</div> <div *ngIf="!day.value.open" class="openingTime">Fermé</div>
<div *ngIf="day.value.open" class="openingTime"> <div *ngIf="day.value.open" class="openingTime">
<ng-container *ngFor="let timeRange of day.value.time; let isLast = last"> <ng-container *ngFor="let timeRange of day.value.time; let isLast = last">
{{ timeRange.formatOpeningDate() }} - {{ timeRange.formatClosingDate() }} <div class="time-range">
<span *ngIf="!isLast"> / </span> {{ timeRange.formatOpeningDate() }} - {{ timeRange.formatClosingDate() }}
</div>
<div *ngIf="!isLast" class="separator">/</div>
</ng-container> </ng-container>
</div> </div>
</li> </li>
......
...@@ -9,34 +9,36 @@ ul.hoursContainer { ...@@ -9,34 +9,36 @@ ul.hoursContainer {
list-style: none; list-style: none;
padding: 0; padding: 0;
margin: 0; margin: 0;
@media (max-width: 370px) {
width: 80%;
}
li.hours { li.hours {
width: 350px; width: 350px;
display: flex; display: flex;
flex-direction: row; flex-direction: row;
align-items: center; align-items: flex-start;
@media (max-width: 370px) {
width: 100%;
}
span.circle { span.circle {
padding: 0 8px;
background-repeat: no-repeat;
background-position: center center;
background-size: 11px 10px;
background-image: url('../../../../assets/form/hours-open.svg');
&.closed {
background-image: url('../../../../assets/form/hours-closed.svg');
}
&::before { &::before {
content: ''; content: '';
position: absolute; position: absolute;
border-left: 1px solid $border-hours; border-left: 1px solid $border-hours;
z-index: -1; z-index: -1;
top: 7px; top: 7px;
left: 8px; left: 8px;
height: 90%; height: 90%;
} }
padding: 0px 8px;
background-repeat: no-repeat;
background-position: center center;
background-size: 11px 10px;
background-image: url('../../../../assets/form/hours-open.svg');
&.closed {
background-image: url('../../../../assets/form/hours-closed.svg');
}
} }
.left { .left {
display: flex; display: flex;
gap: 8px; gap: 8px;
...@@ -50,14 +52,24 @@ ul.hoursContainer { ...@@ -50,14 +52,24 @@ ul.hoursContainer {
@include font-regular-13; @include font-regular-13;
color: $grey-4-5-1; color: $grey-4-5-1;
display: flex; display: flex;
flex-direction: row;
gap: 8px; gap: 8px;
.time-range {
white-space: nowrap;
}
@media (max-width: 370px) {
font-size: 12px;
flex-direction: column;
.separator {
display: none;
}
}
} }
} }
&.compact { &.compact {
gap: 6px; gap: 6px;
li.hours { li.hours {
.left.day { .left .day {
@include font-bold-12; @include font-bold-12;
} }
.openingTime { .openingTime {
......
...@@ -10,9 +10,10 @@ ...@@ -10,9 +10,10 @@
<ng-template #svgIcon> <ng-template #svgIcon>
<svg <svg
aria-hidden="true"
class="icon" class="icon"
role="presentation" [attr.role]="ariaLabel ? 'image' : 'presentation'"
[attr.aria-hidden]="!ariaLabel ? 'true' : 'false'"
[attr.aria-label]="ariaLabel ? ariaLabel : null"
[ngClass]="iconClass" [ngClass]="iconClass"
[attr.fill]="iconColor" [attr.fill]="iconColor"
[attr.stroke]="iconColor" [attr.stroke]="iconColor"
......
...@@ -19,5 +19,6 @@ export class SvgIconComponent { ...@@ -19,5 +19,6 @@ export class SvgIconComponent {
/** If should be interactive */ /** If should be interactive */
@Input() asButton = false; @Input() asButton = false;
@Input() ariaLabel?: string = null; @Input() ariaLabel?: string = null;
@Output() action = new EventEmitter<void>(); @Output() action = new EventEmitter<void>();
} }
...@@ -3,14 +3,16 @@ ...@@ -3,14 +3,16 @@
<span>{{ tclStop.name }}</span> <span>{{ tclStop.name }}</span>
<div class="inline listPoints"> <div class="inline listPoints">
<p *ngFor="let sub of tclStop.subLines"> <p *ngFor="let sub of tclStop.subLines">
<app-svg-icon [folder]="'tcl'" [icon]="sub" [iconClass]="iconClass" /> <app-svg-icon [folder]="'tcl'" [icon]="sub" [iconClass]="iconClass" [ariaLabel]="'Métro ' + sub"/>
</p> </p>
<p *ngFor="let tram of tclStop.tramLines"> <p *ngFor="let tram of tclStop.tramLines">
<app-svg-icon [folder]="'tcl'" [icon]="tram" [iconClass]="iconClass" /> <app-svg-icon [folder]="'tcl'" [icon]="tram" [iconClass]="iconClass" [ariaLabel]="'Tram ' + tram"/>
</p> </p>
<p *ngFor="let bus of tclStop.busLines"> <p *ngFor="let bus of tclStop.busLines">
<app-svg-icon [folder]="'tcl'" [icon]="bus" [iconClass]="iconClass" /> <app-svg-icon [folder]="'tcl'" [icon]="bus" [iconClass]="iconClass" [ariaLabel]="'Bus ' + bus"/>
</p> </p>
</div> </div>
</div> </div>
</div> </div>