diff --git a/frontend/src/app/dev/page.tsx b/frontend/src/app/dev/page.tsx new file mode 100644 index 0000000000..68327c9456 --- /dev/null +++ b/frontend/src/app/dev/page.tsx @@ -0,0 +1,78 @@ +'use client'; + +import React from 'react' +import ModuleList from 'components/ModuleList' + +export default function DevModuleListPage() { + const lessThan5 = ['Module 1', 'Module 2', 'Module 3'] + const exactly5 = ['Module 1', 'Module 2', 'Module 3', 'Module 4', 'Module 5'] + const manyModules = Array.from({ length: 8 }, (_, i) => `Module ${i + 1}`) + const sixModules = Array.from({ length: 6 }, (_, i) => `Module ${i + 1}`) + const longModule = [ + 'This is a very long module name that exceeds fifty characters and should be truncated', + ] + const emptyStrings = ['', 'Valid Module', ''] + + // show empty / undefined / null cases (cast to any to allow rendering in dev) + const undefinedModules = undefined as any + const nullModules = null as any + const emptyArray: string[] = [] + + return ( +
+

Dev: ModuleList

+

+ Use Tab + Enter/Space to test keyboard activation and focus rings. This page renders the + same scenarios covered by the unit tests. +

+ +
+

Less than 5 modules

+ +
+ +
+

Exactly 5 modules

+ +
+ +
+

More than 5 modules (8 items)

+ +
+ +
+

Edge case: exactly 6 modules

+ +
+ +
+

Long module name (truncation)

+ +
+ +
+

Modules with empty strings

+ +
+ +
+

Empty / undefined / null (should render nothing)

+
+
+
Empty array
+ +
+
+
Undefined (dev cast)
+ +
+
+
Null (dev cast)
+ +
+
+
+
+ ) +} \ No newline at end of file diff --git a/frontend/src/components/ActionButton.tsx b/frontend/src/components/ActionButton.tsx index c31d74d1c1..8762e75a52 100644 --- a/frontend/src/components/ActionButton.tsx +++ b/frontend/src/components/ActionButton.tsx @@ -6,11 +6,13 @@ import React, { ReactNode } from 'react' interface ActionButtonProps { url?: string onClick?: () => void + onKeyDown?: React.KeyboardEventHandler tooltipLabel?: string children: ReactNode + className?: string } -const ActionButton: React.FC = ({ url, onClick, tooltipLabel, children }) => { +const ActionButton: React.FC = ({ url, onClick, tooltipLabel, children, onKeyDown, className = '' }) => { const baseStyles = 'flex items-center gap-2 px-2 py-2 rounded-md border border-[#1D7BD7] transition-all whitespace-nowrap justify-center bg-transparent text-[#1D7BD7] hover:bg-[#1D7BD7] hover:text-white dark:hover:text-white' @@ -20,10 +22,11 @@ const ActionButton: React.FC = ({ url, onClick, tooltipLabel, href={url} target="_blank" rel="noopener noreferrer" - className={baseStyles} + className={`${baseStyles} ${className}`} data-tooltip-id="button-tooltip" data-tooltip-content={tooltipLabel} onClick={onClick} + onKeyDown={onKeyDown} aria-label={tooltipLabel} > {children} @@ -31,7 +34,7 @@ const ActionButton: React.FC = ({ url, onClick, tooltipLabel, ) : ( - diff --git a/frontend/src/components/Card.tsx b/frontend/src/components/Card.tsx index 61dcc91886..b3fe8e74da 100644 --- a/frontend/src/components/Card.tsx +++ b/frontend/src/components/Card.tsx @@ -144,7 +144,18 @@ const Card = ({ {/* Action Button */}
- + { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault() + button.onclick?.() + } + }} + className="focus:outline-none focus-visible:ring-2 focus-visible:ring-offset-1" + > {button.icon} {button.label} diff --git a/frontend/src/components/ModuleList.tsx b/frontend/src/components/ModuleList.tsx index 0745199c23..4a1afa5d98 100644 --- a/frontend/src/components/ModuleList.tsx +++ b/frontend/src/components/ModuleList.tsx @@ -22,7 +22,7 @@ const ModuleList: React.FC = ({ modules }) => { return ( diff --git a/frontend/src/components/Search.tsx b/frontend/src/components/Search.tsx index 60a479fa98..7ef0309f38 100644 --- a/frontend/src/components/Search.tsx +++ b/frontend/src/components/Search.tsx @@ -76,8 +76,10 @@ const SearchBar: React.FC = ({ icon={faSearch} className="pointer-events-none absolute top-1/2 left-3 h-4 w-4 -translate-y-1/2 text-gray-400" /> + = ({ /> {searchQuery && ( diff --git a/frontend/src/components/ShowMoreButton.tsx b/frontend/src/components/ShowMoreButton.tsx index a5cc235b7a..2f3331e907 100644 --- a/frontend/src/components/ShowMoreButton.tsx +++ b/frontend/src/components/ShowMoreButton.tsx @@ -17,7 +17,9 @@ const ShowMoreButton = ({ onToggle }: { onToggle: () => void }) => { type="button" disableAnimation onPress={handleToggle} - className="flex items-center bg-transparent px-0 text-blue-400" + aria-expanded={isExpanded} + aria-label={isExpanded ? 'Show less items' : 'Show more items'} + className="flex items-center bg-transparent px-0 text-blue-400 focus:outline-none focus-visible:ring-1 focus-visible:ring-offset-1" > {isExpanded ? ( <> diff --git a/frontend/src/components/SortBy.tsx b/frontend/src/components/SortBy.tsx index ee807fb200..e043517776 100644 --- a/frontend/src/components/SortBy.tsx +++ b/frontend/src/components/SortBy.tsx @@ -24,7 +24,7 @@ const SortBy = ({ classNames={{ label: 'font-medium text-sm text-gray-700 dark:text-gray-300 w-auto select-none pe-0', trigger: - 'bg-transparent data-[hover=true]:bg-transparent focus:outline-none focus:ring-0 border-none shadow-none text-nowrap w-32 min-h-8 h-8 text-sm font-medium text-gray-800 dark:text-gray-200 hover:text-gray-900 dark:hover:text-gray-100 transition-all duration-0', + 'bg-transparent data-[hover=true]:bg-transparent focus:outline-none focus-visible:ring-1 focus-visible:ring-1 focus-visible:ring-offset-1 border-none shadow-none text-nowrap w-32 min-h-8 h-8 text-sm font-medium text-gray-800 dark:text-gray-200 hover:text-gray-900 dark:hover:text-gray-100 transition-all duration-0', value: 'text-gray-800 dark:text-gray-200 font-medium', selectorIcon: 'text-gray-500 dark:text-gray-400 transition-transform duration-200', popoverContent: @@ -42,7 +42,7 @@ const SortBy = ({ {option.label} @@ -61,8 +61,15 @@ const SortBy = ({ closeDelay={100} >