Skip to content

fix(checkbox): safari keyboard navigation#3328

Merged
rivka-ungar merged 4 commits intomasterfrom
fix/checkbox-safari-keyboard-navigation
Mar 11, 2026
Merged

fix(checkbox): safari keyboard navigation#3328
rivka-ungar merged 4 commits intomasterfrom
fix/checkbox-safari-keyboard-navigation

Conversation

@rivka-ungar
Copy link
Copy Markdown
Contributor

@rivka-ungar rivka-ungar commented Mar 11, 2026

User description

https://monday.monday.com/boards/3532714909/views/113184182/pulses/11477130597


PR Type

Bug fix


Description

  • Fix Checkbox not focusable via Tab in Safari by using visually-hidden pattern

  • Move focus ring rendering from input to wrapper label element

  • Add keyboard Space key handler for wrapper-based checkbox activation

  • Set wrapper tabIndex to 0 by default, input to -1 to avoid duplicate Tab stops


Diagram Walkthrough

flowchart LR
  A["Input element<br/>tabIndex=-1"] -- "hidden from Tab order" --> B["Wrapper label<br/>tabIndex=0"]
  B -- "receives focus" --> C["Focus styles applied<br/>via focus-visible"]
  B -- "Space key pressed" --> D["onKeyDownCallback<br/>triggers click"]
  D --> A
Loading

File Walkthrough

Relevant files
Bug fix
Checkbox.module.scss
Update focus styles for Safari keyboard navigation             

packages/core/src/components/Checkbox/Checkbox.module.scss

  • Replace hardcoded focus-visible styles with @include
    focus-style-css(2px) mixin
  • Add focus-visible styles for wrapper label in both separate and
    non-separate modes
  • Add focus-visible styles for checkbox label element in separate mode
  • Add outline: none comment to wrapper to clarify focus ring location
+23/-5   
Checkbox.tsx
Implement Safari-compatible keyboard navigation for Checkbox

packages/core/src/components/Checkbox/Checkbox.tsx

  • Remove iconContainerRef and simplify onMouseUpCallback to use event
    target
  • Add onKeyDownCallback to handle Space key for checkbox activation
  • Set wrapper tabIndex to 0 by default (or custom value), disabled when
    component is disabled
  • Set input tabIndex to -1 to prevent duplicate Tab stops
  • Add keyboard event handler and tabIndex to both separate and
    non-separate label modes
+23/-10 
Tests
Checkbox.test.tsx
Add keyboard navigation tests for Safari compatibility     

packages/core/src/components/Checkbox/tests/Checkbox.test.tsx

  • Add test suite for keyboard navigation and Safari compatibility
  • Test wrapper has tabIndex 0 by default
  • Test hidden input has tabIndex -1
  • Test custom tabIndex is respected on wrapper
  • Test disabled checkbox has no tabIndex
  • Test Space key toggles checkbox when pressed on wrapper
+38/-0   

Use visually-hidden pattern (1px+clip) instead of width/height:0, and
default tabIndex to 0 so Safari includes the input in keyboard Tab order.
@rivka-ungar rivka-ungar requested a review from a team as a code owner March 11, 2026 03:29
@qodo-free-for-open-source-projects
Copy link
Copy Markdown
Contributor

Review Summary by Qodo

Fix Checkbox Safari keyboard navigation and focus management

🐞 Bug fix

Grey Divider

Walkthroughs

Description
• Fix Safari keyboard navigation by making wrapper/label the Tab target
• Replace hidden input pattern with visually-hidden for proper focus handling
• Add Space key handler to toggle checkbox when wrapper receives focus
• Update focus styles to work with both wrapper and checkbox focus states
Diagram
flowchart LR
  A["Safari Tab Order Issue"] -->|"Make wrapper Tab target"| B["Set wrapper tabIndex=0"]
  A -->|"Remove input from Tab order"| C["Set input tabIndex=-1"]
  B -->|"Handle Space key"| D["Add onKeyDown handler"]
  D -->|"Toggle checkbox"| E["Call input.click()"]
  F["Focus styles"] -->|"Update for wrapper focus"| G["Add wrapper:focus-visible rules"]
  F -->|"Update for input focus"| H["Add input:focus-visible rules"]
Loading

Grey Divider

File Changes

1. packages/core/src/components/Checkbox/Checkbox.module.scss 🐞 Bug fix +35/-5

Update focus styles for wrapper and checkbox elements

• Add outline: none to .wrapper to prevent double focus rings
• Replace hardcoded focus styles with @include focus-style-css(2px) mixin
• Add new focus-visible rules for .wrapper element (non-separate mode)
• Add new focus-visible rules for .checkbox label (separate mode)
• Add comments explaining Tab navigation behavior for both modes

packages/core/src/components/Checkbox/Checkbox.module.scss


2. packages/core/src/components/Checkbox/Checkbox.tsx 🐞 Bug fix +32/-10

Implement wrapper-based Tab navigation and Space key handling

• Remove iconContainerRef which is no longer needed
• Update onMouseUpCallback to blur the event target instead of input
• Add onKeyDownCallback to handle Space key presses on wrapper/label
• Set input tabIndex={-1} to remove from Tab order in both modes
• Add wrapperTabIndex computed value (0 by default, undefined when disabled)
• Apply tabIndex and keyboard handlers to wrapper/label elements
• Add comments explaining Safari Tab order fix and focus management

packages/core/src/components/Checkbox/Checkbox.tsx


3. packages/core/src/components/Checkbox/__tests__/Checkbox.test.tsx 🧪 Tests +38/-0

Add keyboard navigation tests for Safari compatibility

• Add new test suite "keyboard navigation (Safari compatibility)"
• Test wrapper has tabIndex=0 by default for Safari Tab order
• Test hidden input has tabIndex=-1 to avoid duplicate Tab stops
• Test custom tabIndex prop is applied to wrapper
• Test disabled checkbox has no tabIndex attribute
• Test Space key press on wrapper toggles checkbox via onChange callback

packages/core/src/components/Checkbox/tests/Checkbox.test.tsx


Grey Divider

Qodo Logo

@qodo-free-for-open-source-projects
Copy link
Copy Markdown
Contributor

qodo-free-for-open-source-projects bot commented Mar 11, 2026

Code Review by Qodo

🐞 Bugs (4) 📘 Rule violations (1) 📎 Requirement gaps (0)

Grey Divider


Action required

1. Label focus lacks semantics 🐞 Bug ✓ Correctness
Description
Checkbox moves Tab focus to a <label> (tabIndex=0 by default) and removes the native checkbox input
from the Tab order (tabIndex=-1). The focused element has no checkbox semantics (role/aria-checked),
so assistive tech won’t announce a checkbox or its checked state for keyboard navigation.
Code

packages/core/src/components/Checkbox/Checkbox.tsx[R209-217]

     // eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions
     <label
       className={cx(styles.wrapper, className)}
+        tabIndex={wrapperTabIndex}
       onMouseUp={onMouseUpCallback}
+        onKeyDown={onKeyDownCallback}
       data-testid={dataTestId || getTestId(ComponentDefaultTestId.CHECKBOX, id)}
       htmlFor={id}
       onClickCapture={onClickCaptureLabel}
Evidence
The component makes the wrapper <label> keyboard-focusable and handles Space on it, while the actual
<input type="checkbox"> is explicitly removed from sequential focus navigation; however, the
focusable label has no role/state attributes, so semantics are no longer attached to the focused
element.

packages/core/src/components/Checkbox/Checkbox.tsx[136-147]
packages/core/src/components/Checkbox/Checkbox.tsx[208-235]
packages/core/src/components/Checkbox/Checkbox.module.scss[96-114]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Checkbox now places keyboard focus on a `&amp;lt;label&amp;gt;` while the real checkbox `&amp;lt;input&amp;gt;` is removed from the Tab order. The focused element does not expose checkbox semantics (role/state), which breaks assistive-technology announcements for keyboard users.
### Issue Context
- `tabIndex` is moved from the `&amp;lt;input type=&amp;quot;checkbox&amp;quot;&amp;gt;` to a wrapper/label and Space key is manually handled.
- CSS focus styling is updated to target the wrapper/label.
### Fix Focus Areas
- packages/core/src/components/Checkbox/Checkbox.tsx[136-147]
- packages/core/src/components/Checkbox/Checkbox.tsx[148-189]
- packages/core/src/components/Checkbox/Checkbox.tsx[208-259]
- packages/core/src/components/Checkbox/Checkbox.module.scss[70-114]
- packages/style/src/mixins/_common.scss[9-14]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Icon label has no name 🐞 Bug ✓ Correctness
Description
In separateLabel mode, the tabbable element is the icon-only `<label
className={styles.checkbox}>, and its only child is ariaHidden`, so the focused control has no
accessible name. Keyboard/screen-reader users will land on an unnamed focus target.
Code

