Skip to content
Closed
Show file tree
Hide file tree
Changes from 5 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: 6 additions & 3 deletions frontend/src/components/ActionButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,13 @@ import React, { ReactNode } from 'react'
interface ActionButtonProps {
url?: string
onClick?: () => void
onKeyDown?: React.KeyboardEventHandler<HTMLAnchorElement | HTMLButtonElement>
tooltipLabel?: string
children: ReactNode
className?: string
}

const ActionButton: React.FC<ActionButtonProps> = ({ url, onClick, tooltipLabel, children }) => {
const ActionButton: React.FC<ActionButtonProps> = ({ 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-blue-600 hover:bg-[#1D7BD7] hover:text-white dark:hover:text-white'

Expand All @@ -20,18 +22,19 @@ const ActionButton: React.FC<ActionButtonProps> = ({ 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}
</Link>
</TooltipWrapper>
) : (
<TooltipWrapper tooltipLabel={tooltipLabel}>
<Button onPress={onClick} className={baseStyles} aria-label={tooltipLabel}>
<Button onPress={onClick} onKeyDown={onKeyDown} className={`${baseStyles} ${className}`} aria-label={tooltipLabel}>
{children}
</Button>
</TooltipWrapper>
Expand Down
13 changes: 12 additions & 1 deletion frontend/src/components/Card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,18 @@ const Card = ({

{/* Action Button */}
<div className="flex sm:justify-end">
<ActionButton tooltipLabel={tooltipLabel} url={button.url} onClick={button.onclick}>
<ActionButton
tooltipLabel={tooltipLabel}
url={button.url}
onClick={button.onclick}
onKeyDown={(e: React.KeyboardEvent) => {
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}
</ActionButton>
Expand Down
4 changes: 2 additions & 2 deletions frontend/src/components/ModuleList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ const ModuleList: React.FC<ModuleListProps> = ({ modules }) => {
return (
<button
key={`${module}-${index}`}
className="rounded-lg border border-gray-400 px-3 py-1 text-sm transition-all duration-200 ease-in-out hover:scale-105 hover:bg-gray-200 dark:border-gray-300 dark:hover:bg-gray-700"
className="rounded-lg border border-gray-400 px-3 py-1 text-sm transition-all duration-200 ease-in-out hover:scale-105 hover:bg-gray-200 dark:border-gray-300 dark:hover:bg-gray-700 focus-visible:ring-1"
title={module.length > 50 ? module : undefined}
type="button"
>
Expand All @@ -36,7 +36,7 @@ const ModuleList: React.FC<ModuleListProps> = ({ modules }) => {
disableAnimation
aria-label={showAll ? 'Show fewer modules' : 'Show more modules'}
onPress={() => setShowAll((prev) => !prev)}
className="mt-4 flex items-center bg-transparent text-blue-400 hover:underline"
className="mt-4 flex items-center bg-transparent text-blue-400 hover:underline focus:ring-1"
>
{showAll ? (
<>
Expand Down
69 changes: 65 additions & 4 deletions frontend/src/components/ProgramActions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
const router = useRouter()
const [dropdownOpen, setDropdownOpen] = useState(false)
const dropdownRef = useRef<HTMLDivElement>(null)

const triggerRef = useRef<HTMLButtonElement | null>(null)
const handleAction = (actionKey: string) => {
switch (actionKey) {
case 'edit Program':
Expand Down Expand Up @@ -65,25 +65,86 @@
}
}, [])

useEffect(() => {
if (!dropdownOpen) return
requestAnimationFrame(() => {
const items = dropdownRef.current?.querySelectorAll<HTMLElement>('[role="menuitem"]')
if (items && items.length > 0) items[0].focus()
})
}, [dropdownOpen])

const handleMenuKeyDown = (e: React.KeyboardEvent<HTMLDivElement>) => {
const items = dropdownRef.current?.querySelectorAll<HTMLElement>('[role="menuitem"]')
if (!items || items.length === 0) return

const current = Array.from(items).indexOf(document.activeElement as HTMLElement)
switch (e.key) {
case 'ArrowDown': {
e.preventDefault()
const next = current < items.length - 1 ? current + 1 : 0
items[next].focus()
break
}
case 'ArrowUp': {
e.preventDefault()
const prev = current > 0 ? current - 1 : items.length - 1
items[prev].focus()
break
}
case 'Home': {
e.preventDefault()
items[0].focus()
break
}
case 'End': {
e.preventDefault()
items[items.length - 1].focus()
break
}
case 'Escape': {
e.preventDefault()
setDropdownOpen(false)
triggerRef.current?.focus()
break
}
default:
break
}
}

return (
<div className="relative" ref={dropdownRef}>
<button
data-testid="program-actions-button"
type="button"
ref={triggerRef}
aria-haspopup="menu"
aria-expanded={dropdownOpen}
onClick={() => setDropdownOpen((prev) => !prev)}
className="rounded px-4 py-2 hover:bg-gray-200 dark:hover:bg-gray-700"
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault()
setDropdownOpen((prev) => !prev)
}
}}
className="rounded px-4 py-2 hover:bg-gray-200 dark:hover:bg-gray-700 focus:outline-none focus-visible:ring-1 focus-visible:ring-offset-1"
>
<FontAwesomeIcon icon={faEllipsisV} />
</button>
{dropdownOpen && (
<div className="absolute right-0 z-20 mt-2 w-40 rounded-md border border-gray-200 bg-white shadow-lg dark:border-gray-700 dark:bg-gray-800">
<div
role="menu"
onKeyDown={handleMenuKeyDown}
className="absolute right-0 z-20 mt-2 w-40 rounded-md border border-gray-200 bg-white shadow-lg dark:border-gray-700 dark:bg-gray-800"
>

Check warning on line 139 in frontend/src/components/ProgramActions.tsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Elements with the 'menu' interactive role must be focusable.

See more on https://sonarcloud.io/project/issues?id=OWASP_Nest&issues=AZrEpdwz3JsuDLQaNAY1&open=AZrEpdwz3JsuDLQaNAY1&pullRequest=2737
{options.map((option) => (
<button
key={option.key}
type="button"
role="menuitem"
tabIndex={-1}
onClick={() => handleAction(option.key)}
className="block w-full px-4 py-2 text-left text-sm hover:bg-gray-100 dark:hover:bg-gray-700"
className="block w-full px-4 py-2 text-left text-sm hover:bg-gray-100 dark:hover:bg-gray-700 focus:outline-none focus-visible:ring-1 focus-visible:ring-offset-1"
>
{option.label}
</button>
Expand Down
13 changes: 12 additions & 1 deletion frontend/src/components/Release.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -70,14 +70,25 @@ const Release: React.FC<ReleaseProps> = ({
<div className="flex flex-1 items-center overflow-hidden">
<FontAwesomeIcon icon={faFolderOpen} className="mr-2 h-5 w-4" />
<button
className="cursor-pointer overflow-hidden text-ellipsis whitespace-nowrap text-gray-600 hover:underline dark:text-gray-400"
type="button"
aria-label={`Open repository ${release.repositoryName}`}
className="cursor-pointer overflow-hidden text-ellipsis whitespace-nowrap text-gray-600 hover:underline dark:text-gray-400 focus-visible:ring-2"
disabled={!release.organizationName || !release.repositoryName}
onClick={() => {
const org = release.organizationName || ''
const repo = release.repositoryName || ''
if (!org || !repo) return
router.push(`/organizations/${org}/repositories/${repo}`)
}}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault()
const org = release.organizationName || ''
const repo = release.repositoryName || ''
if (!org || !repo) return
router.push(`/organizations/${org}/repositories/${repo}`)
}
}}
>
<TruncatedText text={release.repositoryName} />
</button>
Expand Down
6 changes: 5 additions & 1 deletion frontend/src/components/Search.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -76,8 +76,10 @@ const SearchBar: React.FC<SearchProps> = ({
icon={faSearch}
className="pointer-events-none absolute top-1/2 left-3 h-4 w-4 -translate-y-1/2 text-gray-400"
/>
<label htmlFor="search-input" className="sr-only">Search</label>
<input
ref={inputRef}
id="search-input"
type="text"
value={searchQuery}
onChange={handleSearchChange}
Expand All @@ -86,7 +88,9 @@ const SearchBar: React.FC<SearchProps> = ({
/>
{searchQuery && (
<button
className="absolute top-1/2 right-2 -translate-y-1/2 rounded-full p-1 hover:bg-gray-100 focus:ring-2 focus:ring-gray-300 focus:outline-hidden"
type="button"
aria-label="Clear search"
className="absolute top-1/2 right-2 -translate-y-1/2 rounded-full p-1 hover:bg-gray-100 focus:outline-none focus-visible:ring-2 focus-visible:ring-gray-300 focus-visible:ring-offset-1"
onClick={handleClearSearch}
>
<FontAwesomeIcon icon={faTimes} />
Expand Down
4 changes: 3 additions & 1 deletion frontend/src/components/ShowMoreButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Comment on lines +20 to +22
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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"
aria-expanded={isExpanded}
className="flex items-center bg-transparent px-0 text-blue-400 focus:outline-none focus-visible:ring-1 focus-visible:ring-offset-1"

arial-label is redundant here - the inner button text will be used by screen readers.

>
{isExpanded ? (
<>
Expand Down
14 changes: 11 additions & 3 deletions frontend/src/components/SortBy.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I’m not sure this looks quite right.

Image

Is there a way we could make it a bit more subtle?

value: 'text-gray-800 dark:text-gray-200 font-medium',
selectorIcon: 'text-gray-500 dark:text-gray-400 transition-transform duration-200',
popoverContent:
Expand All @@ -42,7 +42,7 @@ const SortBy = ({
<SelectItem
key={option.key}
classNames={{
base: 'text-sm text-gray-700 dark:text-gray-300 hover:bg-transparent dark:hover:bg-transparent focus:bg-gray-100 dark:focus:bg-[#404040] focus:outline-none rounded-sm px-3 py-2 cursor-pointer transition-colors duration-150 data-[selected=true]:bg-blue-50 dark:data-[selected=true]:bg-blue-900/20 data-[selected=true]:text-blue-600 dark:data-[selected=true]:text-blue-400 data-[focus=true]:bg-gray-100 dark:data-[focus=true]:bg-[#404040]',
base:'text-sm text-gray-700 dark:text-gray-300 hover:bg-transparent dark:hover:bg-transparent focus:bg-gray-100 dark:focus:bg-[#404040] focus:outline-none rounded-sm px-3 py-2 cursor-pointer transition-colors duration-150 data-[selected=true]:bg-blue-50 dark:data-[selected=true]:bg-blue-900/20 data-[selected=true]:text-blue-600 dark:data-[selected=true]:text-blue-400 data-[focus=true]:bg-gray-100 dark:data-[focus=true]:bg-[#404040] focus:ring-1 focus-visible:ring-offset-1',
}}
>
{option.label}
Expand All @@ -61,8 +61,16 @@ const SortBy = ({
closeDelay={100}
>
<button
aria-label="Toggle sort order"
aria-pressed={selectedOrder === 'asc'}
onClick={() => onOrderChange(selectedOrder === 'asc' ? 'desc' : 'asc')}
className="inline-flex h-9 w-9 items-center justify-center rounded-lg border border-gray-300 bg-gray-100 p-0 shadow-sm transition-all duration-200 hover:bg-gray-200 hover:shadow-md focus:ring-2 focus:ring-gray-300 focus:ring-offset-1 focus:outline-none dark:border-gray-600 dark:bg-[#323232] dark:hover:bg-[#404040] dark:focus:ring-gray-500"
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault()
onOrderChange(selectedOrder === 'asc' ? 'desc' : 'asc')
}
}}
className="inline-flex h-9 w-9 items-center justify-center rounded-lg border border-gray-300 bg-gray-100 p-0 shadow-sm transition-all duration-200 hover:bg-gray-200 hover:shadow-md focus:outline-none focus-visible:ring-2 focus-visible:ring-gray-300 focus-visible:ring-offset-1 dark:border-gray-600 dark:bg-[#323232] dark:hover:bg-[#404040] dark:focus-visible:ring-gray-500"
>
{selectedOrder === 'asc' ? (
<FontAwesomeIcon
Expand Down
10 changes: 9 additions & 1 deletion frontend/src/components/ToggleableList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,16 @@ const ToggleableList = ({
{(showAll ? items : items.slice(0, limit)).map((item) => (
<button
key={item}
className="rounded-lg border border-gray-400 px-3 py-1 text-sm hover:bg-gray-200 dark:border-gray-300 dark:hover:bg-gray-700"
type="button"
aria-label={`Search projects for ${item}`}
className="rounded-lg border border-gray-400 px-3 py-1 text-sm hover:bg-gray-200 dark:border-gray-300 dark:hover:bg-gray-700 focus:outline-none focus-visible:ring-1 focus-visible:ring-offset-1 transition"
onClick={() => !isDisabled && handleButtonClick({ item })}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault()
if (!isDisabled) handleButtonClick({ item })
}
}}
>
{item}
</button>
Expand Down