fix(ui): add keyboard accessibility to code controls menu
Replace clickable <span> and <li> elements with proper <button> elements and ARIA roles for keyboard and assistive-technology access: - Toggle: <span> → <button> with aria-label and aria-expanded - Menu items: <li> → <li role="none"><button role="menuitem"> - Menu: <ul> gets role="menu" - SCSS: add :focus-visible styles, reset button defaults - JS: toggle handler updates aria-expanded on open/close Also fix tests to match actual behavior: - Remove bogus "close other menus" test that manually closed first menu before opening second (tested nothing) - Remove duplicate "close on re-click" test (same as outside-click) - Add "keep menu open when copy is clicked" (stopPropagation behavior) - Add accessibility markup validation testpull/6955/head
parent
60e29554dd
commit
95f9fac6c4
|
|
@ -7,11 +7,11 @@ function initialize() {
|
|||
|
||||
var appendHTML = `
|
||||
<div class="code-controls">
|
||||
<span class="code-controls-toggle"><span class='cf-icon More'></span></span>
|
||||
<ul class="code-control-options">
|
||||
<li class='copy-code'><span class='cf-icon Duplicate_New'></span> <span class="message">Copy</span></li>
|
||||
<li class='ask-ai-code'><span class='cf-icon Chat'></span> Ask AI</li>
|
||||
<li class='fullscreen-toggle'><span class='cf-icon ExpandB'></span> Fill window</li>
|
||||
<button class="code-controls-toggle" aria-label="Code block options" aria-expanded="false"><span class='cf-icon More'></span></button>
|
||||
<ul class="code-control-options" role="menu">
|
||||
<li role="none"><button role="menuitem" class='copy-code'><span class='cf-icon Duplicate_New'></span> <span class="message">Copy</span></button></li>
|
||||
<li role="none"><button role="menuitem" class='ask-ai-code'><span class='cf-icon Chat'></span> Ask AI</button></li>
|
||||
<li role="none"><button role="menuitem" class='fullscreen-toggle'><span class='cf-icon ExpandB'></span> Fill window</button></li>
|
||||
</ul>
|
||||
</div>
|
||||
`;
|
||||
|
|
@ -28,12 +28,17 @@ function initialize() {
|
|||
|
||||
// Click outside of the code-controls to close them
|
||||
$(document).click(function () {
|
||||
$('.code-controls').removeClass('open');
|
||||
$('.code-controls.open').each(function () {
|
||||
$(this).removeClass('open');
|
||||
$(this).find('.code-controls-toggle').attr('aria-expanded', 'false');
|
||||
});
|
||||
});
|
||||
|
||||
// Click the code controls toggle to open code controls
|
||||
$('.code-controls-toggle').click(function () {
|
||||
$(this).parent('.code-controls').toggleClass('open');
|
||||
var $controls = $(this).parent('.code-controls');
|
||||
var isOpen = $controls.toggleClass('open').hasClass('open');
|
||||
$(this).attr('aria-expanded', String(isOpen));
|
||||
});
|
||||
|
||||
// Stop event propagation for clicks inside of the code-controls div
|
||||
|
|
|
|||
|
|
@ -16,10 +16,12 @@
|
|||
opacity: .5;
|
||||
transition: opacity .2s;
|
||||
border-radius: $radius;
|
||||
border: none;
|
||||
background: none;
|
||||
line-height: 0;
|
||||
cursor: pointer;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
&:hover, &:focus-visible {
|
||||
opacity: 1;
|
||||
background-color: rgba($article-text, .1);
|
||||
backdrop-filter: blur(15px);
|
||||
|
|
@ -35,16 +37,23 @@
|
|||
backdrop-filter: blur(15px);
|
||||
display: none;
|
||||
|
||||
li {
|
||||
button {
|
||||
display: block;
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
margin: 0;
|
||||
padding: .4rem .5rem .6rem;
|
||||
border: none;
|
||||
background: none;
|
||||
border-radius: $radius;
|
||||
color: $article-bold;
|
||||
font-size: .87rem;
|
||||
line-height: 0;
|
||||
cursor: pointer;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {background-color: rgba($article-text, .07)}
|
||||
&:hover, &:focus-visible {
|
||||
background-color: rgba($article-text, .07);
|
||||
}
|
||||
|
||||
.cf-icon {margin-right: .35rem;}
|
||||
|
||||
|
|
@ -67,6 +76,8 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
li {margin: 0;}
|
||||
}
|
||||
|
||||
&.open {
|
||||
|
|
|
|||
|
|
@ -19,13 +19,13 @@
|
|||
* - [x] Controls contain toggle, copy, Ask AI, and fullscreen items
|
||||
* - [x] Menu is hidden by default (toggle visible)
|
||||
* - [x] Ask AI is in the middle (2nd) position
|
||||
* - [x] Controls use accessible markup (buttons, ARIA roles)
|
||||
*
|
||||
* Toggle Behavior:
|
||||
* ----------------
|
||||
* - [x] Clicking toggle opens menu (adds .open class)
|
||||
* - [x] Clicking outside after opening with toggle closes menu
|
||||
* - [x] Clicking outside closes menu
|
||||
* - [x] Only one menu open at a time
|
||||
* - [x] Copy button keeps menu open (stopPropagation)
|
||||
*
|
||||
* Copy to Clipboard:
|
||||
* ------------------
|
||||
|
|
@ -83,18 +83,38 @@ describe('Code Controls', function () {
|
|||
cy.get('.article--content .codeblock')
|
||||
.first()
|
||||
.within(() => {
|
||||
cy.get('.code-control-options li')
|
||||
cy.get('.code-control-options button[role="menuitem"]')
|
||||
.eq(0)
|
||||
.should('have.class', 'copy-code');
|
||||
cy.get('.code-control-options li')
|
||||
cy.get('.code-control-options button[role="menuitem"]')
|
||||
.eq(1)
|
||||
.should('have.class', 'ask-ai-code');
|
||||
cy.get('.code-control-options li')
|
||||
cy.get('.code-control-options button[role="menuitem"]')
|
||||
.eq(2)
|
||||
.should('have.class', 'fullscreen-toggle');
|
||||
});
|
||||
});
|
||||
|
||||
it('should use accessible markup for controls', function () {
|
||||
cy.get('.article--content .codeblock')
|
||||
.first()
|
||||
.within(() => {
|
||||
// Toggle is a button with aria attributes
|
||||
cy.get('.code-controls-toggle')
|
||||
.should('have.attr', 'aria-label', 'Code block options')
|
||||
.and('have.attr', 'aria-expanded', 'false');
|
||||
|
||||
// Menu has role="menu"
|
||||
cy.get('.code-control-options').should('have.attr', 'role', 'menu');
|
||||
|
||||
// Menu items are buttons with role="menuitem"
|
||||
cy.get('.code-control-options button[role="menuitem"]').should(
|
||||
'have.length',
|
||||
3
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('should show toggle and hide menu by default', function () {
|
||||
cy.get('.article--content .code-controls')
|
||||
.first()
|
||||
|
|
@ -121,19 +141,6 @@ describe('Code Controls', function () {
|
|||
.should('be.visible');
|
||||
});
|
||||
|
||||
it('should close menu when clicking outside after opening with toggle', function () {
|
||||
cy.get('.article--content .code-controls-toggle').first().click();
|
||||
cy.get('.article--content .code-controls')
|
||||
.first()
|
||||
.should('have.class', 'open');
|
||||
|
||||
// Click outside to close (document click handler removes .open)
|
||||
cy.get('.article--content h2').first().click({ force: true });
|
||||
cy.get('.article--content .code-controls')
|
||||
.first()
|
||||
.should('not.have.class', 'open');
|
||||
});
|
||||
|
||||
it('should close menu when clicking outside', function () {
|
||||
cy.get('.article--content .code-controls-toggle').first().click();
|
||||
cy.get('.article--content .code-controls')
|
||||
|
|
@ -147,32 +154,17 @@ describe('Code Controls', function () {
|
|||
.should('not.have.class', 'open');
|
||||
});
|
||||
|
||||
it('should close other menus when a new toggle is clicked', function () {
|
||||
// Need at least two code blocks
|
||||
cy.get('.article--content .codeblock').should('have.length.at.least', 2);
|
||||
|
||||
// Open first menu
|
||||
cy.get('.article--content .code-controls-toggle').eq(0).click();
|
||||
it('should keep menu open when copy is clicked', function () {
|
||||
cy.get('.article--content .code-controls-toggle').first().click();
|
||||
cy.get('.article--content .code-controls')
|
||||
.eq(0)
|
||||
.first()
|
||||
.should('have.class', 'open');
|
||||
|
||||
// Click outside to close first (since toggle is hidden when open)
|
||||
cy.get('.article--content h2').first().click({ force: true });
|
||||
// Click copy — menu should stay open (stopPropagation)
|
||||
cy.get('.article--content .copy-code').first().click();
|
||||
cy.get('.article--content .code-controls')
|
||||
.eq(0)
|
||||
.should('not.have.class', 'open');
|
||||
|
||||
// Open second menu
|
||||
cy.get('.article--content .code-controls-toggle').eq(1).click();
|
||||
cy.get('.article--content .code-controls')
|
||||
.eq(1)
|
||||
.first()
|
||||
.should('have.class', 'open');
|
||||
|
||||
// First should remain closed
|
||||
cy.get('.article--content .code-controls')
|
||||
.eq(0)
|
||||
.should('not.have.class', 'open');
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue