From 95f9fac6c4204ffe337eff7c02c475bcdeecf8b2 Mon Sep 17 00:00:00 2001 From: Jason Stirnaman Date: Mon, 16 Mar 2026 19:29:35 -0500 Subject: [PATCH] fix(ui): add keyboard accessibility to code controls menu MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace clickable and
  • elements with proper + `; @@ -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 diff --git a/assets/styles/layouts/_code-controls.scss b/assets/styles/layouts/_code-controls.scss index 6d0e5a2d1..68353bed5 100644 --- a/assets/styles/layouts/_code-controls.scss +++ b/assets/styles/layouts/_code-controls.scss @@ -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 { diff --git a/cypress/e2e/content/code-controls.cy.js b/cypress/e2e/content/code-controls.cy.js index 83ef7d4e5..0480e29a5 100644 --- a/cypress/e2e/content/code-controls.cy.js +++ b/cypress/e2e/content/code-controls.cy.js @@ -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'); }); });