Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Updates to skip link #4482

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 2 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
9 changes: 9 additions & 0 deletions .changeset/gentle-hairs-repair.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
"@salt-ds/lab": minor
---

Updates to Lab `SkipLink`

- Remove `targetRef` prop, added `target` prop to accept a string representing the ID of the target element.
- Updated styling to adhere with the rest of the library styles for consistency.
- Fixed an issue where the `SkipLink` would render when the ref to the target element was broken. Now, the skip link will not render at all if the target element is not found.
97 changes: 16 additions & 81 deletions packages/lab/src/__tests__/__e2e__/skip-link/SkipLink.cy.tsx
Original file line number Diff line number Diff line change
@@ -1,65 +1,9 @@
import { SkipLink, SkipLinks } from "@salt-ds/lab";
import * as skipLinkStories from "@stories/skip-link/skip-link.stories";
import { composeStories } from "@storybook/react";
import { checkAccessibility } from "../../../../../../cypress/tests/checkAccessibility";

const composedStories = composeStories(skipLinkStories);
const { Default, MultipleLinks } = composedStories;

const NoTargetRef = () => {
return (
<>
<div>
<span style={{ height: 50, lineHeight: "50px" }} tabIndex={-1}>
Click here and press the Tab key to see the Skip Link
</span>
<div style={{ position: "relative", maxWidth: 500 }}>
<SkipLinks>
<SkipLink data-testid="skipLink" href="#main">
Skip to main content
</SkipLink>
</SkipLinks>
<div
style={{
borderTop: "2px solid grey",
fontSize: 24,
lineHeight: 3.5,
}}
>
What we do
</div>

<article id="main">
<section>
<h1>Salt</h1>
<p>
Salt provides you with a suite of UI components and a flexible
theming system. With no customisation, the default theme offers
an attractive and modern look-and-feel, with both light and dark
variants and support for a range of UI densities. We have
included a theming system which allows you to easily create
theme variations, or in fact substitute alternate themes.
</p>
</section>
<section>
<h1>Goals</h1>
<p>Salt has been developed with the following design goals:</p>
<ul>
<li>
Providing a comprehensive set of commonly-used UI controls
</li>
<li>Complying with WCAG 2.1 accessibility guidelines</li>
<li> To be lightweight and performant</li>
<li> Offering flexible styling and theming support</li>
<li> Minimizing dependencies on third-party libraries</li>
</ul>
</section>
</article>
</div>
</div>
</>
);
};
const { Default } = composedStories;

describe("GIVEN a SkipLink", () => {
checkAccessibility(composedStories);
Expand All @@ -69,38 +13,29 @@ describe("GIVEN a SkipLink", () => {
cy.findByText(
"Click here and press the Tab key to see the Skip Link",
).click();
cy.findByRole("link", { name: "Skip to main content" }).should(
"not.be.visible",
);
cy.realPress("Tab");
cy.findByTestId("skipLink").should("be.visible");
cy.findByTestId("skipLink").click();

cy.findByRole("link", { name: "Skip to main content" }).should(
"be.visible",
);
cy.findByRole("link", { name: "Skip to main content" }).click();
cy.get("#main").should("be.focused");
Fercas123 marked this conversation as resolved.
Show resolved Hide resolved
cy.findByRole("link", { name: "Skip to main content" }).should(
"not.be.visible",
);
});

it("THEN it should not move focus if no target ref is given", () => {
cy.mount(<NoTargetRef />);
cy.findByText(
"Click here and press the Tab key to see the Skip Link",
).click();
cy.realPress("Tab");
cy.findByTestId("skipLink").should("be.visible");
cy.findByTestId("skipLink").click();
cy.findByTestId("skipLink").should("not.be.focused");
cy.get("#main").should("not.be.focused");
});
});

describe("WHEN there are multiple SkipLinks", () => {
it("THEN it should move focus on to correct target when the appropriate link is interacted with", () => {
cy.mount(<MultipleLinks />);
it("THEN it should hide the skip link if ref is broken", () => {
cy.mount(<Default target="" />);
cy.findByText(
"Click here and press the Tab key to see the Skip Link",
).click();
cy.realPress("Tab");
cy.realPress("Tab");
cy.realPress("Enter");

cy.get("#goals").should("be.focused");
cy.get("#introduction").should("not.be.focused");
cy.findByRole("link", { name: "Skip to main content" }).should(
"not.exist",
);
});
});
});
56 changes: 24 additions & 32 deletions packages/lab/src/skip-link/SkipLink.css
Original file line number Diff line number Diff line change
@@ -1,52 +1,44 @@
/* CSS Variables for the Skip Link */
.saltSkipLink {
--skipLink-padding: var(--saltSkipLink-padding, var(--salt-size-unit));
--skipLink-margin: var(--saltSkipLink-margin, var(--salt-size-unit));
--skipLink-background: var(--saltSkipLink-background, var(--salt-actionable-primary-background));
--skipLink-color: var(--saltSkipLink-color, var(--salt-content-primary-foreground));
}

/* Overrides */
.saltSkipLink {
--saltLink-color-focus: var(--skipLink-color);
}

.saltSkipLink-target {
--skipLink-target-focus: var(--salt-focused-outline);
}

/*Styles applied when the link is focused to hide the Skip Link when not in focus*/
.saltSkipLink {
top: 0;
left: 0;
opacity: 0;
width: 1px;
height: 1px;
display: block;
opacity: 0;
margin: 0;
padding: 0;
overflow: hidden;
position: absolute;

color: var(--salt-content-primary-foreground);
letter-spacing: var(--salt-text-letterSpacing);
text-decoration: var(--salt-navigable-textDecoration);
font-family: var(--salt-text-fontFamily);
white-space: nowrap;
background: var(--saltSkipLink-background, var(--salt-container-primary-background));
}

