Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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,47 @@
/*
* 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'

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

export default defineSlotRecipe({
slots: ['link', 'content'],
base: {
link: {
position: 'absolute',
zIndex: 'skipNav',
left: '-10000px',
top: 'auto',
width: '1px',
height: '1px',
overflow: 'hidden',
_focusVisible: focusStyles,
_focus: focusStyles
},
content: {}
}
})
5 changes: 4 additions & 1 deletion packages/extension-chakra-storefront/src/theme/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ import quantityPicker from './components/project/quantity-picker'
import search from './components/project/search'
import socialIcons from './components/project/social-icons'
import swatchGroup from './components/project/swatch-group'
import skipNav from './components/project/skip-nav'

// Please refer to the Chakra-Ui theme customization docs found
// here https://chakra-ui.com/docs/theming/customize-theme to learn
Expand Down Expand Up @@ -80,6 +81,7 @@ export const overrides = defineConfig({
body: `-apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"`,
mono: `SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace`
},

breakpoints
},
semanticTokens: {
Expand Down Expand Up @@ -126,7 +128,8 @@ export const overrides = defineConfig({
quantityPicker,
socialIcons,
swatchGroup,
search
search,
skipNav
}
// keep these here for reference til we finish the components
// components: {
Expand Down
Loading