packages/core/src/components/Checkbox/Checkbox.tsx[R166-181]

           aria-label={finalAriaLabel}
           aria-labelledby={ariaLabelledBy}
           checked={checked}
-            tabIndex={tabIndex}
+            tabIndex={-1}
         />
         {/* eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions */}
         <label
           htmlFor={id}
           className={cx(styles.checkbox, checkboxClassName)}
           data-testid={getTestId(ComponentDefaultTestId.CHECKBOX_CHECKBOX, id)}
+            tabIndex={wrapperTabIndex}
           onMouseUp={onMouseUpCallback}
+            onKeyDown={onKeyDownCallback}
           onClickCapture={onClickCaptureLabel}
         >
           <Icon
Evidence
When separateLabel is enabled, the element receiving focus (tabIndex applied) is the checkbox icon
label. That label contains only an aria-hidden icon and has no aria-label/aria-labelledby, so it
cannot provide an accessible name while focused.

packages/core/src/components/Checkbox/Checkbox.tsx[148-189]
packages/core/src/components/Checkbox/Checkbox.tsx[130-134]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
In `separateLabel` mode the focusable element is an icon-only `&amp;lt;label&amp;gt;` whose contents are `ariaHidden`, so it has no accessible name when focused.
### Issue Context
This is introduced by moving tab focus from the hidden input to the label.
### Fix Focus Areas
- packages/core/src/components/Checkbox/Checkbox.tsx[148-203]
- packages/core/src/components/Checkbox/Checkbox.module.scss[96-114]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. Ad-hoc data-testid used 📘 Rule violation ✓ Correctness
Description
New Checkbox keyboard navigation tests use a hard-coded data-testid value (cb) instead of the
repository’s constants-based test ID pattern. This can reduce consistency and increase maintenance
overhead across the test suite.
Code

packages/core/src/components/Checkbox/tests/Checkbox.test.tsx[R221-250]

+    it("should have tabIndex 0 on the wrapper label by default so Safari includes it in Tab order", () => {
+      const { getByTestId } = render(<Checkbox label="Option" data-testid="cb" />);
+      const wrapper = getByTestId("cb");
+      expect(wrapper.tabIndex).toBe(0);
+    });
+
+    it("should have tabIndex -1 on the hidden input so it is not a duplicate Tab stop", () => {
+      const { getByLabelText } = render(<Checkbox label="Option" />);
+      const input = getByLabelText<HTMLInputElement>("Option");
+      expect(input.tabIndex).toBe(-1);
+    });
+
+    it("should respect a custom tabIndex on the wrapper", () => {
+      const { getByTestId } = render(<Checkbox label="Option" tabIndex={3} data-testid="cb" />);
+      const wrapper = getByTestId("cb");
+      expect(wrapper.tabIndex).toBe(3);
+    });
+
+    it("should have no tabIndex when disabled so it is excluded from Tab order", () => {
+      const { getByTestId } = render(<Checkbox label="Option" disabled data-testid="cb" />);
+      const wrapper = getByTestId("cb");
+      expect(wrapper.getAttribute("tabindex")).toBeNull();
+    });
+
+    it("should toggle the checkbox when Space is pressed on the wrapper", () => {
+      const onChange = vi.fn();
+      const { getByTestId } = render(<Checkbox label="Option" onChange={onChange} data-testid="cb" />);
+      const wrapper = getByTestId("cb");
+      fireEvent.keyDown(wrapper, { key: " " });
+      expect(onChange).toHaveBeenCalledTimes(1);
Evidence
PR Compliance ID 7 requires tests to follow established test ID patterns from constants when using
test IDs. The added tests pass a custom data-testid="cb" and query via getByTestId("cb"), which
is not derived from the shared constants/utilities.

CLAUDE.md
packages/core/src/components/Checkbox/tests/Checkbox.test.tsx[221-250]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The new Checkbox keyboard navigation tests use a hard-coded `data-testid` value (`cb`) and query it directly, which conflicts with the requirement to use established constants-based test ID patterns.
## Issue Context
The Checkbox component already supports generating its `data-testid` via `getTestId(ComponentDefaultTestId.CHECKBOX, id)` when an `id` is provided and no explicit `data-testid` is passed. Tests should follow that pattern (or use accessible queries where possible).
## Fix Focus Areas
- packages/core/src/components/Checkbox/__tests__/Checkbox.test.tsx[221-250]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


View more (1)
4. Input focus not cleared🐞 Bug ✓ Correctness
Description
Checkbox.onMouseUpCallback blurs only the wrapper/label, but the component’s visual focus/hover
styling is still driven by the hidden input’s focus selectors, so the checkbox can remain in a
focused/hovered visual state after pointer interaction. This can leave a persistent focus
ring/background until focus moves elsewhere, contradicting the intent of the new blur logic.
Code

packages/core/src/components/Checkbox/Checkbox.tsx[R105-114]

+    // Blur the focused wrapper/label after mouse click so the focus ring doesn't
+    // persist after pointer interaction (keyboard focus ring only via :focus-visible).
+    const onMouseUpCallback = useCallback((e: React.MouseEvent<HTMLElement>) => {
+      const target = e.currentTarget;
   window.requestAnimationFrame(() => {
     window.requestAnimationFrame(() => {
-          input.blur();
+          target.blur();
     });
   });
-    }, [inputRef]);
+    }, []);
Evidence
The mouse-up handler explicitly blurs the event currentTarget (wrapper/label) and never blurs the
hidden input. At the same time, Checkbox.module.scss applies visible styling when the hidden input
is focused (including focus-visible ring and hover styling), so if the input becomes the focused
element (e.g., via label activation/click), those styles can remain active even though the wrapper
was blurred.

