Fix progress bar accessibility

This commit is contained in:
Chocobozzz 2024-09-19 14:42:21 +02:00
parent 51ce03e3cf
commit d96ec7da71
No known key found for this signature in database
GPG Key ID: 583A612D890159BE
16 changed files with 179 additions and 151 deletions

View File

@ -118,27 +118,19 @@
</td>
<td *ngIf="isSelected('quota')">
<div class="progress" i18n-title title="Total video quota">
<div
class="progress-bar" role="progressbar" [style]="{ width: getUserVideoQuotaPercentage(user) + '%' }"
[attr.aria-valuenow]="user.rawVideoQuotaUsed" aria-valuemin="0" [attr.aria-valuemax]="user.rawVideoQuota"
<my-progress-bar
i18n-label label="Total video quota" [max]="user.rawVideoQuota" [value]="user.rawVideoQuotaUsed"
[valueFormatted]="user.videoQuotaUsed" [maxFormatted]="user.videoQuota" size="small"
>
</div>
<span>{{ user.videoQuotaUsed }}</span>
<span>{{ user.videoQuota }}</span>
</div>
</my-progress-bar>
</td>
<td *ngIf="isSelected('quotaDaily')">
<div class="progress" i18n-title title="Total daily video quota">
<div
class="progress-bar" role="progressbar" [style]="{ width: getUserVideoQuotaDailyPercentage(user) + '%' }"
[attr.aria-valuenow]="user.rawVideoQuotaUsedDaily" aria-valuemin="0" [attr.aria-valuemax]="user.rawVideoQuotaDaily"
<my-progress-bar
i18n-label label="Total daily video quota" [max]="user.rawVideoQuotaDaily" [value]="user.rawVideoQuotaUsedDaily"
[valueFormatted]="user.videoQuotaUsedDaily" [maxFormatted]="user.videoQuotaDaily" size="small"
>
</div>
<span>{{ user.videoQuotaUsedDaily }}</span>
<span>{{ user.videoQuotaDaily }}</span>
</div>
</my-progress-bar>
</td>
<td *ngIf="isSelected('totalVideoFileSize')">

View File

