Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 1 addition & 4 deletions packages/extension-chakra-storefront/jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'),
Expand Down Expand Up @@ -86,10 +87,6 @@ module.exports = {
'<rootDir>/node_modules/@chakra-ui/react/dist/cjs/$1/index.cjs',
'<rootDir>/node_modules/@chakra-ui/react/dist/cjs/index.cjs'
],
'^@chakra-ui/skip-nav/(.*)$': [
'<rootDir>/node_modules/@chakra-ui/skip-nav/dist/index.js',
'<rootDir>/node_modules/@chakra-ui/skip-nav/dist/$1.js'
],
'^proxy-compare$': '<rootDir>/node_modules/proxy-compare/dist/cjs/index.js',
'^uqr$': '<rootDir>/node_modules/uqr/dist/index.cjs',
// handle pwa-kit extensibility special import
Expand Down
Original file line number Diff line number Diff line change
@@ -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(<SkipNavLink>Skip to Content</SkipNavLink>, {})

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(<SkipNavLink href="#main-content">Skip to Main</SkipNavLink>, {})

const link = screen.getByRole('link', {name: 'Skip to Main'})
expect(link).toHaveAttribute('href', '#main-content')
})

test('has correct accessibility attributes', () => {
renderWithProviders(<SkipNavLink>Skip to Content</SkipNavLink>, {})

const link = screen.getByRole('link', {name: 'Skip to Content'})
expect(link).toHaveAttribute('href', '#skip-to-content')
})

test('has visually hidden styles by default', () => {
renderWithProviders(<SkipNavLink>Skip to Content</SkipNavLink>, {})

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(<SkipNavLink>Skip to Content</SkipNavLink>, {})

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(
<SkipNavContent>
<div>Main content</div>
</SkipNavContent>,
{}
)

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(
<SkipNavContent id="main-content">
<div>Main content</div>
</SkipNavContent>,
{}
)

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(
<SkipNavContent>
<div>Main content</div>
</SkipNavContent>,
{}
)

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(
<SkipNavContent>
<div>Content with theme</div>
</SkipNavContent>,
{}
)

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(
<div>
<SkipNavLink>Skip to Content</SkipNavLink>
<SkipNavContent>
<div>Main content</div>
</SkipNavContent>
</div>,
{}
)

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(
<div>
<SkipNavLink href="#main">Skip to Main</SkipNavLink>
<SkipNavContent id="main">
<div>Main content</div>
</SkipNavContent>
</div>,
{}
)

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(
<div>
<SkipNavLink>Skip to Content</SkipNavLink>
<SkipNavContent>
<div>Main content</div>
</SkipNavContent>
</div>,
{}
)

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')
})
})
Original file line number Diff line number Diff line change
@@ -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 (
<Link href={href} css={styles.link} {...props}>
{children}
</Link>
)
}

/**
* 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 (
<Box id={id} css={{...styles.content, ...css}} tabIndex={-1} {...props}>
{children}
</Box>
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -276,8 +277,7 @@ const withLayout = <P extends object>(WrappedComponent: React.ComponentType<P>)

<ScrollToTop />
<Box id="app" display="flex" flexDirection="column" flex={1}>
{/*TODO: recreating this component because @chakra-ui/skip-nav does not have V3 version*/}
{/*<SkipNavLink zIndex="skipLink">Skip to Content</SkipNavLink>*/}
<SkipNavLink>Skip to Content</SkipNavLink>
<Box css={styles.headerWrapper}>
{!isCheckout ? (
<>
Expand Down Expand Up @@ -318,28 +318,27 @@ const withLayout = <P extends object>(WrappedComponent: React.ComponentType<P>)
</Box>
{!isOnline && <OfflineBanner />}
<AddToCartModalProvider>
{/*TODO: recreating this component because @chakra-ui/skip-nav does not have V3 version*/}
{/*<SkipNavContent*/}
{/* style={{*/}
{/* display: 'flex',*/}
{/* flexDirection: 'column',*/}
{/* flex: 1,*/}
{/* outline: 0*/}
{/* }}*/}
{/*>*/}
<Box
as="main"
id="app-main"
role="main"
display="flex"
flexDirection="column"
flex="1"
<SkipNavContent
css={{
display: 'flex',
flexDirection: 'column',
flex: 1,
outline: 0
}}
>
<OfflineBoundary isOnline={isOnline}>
<WrappedComponent {...(props as P)} />
</OfflineBoundary>
</Box>
{/*</SkipNavContent>*/}
<Box
as="main"
id="app-main"
role="main"
display="flex"
flexDirection="column"
flex="1"
>
<OfflineBoundary isOnline={isOnline}>
<WrappedComponent {...(props as P)} />
</OfflineBoundary>
</Box>
</SkipNavContent>

{!isCheckout ? <Footer /> : <CheckoutFooter />}
<AuthModal {...(authModal as any)} />
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
/*
* 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 {defineSlotRecipe} from '@chakra-ui/react'

export default defineSlotRecipe({
slots: ['link', 'content'],
base: {
link: {
position: 'absolute',
zIndex: 'skipNav',
left: '-10000px',
top: 'auto',
width: '1px',
height: '1px',
overflow: 'hidden',
_focusVisible: {
// !important needed to override the default hidden positioning (absolute + left: -10000px)
position: 'fixed !important',
top: '6px !important',
left: '6px !important',
width: 'auto !important',
height: 'auto !important',
overflow: 'visible !important',
zIndex: 'skipNav',
padding: '8px',
Copy link
Contributor

Choose a reason for hiding this comment

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

Can we use padding: 2 to get the theme value?

backgroundColor: 'white',
color: 'black',
textDecoration: 'none',
border: '2px solid black',
borderRadius: '4px',
fontSize: '14px',
Copy link
Contributor

Choose a reason for hiding this comment

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

Can we use fontSize: 'sm'?

fontWeight: 'bold',
outline: 'none',
boxShadow: '0 2px 4px rgba(0, 0, 0, 0.1)',
whiteSpace: 'nowrap'
},
_focus: {
// !important needed to override the default hidden positioning (absolute + left: -10000px)
position: 'fixed !important',
top: '6px !important',
left: '6px !important',
width: 'auto !important',
height: 'auto !important',
overflow: 'visible !important',
zIndex: 'skipNav',
padding: '8px',
backgroundColor: 'white',
color: 'black',
textDecoration: 'none',
border: '2px solid black',
borderRadius: '4px',
fontSize: '14px',
Copy link
Contributor

Choose a reason for hiding this comment

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

Can we move it to base instead declaring same value for _focus and _focusVisible

fontWeight: 'bold',
outline: 'none',
boxShadow: '0 2px 4px rgba(0, 0, 0, 0.1)',
whiteSpace: 'nowrap'
}
},
content: {}
}
})
Loading
Loading