packages/core/src/components/Checkbox/Checkbox.tsx[105-114]
packages/core/src/components/Checkbox/Checkbox.module.scss[80-96]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`onMouseUpCallback` blurs only the wrapper/label, but focus-driven visuals are still controlled by the hidden `&amp;amp;amp;lt;input&amp;amp;amp;gt;` (`.input:focus...`). If the input ends up focused after pointer activation, its focus styles can persist.
### Issue Context
The PR’s intent (per comment) is to remove persistent focus ring after pointer interaction and keep keyboard-only focus indication via `:focus-visible`.
### Fix Focus Areas
- packages/core/src/components/Checkbox/Checkbox.tsx[105-114]
- packages/core/src/components/Checkbox/Checkbox.module.scss[80-96] (verify focus styles align with the updated blur behavior)
### Suggested approach
In `onMouseUpCallback`, after the double `requestAnimationFrame`, check `document.activeElement` and blur `inputRef.current` if it’s focused (and/or blur both wrapper and input safely). Ensure the callback still works for both `separateLabel` and non-separate modes.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

5. Autofocus hits hidden input 🐞 Bug ⛯ Reliability
Description
autoFocus still targets the visually-hidden input (opacity 0, width/height 0) while Tab focus is
now intended to land on the wrapper/label. This creates an inconsistent initial focus target that is
not the element receiving the new wrapper-based focus behavior.
Code

packages/core/src/components/Checkbox/Checkbox.tsx[R231-235]

         aria-label={finalAriaLabel}
         aria-labelledby={ariaLabelledBy}
         checked={checked}
-          tabIndex={tabIndex}
+          tabIndex={-1}
       />
Evidence
The input remains the element receiving autoFocus, but it is styled as fully hidden (0x0, opacity
0). With the new approach moving keyboard focus handling to the wrapper/label, autoFocus now focuses
a different (hidden) element than the intended Tab target.

packages/core/src/components/Checkbox/Checkbox.tsx[220-235]
packages/core/src/components/Checkbox/Checkbox.module.scss[73-75]
packages/style/src/mixins/_common.scss[9-14]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`autoFocus` currently focuses the hidden input even though the component’s keyboard focus model now uses the wrapper/label as the Tab target.
### Issue Context
The input is styled with `hidden-element()` (0x0, opacity 0), so focusing it is inconsistent with wrapper-targeted keyboard behavior.
### Fix Focus Areas
- packages/core/src/components/Checkbox/Checkbox.tsx[148-189]
- packages/core/src/components/Checkbox/Checkbox.tsx[208-259]
- packages/core/src/components/Checkbox/Checkbox.module.scss[70-90]
- packages/style/src/mixins/_common.scss[9-14]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

ⓘ The new review experience is currently in Beta. Learn more

Grey Divider

