diff --git a/components/common/Segment/index.tsx b/components/common/Segment/index.tsx index 3135f88f..cfc0b3f6 100644 --- a/components/common/Segment/index.tsx +++ b/components/common/Segment/index.tsx @@ -6,11 +6,20 @@ interface Props { groupName: string name: string selected: boolean + tabIndex?: number children: string onClick: (event: React.ChangeEvent) => void } const Segment: React.FC = (props: Props) => { + // Selects the segment when the user presses the spacebar + const handleKeyDown = (event: React.KeyboardEvent) => { + if (event.key === ' ') { + event.preventDefault() + event.currentTarget.click() + } + } + return (
= (props: Props) => { checked={props.selected} onChange={props.onClick} /> - +
) } diff --git a/components/common/SegmentedControl/index.tsx b/components/common/SegmentedControl/index.tsx index 6e82351b..edb6534f 100644 --- a/components/common/SegmentedControl/index.tsx +++ b/components/common/SegmentedControl/index.tsx @@ -6,12 +6,14 @@ interface Props { className?: string elementClass?: string blended?: boolean + tabIndex?: number } const SegmentedControl: React.FC = ({ className, elementClass, blended, + tabIndex, children, }) => { const classes = classNames( @@ -23,7 +25,7 @@ const SegmentedControl: React.FC = ({ elementClass ) return ( -
+
{children}
) diff --git a/components/common/SelectItem/index.scss b/components/common/SelectItem/index.scss index a7efa35f..ce481d8a 100644 --- a/components/common/SelectItem/index.scss +++ b/components/common/SelectItem/index.scss @@ -8,7 +8,8 @@ font-size: $font-regular; padding: ($unit * 1.5) $unit-2x; - &:hover { + &:hover, + &:focus { background-color: var(--option-bg-hover); color: var(--text-primary); cursor: pointer; diff --git a/components/raids/RaidCombobox/index.tsx b/components/raids/RaidCombobox/index.tsx index 306842a5..df3f4540 100644 --- a/components/raids/RaidCombobox/index.tsx +++ b/components/raids/RaidCombobox/index.tsx @@ -1,4 +1,4 @@ -import { createRef, useCallback, useEffect, useState } from 'react' +import { createRef, useCallback, useEffect, useState, useRef } from 'react' import { useRouter } from 'next/router' import { useTranslation } from 'react-i18next' import classNames from 'classnames' @@ -16,7 +16,6 @@ import { appState } from '~utils/appState' interface Props { showAllRaidsOption: boolean currentRaid?: Raid - currentRaidSlug?: string defaultRaid?: Raid minimal?: boolean onChange?: (raid?: Raid) => void @@ -30,6 +29,7 @@ import CrossIcon from '~public/icons/Cross.svg' import './index.scss' const NUM_SECTIONS = 3 +const NUM_ELEMENTS = 5 enum Sort { ASCENDING, @@ -54,9 +54,12 @@ const RaidCombobox = (props: Props) => { const [query, setQuery] = useState('') const [sections, setSections] = useState() const [currentRaid, setCurrentRaid] = useState() + const [tabIndex, setTabIndex] = useState(NUM_ELEMENTS + 1) // Refs const listRef = createRef() + const inputRef = createRef() + const sortButtonRef = createRef() // ---------------------------------------------- // Methods: Lifecycle Hooks @@ -75,12 +78,13 @@ const RaidCombobox = (props: Props) => { } }, []) - // Update current raid when the currentRaidSlug prop changes + // Set current raid and section when the current raid changes useEffect(() => { - if (props.currentRaidSlug) { - setCurrentRaid(slugToRaid(props.currentRaidSlug)) + if (props.currentRaid) { + setCurrentRaid(props.currentRaid) + setCurrentSection(props.currentRaid.group.section) } - }) + }, [props.currentRaid]) // Scroll to the top of the list when the user switches tabs useEffect(() => { @@ -89,10 +93,70 @@ const RaidCombobox = (props: Props) => { } }, [currentSection]) + useEffect(() => { + setTabIndex(NUM_ELEMENTS + 1) + }, [currentSection]) + // ---------------------------------------------- // Methods: Event Handlers // ---------------------------------------------- + // Handle Escape key press event + const handleEscapeKeyPressed = useCallback(() => { + if (listRef.current) { + listRef.current.focus() + } + }, [open, currentRaid, sortButtonRef]) + + // Handle Arrow key press event by focusing the list item above or below the current one based on the direction + const handleArrowKeyPressed = useCallback( + (direction: 'Up' | 'Down') => { + if (!listRef.current) return + + // Get the currently focused item + const current = listRef.current.querySelector('.Raid:focus') + + // Select the item above or below based on direction + if (current) { + // If there is no item below, select the next parent element and then select the first element in that group + if (direction === 'Down' && !current.nextElementSibling) { + const nextParent = + current.parentElement?.parentElement?.nextElementSibling + if (nextParent) { + const next = nextParent.querySelector('.Raid') + if (next) { + ;(next as HTMLElement).focus() + } + } + } + + // If there is no item above, select the previous parent element and then select the first element in that group + if (direction === 'Up' && !current.previousElementSibling) { + const previousParent = + current.parentElement?.parentElement?.previousElementSibling + if (previousParent) { + const next = previousParent.querySelector('.Raid:last-child') + if (next) { + ;(next as HTMLElement).focus() + } + } + } + } + + // Select the item above or below based on direction + if (current) { + const next = + direction === 'Up' + ? current.previousElementSibling + : current.nextElementSibling + if (next) { + ;(next as HTMLElement).focus() + } + } + }, + [open, currentRaid, listRef] + ) + // Scroll to an item in the list when it is selected const scrollToItem = useCallback( (node) => { @@ -101,6 +165,8 @@ const RaidCombobox = (props: Props) => { const { top: itemTop } = node.getBoundingClientRect() listRef.current.scrollTop = itemTop - listTop + console.log('Focusing node') + node.focus() setScrolled(true) } }, @@ -127,6 +193,42 @@ const RaidCombobox = (props: Props) => { [setSections] ) + const handleSortButtonKeyDown = ( + event: React.KeyboardEvent + ) => { + // If the tab key is pressed without the Shift key, focus the raid list + if (event.key === 'Tab' && !event.shiftKey) { + if (listRef.current) { + listRef.current.focus() + } + } + } + + const handleListKeyDown = (event: React.KeyboardEvent) => { + if (event.key === 'Tab' && !event.shiftKey) { + event.preventDefault() + if (inputRef.current) { + inputRef.current.focus() + } + } else if (event.key === 'Tab' && event.shiftKey) { + event.preventDefault() + if (sortButtonRef.current) { + sortButtonRef.current.focus() + } + } + + // If the enter key is pressed, focus the first raid item in the list + else if (event.key === 'Enter') { + event.preventDefault() + if (listRef.current) { + const raid = listRef.current.querySelector('.Raid') + if (raid) { + ;(raid as HTMLElement).focus() + } + } + } + } + // Handle value change for the raid selection function handleValueChange(raid: Raid) { setCurrentRaid(raid) @@ -219,6 +321,13 @@ const RaidCombobox = (props: Props) => { const isRef = isSelected ? scrollToItem : undefined const imageUrl = `${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/raids/${raid.slug}.png` + const sectionIndex = sections?.[currentSection - 1]?.findIndex((group) => + group.raids.some((r) => r.id === raid.id) + ) + + const raidTabIndex = + sectionIndex !== undefined ? tabIndex + sectionIndex : -1 + return ( { key={key} selected={isSelected} ref={isRef} + tabIndex={0} value={raid.slug} + onEscapeKeyPressed={handleEscapeKeyPressed} + onArrowKeyPressed={handleArrowKeyPressed} onSelect={() => handleValueChange(raid)} > {raid.name[locale]} @@ -243,6 +355,7 @@ const RaidCombobox = (props: Props) => { groupName="raid_section" name="events" selected={currentSection === 2} + tabIndex={2} onClick={() => setCurrentSection(2)} > {t('raids.sections.events')} @@ -251,6 +364,7 @@ const RaidCombobox = (props: Props) => { groupName="raid_section" name="raids" selected={currentSection === 1} + tabIndex={3} onClick={() => setCurrentSection(1)} > {t('raids.sections.raids')} @@ -259,6 +373,7 @@ const RaidCombobox = (props: Props) => { groupName="raid_section" name="solo" selected={currentSection === 3} + tabIndex={4} onClick={() => setCurrentSection(3)} > {t('raids.sections.solo')} @@ -284,6 +399,9 @@ const RaidCombobox = (props: Props) => { leftAccessoryIcon={} leftAccessoryClassName={sort === Sort.DESCENDING ? 'Flipped' : ''} onClick={reverseSort} + onKeyDown={handleSortButtonKeyDown} + ref={sortButtonRef} + tabIndex={5} /> ) @@ -294,7 +412,7 @@ const RaidCombobox = (props: Props) => { if (currentRaid) { const element = ( <> - {!props.minimal && ( + {!props.minimal ? (
{currentRaid.group.name[locale]} / @@ -302,6 +420,10 @@ const RaidCombobox = (props: Props) => { {currentRaid.name[locale]}
+ ) : ( + + {currentRaid.name[locale]} + )} {currentRaid.group.extra && !props.minimal && ( @@ -326,6 +448,8 @@ const RaidCombobox = (props: Props) => { @@ -387,6 +511,9 @@ const RaidCombobox = (props: Props) => {
{renderRaidSections()}
diff --git a/components/raids/RaidItem/index.tsx b/components/raids/RaidItem/index.tsx index 9fa4e7c4..798d1ace 100644 --- a/components/raids/RaidItem/index.tsx +++ b/components/raids/RaidItem/index.tsx @@ -12,8 +12,11 @@ interface Props { } extra: boolean selected: boolean + tabIndex?: number value: string | number onSelect: () => void + onArrowKeyPressed?: (direction: 'Up' | 'Down') => void + onEscapeKeyPressed?: () => void } const RaidItem = React.forwardRef>( function Item( @@ -22,7 +25,10 @@ const RaidItem = React.forwardRef>( value, extra, selected, + tabIndex, children, + onEscapeKeyPressed, + onArrowKeyPressed, ...props }: PropsWithChildren, forwardedRef @@ -32,12 +38,34 @@ const RaidItem = React.forwardRef>( props.className ) + const handleKeyDown = (event: React.KeyboardEvent) => { + if (event.key === 'Escape' && onEscapeKeyPressed) { + event.preventDefault() + onEscapeKeyPressed() + } + + if (event.key === 'ArrowUp' || event.key === 'ArrowDown') { + event.preventDefault() + if (onArrowKeyPressed) { + console.log(event.key) + onArrowKeyPressed(event.key === 'ArrowUp' ? 'Up' : 'Down') + } + } + + if (event.key === 'Enter') { + event.preventDefault() + props.onSelect() + } + } + return ( {icon ? {icon.alt} : ''}