MOBILE-4565 a11y: Fix a lot of focus problems

main
Pau Ferrer Ocaña 2024-04-23 12:52:34 +02:00
parent 7e6312a9d5
commit 45fb4cb92a
37 changed files with 277 additions and 237 deletions

View File

@ -1,3 +1,5 @@
@use "theme/globals" as *;
:host .core-block-content ::ng-deep {
ion-label {
max-width: 100%;
@ -31,6 +33,7 @@
vertical-align: baseline;
text-decoration: none;
border-radius: var(--mdl-shape-borderRadius-xs);
@include core-focus-outline();
}
.s20 {
font-size: 2.7em;

View File

@ -54,7 +54,7 @@
</ion-item>
<div id="addon-blog-associations">
@if (associationsExpanded) {
<ion-item>
<ion-item class="ion-no-validation">
@if (associatedModule) {
<ion-toggle formControlName="associateWithModule">
<core-format-text [text]="'addon.blog.associatewithmodule' | translate: {

View File

@ -20,7 +20,7 @@
<core-loading [hideUntil]="loaded">
@if (showMyEntriesToggle) {
<ion-item>
<ion-item class="ion-no-validation">
<ion-toggle [(ngModel)]="onlyMyEntries" (ionChange)="onlyMyEntriesToggleChanged(onlyMyEntries)">
{{ 'addon.blog.showonlyyourentries' | translate }}
</ion-toggle>
@ -65,34 +65,32 @@
</div>
<ion-label>
<div class="entry-summary" [ngClass]="{ 'border-bottom': entry.lastmodified <= entry.created }" [collapsible-item]="64">
<div class="ion-margin-bottom">
<core-format-text [text]="entry.summary" [component]="component" [componentId]="entry.id"
[contextLevel]="contextLevel" [contextInstanceId]="contextInstanceId" [courseId]="entry.courseid" />
</div>
@if (tagsEnabled && entry.tags && entry.tags!.length > 0) {
<ion-item class="ion-text-wrap">
<ion-label>
<div slot="start">{{ 'core.tag.tags' | translate }}:</div>
<core-tag-list [tags]="entry.tags" />
</ion-label>
</ion-item>
}
@for (file of entry.attachmentfiles; track $index) {
<core-file [file]="file" [component]="this.component" [componentId]="entry.id" />
}
@if (entry.uniquehash) {
<ion-item [href]="entry.uniquehash" core-link [detail]="true">
<ion-label>{{ 'addon.blog.linktooriginalentry' | translate }}</ion-label>
</ion-item>
}
<div class="entry-summary" [ngClass]="{ 'border-bottom': entry.lastmodified <= entry.created }" [collapsible-item]="64">
<div class="ion-margin-bottom">
<core-format-text [text]="entry.summary" [component]="component" [componentId]="entry.id" [contextLevel]="contextLevel"
[contextInstanceId]="contextInstanceId" [courseId]="entry.courseid" />
</div>
</ion-label>
@if (tagsEnabled && entry.tags && entry.tags!.length > 0) {
<ion-item class="ion-text-wrap">
<ion-label>
<div slot="start">{{ 'core.tag.tags' | translate }}:</div>
<core-tag-list [tags]="entry.tags" />
</ion-label>
</ion-item>
}
@for (file of entry.attachmentfiles; track $index) {
<core-file [file]="file" [component]="this.component" [componentId]="entry.id" />
}
@if (entry.uniquehash) {
<ion-item [href]="entry.uniquehash" core-link [detail]="true">
<ion-label>{{ 'addon.blog.linktooriginalentry' | translate }}</ion-label>
</ion-item>
}
</div>
@if (entry.lastmodified > entry.created || (entry.userid === currentUserId && entry.publishstate !== 'draft')) {
<ion-item class="entry-last-modification">

View File

@ -7,7 +7,7 @@
}
.entry {
border-top: 1px solid var(--stroke);
border-bottom: 1px solid var(--stroke);
&-visibility-permission {
display: flex;

View File

@ -9,7 +9,7 @@
</ion-header>
<ion-content [fullscreen]="true">
<ion-list>
<ion-item *ngFor="let type of types" class="addon-calendar-event" [ngClass]="['addon-calendar-eventtype-'+type]">
<ion-item *ngFor="let type of types" class="addon-calendar-event ion-no-validation" [ngClass]="['addon-calendar-eventtype-'+type]">
<ion-icon [name]="typeIcons[type]" slot="start" aria-hidden="true" />
<ion-toggle [(ngModel)]="filter[type]" (ionChange)="onChange()">
{{ 'addon.calendar.' + type + 'events' | translate}}

View File

@ -21,7 +21,8 @@
</ion-item-divider>
<ion-card>
<ion-list>
<ion-item class="ion-text-wrap" *ngFor="let device of platform.devices" [class.item-current]="device.current">
<ion-item class="ion-text-wrap ion-no-validation" *ngFor="let device of platform.devices"
[class.item-current]="device.current">
<ion-label>
<p class="item-heading" id="device-{{device.id}}">
<strong>{{ device.name }} {{ device.model }}</strong> ({{platform.platform}} {{ device.version }})

View File

@ -21,7 +21,7 @@
<h2>{{ 'core.settings.general' | translate }}</h2>
</ion-label>
</ion-item-divider>
<ion-item class="ion-text-wrap">
<ion-item class="ion-text-wrap ion-no-validation">
<ion-toggle [(ngModel)]="sendOnEnter" (ngModelChange)="sendOnEnterChanged()">
{{ 'addon.messages.useentertosend' | translate }}
</ion-toggle>
@ -31,7 +31,7 @@
<!-- Contactable privacy. -->
<ion-card>
<ion-item *ngIf="!advancedContactable" class="ion-text-wrap">
<ion-item *ngIf="!advancedContactable" class="ion-text-wrap ion-no-validation">
<ion-toggle [(ngModel)]="contactablePrivacy" (ngModelChange)="saveContactablePrivacy(contactablePrivacy)">
{{ 'addon.messages.blocknoncontacts' | translate }}
</ion-toggle>
@ -95,7 +95,7 @@
</ion-item-divider>
<!-- If notifications not disabled, show toggles.
If notifications are disabled, show "Disabled" instead of toggle. -->
<ion-item *ngFor="let state of ['loggedin', 'loggedoff']" class="ion-text-wrap">
<ion-item *ngFor="let state of ['loggedin', 'loggedoff']" class="ion-text-wrap ion-no-validation">
<ion-label>
<p>{{ 'core.settings.' + state | translate }}</p>
</ion-label>
@ -131,7 +131,7 @@
</ion-item-divider>
<ng-container *ngFor="let processor of notification.processors">
<!-- If group messaging is enabled, display a simplified view. -->
<ion-item class="ion-text-wrap">
<ion-item class="ion-text-wrap ion-no-validation">
<ion-label>
<p>{{ processor.displayname }}</p>
</ion-label>

View File

@ -345,7 +345,7 @@
</ion-item>
<!--- Apply grade to all team members. -->
<ion-item class="ion-text-wrap" *ngIf="assign!.teamsubmission && canSaveGrades">
<ion-item class="ion-text-wrap ion-no-validation" *ngIf="assign!.teamsubmission && canSaveGrades">
<ion-toggle [(ngModel)]="grade.applyToAll">
<p class="item-heading">{{ 'addon.mod_assign.groupsubmissionsettings' | translate }}</p>
<p>{{ 'addon.mod_assign.applytoteam' | translate }}</p>
@ -371,7 +371,7 @@
</p>
</ion-label>
</ion-item>
<ion-item *ngIf="canSaveGrades && allowAddAttempt">
<ion-item *ngIf="canSaveGrades && allowAddAttempt" class="ion-no-validation">
<ion-toggle [(ngModel)]="grade.addAttempt">
<p>{{ 'addon.mod_assign.addattempt' | translate }}</p>
</ion-toggle>

View File

@ -16,7 +16,7 @@
<core-loading [hideUntil]="sessions.loaded">
<core-group-selector [groupInfo]="groupInfo" [(selected)]="groupId" (selectedChange)="reloadSessions()" [courseId]="courseId" />
<ion-item>
<ion-item class="ion-no-validation">
<ion-toggle [(ngModel)]="showAll" (ionChange)="reloadSessions()">
{{ 'addon.mod_chat.showincompletesessions' | translate }}
</ion-toggle>

View File

@ -11,7 +11,7 @@
</ion-toolbar>
</ion-header>
<ion-content>
<ion-item>
<ion-item class="ion-no-validation">
<ion-toggle [(ngModel)]="search.searchingAdvanced">
{{ 'addon.mod_data.advancedsearch' | translate }}
</ion-toggle>

View File

@ -38,7 +38,7 @@
</ion-label>
</ion-item>
<div *ngIf="advanced" id="addon-mod-forum-new-discussion-advanced">
<ion-item *ngIf="showGroups && groupIds.length > 1 && accessInfo.cancanposttomygroups">
<ion-item *ngIf="showGroups && groupIds.length > 1 && accessInfo.cancanposttomygroups" class="ion-no-validation">
<ion-toggle [(ngModel)]="newDiscussion.postToAllGroups" name="postallgroups">
{{ 'addon.mod_forum.posttomygroups' | translate }}
</ion-toggle>
@ -54,12 +54,12 @@
</ion-select-option>
</ion-select>
</ion-item>
<ion-item class="ion-text-wrap">
<ion-item class="ion-text-wrap ion-no-validation">
<ion-toggle [(ngModel)]="newDiscussion.subscribe" name="subscribe">
{{ 'addon.mod_forum.discussionsubscription' | translate }}
</ion-toggle>
</ion-item>
<ion-item *ngIf="canPin" class="ion-text-wrap">
<ion-item *ngIf="canPin" class="ion-text-wrap ion-no-validation">
<ion-toggle [(ngModel)]="newDiscussion.pin" name="pin">
{{ 'addon.mod_forum.discussionpinned' | translate }}
</ion-toggle>

View File

@ -53,17 +53,17 @@
<h2>{{ 'addon.mod_glossary.linking' | translate }}</h2>
</ion-label>
</ion-item-divider>
<ion-item class="ion-text-wrap">
<ion-item class="ion-text-wrap ion-no-validation">
<ion-toggle [(ngModel)]="data.usedynalink" name="usedynalink">
{{ 'addon.mod_glossary.entryusedynalink' | translate }}
</ion-toggle>
</ion-item>
<ion-item class="ion-text-wrap">
<ion-item class="ion-text-wrap ion-no-validation">
<ion-toggle [disabled]="!data.usedynalink" [(ngModel)]="data.casesensitive" name="casesensitive">
{{ 'addon.mod_glossary.casesensitive' | translate }}
</ion-toggle>
</ion-item>
<ion-item class="ion-text-wrap">
<ion-item class="ion-text-wrap ion-no-validation">
<ion-toggle [disabled]="!data.usedynalink" [(ngModel)]="data.fullmatch" name="fullmatch">
{{ 'addon.mod_glossary.fullmatch' | translate }}
</ion-toggle>

View File

@ -99,7 +99,7 @@
<h3 class="item-heading">{{ 'addon.mod_workshop.feedbackauthor' | translate }}</h3>
</ion-label>
</ion-item>
<ion-item class="ion-text-wrap" *ngIf="access.canpublishsubmissions">
<ion-item class="ion-text-wrap ion-no-validation" *ngIf="access.canpublishsubmissions">
<ion-toggle formControlName="published">
<p class="item-heading">{{ 'addon.mod_workshop.publishsubmission' | translate }}</p>
<p>{{ 'addon.mod_workshop.publishsubmission_help' | translate }}</p>

View File

@ -21,12 +21,12 @@
</ion-refresher>
<core-loading [hideUntil]="preferencesLoaded">
<ion-card>
<ion-item class="ion-text-wrap" *ngIf="preferences">
<ion-item class="ion-text-wrap ion-no-validation" *ngIf="preferences">
<ion-toggle [(ngModel)]="preferences.enableall" (ngModelChange)="enableAll(preferences.enableall)">
<p class="item-heading">{{ 'addon.notifications.allownotifications' | translate }}</p>
</ion-toggle>
</ion-item>
<ion-item class="ion-text-wrap" *ngIf="canChangeSound">
<ion-item class="ion-text-wrap ion-no-validation" *ngIf="canChangeSound">
<ion-toggle [(ngModel)]="notificationSound" (ngModelChange)="changeNotificationSound(notificationSound)">
<p class="item-heading">{{ 'addon.notifications.playsound' | translate }}</p>
</ion-toggle>
@ -79,7 +79,7 @@
</ion-card-header>
<ng-container *ngFor="let notification of component.notifications">
<!-- Tablet view -->
<ion-item class="ion-text-wrap ion-hide-md-down addon-notifications-table-content only-links">
<ion-item class="ion-text-wrap ion-hide-md-down addon-notifications-table-content only-links ion-no-validation">
<ion-label>
<ion-row class="ion-no-padding ion-align-items-center">
<ion-col class="ion-margin-horizontal ion-no-padding">

View File

@ -26,12 +26,25 @@
}
}
ion-select,
ion-button {
--icon-margin: 0 4px;
--background: var(--core-combobox-background);
--background-hover: var(--ion-text-color);
--background-activated: var(--ion-text-color);
--background-focused: var(--ion-text-color);
--background-hover-opacity: .04;
&.md {
--background-activated-opacity: 0;
--background-focused-opacity: .12;
}
&.ios {
--background-activated-opacity: .12;
--background-focused-opacity: .15;
}
--border-color: var(--core-combobox-border-color);
--border-style: solid;
@ -53,10 +66,7 @@
overflow: hidden;
box-shadow: var(--box-shadow);
&:focus,
&:focus-within {
@include core-focus-style();
}
--highlight-color: transparent !important;
}
ion-select {
@ -65,6 +75,7 @@
border-width: var(--border-width);
border-radius: var(--core-combobox-radius);
margin: 8px;
width: auto;
&.combobox-icon-only {
&::part(text) {
@ -80,6 +91,21 @@
&::part(icon) {
margin: var(--icon-margin);
opacity: 1;
--highlight-color: currentColor;
}
&:hover {
--background: rgba(var(--ion-text-color-rgb), var(--background-hover-opacity));
}
&:focus,
&:focus-visible,
&.ion-focused {
--background: rgba(var(--ion-text-color-rgb), var(--background-focused-opacity));
}
&.ion-activated {
--background: rgba(var(--ion-text-color-rgb), var(--background-activated-opacity));
}
}
@ -89,21 +115,6 @@
--color-focused: currentcolor;
--color-hover: currentcolor;
--background-hover: black;
--background-activated: black;
--background-focused: black;
--background-hover-opacity: .04;
&.md {
--background-activated-opacity: 0;
--background-focused-opacity: .12;
}
&.ios {
--background-activated-opacity: .12;
--background-focused-opacity: .15;
}
border-radius: var(--core-combobox-radius);
margin: 4px 8px;

View File

@ -3,9 +3,9 @@
<ion-list-header *ngIf="title">
<ion-label>{{title}}</ion-label>
</ion-list-header>
<ion-item class="ion-text-wrap" *ngFor="let item of items" core-link [capture]="item.captureLink" [autoLogin]="item.autoLogin"
[href]="item.href" (click)="itemClicked($event, item)" [attr.aria-label]="item.ariaAction" [hidden]="item.hidden"
[detail]="!!(item.href && !item.iconAction)" role="menuitem" [button]="!!(item.href && !item.iconAction)"
<ion-item class="ion-text-wrap ion-no-validation" *ngFor="let item of items" core-link [capture]="item.captureLink"
[autoLogin]="item.autoLogin" [href]="item.href" (click)="itemClicked($event, item)" [attr.aria-label]="item.ariaAction"
[hidden]="item.hidden" [detail]="!!(item.href && !item.iconAction)" role="menuitem" [button]="!!(item.href && !item.iconAction)"
[showBrowserWarning]="item.showBrowserWarning">
<ion-toggle *ngIf="item.iconAction === 'toggle'" [(ngModel)]="item.toggle" (ionChange)="item.toggleChanged($event)">
<p class="item-heading">

View File

@ -55,7 +55,7 @@ export class CoreModIconComponent implements OnInit, OnChanges {
@HostBinding('attr.role')
get getRole(): string | null {
return !this.showAlt ? 'presentation' : null;
return this.showAlt ? 'img' : 'presentation';
}
@HostBinding('attr.aria-label')

View File

@ -1,5 +1,6 @@
<swiper-container #swiperRef *ngIf="loaded" [attr.aria-busy]="activeSlideIndex ? 'true' : null">
<swiper-slide *ngFor="let item of items; index as index" [attr.aria-hidden]="!isActive(index)">
<swiper-slide *ngFor="let item of items; index as index" [attr.aria-hidden]="!isActive(index)"
[attr.tabindex]="!isActive(index) ? -1 : null">
<ng-container *ngIf="template" [ngTemplateOutlet]="template" [ngTemplateOutletContext]="{item: item, active: isActive(index)}" />
</swiper-slide>
</swiper-container>

View File

@ -8,8 +8,8 @@
</ion-button>
<swiper-container #swiperRef [slidesPerView]="swiperOpts.slidesPerView" role="tablist" [attr.aria-label]="description">
<ng-container *ngFor="let tab of tabs">
<swiper-slide *ngIf="tab.id" role="presentation" [id]="tab.id + '-tab'" tabindex="-1"
[class.selected]="selected === tab.id" class="{{tab.class}}">
<swiper-slide *ngIf="tab.id" role="presentation" [id]="tab.id + '-tab'" [class.selected]="selected === tab.id"
class="{{tab.class}}">
<ion-tab-button (ionTabButtonClick)="selectTab(tab.id, $event)" (keydown)="tabAction.keyDown(tab.id, $event)"
(keyup)="tabAction.keyUp(tab.id, $event)" [tab]="tab.page" [layout]="layout" role="tab"
[attr.aria-controls]="tab.id" [attr.aria-selected]="selected === tab.id"

View File

@ -57,7 +57,9 @@
overflow: hidden;
ion-tab-button {
width: 100%;
max-width: 100%;
ion-label {
text-overflow: ellipsis;
white-space: nowrap;

View File

@ -11,6 +11,5 @@
core-block ::ng-deep ion-card.addon-block-myoverview {
--border-width: 0;
--background: transparent;
margin: 0;
}

View File

@ -41,48 +41,53 @@
}
ion-tab-bar {
height: var(--menutabbar-size);
--background: var(--core-bottom-tabs-background);
--color: var(--core-bottom-tabs-color);
--color-selected: var(--core-bottom-tabs-color-selected);
--background-selected: var(--core-bottom-tabs-background-selected);
core-user-menu-button {
align-items: center;
display: flex;
justify-content: center;
height: var(--menutabbar-size);
}
core-user-menu-button {
align-items: center;
display: flex;
justify-content: center;
}
ion-tab-button {
&.tab-selected {
background: var(--background-selected);
}
ion-tab-button {
&.tab-selected {
background: var(--background-selected);
}
ion-icon.core-tab-icon {
text-overflow: unset;
overflow: visible;
text-align: center;
font-size: var(--mdl-typography-icon-fontSize-lg);
}
ion-icon.core-tab-icon {
text-overflow: unset;
overflow: visible;
text-align: center;
font-size: var(--mdl-typography-icon-fontSize-lg);
}
ion-badge.core-tab-badge {
font-size: 12px;
font-weight: bold;
border-radius: 10px;
padding-left: 6px;
padding-right: 6px;
line-height: 14px;
--background: var(--core-bottom-tabs-badge-color);
--color: var(--core-bottom-tabs-badge-text-color);
}
ion-badge.core-tab-badge {
font-size: 12px;
font-weight: bold;
border-radius: 10px;
padding-left: 6px;
padding-right: 6px;
line-height: 14px;
--background: var(--core-bottom-tabs-badge-color);
--color: var(--core-bottom-tabs-badge-text-color);
}
ion-icon.core-tab-badge {
color: var(--core-bottom-tabs-badge-color);
padding: 3px 6px 2px;
@include position(8px, null, null, calc(50% + 6px));
min-width: 12px;
font-size: 8px;
font-weight: normal;
box-sizing: border-box;
position: absolute;
z-index: 1;
}
ion-icon.core-tab-badge {
color: var(--core-bottom-tabs-badge-color);
padding: 3px 6px 2px;
@include position(8px, null, null, calc(50% + 6px));
min-width: 12px;
font-size: 8px;
font-weight: normal;
box-sizing: border-box;
position: absolute;
z-index: 1;
}
}

View File

@ -17,13 +17,13 @@
</ion-header>
<ion-content>
<ion-list class="list-item-limited-width">
<ion-item class="ion-text-wrap">
<ion-item class="ion-text-wrap ion-no-validation">
<ion-toggle [(ngModel)]="rtl" (ionChange)="RTLChanged()">
<p class="item-heading">Change text direction</p>
<p>{{ direction }}</p>
</ion-toggle>
</ion-item>
<ion-item class="ion-text-wrap">
<ion-item class="ion-text-wrap ion-no-validation">
<ion-toggle [(ngModel)]="forceSafeAreaMargins" (ionChange)="safeAreaChanged()">
<p class="item-heading">Force safe area margins</p>
</ion-toggle>
@ -34,13 +34,13 @@
</ion-toggle>
</ion-item>
<ng-container *ngIf="siteId">
<ion-item class="ion-text-wrap">
<ion-item class="ion-text-wrap ion-no-validation">
<ion-toggle [(ngModel)]="remoteStyles" (ionChange)="remoteStylesChanged()">
<p class="item-heading">Enable remote styles <ion-badge>{{remoteStylesCount}}</ion-badge>
</p>
</ion-toggle>
</ion-item>
<ion-item class="ion-text-wrap">
<ion-item class="ion-text-wrap ion-no-validation">
<ion-toggle [(ngModel)]="pluginStyles" (ionChange)="pluginStylesChanged()">
<p class="item-heading">Enable site plugin styles <ion-badge>{{pluginStylesCount}}</ion-badge>
</p>

View File

@ -18,7 +18,7 @@
<ion-content>
<ion-list class="list-item-limited-width">
<ion-item *ngIf="showDevOptions" [detail]="true" (click)="gotoDevOptions()">
<ion-item *ngIf="showDevOptions" [detail]="true" (click)="gotoDevOptions()" button>
<ion-icon name="fas-terminal" slot="start" aria-hidden="true" />
<ion-label class="ion-text-wrap">
<p class="item-heading">{{ 'core.settings.developeroptions' | translate }}</p>

View File

@ -11,16 +11,16 @@
</ion-header>
<ion-content>
<ion-list class="list-item-limited-width">
<ion-item class="ion-text-wrap" lines="none">
<ion-item class="ion-text-wrap ion-no-validation" lines="none">
<ion-select [(ngModel)]="selectedLanguage" (ionChange)="languageChanged($event)" interface="action-sheet"
[cancelText]="'core.cancel' | translate" [interfaceOptions]="{header: 'core.settings.language' | translate}">
<div slot="label" class="item-heading">{{ 'core.settings.language' | translate }}</div>
<div slot="label" class="item-heading ion-text-wrap">{{ 'core.settings.language' | translate }}</div>
<ion-select-option *ngFor="let entry of languages" [value]="entry.code">{{ entry.name }}</ion-select-option>
</ion-select>
</ion-item>
<ion-item class="ion-text-wrap core-settings-general-font-size item-interactive" lines="none">
<ion-item class="ion-text-wrap core-settings-general-font-size item-interactive ion-no-validation" lines="none">
<ion-label>
<p class="item-heading">{{ 'core.settings.fontsize' | translate }}</p>
<p class="item-heading ion-text-wrap">{{ 'core.settings.fontsize' | translate }}</p>
</ion-label>
<ion-segment [(ngModel)]="selectedZoomLevel" color="primary">
<ion-segment-button *ngFor=" let zoomLevel of zoomLevels" [value]="zoomLevel.value"
@ -33,12 +33,12 @@
</ion-segment-button>
</ion-segment>
</ion-item>
<ion-item class="ion-text-wrap core-settings-general-color-scheme" *ngIf="colorSchemes.length > 0" lines="none">
<ion-item class="ion-text-wrap core-settings-general-color-scheme ion-no-validation" *ngIf="colorSchemes.length > 0" lines="none">
<ion-select [(ngModel)]="selectedScheme" (ionChange)="colorSchemeChanged($event)" interface="action-sheet"
[cancelText]="'core.cancel' | translate" [disabled]="colorSchemeDisabled"
[interfaceOptions]="{header: 'core.settings.colorscheme' | translate}">
<div slot="label">
<p class="item-heading">{{ 'core.settings.colorscheme' | translate }}</p>
<p class="item-heading ion-text-wrap">{{ 'core.settings.colorscheme' | translate }}</p>
<p *ngIf="colorSchemeDisabled" class="text-danger">{{ 'core.settings.forcedsetting' | translate }}</p>
</div>
<ion-select-option *ngFor="let scheme of colorSchemes" [value]="scheme">
@ -47,34 +47,34 @@
</ion-item>
<ion-item *ngIf="colorSchemes.length> 0 && selectedScheme==='system' && isAndroid" lines="none">
<ion-label class="ion-text-wrap">
<p>{{ 'core.settings.colorscheme-system-notice' | translate }}</p>
<p class="ion-text-wrap">{{ 'core.settings.colorscheme-system-notice' | translate }}</p>
</ion-label>
</ion-item>
<ion-item class="ion-text-wrap">
<ion-item class="ion-no-validation">
<ion-toggle [(ngModel)]="richTextEditor" (ionChange)="richTextEditorChanged($event)">
<p class="item-heading">{{ 'core.settings.enablerichtexteditor' | translate }}</p>
<p>{{ 'core.settings.enablerichtexteditordescription' | translate }}</p>
<p class="item-heading ion-text-wrap">{{ 'core.settings.enablerichtexteditor' | translate }}</p>
<p class="ion-text-wrap">{{ 'core.settings.enablerichtexteditordescription' | translate }}</p>
</ion-toggle>
</ion-item>
<ion-item class="ion-text-wrap" *ngIf="displayIframeHelp">
<ion-item *ngIf="displayIframeHelp" class="ion-no-validation">
<ion-label>
<p class="item-heading">{{ 'core.settings.ioscookies' | translate }}</p>
<p>{{ 'core.settings.ioscookiesdescription' | translate }}</p>
<p class="item-heading ion-text-wrap">{{ 'core.settings.ioscookies' | translate }}</p>
<p class="ion-text-wrap">{{ 'core.settings.ioscookiesdescription' | translate }}</p>
<ion-button expand="block" (click)="openNativeSettings($event)">
{{ 'core.opensettings' | translate }}
</ion-button>
</ion-label>
</ion-item>
<ion-item class="ion-text-wrap">
<ion-item class="ion-no-validation">
<ion-toggle [(ngModel)]="debugDisplay" (ionChange)="debugDisplayChanged($event)">
<p class="item-heading">{{ 'core.settings.debugdisplay' | translate }}</p>
<p>{{ 'core.settings.debugdisplaydescription' | translate }}</p>
<p class="item-heading ion-text-wrap">{{ 'core.settings.debugdisplay' | translate }}</p>
<p class="ion-text-wrap">{{ 'core.settings.debugdisplaydescription' | translate }}</p>
</ion-toggle>
</ion-item>
<ion-item class="ion-text-wrap" *ngIf="analyticsAvailable">
<ion-item *ngIf="analyticsAvailable" class="ion-no-validation">
<ion-label>
<p class="item-heading">{{ 'core.settings.enableanalytics' | translate }}</p>
<p>{{ 'core.settings.enableanalyticsdescription' | translate }}</p>
<p class="item-heading ion-text-wrap">{{ 'core.settings.enableanalytics' | translate }}</p>
<p class="ion-text-wrap">{{ 'core.settings.enableanalyticsdescription' | translate }}</p>
</ion-label>
<ion-toggle [(ngModel)]="analyticsEnabled" (ionChange)="analyticsEnabledChanged($event)" slot="end" />
</ion-item>

View File

@ -16,7 +16,7 @@
</ion-refresher>
<core-loading [hideUntil]="handlers.loaded">
<ion-list>
<ion-item *ngFor="let handler of handlerItems" class="core-settings-handler" [ngClass]="handler.class"
<ion-item *ngFor="let handler of handlerItems" class="core-settings-handler ion-no-validation" [ngClass]="handler.class"
[attr.aria-label]="handler.title | translate" (click)="!handler.toggle && handlers.select(handler)"
[button]="!handler.toggle" [detail]="!handler.toggle" [attr.aria-current]="handlers.getItemAriaCurrent(handler)">
<ion-icon [name]="handler.icon" slot="start" *ngIf="handler.icon" aria-hidden="true" />

View File

@ -23,7 +23,7 @@
<h2>{{ 'core.settings.syncsettings' | translate }}</h2>
</ion-label>
</ion-item-divider>
<ion-item class="ion-text-wrap">
<ion-item class="ion-text-wrap ion-no-validation">
<ion-toggle [(ngModel)]="dataSaver" (ngModelChange)="syncOnlyOnWifiChanged()">
{{ 'core.settings.syncdatasaver' | translate }}
</ion-toggle>

View File

@ -299,24 +299,38 @@ export class CoreDomUtilsProvider {
): Promise<void> {
let retries = 10;
let focusElement = element;
let elementToFocus = element;
if ('getInputElement' in focusElement) {
// If it's an Ionic element get the right input to use.
focusElement.componentOnReady && await focusElement.componentOnReady();
focusElement = await focusElement.getInputElement();
/**
* See focusElement function on Ionic Framework utils/helpers.ts.
*/
if (elementToFocus.classList.contains('ion-focusable')) {
const app = elementToFocus.closest('ion-app');
if (app) {
app.setFocus([elementToFocus]);
}
if (document.activeElement === elementToFocus) {
return;
}
}
if (!focusElement || !focusElement.focus) {
if ('getInputElement' in elementToFocus) {
// If it's an Ionic element get the right input to use.
elementToFocus.componentOnReady && await elementToFocus.componentOnReady();
elementToFocus = await elementToFocus.getInputElement();
}
if (!elementToFocus || !elementToFocus.focus) {
throw new CoreError('Element to focus cannot be focused');
}
while (retries > 0 && focusElement !== document.activeElement) {
focusElement.focus();
while (retries > 0 && elementToFocus !== document.activeElement) {
elementToFocus.focus();
if (focusElement === document.activeElement) {
if (elementToFocus === document.activeElement) {
await CoreUtils.nextTick();
if (CorePlatform.isAndroid() && this.supportsInputKeyboard(focusElement)) {
if (CorePlatform.isAndroid() && this.supportsInputKeyboard(elementToFocus)) {
// On some Android versions the keyboard doesn't open automatically.
CoreApp.openKeyboard();
}

View File

@ -25,6 +25,12 @@ body:not(.core-iframe-fullscreen) .collapsible-header-page {
opacity: 0;
}
&.collapsible-header-page-is-collapsed .collapsible-header-floating-title {
pointer-events: none;
user-select: none;
visibility: hidden;
}
&:not(.collapsible-header-page-is-collapsed) .collapsible-header-collapsed {
--core-header-toolbar-border-width: 0;
--core-header-buttons-background: var(--ion-background-color);
@ -35,6 +41,9 @@ body:not(.core-iframe-fullscreen) .collapsible-header-page {
h1 {
opacity: 0;
pointer-events: none;
user-select: none;
visibility: hidden;
}
}
@ -64,7 +73,9 @@ body:not(.core-iframe-fullscreen) .collapsible-header-page {
.collapsible-header-original-title {
pointer-events: none;
user-select: none;
opacity: 0;
visibility: hidden;
}
& > *:not(.collapsible-header-floating-title-wrapper),

View File

@ -8,7 +8,6 @@
--state-color-hover: rgb(40 40 40, 4%); // --gray-900 4%
--state-color-pressed: rgb(40 40 40, 12%); // --gray-900 12%
--state-color-focused: rgb(40 40 40, 12%); // --gray-900 12%
background: var(--background-color);
border-radius: var(--mdl-shape-borderRadius-xs);
@ -78,19 +77,12 @@
transform: rotate(0);
}
@include core-focus-background();
&:hover {
background: var(--state-color-hover);
}
&:focus {
box-shadow: none;
background: var(--state-color-focused);
}
&:focus-visible {
@include core-focus-style();
}
&:active {
background: var(--state-color-pressed);
}

View File

@ -14,6 +14,8 @@ ion-card {
&::part(native) {
--border-width: 0;
@include core-focus-over();
}
ion-item:only-child {

View File

@ -16,7 +16,7 @@ ion-item {
&.ion-invalid {
--inner-border-width: 0 0 1px 0;
&.ion-touched {
&.ion-touched:not(.ion-no-validation) {
&.ion-invalid {
--ion-item-border-color: var(--highlight-color-invalid);
--highlight-background: var(--ion-item-border-color);
@ -30,11 +30,25 @@ ion-item {
}
}
&.ion-no-validation {
--inner-border-width: 0 0 1px 0;
}
// Hide details on items to align badges.
&.hide-detail {
--detail-icon-opacity: 0;
}
&:not(.item-input) {
--show-full-highlight: 0;
--show-inset-highlight: 0;
}
&.item-has-interactive-control:focus-within {
@include core-focus-outline();
}
}
// Fake item.
div.fake-ion-item {
position: relative;

View File

@ -1,6 +0,0 @@
ion-tab-bar.mainmenu-tabs {
--background: var(--core-bottom-tabs-background);
--color: var(--core-bottom-tabs-color);
--color-selected: var(--core-bottom-tabs-color-selected);
--background-selected: var(--core-bottom-tabs-background-selected);
}

View File

@ -66,7 +66,42 @@
}
}
@mixin core-focus() {
@mixin core-focus-over() {
&:focus-visible {
@include core-focus-over-internal();
}
@supports not selector(:focus-visible) {
@at-root:focus {
@include core-focus-over-internal();
}
}
}
@mixin core-focus-outline() {
&:focus-visible {
@include core-focus-outline-internal();
}
@supports not selector(:focus-visible) {
@at-root:focus {
@include core-focus-outline-internal();
}
}
}
@mixin core-focus-background() {
&:focus-visible {
@include core-focus-background-internal();
}
@supports not selector(:focus-visible) {
@at-root:focus {
@include core-focus-background-internal();
}
}
}
@mixin core-focus-over-internal() {
outline: none;
position: relative;
@ -74,17 +109,26 @@
@include position(0px, 0px, 0px, 0px);
position: absolute;
content: "";
opacity: 1;
z-index: 1;
@include core-focus-style();
pointer-events: none;
user-select: none;
@include core-focus-background-internal();
}
}
@mixin core-focus-style() {
box-shadow: var(--a11y-shadow-focus-boxShadow);
border-radius: var(--border-radius);
// Thicker option:
// outline: var(--a11y-shadow-focus-outline);
@mixin core-focus-outline-internal() {
// box-shadow: var(--a11y-shadow-focus-boxShadow);
// border-radius: var(--border-radius);
outline: var(--a11y-shadow-focus-outline);
}
@mixin core-focus-background-internal() {
--background-focused: var(--background-focused, var(--a11y-background-focus-background));
--background-focused-opacity: var(--a11y-background-focus-opacity);
--background: var(--a11y-background-focus-background-rgb);
background: var(--background);
outline: none;
}
@mixin core-transition($properties: all, $duration: 500ms, $timing-function: ease-in-out) {

View File

@ -1122,68 +1122,15 @@ td {
}
// Change default outline.
:focus-visible {
@include core-focus-style();
border-radius: inherit;
outline: none;
.ion-activatable,
.clickable,
.ion-focusable.ion-focused,
.ion-selectable {
@include core-focus-background();
}
// Focus highlight for accessibility.
.ion-focused:not(.item-multiple-inputs):not(:focus),
ion-input.has-focus,
ion-card:focus {
@include core-focus();
:focus-visible,
.clickable:focus {
box-shadow: none;
}
}
.ion-focused.item-multiple-inputs {
ion-toggle:focus-within,
ion-select:focus-within,
ion-checkbox:focus-within,
ion-radio:focus-within {
@include core-focus();
}
}
// Treat cases where there's a focusable element inside an item, like a button.
ion-item.item-input:not(.item-multiple-inputs):not(:focus),
ion-item.item-has-focus:not(.item-multiple-inputs):not(:focus),
ion-item.item-input ion-input.has-focus {
position: relative;
&::after {
box-shadow: revert;
opacity: revert;
z-index: revert;
}
.item-highlight, .item-inner-highlight {
position: unset;
}
}
textarea, button, select, input, a, .clickable {
&:focus {
@include core-focus-style();
outline: none;
}
}
ion-loading:focus-visible,
ion-alert:focus-visible,
ion-popover:focus-visible,
ion-modal:focus-visible {
box-shadow: none;
border-radius: 0;
}
ion-input .native-input {
&:focus, &:focus-visible {
box-shadow: none;
outline: none;
}
:not(.hydrated):not(.native-input) { // Not an ionic component.
@include core-focus-outline();
}
ion-input,

View File

@ -145,6 +145,9 @@ html {
--a11y-shadow-focus-borderWidth: 2px;
--a11y-shadow-focus-boxShadow: inset 0 0 var(--a11y-shadow-focus-borderWidth) 1px var(--a11y-focus-color);
--a11y-shadow-focus-outline: var(--a11y-shadow-focus-borderWidth) solid var(--a11y-focus-color);
--a11y-background-focus-background: var(--ion-text-color);
--a11y-background-focus-opacity: 0.08;
--a11y-background-focus-background-rgb: rgba(var(--ion-text-color-rgb), var(--a11y-background-focus-opacity));
// @TODO ***** VARIABLES TO BE REVIEWED, RENAMED AND TIDIED ***** //
--text-size: var(--mdl-typography-body-fontSize-md);

View File

@ -43,7 +43,6 @@ html {
@import "components/ion-popover.scss";
@import "components/ion-searchbar.scss";
@import "components/ion-spinner.scss";
@import "components/ion-tab-bar.scss";
@import "components/ion-toast.scss";
}