Qodo Logo

Comment on lines +221 to +250
it("should have tabIndex 0 on the wrapper label by default so Safari includes it in Tab order", () => {
const { getByTestId } = render(<Checkbox label="Option" data-testid="cb" />);
const wrapper = getByTestId("cb");
expect(wrapper.tabIndex).toBe(0);
});

it("should have tabIndex -1 on the hidden input so it is not a duplicate Tab stop", () => {
const { getByLabelText } = render(<Checkbox label="Option" />);
const input = getByLabelText<HTMLInputElement>("Option");
expect(input.tabIndex).toBe(-1);
});

it("should respect a custom tabIndex on the wrapper", () => {
const { getByTestId } = render(<Checkbox label="Option" tabIndex={3} data-testid="cb" />);
const wrapper = getByTestId("cb");
expect(wrapper.tabIndex).toBe(3);
});

it("should have no tabIndex when disabled so it is excluded from Tab order", () => {
const { getByTestId } = render(<Checkbox label="Option" disabled data-testid="cb" />);
const wrapper = getByTestId("cb");
expect(wrapper.getAttribute("tabindex")).toBeNull();
});

it("should toggle the checkbox when Space is pressed on the wrapper", () => {
const onChange = vi.fn();
const { getByTestId } = render(<Checkbox label="Option" onChange={onChange} data-testid="cb" />);
const wrapper = getByTestId("cb");
fireEvent.keyDown(wrapper, { key: " " });
expect(onChange).toHaveBeenCalledTimes(1);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

1. Ad-hoc data-testid used 📘 Rule violation ✓ Correctness

New Checkbox keyboard navigation tests use a hard-coded data-testid value (cb) instead of the
repository’s constants-based test ID pattern. This can reduce consistency and increase maintenance
overhead across the test suite.
Agent Prompt
## Issue description
The new Checkbox keyboard navigation tests use a hard-coded `data-testid` value (`cb`) and query it directly, which conflicts with the requirement to use established constants-based test ID patterns.

## Issue Context
The Checkbox component already supports generating its `data-testid` via `getTestId(ComponentDefaultTestId.CHECKBOX, id)` when an `id` is provided and no explicit `data-testid` is passed. Tests should follow that pattern (or use accessible queries where possible).

## Fix Focus Areas
- packages/core/src/components/Checkbox/__tests__/Checkbox.test.tsx[221-250]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +105 to +114
// Blur the focused wrapper/label after mouse click so the focus ring doesn't
// persist after pointer interaction (keyboard focus ring only via :focus-visible).
const onMouseUpCallback = useCallback((e: React.MouseEvent<HTMLElement>) => {
const target = e.currentTarget;
window.requestAnimationFrame(() => {
window.requestAnimationFrame(() => {
input.blur();
target.blur();
});
});
}, [inputRef]);
}, []);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

2. Input focus not cleared 🐞 Bug ✓ Correctness

Checkbox.onMouseUpCallback blurs only the wrapper/label, but the component’s visual focus/hover
styling is still driven by the hidden input’s focus selectors, so the checkbox can remain in a
focused/hovered visual state after pointer interaction. This can leave a persistent focus
ring/background until focus moves elsewhere, contradicting the intent of the new blur logic.
Agent Prompt
### Issue description
`onMouseUpCallback` blurs only the wrapper/label, but focus-driven visuals are still controlled by the hidden `<input>` (`.input:focus...`). If the input ends up focused after pointer activation, its focus styles can persist.

### Issue Context
The PR’s intent (per comment) is to remove persistent focus ring after pointer interaction and keep keyboard-only focus indication via `:focus-visible`.

### Fix Focus Areas
- packages/core/src/components/Checkbox/Checkbox.tsx[105-114]
- packages/core/src/components/Checkbox/Checkbox.module.scss[80-96] (verify focus styles align with the updated blur behavior)

### Suggested approach
In `onMouseUpCallback`, after the double `requestAnimationFrame`, check `document.activeElement` and blur `inputRef.current` if it’s focused (and/or blur both wrapper and input safely). Ensure the callback still works for both `separateLabel` and non-separate modes.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

@qodo-free-for-open-source-projects
Copy link
Copy Markdown
Contributor

qodo-free-for-open-source-projects bot commented Mar 11, 2026

Persistent review updated to latest commit e42aa53