/* Styles applied when the link is focused to display the Skip Link only when in focus*/
.saltSkipLink:focus {
opacity: 1;
width: auto;
height: auto;
white-space: nowrap;
margin: var(--skipLink-margin);
padding: calc(var(--skipLink-padding) - 1px) var(--skipLink-padding) var(--skipLink-padding);
background: var(--skipLink-background);
color: var(--skipLink-color);
box-shadow: var(--salt-overlayable-shadow-popout);
height: max(var(--salt-size-base), auto);
padding: var(--salt-spacing-100) var(--salt-spacing-300);
outline: var(--salt-focused-outline);
outline-offset: calc(-1 * var(--salt-focused-outlineWidth));
box-shadow: var(--salt-overlayable-shadow);
}

.saltSkipLink {
font-size: var(--salt-text-fontSize);
font-family: var(--saltSkipLink-fontFamily, var(--salt-text-fontFamily));
line-height: var(--saltSkipLink-lineHeight, var(--salt-text-lineHeight));
@keyframes fade-in-out-outline {
0% {
outline-color: var(--salt-focused-outlineColor);
}
100% {
outline-color: transparent;
}
}

/*Styles applied to the skip link focus target*/
.saltSkipLink-target {
outline: var(--skipLink-target-focus);
animation: fade-in-out-outline var(--salt-duration-notable) var(--salt-animation-timing-function) both;
outline: var(--salt-focused-outline);
Fercas123 marked this conversation as resolved.
Show resolved Hide resolved
}
56 changes: 35 additions & 21 deletions packages/lab/src/skip-link/SkipLink.tsx
Original file line number Diff line number Diff line change
@@ -1,48 +1,62 @@
import { Link, type LinkProps, makePrefixer } from "@salt-ds/core";
import { makePrefixer } from "@salt-ds/core";
import { useComponentCssInjection } from "@salt-ds/styles";
import { useWindow } from "@salt-ds/window";
import { clsx } from "clsx";
import { type RefObject, forwardRef } from "react";
import {
type ComponentPropsWithoutRef,
forwardRef,
useEffect,
useRef,
useState,
} from "react";
import { useManageFocusOnTarget } from "./internal/useManageFocusOnTarget";

import skipLinkCss from "./SkipLink.css";

interface SkipLinkProps extends LinkProps {
interface SkipLinkProps extends ComponentPropsWithoutRef<"a"> {
/**
* This is a ref that has access to the target element.
*
* This will be used to apply focus to that element
*
* Refs are referentially stable so if this changes it won't be picked up
* will need to find a better way of passing in the target element to apply the attributes
* The ID of the target element to apply focus when the link is clicked.
* If the element with this ID is not found, the SkipLink will not be rendered.
*/
targetRef?: RefObject<HTMLElement>;
target: string;
}
Fercas123 marked this conversation as resolved.
Show resolved Hide resolved

const withBaseName = makePrefixer("saltSkipLink");

export const SkipLink = forwardRef<HTMLAnchorElement, SkipLinkProps>(
function SkipLink({ className, targetRef, ...rest }, ref) {
function SkipLink({ className, target, children, ...rest }, ref) {
const [isTargetAvailable, setIsTargetAvailable] = useState(false);
const targetWindow = useWindow();
useComponentCssInjection({
testId: "salt-skip-link",
css: skipLinkCss,
window: targetWindow,
});

const targetClass = clsx(withBaseName("target"), className);
const targetRef = useRef<HTMLElement | null>(null);

const eventHandlers = useManageFocusOnTarget({ targetRef, targetClass });
useEffect(() => {
targetRef.current = document.getElementById(target);
setIsTargetAvailable(!!targetRef.current);
}, [target]);

const eventHandlers = useManageFocusOnTarget({
targetRef,
Fercas123 marked this conversation as resolved.
Show resolved Hide resolved
targetClass: withBaseName("target"),
});

if (!isTargetAvailable) return null;
return (
<li>
<Link
{...eventHandlers}
{...rest}
className={clsx(withBaseName(), className)}
ref={ref}
/>
</li>
<a
className={clsx(withBaseName(), className)}
href={`#${target}`}
ref={ref}
target="_self"
{...eventHandlers}
{...rest}
>
Fercas123 marked this conversation as resolved.
Show resolved Hide resolved
{children}
</a>
);
},
);
7 changes: 0 additions & 7 deletions packages/lab/src/skip-link/SkipLinks.css

This file was deleted.

29 changes: 0 additions & 29 deletions packages/lab/src/skip-link/SkipLinks.tsx

This file was deleted.

1 change: 0 additions & 1 deletion packages/lab/src/skip-link/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1 @@
export * from "./SkipLink";
export * from "./SkipLinks";
26 changes: 19 additions & 7 deletions packages/lab/stories/skip-link/skip-link.stories.css
Original file line number Diff line number Diff line change
@@ -1,9 +1,21 @@
.saltTargetBlankNoIcon-Link .saltLink-icon {
display: none;
}
.header {
padding-left: var(--salt-spacing-300);
padding-right: var(--salt-spacing-300);
background-color: var(--salt-container-primary-background);
position: fixed;
width: 100%;

.goalsList {
font-size: var(--salt-text-fontSize);
font-family: var(--salt-text-fontFamily);
line-height: var(--salt-text-lineHeight);
border-bottom: var(--salt-size-border) var(--salt-container-borderStyle) var(--salt-separable-primary-borderColor);
}
.navigation {
display: flex;
list-style: none;
padding: 0;
margin: 0;
}
.center {
margin: calc(var(--salt-spacing-300) * 2);
}
.article {
max-width: var(--max-content-width);
}
Loading
Loading