diff --git a/libs/react-components/specs/checkbox.browser.spec.tsx b/libs/react-components/specs/checkbox.browser.spec.tsx
index 72d7e3fc0..53f1d0c6d 100644
--- a/libs/react-components/specs/checkbox.browser.spec.tsx
+++ b/libs/react-components/specs/checkbox.browser.spec.tsx
@@ -84,4 +84,64 @@ describe("Checkbox", () => {
expect(childValue.element().textContent).toBe("false");
});
});
+
+ it("should have a 44px x 44px touch target area", async () => {
+ const result = render(
+
+ );
+
+ const checkbox = result.getByTestId("test-checkbox");
+ await vi.waitFor(() => {
+ expect(checkbox.element()).toBeTruthy();
+ });
+
+ const container = checkbox.element().querySelector(".container") as HTMLElement;
+ expect(container).toBeTruthy();
+
+ // Get computed styles for the ::before pseudo-element (touch target)
+ const beforeStyles = window.getComputedStyle(container, "::before");
+
+ // Verify the touch target dimensions
+ expect(beforeStyles.width).toBe("44px");
+ expect(beforeStyles.height).toBe("44px");
+ expect(beforeStyles.position).toBe("absolute");
+
+ // Verify the container itself has position: relative for proper positioning context
+ const containerStyles = window.getComputedStyle(container);
+ expect(containerStyles.position).toBe("relative");
+
+ // Verify the actual visual size of the container (24px) vs touch target (44px)
+ const containerRect = container.getBoundingClientRect();
+ expect(containerRect.width).toBe(24); // Visual checkbox is 24px
+ expect(containerRect.height).toBe(24); // Visual checkbox is 24px
+
+ // Verify the transform is applied correctly for centering
+ // CSS: transform: translate(-50%, -50%) converts to matrix(a, b, c, d, tx, ty)
+ // Matrix breakdown:
+ // - (1, 0, 0, 1) = identity matrix (no scaling/rotation)
+ // - (-22, -22) = translate by -22px in X and Y directions
+ // Math: 50% of 44px = 22px, so translate(-50%, -50%) = translate(-22px, -22px)
+ // Regex explanation:
+ // - matrix\(1, 0, 0, 1, = identity matrix
+ // - -2[0-9.]+ = negative number starting with -2 (e.g., -22, -23.6)
+ // - Flexible pattern accounts for border widths and sub-pixel rendering
+ expect(beforeStyles.transform).toMatch(/matrix\(1, 0, 0, 1, -2[0-9.]+, -2[0-9.]+\)/);
+
+ // Check ::after pseudo-element (should not interfere with touch target)
+ const afterStyles = window.getComputedStyle(container, "::after");
+ // ::after should not have conflicting dimensions or positioning
+ expect(afterStyles.position).not.toBe("absolute");
+
+ // Final verification: Check that all styles are applied and rendered
+ // After the page is fully loaded and all CSS is computed
+ await vi.waitFor(() => {
+ const finalContainerStyles = window.getComputedStyle(container);
+ const finalBeforeStyles = window.getComputedStyle(container, "::before");
+
+ // Verify final computed styles match expectations
+ expect(finalContainerStyles.position).toBe("relative");
+ expect(finalBeforeStyles.width).toBe("44px");
+ expect(finalBeforeStyles.height).toBe("44px");
+ });
+ });
});
diff --git a/libs/react-components/specs/radio.browser.spec.tsx b/libs/react-components/specs/radio.browser.spec.tsx
index 5d15c59ca..1145dabc9 100644
--- a/libs/react-components/specs/radio.browser.spec.tsx
+++ b/libs/react-components/specs/radio.browser.spec.tsx
@@ -98,4 +98,67 @@ describe("Radio", () => {
expect(selectedValue.element().textContent).toBe("apple");
});
});
+
+ it("should have a 44px x 44px touch target area", async () => {
+ const result = render(
+
+
+
+ );
+
+ const radioInput = result.getByTestId("radio-option-option1");
+ await vi.waitFor(() => {
+ expect(radioInput.element()).toBeTruthy();
+ });
+
+ // Get the parent label element and find the .icon element
+ const label = radioInput.element().closest("label");
+ expect(label).toBeTruthy();
+
+ const icon = label?.querySelector(".icon") as HTMLElement;
+ expect(icon).toBeTruthy();
+
+ // Get computed styles for the ::before pseudo-element (touch target)
+ const beforeStyles = window.getComputedStyle(icon, "::before");
+
+ // Verify the touch target dimensions
+ expect(beforeStyles.width).toBe("44px");
+ expect(beforeStyles.height).toBe("44px");
+ expect(beforeStyles.position).toBe("absolute");
+
+ // Verify the icon itself has position: relative for proper positioning context
+ const iconStyles = window.getComputedStyle(icon);
+ expect(iconStyles.position).toBe("relative");
+
+ // Verify the actual visual size of the icon (24px) vs touch target (44px)
+ const iconRect = icon.getBoundingClientRect();
+ expect(iconRect.width).toBe(24); // Visual icon is 24px
+ expect(iconRect.height).toBe(24); // Visual icon is 24px
+
+ // Verify the transform is applied correctly for centering
+ // CSS: transform: translate(-50%, -50%) converts to matrix(a, b, c, d, tx, ty)
+ // Matrix breakdown:
+ // - (1, 0, 0, 1) = identity matrix (no scaling/rotation)
+ // - (-22, -22) = translate by -22px in X and Y directions
+ // Math: 50% of 44px = 22px, so translate(-50%, -50%) = translate(-22px, -22px)
+ // This centers the 44px touch target on the 24px icon
+ expect(beforeStyles.transform).toBe("matrix(1, 0, 0, 1, -22, -22)");
+
+ // Check ::after pseudo-element (should not interfere with touch target)
+ const afterStyles = window.getComputedStyle(icon, "::after");
+ // ::after should not have conflicting dimensions or positioning
+ expect(afterStyles.position).not.toBe("absolute");
+
+ // Final verification: Check that all styles are applied and rendered
+ // After the page is fully loaded and all CSS is computed
+ await vi.waitFor(() => {
+ const finalIconStyles = window.getComputedStyle(icon);
+ const finalBeforeStyles = window.getComputedStyle(icon, "::before");
+
+ // Verify final computed styles match expectations
+ expect(finalIconStyles.position).toBe("relative");
+ expect(finalBeforeStyles.width).toBe("44px");
+ expect(finalBeforeStyles.height).toBe("44px");
+ });
+ });
});
diff --git a/libs/web-components/src/components/checkbox/Checkbox.svelte b/libs/web-components/src/components/checkbox/Checkbox.svelte
index 384c9b881..bcfc2270e 100644
--- a/libs/web-components/src/components/checkbox/Checkbox.svelte
+++ b/libs/web-components/src/components/checkbox/Checkbox.svelte
@@ -165,12 +165,12 @@
const checkboxEl = (_rootEl.getRootNode() as ShadowRoot)?.host as HTMLElement;
const fromCheckboxList = checkboxEl?.closest("goa-checkbox-list") !== null;
- relay(
- _rootEl,
- FormFieldMountMsg,
- { name, el: _rootEl },
- { bubbles: !fromCheckboxList, timeout: 10 },
- );
+ relay(
+ _rootEl,
+ FormFieldMountMsg,
+ { name, el: _rootEl },
+ { bubbles: !fromCheckboxList, timeout: 10 },
+ );
}
function onChange(e: Event) {
@@ -387,6 +387,7 @@ max-width: ${maxwidth};
/* Container */
.container {
+ position: relative;
box-sizing: border-box;
border: var(--goa-checkbox-border);
border-radius: var(--goa-checkbox-border-radius);
@@ -398,6 +399,17 @@ max-width: ${maxwidth};
justify-content: center;
flex: 0 0 auto; /* prevent squishing of checkbox */
}
+
+ .container::before {
+ content: '';
+ position: absolute;
+ width: 44px;
+ height: 44px;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -50%);
+ }
+
.container:hover {
border: var(--goa-checkbox-border-hover);
}
diff --git a/libs/web-components/src/components/radio-item/RadioItem.svelte b/libs/web-components/src/components/radio-item/RadioItem.svelte
index a48bc88f2..f9ef00972 100644
--- a/libs/web-components/src/components/radio-item/RadioItem.svelte
+++ b/libs/web-components/src/components/radio-item/RadioItem.svelte
@@ -342,6 +342,7 @@
}
.icon {
+ position: relative;
display: inline-block;
height: var(--goa-radio-size);
width: var(--goa-radio-size);
@@ -354,6 +355,16 @@
margin-top: var(--font-valign-fix);
}
+ .icon::before {
+ content: '';
+ position: absolute;
+ width: 44px;
+ height: 44px;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -50%);
+ }
+
.radio--disabled .label,
.radio--disabled ~ .description {
color: var(--goa-radio-label-color-disabled);