Comment on lines +136 to +144
const onKeyDownCallback = useCallback(
(e: React.KeyboardEvent) => {
if (e.key === " ") {
e.preventDefault();
inputRef.current?.click();
}
},
[inputRef]
);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

1. Space toggles repeatedly 🐞 Bug ✓ Correctness

Checkbox.tsx triggers a programmatic input click on every Space keydown; holding Space will
auto-repeat keydown events and can toggle the checkbox multiple times (multiple onChange calls) for
a single long press. The handler is also brittle because it only matches e.key === " ", which can
miss Space in some browsers/AT combinations.
Agent Prompt
### Issue description
The Checkbox uses `onKeyDown` to call `inputRef.current?.click()` when Space is pressed. Because `keydown` auto-repeats while Space is held, this can trigger multiple toggles for a single long press. The handler also only matches `e.key === " "`, which can be inconsistent across environments.

### Issue Context
The PR moved the Tab focus target to the wrapper/label and added a Space key handler to preserve keyboard activation.

### Fix Focus Areas
- packages/core/src/components/Checkbox/Checkbox.tsx[136-144]
- packages/core/src/components/Checkbox/Checkbox.tsx[172-180]
- packages/core/src/components/Checkbox/Checkbox.tsx[208-218]

### Implementation notes
- Consider moving activation to `onKeyUp` for Space, or add `if (e.repeat) return;`.
- Prefer `e.code === "Space"` (and/or support legacy `e.key` values) to avoid missing Space in some browsers.
- Add/extend tests to cover `e.repeat: true` or multiple keydown events and ensure only one toggle per press.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

@qodo-free-for-open-source-projects
Copy link
Copy Markdown
Contributor

qodo-free-for-open-source-projects bot commented Mar 11, 2026

Persistent review updated to latest commit 6c5f357

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Mar 11, 2026

📦 Bundle Size Analysis

✅ No bundle size changes detected.

