diff --git a/cypress/e2e/design-system-page.cy.ts b/cypress/e2e/design-system-page.cy.ts new file mode 100644 index 000000000..4b37f18cf --- /dev/null +++ b/cypress/e2e/design-system-page.cy.ts @@ -0,0 +1,72 @@ +import { BASE_COMMANDS, SELECT_COMMANDS } from '../utils/commands'; +import { gherkin } from '../utils/gherkin'; + +describe(`Design System Components`, () => { + const buttonVariantsContainerId = `button-variants-container`; + + const given = gherkin({ + ...BASE_COMMANDS, + ...SELECT_COMMANDS, + 'I navigate to design system page': () => { + cy.visit(`/design-system`); + }, + 'I take screenshot of button variants': () => { + cy.get(`[data-testid="${buttonVariantsContainerId}"]`).scrollIntoView(); + BASE_COMMANDS[`System takes element picture`]( + `[data-testid="${buttonVariantsContainerId}"]`, + `button-variants-screenshot`, + ); + }, + 'I take screenshot of individual button': (label: string) => { + cy.contains(label).scrollIntoView(); + cy.contains(label) + .parent() + .find(`button`) + .should(`be.visible`) + .then(($button) => { + const buttonId = `button-${label.replace(/[^a-zA-Z0-9]/g, `-`).toLowerCase()}`; + cy.wrap($button).invoke(`attr`, `data-test-button-id`, buttonId); + BASE_COMMANDS[`System takes element picture`]( + `[data-test-button-id="${buttonId}"]`, + `button-variant-${label.replace(/[^a-zA-Z0-9]/g, `-`).toLowerCase()}`, + ); + cy.wrap($button).invoke(`removeAttr`, `data-test-button-id`); + }); + }, + }); + + it(`captures screenshots of all button variants`, () => { + given(`System has accepted cookies`) + .and(`I set white theme`) + .when(`I navigate to design system page`) + .then(`I take screenshot of button variants`); + + const buttonLabels = [ + `s=1, i=1`, + `s=1, i=2`, + `s=2, i=1`, + `s=2, i=2`, + `auto s=1, i=1`, + `auto s=1, i=2`, + `auto s=2, i=1`, + `auto s=2, i=2`, + `pill s=1, i=2`, + `pill s=2, i=2`, + `disabled`, + ]; + + buttonLabels.forEach((label) => { + given(`I take screenshot of individual button`, label); + }); + }); + + it(`verifies select component behavior`, () => { + given(`System has accepted cookies`) + .and(`I set white theme`) + .when(`I navigate to design system page`) + .and(`I select from`, `combobox`, `Banana`) + .then(`I verify select value`, `combobox`, `Banana`) + .when(`I select from`, `combobox`, `Apple`) + .then(`I verify select value`, `combobox`, `Apple`); + }); +}); diff --git a/cypress/snapshots/design-system-button.cy.ts/button-variant-auto-s-1--i-1.snap.png b/cypress/snapshots/design-system-button.cy.ts/button-variant-auto-s-1--i-1.snap.png new file mode 100644 index 000000000..6c07a6e20 Binary files /dev/null and b/cypress/snapshots/design-system-button.cy.ts/button-variant-auto-s-1--i-1.snap.png differ diff --git a/cypress/snapshots/design-system-button.cy.ts/button-variant-auto-s-1--i-2.snap.png b/cypress/snapshots/design-system-button.cy.ts/button-variant-auto-s-1--i-2.snap.png new file mode 100644 index 000000000..079480a13 Binary files /dev/null and b/cypress/snapshots/design-system-button.cy.ts/button-variant-auto-s-1--i-2.snap.png differ diff --git a/cypress/snapshots/design-system-button.cy.ts/button-variant-auto-s-2--i-1.snap.png b/cypress/snapshots/design-system-button.cy.ts/button-variant-auto-s-2--i-1.snap.png new file mode 100644 index 000000000..dc6e1d176 Binary files /dev/null and b/cypress/snapshots/design-system-button.cy.ts/button-variant-auto-s-2--i-1.snap.png differ diff --git a/cypress/snapshots/design-system-button.cy.ts/button-variant-auto-s-2--i-2.snap.png b/cypress/snapshots/design-system-button.cy.ts/button-variant-auto-s-2--i-2.snap.png new file mode 100644 index 000000000..68b01b0ad Binary files /dev/null and b/cypress/snapshots/design-system-button.cy.ts/button-variant-auto-s-2--i-2.snap.png differ diff --git a/cypress/snapshots/design-system-button.cy.ts/button-variant-disabled.snap.png b/cypress/snapshots/design-system-button.cy.ts/button-variant-disabled.snap.png new file mode 100644 index 000000000..2ad1eb779 Binary files /dev/null and b/cypress/snapshots/design-system-button.cy.ts/button-variant-disabled.snap.png differ diff --git a/cypress/snapshots/design-system-button.cy.ts/button-variant-pill-s-1--i-2.snap.png b/cypress/snapshots/design-system-button.cy.ts/button-variant-pill-s-1--i-2.snap.png new file mode 100644 index 000000000..c35083d38 Binary files /dev/null and b/cypress/snapshots/design-system-button.cy.ts/button-variant-pill-s-1--i-2.snap.png differ diff --git a/cypress/snapshots/design-system-button.cy.ts/button-variant-pill-s-2--i-2.snap.png b/cypress/snapshots/design-system-button.cy.ts/button-variant-pill-s-2--i-2.snap.png new file mode 100644 index 000000000..a21dcb291 Binary files /dev/null and b/cypress/snapshots/design-system-button.cy.ts/button-variant-pill-s-2--i-2.snap.png differ diff --git a/cypress/snapshots/design-system-button.cy.ts/button-variant-s-1--i-1.snap.png b/cypress/snapshots/design-system-button.cy.ts/button-variant-s-1--i-1.snap.png new file mode 100644 index 000000000..55a6909a9 Binary files /dev/null and b/cypress/snapshots/design-system-button.cy.ts/button-variant-s-1--i-1.snap.png differ diff --git a/cypress/snapshots/design-system-button.cy.ts/button-variant-s-1--i-2.snap.png b/cypress/snapshots/design-system-button.cy.ts/button-variant-s-1--i-2.snap.png new file mode 100644 index 000000000..24591e43e Binary files /dev/null and b/cypress/snapshots/design-system-button.cy.ts/button-variant-s-1--i-2.snap.png differ diff --git a/cypress/snapshots/design-system-button.cy.ts/button-variant-s-2--i-1.snap.png b/cypress/snapshots/design-system-button.cy.ts/button-variant-s-2--i-1.snap.png new file mode 100644 index 000000000..2b3ef7ee8 Binary files /dev/null and b/cypress/snapshots/design-system-button.cy.ts/button-variant-s-2--i-1.snap.png differ diff --git a/cypress/snapshots/design-system-button.cy.ts/button-variant-s-2--i-2.snap.png b/cypress/snapshots/design-system-button.cy.ts/button-variant-s-2--i-2.snap.png new file mode 100644 index 000000000..2fe6172f7 Binary files /dev/null and b/cypress/snapshots/design-system-button.cy.ts/button-variant-s-2--i-2.snap.png differ diff --git a/cypress/snapshots/design-system-button.cy.ts/button-variants-screenshot.snap.png b/cypress/snapshots/design-system-button.cy.ts/button-variants-screenshot.snap.png new file mode 100644 index 000000000..8b4dd9931 Binary files /dev/null and b/cypress/snapshots/design-system-button.cy.ts/button-variants-screenshot.snap.png differ diff --git a/cypress/snapshots/design-system-page.cy.ts/button-variant-auto-s-1--i-1.snap.png b/cypress/snapshots/design-system-page.cy.ts/button-variant-auto-s-1--i-1.snap.png new file mode 100644 index 000000000..6c07a6e20 Binary files /dev/null and b/cypress/snapshots/design-system-page.cy.ts/button-variant-auto-s-1--i-1.snap.png differ diff --git a/cypress/snapshots/design-system-page.cy.ts/button-variant-auto-s-1--i-2.snap.png b/cypress/snapshots/design-system-page.cy.ts/button-variant-auto-s-1--i-2.snap.png new file mode 100644 index 000000000..a4b385f84 Binary files /dev/null and b/cypress/snapshots/design-system-page.cy.ts/button-variant-auto-s-1--i-2.snap.png differ diff --git a/cypress/snapshots/design-system-page.cy.ts/button-variant-auto-s-2--i-1.snap.png b/cypress/snapshots/design-system-page.cy.ts/button-variant-auto-s-2--i-1.snap.png new file mode 100644 index 000000000..727b52460 Binary files /dev/null and b/cypress/snapshots/design-system-page.cy.ts/button-variant-auto-s-2--i-1.snap.png differ diff --git a/cypress/snapshots/design-system-page.cy.ts/button-variant-auto-s-2--i-2.snap.png b/cypress/snapshots/design-system-page.cy.ts/button-variant-auto-s-2--i-2.snap.png new file mode 100644 index 000000000..68b01b0ad Binary files /dev/null and b/cypress/snapshots/design-system-page.cy.ts/button-variant-auto-s-2--i-2.snap.png differ diff --git a/cypress/snapshots/design-system-page.cy.ts/button-variant-disabled.snap.png b/cypress/snapshots/design-system-page.cy.ts/button-variant-disabled.snap.png new file mode 100644 index 000000000..2ad1eb779 Binary files /dev/null and b/cypress/snapshots/design-system-page.cy.ts/button-variant-disabled.snap.png differ diff --git a/cypress/snapshots/design-system-page.cy.ts/button-variant-pill-s-1--i-2.snap.png b/cypress/snapshots/design-system-page.cy.ts/button-variant-pill-s-1--i-2.snap.png new file mode 100644 index 000000000..dc32b2b3a Binary files /dev/null and b/cypress/snapshots/design-system-page.cy.ts/button-variant-pill-s-1--i-2.snap.png differ diff --git a/cypress/snapshots/design-system-page.cy.ts/button-variant-pill-s-2--i-2.snap.png b/cypress/snapshots/design-system-page.cy.ts/button-variant-pill-s-2--i-2.snap.png new file mode 100644 index 000000000..cdbcebb14 Binary files /dev/null and b/cypress/snapshots/design-system-page.cy.ts/button-variant-pill-s-2--i-2.snap.png differ diff --git a/cypress/snapshots/design-system-page.cy.ts/button-variant-s-1--i-1.snap.png b/cypress/snapshots/design-system-page.cy.ts/button-variant-s-1--i-1.snap.png new file mode 100644 index 000000000..3e0253e34 Binary files /dev/null and b/cypress/snapshots/design-system-page.cy.ts/button-variant-s-1--i-1.snap.png differ diff --git a/cypress/snapshots/design-system-page.cy.ts/button-variant-s-1--i-2.snap.png b/cypress/snapshots/design-system-page.cy.ts/button-variant-s-1--i-2.snap.png new file mode 100644 index 000000000..24591e43e Binary files /dev/null and b/cypress/snapshots/design-system-page.cy.ts/button-variant-s-1--i-2.snap.png differ diff --git a/cypress/snapshots/design-system-page.cy.ts/button-variant-s-2--i-1.snap.png b/cypress/snapshots/design-system-page.cy.ts/button-variant-s-2--i-1.snap.png new file mode 100644 index 000000000..c4d704331 Binary files /dev/null and b/cypress/snapshots/design-system-page.cy.ts/button-variant-s-2--i-1.snap.png differ diff --git a/cypress/snapshots/design-system-page.cy.ts/button-variant-s-2--i-2.snap.png b/cypress/snapshots/design-system-page.cy.ts/button-variant-s-2--i-2.snap.png new file mode 100644 index 000000000..9f26e0218 Binary files /dev/null and b/cypress/snapshots/design-system-page.cy.ts/button-variant-s-2--i-2.snap.png differ diff --git a/cypress/snapshots/design-system-page.cy.ts/button-variants-screenshot.snap.png b/cypress/snapshots/design-system-page.cy.ts/button-variants-screenshot.snap.png new file mode 100644 index 000000000..5b7c4f216 Binary files /dev/null and b/cypress/snapshots/design-system-page.cy.ts/button-variants-screenshot.snap.png differ diff --git a/cypress/utils/commands.ts b/cypress/utils/commands.ts index 4f74a494e..8ac373542 100644 --- a/cypress/utils/commands.ts +++ b/cypress/utils/commands.ts @@ -249,5 +249,24 @@ const BASE_COMMANDS = { }, } as const; +export const SELECT_COMMANDS = { + 'I select from': (selectRole: string, optionLabel: string) => { + cy.get(`[role="${selectRole}"]`).first().click(); + cy.get(`[role="listbox"]`).contains(optionLabel).click(); + }, + 'I verify select value': (selectRole: string, value: string) => { + cy.get(`[role="${selectRole}"]`).contains(value).should(`be.visible`); + }, + 'I press key on': (selectRole: string, key: string) => { + cy.get(`[role="${selectRole}"]`).type(key); + }, + 'I verify select option': (option: string) => { + cy.get(`[role="listbox"]`).contains(option).should(`be.visible`); + }, + 'I verify disabled select': () => { + cy.get(`[role="combobox"][aria-disabled="true"]`).should(`exist`); + }, +} as const; + export type { Element, ClickableControls }; export { BASE_COMMANDS }; diff --git a/meta.ts b/meta.ts index 6b09134ff..ef8a24421 100644 --- a/meta.ts +++ b/meta.ts @@ -39,5 +39,6 @@ export const meta = { }, notFound: `/404/`, privacyPolicy: `/privacy-policy/`, + designSystem: `/design-system/`, }, } as const; diff --git a/src/containers/app-footer.container.tsx b/src/containers/app-footer.container.tsx index 4b2a92314..7ad1c110f 100644 --- a/src/containers/app-footer.container.tsx +++ b/src/containers/app-footer.container.tsx @@ -136,6 +136,13 @@ const AppFooterContainer = () => { > Education Zone + + Design System + { + return ( + + + Design System + + ); +}; + +export { DesignSystemLinkContainer }; diff --git a/src/design-system/select.tsx b/src/design-system/select.tsx new file mode 100644 index 000000000..0b3c0d0df --- /dev/null +++ b/src/design-system/select.tsx @@ -0,0 +1,255 @@ +'use client'; + +import React, { + type HTMLAttributes, + type DetailedHTMLProps, + type ReactNode, +} from 'react'; +import c from 'classnames'; +import { BiChevronDown, BiChevronUp, BiCheck } from 'react-icons/bi'; +import { useSimpleFeature } from '@greenonsoftware/react-kit'; + +export interface SelectOption { + value: string; + label: string; +} + +interface SelectProps + extends Omit< + DetailedHTMLProps, HTMLDivElement>, + 'onChange' | 'value' + > { + options: SelectOption[]; + value?: string; + onChange?: (value: string) => void; + placeholder?: string; + disabled?: boolean; +} + +const Select = ({ + className, + options, + value, + onChange, + placeholder = `Select an option`, + disabled, + ...props +}: SelectProps) => { + const dropdownMenu = useSimpleFeature(); + const [highlightedIndex, setHighlightedIndex] = React.useState(0); + const containerRef = React.useRef(null); + const listboxRef = React.useRef(null); + + const selectedOption = options.find((option) => option.value === value); + + const toggleOpen = React.useCallback( + (_: React.MouseEvent): void => { + if (!disabled) { + dropdownMenu.toggle(); + } + }, + [disabled, dropdownMenu], + ); + + const handleOptionClick = React.useCallback( + (option: SelectOption) => { + onChange?.(option.value); + dropdownMenu.off(); + }, + [onChange, dropdownMenu], + ); + + const handleKeyDown = React.useCallback( + (e: React.KeyboardEvent): void => { + if (disabled) return; + + switch (e.key) { + case `Enter`: + case ` `: + if (!dropdownMenu.isOn) { + dropdownMenu.on(); + } else { + const option = options[highlightedIndex]; + if (option) { + handleOptionClick(option); + } + } + e.preventDefault(); + break; + case `ArrowDown`: + case `ArrowRight`: + if (dropdownMenu.isOn) { + setHighlightedIndex((prev) => + prev < options.length - 1 ? prev + 1 : prev, + ); + } else { + dropdownMenu.on(); + } + e.preventDefault(); + break; + case `ArrowUp`: + case `ArrowLeft`: + if (dropdownMenu.isOn) { + setHighlightedIndex((prev) => (prev > 0 ? prev - 1 : prev)); + } else { + dropdownMenu.on(); + } + e.preventDefault(); + break; + case `Escape`: + dropdownMenu.off(); + e.preventDefault(); + break; + case `Tab`: + if (dropdownMenu.isOn) { + dropdownMenu.off(); + } + break; + } + }, + [options, highlightedIndex, disabled, handleOptionClick, dropdownMenu], + ); + + React.useEffect(() => { + if (dropdownMenu.isOn && listboxRef.current) { + const highlightedElement = listboxRef.current.children[ + highlightedIndex + ] as HTMLElement; + if (highlightedElement) { + highlightedElement.scrollIntoView({ block: `nearest` }); + } + } + }, [dropdownMenu.isOn, highlightedIndex]); + + // Close dropdown when clicking outside + React.useEffect(() => { + const handleClickOutside = (e: MouseEvent) => { + if ( + containerRef.current && + !containerRef.current.contains(e.target as Node) + ) { + dropdownMenu.off(); + } + }; + + if (dropdownMenu.isOn) { + document.addEventListener(`mousedown`, handleClickOutside); + } + + return () => { + document.removeEventListener(`mousedown`, handleClickOutside); + }; + }, [dropdownMenu.isOn, dropdownMenu]); + + React.useEffect(() => { + if (dropdownMenu.isOn) { + const selectedIndex = options.findIndex( + (option) => option.value === value, + ); + setHighlightedIndex(selectedIndex >= 0 ? selectedIndex : 0); + } + }, [dropdownMenu.isOn, options, value]); + + return ( +
+
+ + {selectedOption ? selectedOption.label : placeholder} + + + {dropdownMenu.isOn ? ( + + ) : ( + + )} + +
+ + {dropdownMenu.isOn && ( +
    + {options.map((option, index) => ( +
  • handleOptionClick(option)} + role="option" + aria-selected={option.value === value} + > +
    + {option.label} + {option.value === value && } +
    +
  • + ))} +
+ )} +
+ ); +}; + +export const SelectField = ({ + className, + label, + hint, + ...props +}: { + className?: string; + label?: ReactNode; + hint?: ReactNode; +} & SelectProps) => { + return ( +
+ {label && ( + + )} + + + + +
+

Selected value: {value || `None`}

+

Try using keyboard navigation:

+
    +
  • Tab to focus the select
  • +
  • Space/Enter to open dropdown
  • +
  • Arrow keys to navigate options
  • +
  • Enter to select an option
  • +
  • Escape to close dropdown
  • +
+
+ + ); +}; + +const DesignSystemPage = () => { + return ( + <> + + + + + + + + + + + ); +}; +export default DesignSystemPage; + +export const Head: HeadFC = () => { + return ( + + ); +};