Skip to content

Commit 3d21e4e

Browse files
authored
Merge pull request #275 from ahamelers/release/2.2
Fix keyboard navigation and form input label accessibility issues
2 parents a273e64 + 79048a4 commit 3d21e4e

16 files changed

Lines changed: 165 additions & 72 deletions

.vscode/settings.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
"esmodules",
88
"exportparts",
99
"kyleshockey",
10+
"labelledby",
1011
"prismjs",
1112
"randombytes",
1213
"rapidoc",

src/components/api-request.js

Lines changed: 28 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -79,8 +79,8 @@ export default class ApiRequest extends LitElement {
7979
return keyed(id, html`
8080
<div id="api-request-${id}"
8181
class="api-request col regular-font request-panel ${(this.renderStyle === 'focused' || this.callback === 'true') ? 'focused-mode' : 'view-mode'}">
82-
<div class=" ${this.callback === 'true' ? 'tiny-title' : 'req-res-title'} ">
83-
${this.callback === 'true' ? 'CALLBACK REQUEST' : getI18nText('operations.request')}
82+
<div class=" ${this.callback === 'true' ? 'tiny-title' : 'req-res-title'} " role="heading" aria-level="${this.renderStyle === 'focused' ? 3 : 4}">
83+
${this.callback === 'true' ? getI18nText('operations.callback-request') : getI18nText('operations.request')}
8484
</div>
8585
<div>
8686
${this.inputParametersTemplate('path')}
@@ -127,10 +127,10 @@ export default class ApiRequest extends LitElement {
127127
}
128128

129129
const title = {
130-
path: 'PATH PARAMETERS',
131-
query: 'QUERY-STRING PARAMETERS',
132-
header: 'REQUEST HEADERS',
133-
cookie: 'COOKIES'
130+
path: getI18nText('parameters.path'),
131+
query: getI18nText('parameters.string'),
132+
header: getI18nText('parameters.headers'),
133+
cookie: getI18nText('parameters.cookies')
134134
}[paramLocation];
135135

136136
const tableRows = [];
@@ -158,7 +158,7 @@ export default class ApiRequest extends LitElement {
158158
return html`
159159
<tr>
160160
<td colspan="1" style="width:160px; min-width:50px; vertical-align: top">
161-
<div class="param-name ${generatedParamSchema.deprecated ? 'deprecated' : ''}" style="margin-top: 1rem;">
161+
<div class="param-name ${generatedParamSchema.deprecated ? 'deprecated' : ''}" style="margin-top: 1rem;" id="request-${paramName}-label">
162162
${paramName}${!generatedParamSchema.deprecated && paramRequired ? html`<span style='color:var(--red);'>*</span>` : ''}
163163
</div>
164164
<div class="param-type" style="margin-bottom: 1rem;">
@@ -175,6 +175,7 @@ export default class ApiRequest extends LitElement {
175175
<tag-input class="request-param"
176176
autocomplete="on"
177177
id = "request-param-${paramName}"
178+
aria-labelledby = "request-${paramName}-label"
178179
style = "width:100%;"
179180
data-ptype = "${paramLocation}"
180181
data-pname = "${paramName}"
@@ -190,6 +191,7 @@ export default class ApiRequest extends LitElement {
190191
<textarea
191192
autocomplete="on"
192193
id = "request-param-${paramName}"
194+
aria-labelledby = "request-${paramName}-label"
193195
@input="${() => { this.computeCurlSyntax(); }}"
194196
class = "textarea small request-param"
195197
part = "textarea small textarea-param"
@@ -219,6 +221,7 @@ export default class ApiRequest extends LitElement {
219221
<input type="${generatedParamSchema.format === 'password' ? 'password' : 'text'}" spellcheck="false" style="width:100%; margin-top: 1rem; margin-bottom: 1rem;"
220222
autocomplete="on"
221223
id="request-param-${paramName}"
224+
aria-labelledby = "request-${paramName}-label"
222225
@input="${() => { this.computeCurlSyntax(); }}"
223226
placeholder="${generatedParamSchema.example || defaultVal || ''}"
224227
class="request-param"
@@ -384,7 +387,7 @@ export default class ApiRequest extends LitElement {
384387
this.selectedRequestBodyExample = e.target.value;
385388
const exampleDropdownEl = e.target;
386389
window.setTimeout((selectEl) => {
387-
const exampleTextareaEl = selectEl.closest('.example-panel').querySelector(`.request-body-param[data-example="${this.selectedRequestBodyExample}"`);
390+
const exampleTextareaEl = selectEl.closest('.example-panel').querySelector('.request-body-param');
388391
const userInputExampleTextareaEl = selectEl.closest('.example-panel').querySelector('.request-body-param-user-input');
389392
userInputExampleTextareaEl.value = exampleTextareaEl.value;
390393
this.computeCurlSyntax();
@@ -493,6 +496,7 @@ export default class ApiRequest extends LitElement {
493496
@input="${() => { this.computeCurlSyntax(); }}"
494497
class = "textarea request-body-param-user-input"
495498
part = "textarea textarea-param"
499+
aria-label = "${getI18nText('operations.request-body')}"
496500
spellcheck = "false"
497501
data-ptype = "${reqBody.mimeType}"
498502
data-default = "${displayedBodyExample.exampleFormat === 'text' ? displayedBodyExample.exampleValue : JSON.stringify(displayedBodyExample.exampleValue, null, 8)}"
@@ -501,20 +505,19 @@ export default class ApiRequest extends LitElement {
501505
.value="${this.fillRequestWithDefault === 'true' ? (displayedBodyExample.exampleFormat === 'text' ? displayedBodyExample.exampleValue : JSON.stringify(displayedBodyExample.exampleValue, null, 8)) : ''}"
502506
></textarea>
503507
</slot>
508+
509+
<!-- This textarea(hidden) is to store the original example value, this will remain unchanged when users switches from one example to another, its is used to populate the editable textarea -->
510+
<textarea
511+
class = "textarea is-hidden request-body-param ${reqBody.mimeType.substring(reqBody.mimeType.indexOf('/') + 1)}"
512+
spellcheck = "false"
513+
data-ptype = "${reqBody.mimeType}"
514+
style="width:100%; resize:vertical; display:none"
515+
.value="${(displayedBodyExample.exampleFormat === 'text' ? displayedBodyExample.exampleValue : JSON.stringify(displayedBodyExample.exampleValue, null, 8))}"
516+
></textarea>
504517
</div>`
505518
: ''}
506-
${reqBodyExamples.map((bodyExample) => html`
507-
<!-- This textarea(hidden) is to store the original example value, this will remain unchanged when users switches from one example to another, its is used to populate the editable textarea -->
508-
<textarea
509-
class = "textarea is-hidden request-body-param ${reqBody.mimeType.substring(reqBody.mimeType.indexOf('/') + 1)}"
510-
spellcheck = "false"
511-
data-ptype = "${reqBody.mimeType}"
512-
data-example = "${bodyExample.exampleId}"
513-
style="width:100%; resize:vertical; display:none"
514-
.value="${(bodyExample.exampleFormat === 'text' ? bodyExample.exampleValue : JSON.stringify(bodyExample.exampleValue, null, 8))}"
515-
></textarea>
516-
`)}
517-
</div>
519+
520+
</div>
518521
`;
519522
} else if (this.selectedRequestBodyType.includes('form-urlencoded') || this.selectedRequestBodyType.includes('form-data')) {
520523
bodyTabNameUseBody = false;
@@ -583,9 +586,9 @@ export default class ApiRequest extends LitElement {
583586
${reqBodySchemaHtml || reqBodyDefaultHtml
584587
? html`
585588
<div class="tab-panel col" style="border-width:0 0 1px 0;">
586-
<div class="tab-buttons row" @click="${(e) => { if (e.target.tagName.toLowerCase() === 'button') { this.activeSchemaTab = e.target.dataset.tab; } }}">
587-
<button class="tab-btn ${this.activeSchemaTab === 'model' ? 'active' : ''}" data-tab="model" >${getI18nText('operations.model')}</button>
588-
<button class="tab-btn ${this.activeSchemaTab !== 'model' ? 'active' : ''}" data-tab="body">${bodyTabNameUseBody ? getI18nText('operations.body') : getI18nText('operations.form')}</button>
589+
<div class="tab-buttons row" role="group" @click="${(e) => { if (e.target.tagName.toLowerCase() === 'button') { this.activeSchemaTab = e.target.dataset.tab; } }}">
590+
<button class="tab-btn ${this.activeSchemaTab === 'model' ? 'active' : ''}" aria-current="${this.activeSchemaTab === 'model'}" data-tab="model" >${getI18nText('operations.model')}</button>
591+
<button class="tab-btn ${this.activeSchemaTab !== 'model' ? 'active' : ''}" aria-current="${this.activeSchemaTab !== 'model'}" data-tab="body">${bodyTabNameUseBody ? getI18nText('operations.body') : getI18nText('operations.form')}</button>
589592
</div>
590593
${html`<div class="tab-content col" style="display: ${this.activeSchemaTab === 'model' ? 'block' : 'none'}"> ${reqBodySchemaHtml}</div>`}
591594
${html`<div class="tab-content col" style="display: ${this.activeSchemaTab === 'model' ? 'none' : 'block'}"> ${reqBodyDefaultHtml}</div>`}
@@ -621,7 +624,7 @@ export default class ApiRequest extends LitElement {
621624
</div>` : ''
622625
}
623626
<div style="flex:1"></div>
624-
${!hasResponse ? '' : html`<button class="m-btn" part="btn btn-outline" @click="${this.clearResponseData}">CLEAR RESPONSE</button>`}
627+
${!hasResponse ? '' : html`<button class="m-btn" part="btn btn-outline" @click="${this.clearResponseData}">${getI18nText('operations.clear-response')}</button>`}
625628
</div>
626629
<div class="tab-panel col" style="border-width:0 0 1px 0;">
627630
<div id="tab_buttons" class="tab-buttons row" @click="${(e) => {
@@ -630,7 +633,7 @@ export default class ApiRequest extends LitElement {
630633
}}">
631634
<br>
632635
<div style="width: 100%">
633-
<button class="tab-btn ${!hasResponse || this.activeResponseTab === 'curl' ? 'active' : ''}" data-tab = 'curl'>REQUEST</button>
636+
<button class="tab-btn ${!hasResponse || this.activeResponseTab === 'curl' ? 'active' : ''}" data-tab = 'curl'>${getI18nText('operations.request')}</button>
634637
${!hasResponse ? '' : html`
635638
<button class="tab-btn ${this.activeResponseTab === 'response' ? 'active' : ''}" data-tab = 'response'>${getI18nText('operations.response')}</button>
636639
<button class="tab-btn ${this.activeResponseTab === 'headers' ? 'active' : ''}" data-tab = 'headers'>${getI18nText('operations.response-headers')}</button>`

src/components/api-response.js

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -103,8 +103,8 @@ export default class ApiResponse extends LitElement {
103103
render() {
104104
return html`
105105
<div class="col regular-font response-panel ${this.renderStyle}-mode">
106-
<div class=" ${this.callback === 'true' ? 'tiny-title' : 'req-res-title'} ">
107-
${this.callback === 'true' ? 'CALLBACK RESPONSE' : getI18nText('operations.response')}
106+
<div class=" ${this.callback === 'true' ? 'tiny-title' : 'req-res-title'} " role="heading" aria-level="${this.renderStyle === 'focused' ? 3 : 4}">
107+
${this.callback === 'true' ? getI18nText('operations.callback-response') : getI18nText('operations.response')}
108108
</div>
109109
<div>
110110
${this.responseTemplate()}
@@ -158,7 +158,7 @@ export default class ApiResponse extends LitElement {
158158
this.headersForEachRespStatus[statusCode] = tempHeaders;
159159
this.mimeResponsesForEachStatus[statusCode] = allMimeResp;
160160
}
161-
return html`<div class='row' style='flex-wrap:wrap'>
161+
return html`<div class='row' style='flex-wrap:wrap' role="group">
162162
${Object.keys(this.responses).map((respStatus) => html`
163163
${respStatus === '$$ref' // Swagger-Client parser creates '$$ref' object if JSON references are used to create responses - this should be ignored
164164
? ''
@@ -172,6 +172,7 @@ export default class ApiResponse extends LitElement {
172172
this.selectedMimeType = undefined;
173173
}
174174
}}"
175+
aria-current="${this.selectedStatus === respStatus}"
175176
class='m-btn small ${this.selectedStatus === respStatus ? 'primary' : ''}'
176177
part="btn--resp ${this.selectedStatus === respStatus ? 'btn-fill--resp' : 'btn-outline--resp'} btn-response-status"
177178
style='margin: 8px 4px 0 0; text-transform: capitalize'>
@@ -194,9 +195,9 @@ export default class ApiResponse extends LitElement {
194195
? ''
195196
: html`
196197
<div class="tab-panel col">
197-
<div class="tab-buttons row" @click="${(e) => { if (e.target.tagName.toLowerCase() === 'button') { this.activeSchemaTab = e.target.dataset.tab; } }}" >
198-
<button class="tab-btn ${this.activeSchemaTab === 'model' ? 'active' : ''}" data-tab='model'>${getI18nText('operations.model')}</button>
199-
<button class="tab-btn ${this.activeSchemaTab !== 'model' ? 'active' : ''}" data-tab='body'>${getI18nText('operations.example')}</button>
198+
<div class="tab-buttons row" role="group" @click="${(e) => { if (e.target.tagName.toLowerCase() === 'button') { this.activeSchemaTab = e.target.dataset.tab; } }}" >
199+
<button class="tab-btn ${this.activeSchemaTab === 'model' ? 'active' : ''}" aria-current="${this.activeSchemaTab === 'model'}" data-tab='model'>${getI18nText('operations.model')}</button>
200+
<button class="tab-btn ${this.activeSchemaTab !== 'model' ? 'active' : ''}" aria-current="${this.activeSchemaTab !== 'model'}" data-tab='body'>${getI18nText('operations.example')}</button>
200201
<div style="flex:1"></div>
201202
${Object.keys(this.mimeResponsesForEachStatus[status]).length === 1
202203
? html`<span class='small-font-size gray-text' style='align-self:center; margin-top:8px;'> ${Object.keys(this.mimeResponsesForEachStatus[status])[0]} </span>`

src/languages/en.js

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,16 @@
11
export default {
22
translation: {
33
'menu': {
4+
'menu': 'API Menu',
45
'filter': 'Filter',
56
'search': 'Search',
67
'overview': 'Overview',
78
'api-servers': 'API Servers',
89
'authentication': 'Authentication',
910
'operations': 'OPERATIONS',
1011
'components': 'COMPONENTS',
11-
'schemas': 'Schemas'
12+
'schemas': 'Schemas',
13+
'callbacks': 'CALLBACKS'
1214
},
1315
'headers': {
1416
'api-servers': 'API SERVER',
@@ -23,6 +25,7 @@ export default {
2325
'selected': 'SELECTED'
2426
},
2527
'authentication': {
28+
'auth-header': 'Authorization header',
2629
'no-api-key-applied': 'No API key applied',
2730
'http-basic': 'HTTP Basic',
2831
'http-basic-desc': 'Sends the <code>Authorization header</code> containing the token type <code>Basic</code> followed by the base64 encoded <code>username:password</code> string.',
@@ -31,10 +34,15 @@ export default {
3134
'requires': 'Requires',
3235
'http-basic-note': 'Base 64 encoded username:password',
3336
'in-auth-header': 'in Authorization header',
34-
'set': 'SET'
37+
'set': 'SET',
38+
'remove': 'REMOVE',
39+
'clear': 'CLEAR ALL API KEYS',
40+
'update': 'UPDATE',
41+
'get': 'GET TOKEN'
3542
},
3643
'operations': {
3744
'request': 'REQUEST',
45+
'callback-request': 'CALLBACK REQUEST',
3846
'request-body': 'REQUEST BODY',
3947
'model': 'MODEL',
4048
'body': 'BODY',
@@ -46,11 +54,21 @@ export default {
4654
'response': 'RESPONSE',
4755
'response-headers': 'RESPONSE HEADERS',
4856
'example': 'EXAMPLE',
57+
'webhook': 'WEBHOOK',
58+
'deprecated': 'DEPRECATED',
59+
'callback-response': 'CALLBACK RESPONSE',
4960
'response-status': 'Response Status',
5061
'fetch-fail': 'Failed to fetch (Check the browser network tab for more information.)',
5162
'copy': 'Copy',
5263
'copied': 'Copied'
5364
},
65+
'parameters': {
66+
path: 'PATH PARAMETERS',
67+
string: 'QUERY-STRING PARAMETERS',
68+
headers: 'REQUEST HEADERS',
69+
cookies: 'COOKIES',
70+
samples: 'CODE SAMPLES'
71+
},
5472
'schemas': {
5573
'collapse-desc': 'Collapse',
5674
'expand-desc': 'Expand',

src/languages/fr.js

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,16 @@
11
export default {
22
translation: {
33
'menu': {
4+
'menu': "Menu de l'API",
45
'filter': 'Filtre',
56
'search': 'Chercher',
67
'overview': 'Aperçu',
78
'api-servers': 'Serveur API',
89
'authentication': 'Authentification',
910
'operations': 'OPÉRATIONS',
1011
'components': 'COMPOSANTS',
11-
'schemas': 'Schémas'
12+
'schemas': 'Schémas',
13+
'callbacks': 'RAPPELS'
1214
},
1315
'headers': {
1416
'api-servers': 'SERVEUR API',
@@ -23,6 +25,7 @@ export default {
2325
'selected': 'CHOISI'
2426
},
2527
'authentication': {
28+
'auth-header': "l'en-tête Authorization",
2629
'no-api-key-applied': "Aucune clé d'API appliquée",
2730
'http-basic': 'HTTP Basique',
2831
'http-basic-desc': "Envoyez l'en-tête <code>Authorization contenant</code> le type <code>Basic</code> suivi d'un espace et d'une chaîne encodée en base64 de nom <code>d'utilisateur:mot</code> de passe",
@@ -31,10 +34,15 @@ export default {
3134
'requires': 'Nécessite',
3235
'http-basic-note': "un nom d'utilisateur/mot de passe encodé en base64",
3336
'in-auth-header': "dans l'en-tête Authorization",
34-
'set': 'DÉFINIR'
37+
'set': 'DÉFINIR',
38+
'remove': 'RETIRER',
39+
'clear': 'EFFACER TOUTES LES CLÉS API',
40+
'update': 'MISE À JOUR',
41+
'get': 'OBTENIR UN JETON'
3542
},
3643
'operations': {
3744
'request': 'REQUÊTE',
45+
'callback-request': 'REQUÊTE DE RAPPEL',
3846
'request-body': 'CORPS DE LA REQUÊTE',
3947
'model': 'MODÈLE',
4048
'body': 'CORPS',
@@ -44,13 +52,23 @@ export default {
4452
'clear-response': 'VIDER LA RÉPONSE',
4553
'execute': 'EXÉCUTER',
4654
'response': 'RÉPONSE',
55+
'callback-response': 'RÉPONSE DE RAPPEL',
4756
'response-headers': 'EN-TÊTES DE LA RÉPONSE',
4857
'example': 'EXEMPLE',
58+
'webhook': 'WEBHOOK',
59+
'deprecated': 'OBSOLÈTE',
4960
'response-status': 'Statut de réponse',
5061
'fetch-fail': "Échec d'obtenir (Consultez l'onglet Réseau de navigateur pour plus d'information.)",
5162
'copy': 'Copier',
5263
'copied': 'Copié'
5364
},
65+
'parameters': {
66+
path: 'PARAMÈTRES DE CHEMIN',
67+
string: 'PARAMÈTRES DE CHAÎNE DE REQUÊTE',
68+
headers: 'EN-TÊTES DE REQUÊTE',
69+
cookies: 'COOKIES',
70+
samples: 'EXEMPLES DE CODES'
71+
},
5472
'schemas': {
5573
'collapse-desc': 'Réduire',
5674
'expand-desc': 'Agrandir',

src/styles/input-styles.js

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,12 +37,21 @@ export default css`
3737
.m-btn.small { padding:5px 12px; }
3838
.m-btn.tiny { padding:5px 6px; }
3939
.m-btn.circle { border-radius: 50%; }
40-
.m-btn:hover {
40+
41+
.m-btn:hover,
42+
.m-btn:focus-visible,
43+
.m-btn.outline-primary:focus-visible {
44+
color: var(--secondary-color);
45+
border-color: var(--secondary-color);
46+
}
47+
.m-btn.primary:focus-visible {
48+
color: var(--secondary-color);
4149
background-color: var(--primary-color);
42-
color: var(--primary-btn-text-color);
50+
border-color: var(--secondary-color);
4351
}
4452
.m-btn.nav { border: 2px solid var(--secondary-color); }
45-
.m-btn.nav:hover {
53+
.m-btn.nav:hover,
54+
.m-btn.nav:focus-visible {
4655
background-color: var(--secondary-color);
4756
}
4857
.m-btn:disabled{

src/styles/nav-styles.js

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,15 +141,37 @@ export default css`
141141
background-color: var(--nav-hover-bg-color);
142142
}
143143
144+
a:focus-visible,
145+
section .nav-bar-path:focus-visible span {
146+
outline:thin solid var(--secondary-color);
147+
}
148+
section .nav-bar-path:focus-visible span {
149+
outline-offset: 2px;
150+
}
151+
152+
.nav-bar-h1:focus-visible,
153+
.nav-bar-h2:focus-visible,
154+
.nav-bar-info:focus-visible, slot[name=nav-section]::slotted(*:focus-visible),
155+
.nav-bar-tag:focus-visible,
156+
.nav-bar-path:focus-visible,
144157
.nav-bar-h1:hover,
145158
.nav-bar-h2:hover,
146159
.nav-bar-info:hover, slot[name=nav-section]::slotted(*:hover),
147160
.nav-bar-tag:hover,
148161
.nav-bar-path:hover {
162+
outline: none;
149163
color:var(--nav-hover-text-color);
150164
background-color:var(--nav-hover-bg-color);
151165
}
152166
167+
.nav-bar-h1.active:focus-visible,
168+
.nav-bar-h2.active:focus-visible,
169+
.nav-bar-info.active:focus-visible, slot[name=nav-section]::slotted(*.active:focus-visible),
170+
.nav-bar-tag.active:focus-visible,
171+
.nav-bar-path.active:focus-visible {
172+
outline:thin solid var(--secondary-color);
173+
}
174+
153175
.conditional-custom-section.custom-section::slotted(*) {
154176
display: none;
155177
}

0 commit comments

Comments
 (0)