Unchanged Components
Component Base PR Diff
@vibe/button 17.74KB 17.79KB +57B 🔺
@vibe/clickable 6.07KB 6.05KB -21B 🟢
@vibe/dialog 53.84KB 53.77KB -74B 🟢
@vibe/icon-button 68.11KB 68KB -107B 🟢
@vibe/icon 13.01KB 13.01KB +3B 🔺
@vibe/layer 2.96KB 2.96KB 0B ➖
@vibe/layout 10.56KB 10.54KB -24B 🟢
@vibe/loader 5.8KB 5.82KB +16B 🔺
@vibe/tooltip 63KB 62.97KB -33B 🟢
@vibe/typography 65.48KB 65.47KB -8B 🟢
Accordion 6.37KB 6.34KB -32B 🟢
AccordionItem 68.18KB 68.13KB -59B 🟢
AlertBanner 72.93KB 72.91KB -24B 🟢
AlertBannerButton 19.23KB 19.24KB +12B 🔺
AlertBannerLink 15.56KB 15.5KB -62B 🟢
AlertBannerText 65.54KB 65.51KB -30B 🟢
AttentionBox 74.49KB 74.47KB -21B 🟢
AttentionBoxLink 15.45KB 15.38KB -74B 🟢
Avatar 68.26KB 68.35KB +87B 🔺
AvatarGroup 96.05KB 96KB -59B 🟢
Badge 43.56KB 43.49KB -74B 🟢
BreadcrumbItem 66.22KB 66.13KB -95B 🟢
BreadcrumbMenu 70.39KB 70.32KB -72B 🟢
BreadcrumbMenuItem 79.43KB 79.38KB -48B 🟢
BreadcrumbsBar 5.79KB 5.79KB -5B 🟢
ButtonGroup 70.29KB 70.28KB -7B 🟢
Checkbox 68.44KB 68.58KB +144B 🔺
Chips 77.07KB 77.18KB +118B 🔺
ColorPicker 76.41KB 76.26KB -154B 🟢
ColorPickerContent 75.59KB 75.55KB -46B 🟢
Combobox 86.31KB 86.33KB +16B 🔺
Counter 42.47KB 42.43KB -33B 🟢
DatePicker 134.56KB 134.57KB +13B 🔺
Divider 5.54KB 5.55KB +18B 🔺
Dropdown 126.07KB 125.79KB -286B 🟢
menu 59.97KB 59.84KB -126B 🟢
option 93.21KB 93.18KB -27B 🟢
singleValue 93.09KB 93.13KB +37B 🔺
EditableHeading 68.36KB 68.33KB -32B 🟢
EditableText 68.22KB 68.33KB +111B 🔺
EmptyState 72.71KB 72.68KB -31B 🟢
ExpandCollapse 68.04KB 68KB -44B 🟢
FormattedNumber 5.9KB 5.94KB +40B 🔺
GridKeyboardNavigationContext 4.66KB 4.65KB -5B 🟢
HiddenText 5.45KB 5.45KB +2B 🔺
Info 74.32KB 74.37KB +49B 🔺
Label 70.42KB 70.4KB -13B 🟢
LegacyModal 76.83KB 76.92KB +90B 🔺
LegacyModalContent 66.89KB 66.83KB -62B 🟢
LegacyModalFooter 3.45KB 3.45KB 0B ➖
LegacyModalFooterButtons 20.67KB 20.66KB -2B 🟢
LegacyModalHeader 72.95KB 72.85KB -102B 🟢
Link 15.22KB 15.15KB -75B 🟢
List 74.94KB 74.9KB -41B 🟢
ListItem 67.32KB 67.32KB +7B 🔺
ListItemAvatar 68.49KB 68.55KB +66B 🔺
ListItemIcon 14.23KB 14.18KB -46B 🟢
ListTitle 66.79KB 66.73KB -58B 🟢
Menu 8.76KB 8.72KB -41B 🟢
MenuDivider 5.69KB 5.69KB +7B 🔺
MenuGridItem 7.24KB 7.21KB -30B 🟢
MenuItem 79.32KB 79.3KB -17B 🟢
MenuItemButton 72.32KB 72.2KB -126B 🟢
MenuTitle 67.18KB 67.14KB -43B 🟢
MenuButton 67.75KB 67.82KB +66B 🔺
Modal 111.94KB 111.89KB -52B 🟢
ModalContent 4.77KB 4.77KB +2B 🔺
ModalHeader 67.63KB 67.54KB -90B 🟢
ModalMedia 7.79KB 7.74KB -43B 🟢
ModalFooter 69.48KB 69.51KB +24B 🔺
ModalFooterWizard 70.48KB 70.51KB +29B 🔺
ModalBasicLayout 9.21KB 9.21KB -2B 🟢
ModalMediaLayout 8.32KB 8.35KB +35B 🔺
ModalSideBySideLayout 6.36KB 6.38KB +23B 🔺
MultiStepIndicator 53.27KB 53.38KB +115B 🔺
NumberField 74.95KB 74.85KB -105B 🟢
LinearProgressBar 7.56KB 7.58KB +20B 🔺
RadioButton 67.62KB 67.57KB -48B 🟢
Search 72.49KB 72.49KB +2B 🔺
Skeleton 6.21KB 6.2KB -15B 🟢
Slider 75.82KB 75.85KB +24B 🔺
SplitButton 68.78KB 68.84KB +58B 🔺
SplitButtonMenu 8.89KB 8.89KB -2B 🟢
Steps 73.5KB 73.47KB -34B 🟢
Table 7.33KB 7.32KB -10B 🟢
TableBody 68.7KB 68.69KB -8B 🟢
TableCell 66.89KB 66.97KB +86B 🔺
TableContainer 5.38KB 5.36KB -15B 🟢
TableHeader 5.69KB 5.71KB +18B 🔺
TableHeaderCell 74.24KB 74.24KB -2B 🟢
TableRow 5.63KB 5.63KB +4B 🔺
TableRowMenu 70.63KB 70.6KB -33B 🟢
TableVirtualizedBody 73.32KB 73.36KB +36B 🔺
Tab 65.52KB 65.6KB +84B 🔺
TabList 8.92KB 8.92KB -2B 🟢
TabPanel 5.33KB 5.34KB +9B 🔺
TabPanels 5.97KB 5.92KB -51B 🟢
TabsContext 5.55KB 5.56KB +18B 🔺
TextArea 68.05KB 68.1KB +54B 🔺
TextField 71.45KB 71.34KB -109B 🟢
TextWithHighlight 65.91KB 65.87KB -47B 🟢
ThemeProvider 4.68KB 4.69KB +7B 🔺
Tipseen 73.26KB 73.27KB +5B 🔺
TipseenContent 73.69KB 73.62KB -68B 🟢
TipseenImage 73.5KB 73.56KB +69B 🔺
TipseenMedia 73.48KB 73.45KB -32B 🟢
TipseenWizard 76.03KB 75.96KB -70B 🟢
Toast 76.23KB 76.24KB +10B 🔺
ToastButton 19.07KB 19.04KB -32B 🟢
ToastLink 15.4KB 15.39KB -13B 🟢
Toggle 68.39KB 68.34KB -46B 🟢
TransitionView 37.69KB 37.7KB +15B 🔺
VirtualizedGrid 12.62KB 12.6KB -17B 🟢
VirtualizedList 12.42KB 12.41KB -10B 🟢
AttentionBox (Next) 76.43KB 76.49KB +65B 🔺
DatePicker (Next) 114.55KB 114.63KB +82B 🔺
Dialog (Next) 52.79KB 52.7KB -84B 🟢
Dropdown (Next) 97.47KB 97.37KB -107B 🟢
List (Next) 8.21KB 8.21KB +5B 🔺
ListItem (Next) 71.68KB 71.64KB -37B 🟢
ListTitle (Next) 67.04KB 67.08KB +38B 🔺

