diff options
author | stanislavh <stanislav.honcharov@sonarsource.com> | 2024-11-28 15:07:52 +0100 |
---|---|---|
committer | sonartech <sonartech@sonarsource.com> | 2024-11-29 20:03:05 +0000 |
commit | 86455202929335f50ad609427eef71f43a2e753c (patch) | |
tree | f3c6edf1eaf78b92658cc021fc386f598969ac2f | |
parent | 8c5df648a3ac7917027ad002d3a59403d90a4ea6 (diff) | |
download | sonarqube-86455202929335f50ad609427eef71f43a2e753c.tar.gz sonarqube-86455202929335f50ad609427eef71f43a2e753c.zip |
SONAR-22326 Fix a11y issues in header and project branch selector
5 files changed, 75 insertions, 28 deletions
diff --git a/server/sonar-web/src/main/js/app/components/global-search/GlobalSearch.tsx b/server/sonar-web/src/main/js/app/components/global-search/GlobalSearch.tsx index 5d3c988f100..684ef20ec50 100644 --- a/server/sonar-web/src/main/js/app/components/global-search/GlobalSearch.tsx +++ b/server/sonar-web/src/main/js/app/components/global-search/GlobalSearch.tsx @@ -18,10 +18,10 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -import { ButtonIcon, ButtonVariety, IconSearch } from '@sonarsource/echoes-react'; +import { ButtonIcon, ButtonVariety, IconSearch, Text } from '@sonarsource/echoes-react'; import { debounce, isEmpty, uniqBy } from 'lodash'; import * as React from 'react'; -import { DropdownMenu, InputSearch, Popup, PopupZLevel, TextMuted } from '~design-system'; +import { DropdownMenu, InputSearch, Popup, PopupZLevel } from '~design-system'; import { withRouter } from '~sonar-aligned/components/hoc/withRouter'; import { ComponentQualifier } from '~sonar-aligned/types/component'; import { Router } from '~sonar-aligned/types/router'; @@ -345,7 +345,7 @@ export class GlobalSearch extends React.PureComponent<Props, State> { ); renderNoResults = () => ( - <div className="sw-px-3 sw-py-2" aria-live="assertive"> + <div className="sw-px-3 sw-py-2"> {translateWithParameters('no_results_for_x', this.state.query)} </div> ); @@ -369,6 +369,7 @@ export class GlobalSearch extends React.PureComponent<Props, State> { aria-owns="global-search-input" > <GlobalSearchResults + loading={loading} query={query} loadingMore={loadingMore} more={more} @@ -381,7 +382,7 @@ export class GlobalSearch extends React.PureComponent<Props, State> { /> {list.length > 0 && ( <li className="sw-px-3 sw-pt-1"> - <TextMuted text={translate('global_search.shortcut_hint')} /> + <Text isSubdued>{translate('global_search.shortcut_hint')}</Text> </li> )} </DropdownMenu> diff --git a/server/sonar-web/src/main/js/app/components/global-search/GlobalSearchResults.tsx b/server/sonar-web/src/main/js/app/components/global-search/GlobalSearchResults.tsx index 6714b6a437e..85d23c8a1d8 100644 --- a/server/sonar-web/src/main/js/app/components/global-search/GlobalSearchResults.tsx +++ b/server/sonar-web/src/main/js/app/components/global-search/GlobalSearchResults.tsx @@ -19,12 +19,14 @@ */ import * as React from 'react'; +import { useIntl } from 'react-intl'; import { ItemDivider, ItemHeader } from '~design-system'; import { translate } from '../../../helpers/l10n'; import GlobalSearchShowMore from './GlobalSearchShowMore'; import { ComponentResult, More, Results, sortQualifiers } from './utils'; export interface Props { + loading: boolean; loadingMore?: string; more: More; onMoreClick: (qualifier: string) => void; @@ -37,6 +39,7 @@ export interface Props { } export default function GlobalSearchResults(props: Props): React.ReactElement<Props> { + const intl = useIntl(); const qualifiers = Object.keys(props.results); const renderedComponents: React.ReactNode[] = []; const allowMore = props.query.length !== 1; @@ -72,5 +75,22 @@ export default function GlobalSearchResults(props: Props): React.ReactElement<Pr } }); - return renderedComponents.length > 0 ? <>{renderedComponents}</> : props.renderNoResults(); + const resultCount = Object.values(props.results).reduce( + (acc, components) => acc + (components?.length ?? 0), + 0, + ); + + return ( + <> + <output aria-busy={props.loading || Boolean(props.loadingMore)}> + {renderedComponents.length === 0 && props.renderNoResults()} + {renderedComponents.length > 0 && ( + <span className="sw-sr-only"> + {intl.formatMessage({ id: 'results_shown_x' }, { count: resultCount })} + </span> + )} + </output> + {renderedComponents.length > 0 && renderedComponents} + </> + ); } diff --git a/server/sonar-web/src/main/js/app/components/nav/component/branch-like/Menu.tsx b/server/sonar-web/src/main/js/app/components/nav/component/branch-like/Menu.tsx index 7faaff1de44..1c608d237b7 100644 --- a/server/sonar-web/src/main/js/app/components/nav/component/branch-like/Menu.tsx +++ b/server/sonar-web/src/main/js/app/components/nav/component/branch-like/Menu.tsx @@ -173,6 +173,7 @@ export class Menu extends React.PureComponent<Props, State> { searchInputAriaLabel={translate('search_verb')} /> <MenuItemList + search={query} branchLikeTree={branchLikesToDisplayTree} hasResults={hasResults} onSelect={this.handleOnSelect} diff --git a/server/sonar-web/src/main/js/app/components/nav/component/branch-like/MenuItemList.tsx b/server/sonar-web/src/main/js/app/components/nav/component/branch-like/MenuItemList.tsx index 5b84a3ccf6a..7315a367f50 100644 --- a/server/sonar-web/src/main/js/app/components/nav/component/branch-like/MenuItemList.tsx +++ b/server/sonar-web/src/main/js/app/components/nav/component/branch-like/MenuItemList.tsx @@ -19,6 +19,7 @@ */ import * as React from 'react'; +import { useIntl } from 'react-intl'; import { HelperHintIcon, ItemDivider, ItemHeader } from '~design-system'; import HelpTooltip from '~sonar-aligned/components/controls/HelpTooltip'; import { getBranchLikeKey, isSameBranchLike } from '../../../../../helpers/branch-like'; @@ -31,10 +32,12 @@ export interface MenuItemListProps { branchLikeTree: BranchLikeTree; hasResults: boolean; onSelect: (branchLike: BranchLike) => void; + search: string; selectedBranchLike: BranchLike | undefined; } export function MenuItemList(props: MenuItemListProps) { + const intl = useIntl(); let selectedNode: HTMLLIElement | null = null; React.useEffect(() => { @@ -44,7 +47,7 @@ export function MenuItemList(props: MenuItemListProps) { } }); - const { branchLikeTree, hasResults, onSelect, selectedBranchLike } = props; + const { branchLikeTree, hasResults, onSelect, selectedBranchLike, search } = props; const renderItem = (branchLike: BranchLike, indent = false) => ( <MenuItem @@ -57,46 +60,65 @@ export function MenuItemList(props: MenuItemListProps) { /> ); - const branches = [branchLikeTree.mainBranchTree, ...branchLikeTree.branchTree]; + const branches = [branchLikeTree.mainBranchTree, ...branchLikeTree.branchTree].filter(isDefined); + const total = + branches.length + + branches.reduce((t, branchTree) => t + (branchTree?.pullRequests.length ?? 0), 0) + + branchLikeTree.parentlessPullRequests.length + + branchLikeTree.orphanPullRequests.length; return ( - <ul className="item-list sw-overflow-y-auto sw-overflow-x-hidden"> - {!hasResults && ( - <div className="sw-px-3 sw-py-2"> - <span>{translate('no_results')}</span> - </div> - )} + <ul + aria-label={`- ${translate('branch_like_navigation.list')}`} + className="item-list sw-overflow-y-auto sw-overflow-x-hidden" + > + <output> + {!hasResults && ( + <div className="sw-px-3 sw-py-2"> + <span>{intl.formatMessage({ id: 'no_results_for_x' }, { '0': search })}</span> + </div> + )} + {hasResults && ( + <span className="sw-sr-only"> + {intl.formatMessage({ id: 'results_shown_x' }, { count: total })} + </span> + )} + </output> {/* BRANCHES & PR */} - {branches.filter(isDefined).map((tree, treeIndex) => ( + {branches.map((tree, treeIndex) => ( <React.Fragment key={getBranchLikeKey(tree.branch)}> {renderItem(tree.branch)} {tree.pullRequests.length > 0 && ( - <> - <ItemDivider /> - <ItemHeader>{translate('branch_like_navigation.pull_requests')}</ItemHeader> - <ItemDivider /> + <ul + aria-label={` - ${intl.formatMessage({ id: 'branch_like_navigation.pull_requests_targeting' }, { branch: tree.branch.name })}`} + > + <ItemDivider aria-hidden /> + <ItemHeader aria-hidden> + {translate('branch_like_navigation.pull_requests')} + </ItemHeader> + <ItemDivider aria-hidden /> {tree.pullRequests.map((pr) => renderItem(pr, true))} {tree.pullRequests.length > 0 && treeIndex !== branches.length - 1 && <ItemDivider />} - </> + </ul> )} </React.Fragment> ))} {/* PARENTLESS PR (for display during search) */} {branchLikeTree.parentlessPullRequests.length > 0 && ( - <> - <ItemDivider /> - <ItemHeader>{translate('branch_like_navigation.pull_requests')}</ItemHeader> - <ItemDivider /> + <ul aria-label={` - ${translate('branch_like_navigation.pull_requests')}`}> + <ItemDivider aria-hidden /> + <ItemHeader aria-hidden>{translate('branch_like_navigation.pull_requests')}</ItemHeader> + <ItemDivider aria-hidden /> {branchLikeTree.parentlessPullRequests.map((pr) => renderItem(pr))} - </> + </ul> )} {/* ORPHAN PR */} {branchLikeTree.orphanPullRequests.length > 0 && ( - <> - <ItemDivider /> + <ul aria-label={` - ${translate('branch_like_navigation.orphan_pull_requests')}`}> + <ItemDivider aria-hidden /> <ItemHeader> {translate('branch_like_navigation.orphan_pull_requests')} <HelpTooltip @@ -106,9 +128,9 @@ export function MenuItemList(props: MenuItemListProps) { <HelperHintIcon /> </HelpTooltip> </ItemHeader> - <ItemDivider /> + <ItemDivider aria-hidden /> {branchLikeTree.orphanPullRequests.map((pr) => renderItem(pr))} - </> + </ul> )} </ul> ); diff --git a/sonar-core/src/main/resources/org/sonar/l10n/core.properties b/sonar-core/src/main/resources/org/sonar/l10n/core.properties index 21d5616bea8..c4f1ee47089 100644 --- a/sonar-core/src/main/resources/org/sonar/l10n/core.properties +++ b/sonar-core/src/main/resources/org/sonar/l10n/core.properties @@ -334,6 +334,7 @@ new_violations=New violations new_window=New window no_data=No data no_measure_value_x=No measure value for {0} +results_shown_x={count} results shown no_results=No results no_results_for_x=No results for "{0}" no_results_search=We couldn't find any results matching selected criteria. @@ -5545,7 +5546,9 @@ branches.see_the_pr_on_x=See the PR on {0} #------------------------------------------------------------------------------ branch_like_navigation.manage=Manage branches and Pull Requests branch_like_navigation.search_for_branch_like=Search for branches or Pull Requests... +branch_like_navigation.list=Branches and Pull Requests branch_like_navigation.pull_requests=Pull Requests +branch_like_navigation.pull_requests_targeting=Pull Requests targeting "{branch}" branch_like_navigation.orphan_pull_requests=Orphan Pull Requests branch_like_navigation.orphan_pull_requests.tooltip=When the base of a Pull Request is deleted, this Pull Request becomes orphan. branch_like_navigation.for_merge_into_x_from_y=for merge into {target} from {branch} |