diff --git a/packages/extension-chakra-storefront/jest.config.js b/packages/extension-chakra-storefront/jest.config.js index 38581e981d..516c9b6a9a 100644 --- a/packages/extension-chakra-storefront/jest.config.js +++ b/packages/extension-chakra-storefront/jest.config.js @@ -39,6 +39,7 @@ module.exports = { createTestGlob('components/swatch-group'), createTestGlob('components/toaster'), createTestGlob('components/list-menu'), + createTestGlob('components/skip-nav'), createTestGlob('components/login'), createTestGlob('components/register'), createTestGlob('components/email-confirmation'), @@ -86,10 +87,6 @@ module.exports = { '/node_modules/@chakra-ui/react/dist/cjs/$1/index.cjs', '/node_modules/@chakra-ui/react/dist/cjs/index.cjs' ], - '^@chakra-ui/skip-nav/(.*)$': [ - '/node_modules/@chakra-ui/skip-nav/dist/index.js', - '/node_modules/@chakra-ui/skip-nav/dist/$1.js' - ], '^proxy-compare$': '/node_modules/proxy-compare/dist/cjs/index.js', '^uqr$': '/node_modules/uqr/dist/index.cjs', // handle pwa-kit extensibility special import diff --git a/packages/extension-chakra-storefront/src/components/skip-nav/index.test.tsx b/packages/extension-chakra-storefront/src/components/skip-nav/index.test.tsx new file mode 100644 index 0000000000..2099b36688 --- /dev/null +++ b/packages/extension-chakra-storefront/src/components/skip-nav/index.test.tsx @@ -0,0 +1,179 @@ +/* + * Copyright (c) 2025, Salesforce, Inc. + * All rights reserved. + * SPDX-License-Identifier: BSD-3-Clause + * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause + */ + +import React from 'react' +import {screen} from '@testing-library/react' +import {SkipNavLink, SkipNavContent} from './index' +import {renderWithProviders} from '../../utils/test-utils' + +describe('SkipNavLink', () => { + test('renders with default props', () => { + renderWithProviders(Skip to Content, {}) + + const link = screen.getByRole('link', {name: 'Skip to Content'}) + expect(link).toBeInTheDocument() + expect(link).toHaveAttribute('href', '#skip-to-content') + }) + + test('renders with custom href', () => { + renderWithProviders(Skip to Main, {}) + + const link = screen.getByRole('link', {name: 'Skip to Main'}) + expect(link).toHaveAttribute('href', '#main-content') + }) + + test('has correct accessibility attributes', () => { + renderWithProviders(Skip to Content, {}) + + const link = screen.getByRole('link', {name: 'Skip to Content'}) + expect(link).toHaveAttribute('href', '#skip-to-content') + }) + + test('has visually hidden styles by default', () => { + renderWithProviders(Skip to Content, {}) + + const link = screen.getByRole('link', {name: 'Skip to Content'}) + + // The link should be focusable (no aria-hidden) but positioned off-screen + expect(link).not.toHaveAttribute('aria-hidden') + expect(link).toHaveAttribute('href', '#skip-to-content') + }) + + test('uses theme styles', () => { + renderWithProviders(Skip to Content, {}) + + const link = screen.getByRole('link', {name: 'Skip to Content'}) + expect(link).toBeInTheDocument() + // The component should use theme styles from the skipNav slot recipe + expect(link).toHaveAttribute('href', '#skip-to-content') + }) +}) + +describe('SkipNavContent', () => { + test('renders with default props', () => { + renderWithProviders( + +
Main content
+
, + {} + ) + + const content = screen.getByText('Main content') + const container = content.parentElement + + expect(container).toHaveAttribute('id', 'skip-to-content') + expect(container).toHaveAttribute('tabIndex', '-1') + }) + + test('renders with custom id', () => { + renderWithProviders( + +
Main content
+
, + {} + ) + + const content = screen.getByText('Main content') + const container = content.parentElement + + expect(container).toHaveAttribute('id', 'main-content') + expect(container).toHaveAttribute('tabIndex', '-1') + }) + + test('has correct accessibility attributes', () => { + renderWithProviders( + +
Main content
+
, + {} + ) + + const content = screen.getByText('Main content') + const container = content.parentElement + + expect(container).toHaveAttribute('id', 'skip-to-content') + expect(container).toHaveAttribute('tabIndex', '-1') + }) + + test('uses theme styles', () => { + renderWithProviders( + +
Content with theme
+
, + {} + ) + + const content = screen.getByText('Content with theme') + const container = content.parentElement + + expect(container).toBeInTheDocument() + // The component should use theme styles from the skipNav slot recipe + expect(container).toHaveAttribute('id', 'skip-to-content') + }) +}) + +describe('SkipNavLink and SkipNavContent integration', () => { + test('link href matches content id', () => { + renderWithProviders( +
+ Skip to Content + +
Main content
+
+
, + {} + ) + + const link = screen.getByRole('link', {name: 'Skip to Content'}) + const content = screen.getByText('Main content') + const container = content.parentElement + + expect(link).toHaveAttribute('href', '#skip-to-content') + expect(container).toHaveAttribute('id', 'skip-to-content') + }) + + test('custom href and id work together', () => { + renderWithProviders( +
+ Skip to Main + +
Main content
+
+
, + {} + ) + + const link = screen.getByRole('link', {name: 'Skip to Main'}) + const content = screen.getByText('Main content') + const container = content.parentElement + + expect(link).toHaveAttribute('href', '#main') + expect(container).toHaveAttribute('id', 'main') + }) + + test('both components use skip-nav theme styles', () => { + renderWithProviders( +
+ Skip to Content + +
Main content
+
+
, + {} + ) + + const link = screen.getByRole('link', {name: 'Skip to Content'}) + const content = screen.getByText('Main content') + const container = content.parentElement + + // Both components should be rendered and use theme styles + expect(link).toBeInTheDocument() + expect(container).toBeInTheDocument() + expect(link).toHaveAttribute('href', '#skip-to-content') + expect(container).toHaveAttribute('id', 'skip-to-content') + }) +}) diff --git a/packages/extension-chakra-storefront/src/components/skip-nav/index.tsx b/packages/extension-chakra-storefront/src/components/skip-nav/index.tsx new file mode 100644 index 0000000000..3b8ecf8d24 --- /dev/null +++ b/packages/extension-chakra-storefront/src/components/skip-nav/index.tsx @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2025, Salesforce, Inc. + * All rights reserved. + * SPDX-License-Identifier: BSD-3-Clause + * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause + */ + +import React from 'react' +import {Box, Link, useSlotRecipe} from '@chakra-ui/react' + +interface SkipNavLinkProps { + children: any + href?: string + [key: string]: any +} + +interface SkipNavContentProps { + children: any + css?: any + id?: string + [key: string]: any +} + +/** + * SkipNavLink component provides a skip link for keyboard navigation + * with initial state screen reader accessible but visually hidden + */ +export const SkipNavLink = ({children, href = '#skip-to-content', ...props}: SkipNavLinkProps) => { + const recipe = useSlotRecipe({key: 'skipNav'}) + const styles = recipe() + + return ( + + {children} + + ) +} + +/** + * SkipNavContent component provides the target content area for skip navigation + */ +export const SkipNavContent = ({ + children, + css, + id = 'skip-to-content', + ...props +}: SkipNavContentProps) => { + const recipe = useSlotRecipe({key: 'skipNav'}) + const styles = recipe() + + return ( + + {children} + + ) +} diff --git a/packages/extension-chakra-storefront/src/components/with-layout/with-layout.tsx b/packages/extension-chakra-storefront/src/components/with-layout/with-layout.tsx index 61d6e20802..7448ac3006 100644 --- a/packages/extension-chakra-storefront/src/components/with-layout/with-layout.tsx +++ b/packages/extension-chakra-storefront/src/components/with-layout/with-layout.tsx @@ -33,6 +33,7 @@ import { // Local Project Components import {DrawerMenu} from '../drawer-menu' +import {SkipNavLink, SkipNavContent} from '../skip-nav' import {getPathWithLocale} from '../../utils/url' import {HideOnDesktop, HideOnMobile} from '../responsive' import {ListMenu, ListMenuContent} from '../list-menu' @@ -276,8 +277,7 @@ const withLayout =

(WrappedComponent: React.ComponentType

) - {/*TODO: recreating this component because @chakra-ui/skip-nav does not have V3 version*/} - {/*Skip to Content*/} + Skip to Content {!isCheckout ? ( <> @@ -318,28 +318,27 @@ const withLayout =

(WrappedComponent: React.ComponentType

) {!isOnline && } - {/*TODO: recreating this component because @chakra-ui/skip-nav does not have V3 version*/} - {/**/} - - - - - - {/**/} + + + + + + {!isCheckout ?