Skip to content

Commit aaa87f0

Browse files
committed
use tab roles for tabs
1 parent 241dc32 commit aaa87f0

3 files changed

Lines changed: 116 additions & 26 deletions

File tree

src/components/api-request.js

Lines changed: 28 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -575,7 +575,7 @@ export default class ApiRequest extends LitElement {
575575

576576
return html`
577577
<div class='request-body-container' data-selected-request-body-type="${this.selectedRequestBodyType}">
578-
<div class="table-title top-gap row">
578+
<div class="table-title top-gap row" role="heading" aria-level="${this.renderStyle === 'focused' ? 4 : 5}" id="request-body-header">
579579
${getI18nText('operations.request-body')} ${this.request_body.required ? html`<span class="mono-font" style='color:var(--red)'>*</span>` : ''}
580580
<span style = "font-weight:normal; margin-left:5px"> ${this.selectedRequestBodyType}</span>
581581
<span style="flex:1"></span>
@@ -586,12 +586,34 @@ export default class ApiRequest extends LitElement {
586586
${reqBodySchemaHtml || reqBodyDefaultHtml
587587
? html`
588588
<div class="tab-panel col" style="border-width:0 0 1px 0;">
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>
589+
<div class="tab-buttons row" role="tablist" aria-labelledby="request-body-header" @click="${(e) => { if (e.target.tagName.toLowerCase() === 'button') { this.activeSchemaTab = e.target.dataset.tab; } }}" @keydown="${(e) => {
590+
const b = e.target;
591+
if (b.tagName.toLowerCase() !== 'button') {return;}
592+
const i = Array.from(b.parentNode.children).indexOf(b);
593+
let newIndex = 0;
594+
switch (e.key) {
595+
case 'ArrowRight':
596+
newIndex = (i + 1) % 2;
597+
break;
598+
case 'ArrowLeft':
599+
newIndex = (i - 1 + 2) % 2;
600+
break;
601+
case 'Home':
602+
newIndex = 0;
603+
break;
604+
case 'End':
605+
newIndex = 1;
606+
break;
607+
default:
608+
return;
609+
}
610+
e.target.parentElement.children[newIndex].focus();
611+
}}">
612+
<button class="tab-btn ${this.activeSchemaTab === 'model' ? 'active' : ''}" id="schema-model-button" aria-controls="schema-model-body" aria-selected="${this.activeSchemaTab === 'model'}" tabindex="${this.activeSchemaTab === 'model' ? 0 : '-1'}" data-tab="model" >${getI18nText('operations.model')}</button>
613+
<button class="tab-btn ${this.activeSchemaTab !== 'model' ? 'active' : ''}" id="schema-body-button" aria-controls="schema-body-body" aria-selected="${this.activeSchemaTab !== 'model'}" tabindex="${this.activeSchemaTab !== 'model' ? 0 : '-1'}" data-tab="body">${bodyTabNameUseBody ? getI18nText('operations.body') : getI18nText('operations.form')}</button>
592614
</div>
593-
${html`<div class="tab-content col" style="display: ${this.activeSchemaTab === 'model' ? 'block' : 'none'}"> ${reqBodySchemaHtml}</div>`}
594-
${html`<div class="tab-content col" style="display: ${this.activeSchemaTab === 'model' ? 'none' : 'block'}"> ${reqBodyDefaultHtml}</div>`}
615+
${html`<div class="tab-content col" id="schema-model-body" tabindex="0" aria-labelledby="schema-model-button" role="tabpanel" style="display: ${this.activeSchemaTab === 'model' ? 'block' : 'none'}"> ${reqBodySchemaHtml}</div>`}
616+
${html`<div class="tab-content col" id="schema-body-body" tabindex="0" aria-labelledby="schema-body-button" role="tabpanel" style="display: ${this.activeSchemaTab === 'model' ? 'none' : 'block'}"> ${reqBodyDefaultHtml}</div>`}
595617
</div>`
596618
: html`${reqBodyFileInputHtml}`
597619
}

src/components/api-response.js

Lines changed: 61 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@ 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'} " role="heading" aria-level="${this.renderStyle === 'focused' ? 3 : 4}">
106+
<div class=" ${this.callback === 'true' ? 'tiny-title' : 'req-res-title'} " role="heading" aria-level="${this.renderStyle === 'focused' ? 3 : 4}" id="response-title">
107107
${this.callback === 'true' ? getI18nText('operations.callback-response') : getI18nText('operations.response')}
108108
</div>
109109
<div>
@@ -159,8 +159,8 @@ export default class ApiResponse extends LitElement {
159159
this.headersForEachRespStatus[statusCode] = tempHeaders;
160160
this.mimeResponsesForEachStatus[statusCode] = allMimeResp;
161161
}
162-
return html`<div class='row' style='flex-wrap:wrap' role="group">
163-
${Object.keys(this.responses).map((respStatus) => html`
162+
return html`<div class='row' style='flex-wrap:wrap' role="tablist" aria-labelledby="response-title">
163+
${Object.keys(this.responses).map((respStatus, i) => html`
164164
${respStatus === '$$ref' // Swagger-Client parser creates '$$ref' object if JSON references are used to create responses - this should be ignored
165165
? ''
166166
: html`
@@ -173,7 +173,32 @@ export default class ApiResponse extends LitElement {
173173
this.selectedMimeType = undefined;
174174
}
175175
}}"
176-
aria-current="${this.selectedStatus === respStatus}"
176+
@keydown="${(e) => {
177+
const keys = Object.keys(this.responses);
178+
let newIndex = 0;
179+
switch (e.key) {
180+
case 'ArrowRight':
181+
newIndex = (i + 1) % keys.length;
182+
break;
183+
case 'ArrowLeft':
184+
newIndex = (i - 1 + keys.length) % keys.length;
185+
break;
186+
case 'Home':
187+
newIndex = 0;
188+
break;
189+
case 'End':
190+
newIndex = keys.length - 1;
191+
break;
192+
default:
193+
return;
194+
}
195+
this.shadowRoot.getElementById(`button${keys[newIndex]}`).focus();
196+
}}"
197+
role="tab"
198+
aria-selected="${this.selectedStatus === respStatus}"
199+
aria-controls="status${respStatus}"
200+
tabindex="${this.selectedStatus === respStatus ? '0' : '-1'}"
201+
id="button${respStatus}"
177202
class='m-btn small ${this.selectedStatus === respStatus ? 'primary' : ''}'
178203
part="btn--resp ${this.selectedStatus === respStatus ? 'btn-fill--resp' : 'btn-outline--resp'} btn-response-status"
179204
style='margin: 8px 4px 0 0; text-transform: capitalize'>
@@ -184,7 +209,7 @@ export default class ApiResponse extends LitElement {
184209
</div>
185210
186211
${Object.keys(this.responses).map((status) => html`
187-
<div style = 'display: ${status === this.selectedStatus ? 'block' : 'none'}' >
212+
<div id="status${status}" role="tabpanel" aria-labelledby="button${status}" tabindex="0" style="display: ${status === this.selectedStatus ? 'block' : 'none'}">
188213
<div class="top-gap">
189214
<span class="resp-descr m-markdown ">${unsafeHTML(toMarkdown(this.responses[status] && this.responses[status].description || ''))}</span>
190215
${(this.headersForEachRespStatus[status] && this.headersForEachRespStatus[status].length > 0)
@@ -196,23 +221,43 @@ export default class ApiResponse extends LitElement {
196221
? ''
197222
: html`
198223
<div class="tab-panel col">
199-
<div class="tab-buttons row" role="group" @click="${(e) => { if (e.target.tagName.toLowerCase() === 'button') { this.activeSchemaTab = e.target.dataset.tab; } }}" >
200-
<button class="tab-btn ${this.activeSchemaTab === 'model' ? 'active' : ''}" aria-current="${this.activeSchemaTab === 'model'}" data-tab='model'>${getI18nText('operations.model')}</button>
201-
<button class="tab-btn ${this.activeSchemaTab !== 'model' ? 'active' : ''}" aria-current="${this.activeSchemaTab !== 'model'}" data-tab='body'>${getI18nText('operations.example')}</button>
224+
<div class="tab-buttons row" role="ta" @click="${(e) => { if (e.target.tagName.toLowerCase() === 'button') { this.activeSchemaTab = e.target.dataset.tab; } }}" @keydown="${(e) => {
225+
const b = e.target;
226+
if (b.tagName.toLowerCase() !== 'button') {return;}
227+
const i = Array.from(b.parentNode.children).indexOf(b);
228+
let newIndex = 0;
229+
switch (e.key) {
230+
case 'ArrowRight':
231+
newIndex = (i + 1) % 2;
232+
break;
233+
case 'ArrowLeft':
234+
newIndex = (i - 1 + 2) % 2;
235+
break;
236+
case 'Home':
237+
newIndex = 0;
238+
break;
239+
case 'End':
240+
newIndex = 1;
241+
break;
242+
default:
243+
return;
244+
}
245+
e.target.parentElement.children[newIndex].focus();
246+
}}">
247+
<button class="tab-btn ${this.activeSchemaTab === 'model' ? 'active' : ''}" id="resp-model-button" aria-controls="resp-model-body" aria-selected="${this.activeSchemaTab === 'model'}" tabindex="${this.activeSchemaTab === 'model' ? 0 : '-1'}" data-tab='model'>${getI18nText('operations.model')}</button>
248+
<button class="tab-btn ${this.activeSchemaTab !== 'model' ? 'active' : ''}" id="resp-body-button" aria-controls="resp-body-body" aria-selected="${this.activeSchemaTab !== 'model'}" tabindex="${this.activeSchemaTab !== 'model' ? 0 : '-1'}" data-tab='body'>${getI18nText('operations.example')}</button>
202249
<div style="flex:1"></div>
203250
${Object.keys(this.mimeResponsesForEachStatus[status]).length === 1
204251
? html`<span class='small-font-size gray-text' style='align-self:center; margin-top:8px;'> ${Object.keys(this.mimeResponsesForEachStatus[status])[0]} </span>`
205252
: html`${this.mimeTypeDropdownTemplate(Object.keys(this.mimeResponsesForEachStatus[status]))}`
206253
}
207254
</div>
208-
${this.activeSchemaTab === 'body'
209-
? html`<div class='tab-content col' style='flex:1;'>
210-
${this.mimeExampleTemplate(this.mimeResponsesForEachStatus[status][this.selectedMimeType])}
211-
</div>`
212-
: html`<div class='tab-content col' style='flex:1;'>
213-
${this.mimeSchemaTemplate(this.mimeResponsesForEachStatus[status][this.selectedMimeType])}
214-
</div>`
215-
}
255+
${html`<div class='tab-content col' role="tabpanel" tabindex="0" id='resp-body-body' aria-labelledby='resp-body-button' style='flex:1; display: ${this.activeSchemaTab === 'body' ? 'block' : 'none'}'>
256+
${this.mimeExampleTemplate(this.mimeResponsesForEachStatus[status][this.selectedMimeType])}}
257+
</div>`}
258+
${html`<div class='tab-content col' role="tabpanel" tabindex="0" id='resp-model-body' style='flex:1; display: ${this.activeSchemaTab === 'body' ? 'none' : 'block'}'>
259+
${this.mimeSchemaTemplate(this.mimeResponsesForEachStatus[status][this.selectedMimeType])}
260+
</div>`}
216261
</div>
217262
`
218263
}`)