📊 Summary:

  • Total Base Size: 5.88MB
  • Total PR Size: 5.88MB
  • Total Difference: 1.62KB

Comment on lines 209 to 217
// eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions
<label
className={cx(styles.wrapper, className)}
tabIndex={wrapperTabIndex}
onMouseUp={onMouseUpCallback}
onKeyDown={onKeyDownCallback}
data-testid={dataTestId || getTestId(ComponentDefaultTestId.CHECKBOX, id)}
htmlFor={id}
onClickCapture={onClickCaptureLabel}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

1. Label focus lacks semantics 🐞 Bug ✓ Correctness

Checkbox moves Tab focus to a <label> (tabIndex=0 by default) and removes the native checkbox input
from the Tab order (tabIndex=-1). The focused element has no checkbox semantics (role/aria-checked),
so assistive tech won’t announce a checkbox or its checked state for keyboard navigation.
Agent Prompt
### Issue description
Checkbox now places keyboard focus on a `<label>` while the real checkbox `<input>` is removed from the Tab order. The focused element does not expose checkbox semantics (role/state), which breaks assistive-technology announcements for keyboard users.

### Issue Context
- `tabIndex` is moved from the `<input type="checkbox">` to a wrapper/label and Space key is manually handled.
- CSS focus styling is updated to target the wrapper/label.

### Fix Focus Areas
- packages/core/src/components/Checkbox/Checkbox.tsx[136-147]
- packages/core/src/components/Checkbox/Checkbox.tsx[148-189]
- packages/core/src/components/Checkbox/Checkbox.tsx[208-259]
- packages/core/src/components/Checkbox/Checkbox.module.scss[70-114]
- packages/style/src/mixins/_common.scss[9-14]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines 166 to 181
aria-label={finalAriaLabel}
aria-labelledby={ariaLabelledBy}
checked={checked}
tabIndex={tabIndex}
tabIndex={-1}
/>
{/* eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions */}
<label
htmlFor={id}
className={cx(styles.checkbox, checkboxClassName)}
data-testid={getTestId(ComponentDefaultTestId.CHECKBOX_CHECKBOX, id)}
tabIndex={wrapperTabIndex}
onMouseUp={onMouseUpCallback}
onKeyDown={onKeyDownCallback}
onClickCapture={onClickCaptureLabel}
>
<Icon
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

2. Icon label has no name 🐞 Bug ✓ Correctness

In separateLabel mode, the tabbable element is the icon-only `<label
className={styles.checkbox}>, and its only child is ariaHidden`, so the focused control has no
accessible name. Keyboard/screen-reader users will land on an unnamed focus target.
Agent Prompt
### Issue description
In `separateLabel` mode the focusable element is an icon-only `<label>` whose contents are `ariaHidden`, so it has no accessible name when focused.

### Issue Context
This is introduced by moving tab focus from the hidden input to the label.

### Fix Focus Areas
- packages/core/src/components/Checkbox/Checkbox.tsx[148-203]
- packages/core/src/components/Checkbox/Checkbox.module.scss[96-114]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

@rivka-ungar rivka-ungar merged commit 8614a7b into master Mar 11, 2026
17 checks passed
@rivka-ungar rivka-ungar deleted the fix/checkbox-safari-keyboard-navigation branch March 11, 2026 07:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants