diff --git a/assets/js/code-controls.js b/assets/js/code-controls.js
index 2a49bee1e..d8ce5a9f9 100644
--- a/assets/js/code-controls.js
+++ b/assets/js/code-controls.js
@@ -7,11 +7,11 @@ function initialize() {
var appendHTML = `
-
-
- - Copy
- - Ask AI
- - Fill window
+
+
+
+
+
`;
@@ -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');
});
});