src/templates/code-samples-template.js

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,17 +17,40 @@ export default function codeSamplesTemplate(xCodeSamples) {
1717
tabContents.forEach((tabBodyEl) => { tabBodyEl.style.display = (tabBodyEl.dataset.tab === clickedTab ? 'block' : 'none'); });
1818
}
1919
}">
20-
<div class="tab-buttons row" role="group" style="width:100; overflow">
21-
${xCodeSamples.map((v, i) => html`<button class="tab-btn ${i === 0 ? 'active' : ''}" aria-current='${i === 0}' data-tab = '${v.lang}${i}'> ${v.label || v.lang} </button>`)}
20+
<div class="tab-buttons row" role="tablist" style="width:100; overflow">
21+
${xCodeSamples.map((v, i) => html`<button class="tab-btn ${i === 0 ? 'active' : ''}" role="tab" id="codesample${i}-button" aria-controls="codesample${i}" aria-selected='${i === 0}' tabindex="${i === 0 ? 0 : '-1'}'" data-tab = '${v.lang}${i}' @keydown="${(e) => {
22+
const samps = xCodeSamples;
23+
let newIndex = 0;
24+
switch (e.key) {
25+
case 'ArrowRight':
26+
newIndex = (i + 1) % samps.length;
27+
break;
28+
case 'ArrowLeft':
29+
newIndex = (i - 1 + samps.length) % samps.length;
30+
break;
31+
case 'Home':
32+
newIndex = 0;
33+
break;
34+
case 'End':
35+
newIndex = samps.length - 1;
36+
break;
37+
default:
38+
return;
39+
}
40+
const button = this.shadowRoot.getElementById(`codesample${newIndex}-button`);
41+
Array.from(button.parentElement.children).forEach((b) => b.tabIndex = '-1');
42+
button.tabIndex = 0;
43+
button.focus();
44+
}}"> ${v.label || v.lang} </button>`)}
2245
</div>
2346
${xCodeSamples.map((v, i) => {
2447
// We skip the first line because it could be there is no padding there, but padding on the next lines which needs to be removed
2548
const paddingToRemove = Math.min(...v.source.split('\n').slice(1).map(l => l.match(/^(\s*).*$/m)?.[1].length).filter(l => typeof l !== 'undefined'));
2649
const sanitizedSource = v.source.split('\n').map(s => s.substring(0, paddingToRemove).match(/^\s+$/) ? s.substring(paddingToRemove) : s);
2750
const fullSource = sanitizedSource.join('\n');
2851
return html`
29-
<div class="tab-content m-markdown code-sample-wrapper" style= "display:${i === 0 ? 'block' : 'none'}" data-tab = '${v.lang}${i}'>
30-
<syntax-highlighter language="${v.lang}" .content="${fullSource}"/>
52+
<div class="tab-content m-markdown code-sample-wrapper" id="codesample${i}" role="tabpanel" aria-labelledby="codesample${i}-button" tabindex="0" style= "display:${i === 0 ? 'block' : 'none'}" data-tab = '${v.lang}${i}'>
53+
<syntax-highlighter language="${v.lang}" .content="${fullSource}" aria-label="Code sample"/>
3154
</div>`;
3255
})
3356
}

0 commit comments

Comments
 (0)