@ -24,18 +24,7 @@ my-global-icon {
width: 18px;
}
.progress {
width: auto;
max-width: 100%;
@include progressbar($small: true);
}
@media screen and (max-width: $primeng-breakpoint) {
.progress {
width: 100%;
}
.empty-cell {
padding: 0;
}

View File

@ -28,6 +28,7 @@ import {
} from '../../../../shared/shared-moderation/user-moderation-dropdown.component'
import { TableExpanderIconComponent } from '../../../../shared/shared-tables/table-expander-icon.component'
import { UserEmailInfoComponent } from '../../../shared/user-email-info.component'
import { ProgressBarComponent } from '@app/shared/shared-main/misc/progress-bar.component'
type UserForList = User & {
rawVideoQuota: number
@ -65,7 +66,8 @@ type UserForList = User & {
AutoColspanDirective,
UserBanModalComponent,
DatePipe,
BytesPipe
BytesPipe,
ProgressBarComponent
]
})
export class UserListComponent extends RestTable <User> implements OnInit {

View File

@ -0,0 +1,11 @@
<div
role="progressbar" class="progress-container" tabindex="0" [ngbTooltip]="label"
[attr.aria-valuenow]="value" aria-valuemin="0" [attr.aria-valuemax]="max"
[attr.aria-label]="label"
[ngClass]="{ red: theme === 'red', green: theme === 'green', small: size === 'small' }"
>
<div class="progress-bar" [style]="{ width: percentage() + '%' }"></div>
<span class="value">{{ valueFormatted }}</span>
<span class="max" *ngIf="maxFormatted">{{ maxFormatted }}</span>
</div>

View File

@ -0,0 +1,69 @@
@use '_variables' as *;
@use '_mixins' as *;
.progress-container {
background-color: pvar(--greyBackgroundColor);
display: flex;
overflow: hidden;
height: 2rem;
font-size: 0.85rem;
font-weight: $font-semibold;
border-radius: 0.25rem;
position: relative;
span {
position: absolute;
color: pvar(--greyForegroundColor);
line-height: 2rem;
margin: 0 12px;
}
.value {
left: 0;
}
.max {
right: 0;
}
.progress-bar {
font-weight: $font-semibold;
color: pvar(--mainBackgroundColor);
background-color: pvar(--mainColor);
display: flex;
flex-direction: column;
justify-content: center;
text-align: center;
white-space: nowrap;
transition: width 0.6s ease;
}
&.red,
&.green {
span {
color: $white;
}
}
&.red .progress-bar {
background-color: $red;
}
&.green .progress-bar {
background-color: $green;
}
&.small {
height: 1rem;
font-weight: normal;
font-size: 0.75rem;
span {
margin: 0 5px;
line-height: normal;
}
}
@include disable-outline;
@include button-focus(pvar(--mainColorLightest));
}

View File

@ -0,0 +1,34 @@
import { CommonModule } from '@angular/common'
import { Component, Input, numberAttribute } from '@angular/core'
import { NgbTooltip } from '@ng-bootstrap/ng-bootstrap'
@Component({
selector: 'my-progress-bar',
styleUrls: [ './progress-bar.component.scss' ],
templateUrl: './progress-bar.component.html',
standalone: true,
imports: [
CommonModule,
NgbTooltip
]
})
export class ProgressBarComponent {
@Input({ required: true, transform: numberAttribute }) value: number
@Input({ required: true }) label: string
@Input({ required: true }) valueFormatted: string | number
@Input() maxFormatted: string
@Input() size: 'normal' | 'small' = 'normal'
@Input({ transform: numberAttribute }) max = 100
@Input({ transform: numberAttribute }) min = 0
@Input() theme: 'green' | 'red' | 'main' = 'main'
percentage () {
return this.value * 100 / this.max
}
}

View File

@ -2,22 +2,21 @@
<div>
<div class="mb-2 fw-bold" i18n>Total video quota</div>
<div class="progress" tabindex="0" [ngbTooltip]="titleVideoQuota()">
<div class="progress-bar" tabindex="0" role="progressbar" [style]="{ width: userVideoQuotaPercentage + '%' }"
[attr.aria-valuenow]="userVideoQuotaUsed" aria-valuemin="0" [attr.aria-valuemax]="user?.videoQuota"></div>
<span>{{ userVideoQuotaUsed | bytes: 1 }}</span>
<span>{{ userVideoQuota }}</span>
</div>
<my-progress-bar
[label]="labelQuota()" [max]="user?.videoQuota || 0" [value]="userVideoQuotaUsed"
[valueFormatted]="userVideoQuotaUsed | bytes: 1" [maxFormatted]="userVideoQuota"
height="2rem"
>
</my-progress-bar>
</div>
<div *ngIf="hasDailyQuota()" class="mt-3">
<div class="mb-2 fw-bold" i18n>Daily video quota</div>
<div class="progress" tabindex="0" [ngbTooltip]="titleVideoQuotaDaily()">
<div class="progress-bar" role="progressbar" [style]="{ width: userVideoQuotaDailyPercentage + '%' }"
[attr.aria-valuenow]="userVideoQuotaUsedDaily" aria-valuemin="0" [attr.aria-valuemax]="user?.videoQuotaDaily"></div>
<span>{{ userVideoQuotaUsedDaily | bytes: 1 }}</span>
<span>{{ userVideoQuotaDaily }}</span>
</div>
</div>
<my-progress-bar
[label]="labelQuotaDaily()" [max]="user?.videoQuotaDaily || 0" [value]="userVideoQuotaUsedDaily"
[valueFormatted]="userVideoQuotaUsedDaily | bytes: 1" [maxFormatted]="userVideoQuotaDaily"
height="2rem"
>
</my-progress-bar>
</div>

View File

@ -1,24 +0,0 @@
@use '_variables' as *;
@use '_mixins' as *;
.user-quota {
label {
@include margin-right(5px);
}
&,
.progress {
width: 100% !important;
}
.progress {
@include disable-outline;
@include button-focus(pvar(--mainColorLightest));
@include progressbar($height: 2rem);
span {
align-self: center;
}
}
}

View File

@ -1,16 +1,15 @@
import { NgIf } from '@angular/common'
import { Component, OnInit } from '@angular/core'
import { AuthService, UserService } from '@app/core'
import { NgbTooltip } from '@ng-bootstrap/ng-bootstrap'
import { first } from 'rxjs'
import { BytesPipe } from '../angular/bytes.pipe'
import { ProgressBarComponent } from '../misc/progress-bar.component'
@Component({
selector: 'my-user-quota',
templateUrl: './user-quota.component.html',
styleUrls: [ './user-quota.component.scss' ],
standalone: true,
imports: [ NgbTooltip, NgIf, BytesPipe ]
imports: [ NgIf, BytesPipe, ProgressBarComponent ]
})
export class UserQuotaComponent implements OnInit {
@ -62,11 +61,11 @@ export class UserQuotaComponent implements OnInit {
return this.user.videoQuotaDaily !== -1
}
titleVideoQuota () {
return `${new BytesPipe().transform(this.userVideoQuotaUsed, 0).toString()} / ${this.userVideoQuota}`
labelQuota () {
return `Total video quota: ${new BytesPipe().transform(this.userVideoQuotaUsed, 0).toString()} / ${this.userVideoQuota}`
}
titleVideoQuotaDaily () {
return `${new BytesPipe().transform(this.userVideoQuotaUsedDaily, 0).toString()} / ${this.userVideoQuotaDaily}`
labelQuotaDaily () {
return `Total daily video quota: ${new BytesPipe().transform(this.userVideoQuotaUsedDaily, 0).toString()} / ${this.userVideoQuotaDaily}`
}
}

View File

@ -1,14 +1,10 @@
<!-- Upload progress/cancel/error/success header -->
<div *ngIf="isUploading && !error" class="upload-progress-cancel">
<div class="progress" i18n-title title="Total uploaded">
<div
class="progress-bar" role="progressbar"
[style]="{ width: uploadPercents + '%' }" [attr.aria-valuenow]="uploadPercents" aria-valuemin="0" [attr.aria-valuemax]="100"
<my-progress-bar
i18n-label label="Total uploaded" theme="green"
[value]="uploadPercents" [valueFormatted]="getUploadingLabel()"
>
<span *ngIf="uploadPercents === 100 && uploaded === false" i18n>Processing…</span>
<span *ngIf="uploadPercents !== 100 || uploaded">{{ uploadPercents }}%</span>
</div>
</div>
</my-progress-bar>
<input
*ngIf="uploaded === false"
@ -17,11 +13,11 @@
</div>
<div *ngIf="error && enableRetryAfterError" class="upload-progress-retry">
<div class="progress">
<div class="progress-bar red" role="progressbar" [style]="{ width: '100%' }" [attr.aria-valuenow]="100" aria-valuemin="0" [attr.aria-valuemax]="100">
<span>{{ error }}</span>
</div>
</div>
<my-progress-bar
[label]="error" value="100" [valueFormatted]="error"
theme="red"
>
</my-progress-bar>
<input type="button" class="peertube-button grey-button ms-1" i18n-value="Retry failed upload" value="Retry" (click)="retry.emit()" />
<input type="button" class="peertube-button grey-button ms-1" i18n-value="Cancel ongoing upload" value="Cancel" (click)="cancel.emit()" />

View File

@ -5,23 +5,8 @@
.upload-progress-cancel {
display: flex;
margin-bottom: 40px;
.progress {
flex-grow: 1;
@include progressbar($height: 30px, $font-size: 14px, $background-color: rgba(11, 204, 41, 0.16));
.progress-bar {
background-color: pvar(--green);
line-height: 30px;
text-align: start;
font-weight: $font-semibold;
span {
color: pvar(--mainBackgroundColor);
@include margin-left(13px);
}
}
}
}
my-progress-bar {
width: 100%;
}

View File

@ -1,11 +1,12 @@
import { CommonModule } from '@angular/common'
import { Component, EventEmitter, Input, Output } from '@angular/core'
import { ProgressBarComponent } from '../shared-main/misc/progress-bar.component'
@Component({
selector: 'my-upload-progress',
templateUrl: './upload-progress.component.html',
styleUrls: [ './upload-progress.component.scss' ],
imports: [ CommonModule ],
imports: [ CommonModule, ProgressBarComponent ],
standalone: true
})
export class UploadProgressComponent {
@ -17,4 +18,12 @@ export class UploadProgressComponent {
@Output() cancel = new EventEmitter()
@Output() retry = new EventEmitter()
getUploadingLabel () {
if (this.uploadPercents === 100 && this.uploaded === false) {
return $localize`Processing…`
}
return $localize`${this.uploadPercents}%`
}
}

View File

@ -24,6 +24,7 @@ body {
// for css custom properties #{$var}
--red: #{$red};
--green: #{$green};
--white: #{$white};
--mainColor: #{$main-color};
--mainColorLighter: #{$main-color-lighter};

View File

@ -628,47 +628,6 @@
}
}
@mixin progressbar($small: false, $height: 1rem, $font-size: 0.75rem, $background-color: pvar(--greyBackgroundColor)) {
background-color: $background-color;
display: flex;
height: $height;
overflow: hidden;
font-size: $font-size;
border-radius: 0.25rem;
position: relative;
span {
position: absolute;
color: pvar(--greyForegroundColor);
@if $small {
top: -1px;
}
&:nth-of-type(1) {
left: .2rem;
}
&:nth-of-type(2) {
right: .2rem;
}
}
.progress-bar {
color: pvar(--mainBackgroundColor);
background-color: pvar(--mainColor);
display: flex;
flex-direction: column;
justify-content: center;
text-align: center;
white-space: nowrap;
transition: width 0.6s ease;
&.red {
background-color: color.adjust($color: #c54130, $lightness: 10%);
}
}
}
@mixin divider($color: pvar(--submenuBackgroundColor), $background: pvar(--mainBackgroundColor)) {
width: 95%;
border-top: .05rem solid $color;

View File

@ -32,7 +32,8 @@ $bg-color: #fff;
$fg-color: #212529;
$red: #EE0700;
$green: #338809;
$green: #278904;
$white: #fff;
$expanded-horizontal-margins: 150px;
$not-expanded-horizontal-margins: 30px;
@ -108,6 +109,7 @@ $primeng-breakpoint: 960px;
$variables: (
--red: var(--red),
--green: var(--green),
--white: var(--white),
--mainColor: var(--mainColor),
--mainColorLighter: var(--mainColorLighter),

View File

@ -100,7 +100,12 @@ export const videosAddResumableValidator = [
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
const user = res.locals.oauth.token.User
const file = buildUploadXFile(req.body as express.CustomUploadXFile<express.UploadNewVideoXFileMetadata>)
const cleanup = () => safeUploadXCleanup(file)
const cleanup = () => {
safeUploadXCleanup(file)
Redis.Instance.deleteUploadSession(req.query.upload_id)
.catch(err => logger.error('Cannot delete upload session', { err }))
}
const uploadId = req.query.upload_id
const sessionExists = await Redis.Instance.doesUploadSessionExist(uploadId)