diff --git a/.changeset/gentle-hairs-repair.md b/.changeset/gentle-hairs-repair.md new file mode 100644 index 00000000000..67b06a5f3e8 --- /dev/null +++ b/.changeset/gentle-hairs-repair.md @@ -0,0 +1,9 @@ +--- +"@salt-ds/lab": patch +--- + +Updates to Lab `SkipLink` + +- Deprecated `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. diff --git a/packages/lab/src/__tests__/__e2e__/skip-link/SkipLink.cy.tsx b/packages/lab/src/__tests__/__e2e__/skip-link/SkipLink.cy.tsx index 2f9bf628278..4c053148907 100644 --- a/packages/lab/src/__tests__/__e2e__/skip-link/SkipLink.cy.tsx +++ b/packages/lab/src/__tests__/__e2e__/skip-link/SkipLink.cy.tsx @@ -1,4 +1,3 @@ -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"; @@ -6,61 +5,6 @@ import { checkAccessibility } from "../../../../../../cypress/tests/checkAccessi const composedStories = composeStories(skipLinkStories); const { Default, MultipleLinks } = composedStories; -const NoTargetRef = () => { - return ( - <> -
- - Click here and press the Tab key to see the Skip Link - -
- - - Skip to main content - - -
- What we do -
- -
-
-

Salt

-

- 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. -

-
-
-

Goals

-

Salt has been developed with the following design goals:

-
    -
  • - Providing a comprehensive set of commonly-used UI controls -
  • -
  • Complying with WCAG 2.1 accessibility guidelines
  • -
  • To be lightweight and performant
  • -
  • Offering flexible styling and theming support
  • -
  • Minimizing dependencies on third-party libraries
  • -
-
-
-
-
- - ); -}; - describe("GIVEN a SkipLink", () => { checkAccessibility(composedStories); describe("WHEN there is a single SkipLink", () => { @@ -69,23 +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"); + 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(); + it("THEN it should hide the skip link if ref is broken", () => { + cy.mount(); 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"); + cy.findByRole("link", { name: "Skip to main content" }).should( + "not.exist", + ); }); }); @@ -96,11 +46,25 @@ describe("GIVEN a SkipLink", () => { "Click here and press the Tab key to see the Skip Link", ).click(); cy.realPress("Tab"); + cy.findByRole("link", { name: "Skip to Introduction" }).should( + "be.visible", + ); + cy.findByRole("link", { name: "Skip to Introduction" }).should( + "have.css", + "left", + "0px", + ); + cy.findByRole("link", { name: "Skip to Goals" }).should("not.be.visible"); cy.realPress("Tab"); + cy.findByRole("link", { name: "Skip to Goals" }).should("be.visible"); + cy.findByRole("link", { name: "Skip to Goals" }).should( + "have.css", + "left", + "0px", + ); cy.realPress("Enter"); cy.get("#goals").should("be.focused"); - cy.get("#introduction").should("not.be.focused"); }); }); }); diff --git a/packages/lab/src/skip-link/SkipLink.css b/packages/lab/src/skip-link/SkipLink.css index da55c1c1650..57ac2f1cb29 100644 --- a/packages/lab/src/skip-link/SkipLink.css +++ b/packages/lab/src/skip-link/SkipLink.css @@ -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); } diff --git a/packages/lab/src/skip-link/SkipLink.tsx b/packages/lab/src/skip-link/SkipLink.tsx index 3c8359c2ff5..cab72b388c1 100644 --- a/packages/lab/src/skip-link/SkipLink.tsx +++ b/packages/lab/src/skip-link/SkipLink.tsx @@ -1,28 +1,29 @@ -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, +} 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; + target: string; } const withBaseName = makePrefixer("saltSkipLink"); export const SkipLink = forwardRef( - function SkipLink({ className, targetRef, ...rest }, ref) { + function SkipLink({ className, target, children, ...rest }, ref) { const targetWindow = useWindow(); useComponentCssInjection({ testId: "salt-skip-link", @@ -30,19 +31,30 @@ export const SkipLink = forwardRef( window: targetWindow, }); - const targetClass = clsx(withBaseName("target"), className); + const targetRef = useRef(null); - const eventHandlers = useManageFocusOnTarget({ targetRef, targetClass }); + useEffect(() => { + targetRef.current = document.getElementById(target); + }, [target]); + + const eventHandlers = useManageFocusOnTarget({ + targetRef, + targetClass: withBaseName("target"), + }); return ( -
  • - -
  • + target="_self" + {...eventHandlers} + {...rest} + > + {children} + + ) ); }, ); diff --git a/packages/lab/src/skip-link/SkipLinks.css b/packages/lab/src/skip-link/SkipLinks.css deleted file mode 100644 index 763cbb7604a..00000000000 --- a/packages/lab/src/skip-link/SkipLinks.css +++ /dev/null @@ -1,7 +0,0 @@ -.saltSkipLinks { - position: relative; - float: left; - list-style: none; - margin: 0; - padding: 0; -} diff --git a/packages/lab/src/skip-link/SkipLinks.tsx b/packages/lab/src/skip-link/SkipLinks.tsx deleted file mode 100644 index 7a0b8e83804..00000000000 --- a/packages/lab/src/skip-link/SkipLinks.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import { makePrefixer } from "@salt-ds/core"; -import { clsx } from "clsx"; -import { type HTMLAttributes, forwardRef } from "react"; - -import { useComponentCssInjection } from "@salt-ds/styles"; -import { useWindow } from "@salt-ds/window"; - -import skipLinksCss from "./SkipLinks.css"; - -const withBaseName = makePrefixer("saltSkipLinks"); - -export const SkipLinks = forwardRef< - HTMLUListElement, - HTMLAttributes ->(function SkipLinks(props, ref) { - const { className, children, ...restProps } = props; - const targetWindow = useWindow(); - useComponentCssInjection({ - testId: "salt-skip-links", - css: skipLinksCss, - window: targetWindow, - }); - - return ( -
      - {children} -
    - ); -}); diff --git a/packages/lab/src/skip-link/index.ts b/packages/lab/src/skip-link/index.ts index 1de755de460..cb4f10ea24e 100644 --- a/packages/lab/src/skip-link/index.ts +++ b/packages/lab/src/skip-link/index.ts @@ -1,2 +1 @@ export * from "./SkipLink"; -export * from "./SkipLinks"; diff --git a/packages/lab/stories/skip-link/skip-link.stories.tsx b/packages/lab/stories/skip-link/skip-link.stories.tsx index 1f264821f8d..48c43046be9 100644 --- a/packages/lab/stories/skip-link/skip-link.stories.tsx +++ b/packages/lab/stories/skip-link/skip-link.stories.tsx @@ -1,132 +1,149 @@ -import { Button } from "@salt-ds/core"; -import { SkipLink, SkipLinks } from "@salt-ds/lab"; +import { + BorderItem, + BorderLayout, + Button, + FlexItem, + FlexLayout, + NavigationItem, + SplitLayout, + StackLayout, +} from "@salt-ds/core"; +import { GithubIcon, StackoverflowIcon, SymphonyIcon } from "@salt-ds/icons"; +import { SkipLink } from "@salt-ds/lab"; import type { Meta, StoryFn } from "@storybook/react"; -import { useRef } from "react"; -import "./skip-link.stories.css"; +import { useEffect, useState } from "react"; export default { title: "Lab/Skip Link", component: SkipLink, } as Meta; -export const Default: StoryFn = () => { - const articleRef = useRef(null); - +const Item = () => { return ( - <> - - Click here and press the Tab key to see the Skip Link - -
    - - - Skip to main content - - - -
    - What we do -
    - -
    -
    -

    Salt

    -

    - 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. -

    -
    -
    -

    Goals

    -

    Salt has been developed with the following design goals:

    -
      -
    • - Providing a comprehensive set of commonly-used UI controls -
    • -
    • Complying with WCAG 2.1 accessibility guidelines
    • -
    • To be lightweight and performant
    • -
    • Offering flexible styling and theming support
    • -
    • Minimizing dependencies on third-party libraries
    • -
    -
    -
    -
    - -
    -
    - +
    ); }; -export const MultipleLinks: StoryFn = () => { - const sectionRef1 = useRef(null); - const sectionRef2 = useRef(null); +const DefaultStory: StoryFn = (args) => { + const headerItems = ["Home", "Transactions", "FX", "Credit Manager"]; - return ( - <> - - Click here and press the Tab key to see the Skip Link - -
    - - - Skip to Introduction - - - Skip to Goals - - + const headerUtilities = [ + { + icon: , + key: "Symphony", + }, + { + icon: , + key: "Stack Overflow", + }, + { + icon: , + key: "GitHub", + }, + ]; + + const [activeHeaderNav, setActiveHeaderNav] = useState(headerItems[0]); + const [offset, setOffset] = useState(0); -
    - What we do -
    + const setScroll = () => { + setOffset(window.scrollY); + }; -
    -
    -

    Salt

    -

    - 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. -

    -
    -
    -

    Goals

    -

    Salt has been developed with the following design goals:

    -
      -
    • - Providing a comprehensive set of commonly-used UI controls -
    • -
    • Complying with WCAG 2.1 accessibility guidelines
    • -
    • To be lightweight and performant
    • -
    • Offering flexible styling and theming support
    • -
    • Minimizing dependencies on third-party libraries
    • -
    -
    + useEffect(() => { + window.addEventListener("scroll", setScroll); + return () => { + window.removeEventListener("scroll", setScroll); + }; + }, []); + + return ( + + +
    + 0 ? "var(--salt-overlayable-shadow-scroll)" : "none", + borderBottom: + "var(--salt-size-border) var(--salt-container-borderStyle) var(--salt-separable-primary-borderColor)", + }} + justify="space-between" + gap={3} + > + + Click here and press the Tab key to see the Skip Link + + + + + {headerUtilities?.map((utility) => ( + + ))} + + + +
    +
    + +
    + + +
    -
    - -
    -
    - + Next} /> + + ); }; + +export const Default = DefaultStory.bind({}); +Default.args = { + target: "main", +}; +Default.parameters = { + layout: "fullscreen", +};